dovecot.py 7.1 KB

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