dovecot.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2014 Giel van Schijndel
  3. # Copyright © 2019 (GalaxyMaster)
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. import base64
  18. import itertools
  19. import os
  20. import socket
  21. from contextlib import closing
  22. from radicale import auth
  23. from radicale.log import logger
  24. class Auth(auth.BaseAuth):
  25. def __init__(self, configuration):
  26. super().__init__(configuration)
  27. self.timeout = 5
  28. self.request_id_gen = itertools.count(1)
  29. config_family = configuration.get("auth", "dovecot_connection_type")
  30. if config_family == "AF_UNIX":
  31. self.family = socket.AF_UNIX
  32. self.address = configuration.get("auth", "dovecot_socket")
  33. return
  34. self.address = configuration.get("auth", "dovecot_host"), configuration.get("auth", "dovecot_port")
  35. if config_family == "AF_INET":
  36. self.family = socket.AF_INET
  37. else:
  38. self.family = socket.AF_INET6
  39. def _login(self, login, password):
  40. """Validate credentials.
  41. Check if the ``login``/``password`` pair is valid according to Dovecot.
  42. This implementation communicates with a Dovecot server through the
  43. Dovecot Authentication Protocol v1.1.
  44. https://dovecot.org/doc/auth-protocol.txt
  45. """
  46. logger.info("Authentication request (dovecot): '{}'".format(login))
  47. if not login or not password:
  48. return ""
  49. with closing(socket.socket(
  50. self.family,
  51. socket.SOCK_STREAM)
  52. ) as sock:
  53. try:
  54. sock.settimeout(self.timeout)
  55. sock.connect(self.address)
  56. buf = bytes()
  57. supported_mechs = []
  58. done = False
  59. seen_part = [0, 0, 0]
  60. # Upon the initial connection we only care about the
  61. # handshake, which is usually just around 100 bytes long,
  62. # e.g.
  63. #
  64. # VERSION 1 2
  65. # MECH PLAIN plaintext
  66. # SPID 22901
  67. # CUID 1
  68. # COOKIE 2dbe4116a30fb4b8a8719f4448420af7
  69. # DONE
  70. #
  71. # Hence, we try to read just once with a buffer big
  72. # enough to hold all of it.
  73. buf = sock.recv(1024)
  74. while b'\n' in buf and not done:
  75. line, buf = buf.split(b'\n', 1)
  76. parts = line.split(b'\t')
  77. first, parts = parts[0], parts[1:]
  78. if first == b'VERSION':
  79. if seen_part[0]:
  80. logger.warning(
  81. "Server presented multiple VERSION "
  82. "tokens, ignoring"
  83. )
  84. continue
  85. version = parts
  86. logger.debug("Dovecot server version: '{}'".format(
  87. (b'.'.join(version)).decode()
  88. ))
  89. if int(version[0]) != 1:
  90. logger.fatal(
  91. "Only Dovecot 1.x versions are supported!"
  92. )
  93. return ""
  94. seen_part[0] += 1
  95. elif first == b'MECH':
  96. supported_mechs.append(parts[0])
  97. seen_part[1] += 1
  98. elif first == b'DONE':
  99. seen_part[2] += 1
  100. if not (seen_part[0] and seen_part[1]):
  101. logger.fatal(
  102. "An unexpected end of the server "
  103. "handshake received!"
  104. )
  105. return ""
  106. done = True
  107. if not done:
  108. logger.fatal("Encountered a broken server handshake!")
  109. return ""
  110. logger.debug(
  111. "Supported auth methods: '{}'"
  112. .format((b"', '".join(supported_mechs)).decode())
  113. )
  114. if b'PLAIN' not in supported_mechs:
  115. logger.info(
  116. "Authentication method 'PLAIN' is not supported, "
  117. "but is required!"
  118. )
  119. return ""
  120. # Handshake
  121. logger.debug("Sending auth handshake")
  122. sock.send(b'VERSION\t1\t1\n')
  123. sock.send(b'CPID\t%u\n' % os.getpid())
  124. request_id = next(self.request_id_gen)
  125. logger.debug(
  126. "Authenticating with request id: '{}'"
  127. .format(request_id)
  128. )
  129. sock.send(
  130. b'AUTH\t%u\tPLAIN\tservice=radicale\tresp=%b\n' %
  131. (
  132. request_id, base64.b64encode(
  133. b'\0%b\0%b' %
  134. (login.encode(), password.encode())
  135. )
  136. )
  137. )
  138. logger.debug("Processing auth response")
  139. buf = sock.recv(1024)
  140. line = buf.split(b'\n', 1)[0]
  141. parts = line.split(b'\t')[:2]
  142. resp, reply_id, params = (
  143. parts[0], int(parts[1]),
  144. dict(part.split('=', 1) for part in parts[2:])
  145. )
  146. logger.debug(
  147. "Auth response: result='{}', id='{}', parameters={}"
  148. .format(resp.decode(), reply_id, params)
  149. )
  150. if request_id != reply_id:
  151. logger.fatal(
  152. "Unexpected reply ID {} received (expected {})"
  153. .format(
  154. reply_id, request_id
  155. )
  156. )
  157. return ""
  158. if resp == b'OK':
  159. return login
  160. except socket.error as e:
  161. logger.fatal(
  162. "Failed to communicate with Dovecot: %s" %
  163. (e)
  164. )
  165. return ""