sasl.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """SASL mechanisms for AMQP authentication."""
  2. from __future__ import absolute_import, unicode_literals
  3. import socket
  4. import warnings
  5. from io import BytesIO
  6. from amqp.serialization import _write_table
  7. class SASL(object):
  8. """The base class for all amqp SASL authentication mechanisms.
  9. You should sub-class this if you're implementing your own authentication.
  10. """
  11. @property
  12. def mechanism(self):
  13. """Return a bytes containing the SASL mechanism name."""
  14. raise NotImplementedError
  15. def start(self, connection):
  16. """Return the first response to a SASL challenge as a bytes object."""
  17. raise NotImplementedError
  18. class PLAIN(SASL):
  19. """PLAIN SASL authentication mechanism.
  20. See https://tools.ietf.org/html/rfc4616 for details
  21. """
  22. mechanism = b'PLAIN'
  23. def __init__(self, username, password):
  24. self.username, self.password = username, password
  25. def start(self, connection):
  26. if self.username is None or self.password is None:
  27. return NotImplemented
  28. login_response = BytesIO()
  29. login_response.write(b'\0')
  30. login_response.write(self.username.encode('utf-8'))
  31. login_response.write(b'\0')
  32. login_response.write(self.password.encode('utf-8'))
  33. return login_response.getvalue()
  34. class AMQPLAIN(SASL):
  35. """AMQPLAIN SASL authentication mechanism.
  36. This is a non-standard mechanism used by AMQP servers.
  37. """
  38. mechanism = b'AMQPLAIN'
  39. def __init__(self, username, password):
  40. self.username, self.password = username, password
  41. def start(self, connection):
  42. if self.username is None or self.password is None:
  43. return NotImplemented
  44. login_response = BytesIO()
  45. _write_table({b'LOGIN': self.username, b'PASSWORD': self.password},
  46. login_response.write, [])
  47. # Skip the length at the beginning
  48. return login_response.getvalue()[4:]
  49. def _get_gssapi_mechanism():
  50. try:
  51. import gssapi
  52. import gssapi.raw.misc # Fail if the old python-gssapi is installed
  53. except ImportError:
  54. class FakeGSSAPI(SASL):
  55. """A no-op SASL mechanism for when gssapi isn't available."""
  56. mechanism = None
  57. def __init__(self, client_name=None, service=b'amqp',
  58. rdns=False, fail_soft=False):
  59. if not fail_soft:
  60. raise NotImplementedError(
  61. "You need to install the `gssapi` module for GSSAPI "
  62. "SASL support")
  63. def start(self): # pragma: no cover
  64. return NotImplemented
  65. return FakeGSSAPI
  66. else:
  67. class GSSAPI(SASL):
  68. """GSSAPI SASL authentication mechanism.
  69. See https://tools.ietf.org/html/rfc4752 for details
  70. """
  71. mechanism = b'GSSAPI'
  72. def __init__(self, client_name=None, service=b'amqp',
  73. rdns=False, fail_soft=False):
  74. if client_name and not isinstance(client_name, bytes):
  75. client_name = client_name.encode('ascii')
  76. self.client_name = client_name
  77. self.fail_soft = fail_soft
  78. self.service = service
  79. self.rdns = rdns
  80. def get_hostname(self, connection):
  81. sock = connection.transport.sock
  82. if self.rdns and sock.family in (socket.AF_INET,
  83. socket.AF_INET6):
  84. peer = sock.getpeername()
  85. hostname, _, _ = socket.gethostbyaddr(peer[0])
  86. else:
  87. hostname = connection.transport.host
  88. if not isinstance(hostname, bytes):
  89. hostname = hostname.encode('ascii')
  90. return hostname
  91. def start(self, connection):
  92. try:
  93. if self.client_name:
  94. creds = gssapi.Credentials(
  95. name=gssapi.Name(self.client_name))
  96. else:
  97. creds = None
  98. hostname = self.get_hostname(connection)
  99. name = gssapi.Name(b'@'.join([self.service, hostname]),
  100. gssapi.NameType.hostbased_service)
  101. context = gssapi.SecurityContext(name=name, creds=creds)
  102. return context.step(None)
  103. except gssapi.raw.misc.GSSError:
  104. if self.fail_soft:
  105. return NotImplemented
  106. else:
  107. raise
  108. return GSSAPI
  109. GSSAPI = _get_gssapi_mechanism()
  110. class EXTERNAL(SASL):
  111. """EXTERNAL SASL mechanism.
  112. Enables external authentication, i.e. not handled through this protocol.
  113. Only passes 'EXTERNAL' as authentication mechanism, but no further
  114. authentication data.
  115. """
  116. mechanism = b'EXTERNAL'
  117. def start(self, connection):
  118. return b''
  119. class RAW(SASL):
  120. """A generic custom SASL mechanism.
  121. This mechanism takes a mechanism name and response to send to the server,
  122. so can be used for simple custom authentication schemes.
  123. """
  124. mechanism = None
  125. def __init__(self, mechanism, response):
  126. assert isinstance(mechanism, bytes)
  127. assert isinstance(response, bytes)
  128. self.mechanism, self.response = mechanism, response
  129. warnings.warn("Passing login_method and login_response to Connection "
  130. "is deprecated. Please implement a SASL subclass "
  131. "instead.", DeprecationWarning)
  132. def start(self, connection):
  133. return self.response