dovecot.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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.socket = configuration.get("auth", "dovecot_socket")
  28. self.timeout = 5
  29. self.request_id_gen = itertools.count(1)
  30. def login(self, login, password):
  31. """Validate credentials.
  32. Check if the ``login``/``password`` pair is valid according to Dovecot.
  33. This implementation communicates with a Dovecot server through the
  34. Dovecot Authentication Protocol v1.1.
  35. https://dovecot.org/doc/auth-protocol.txt
  36. """
  37. logger.info("Authentication request (dovecot): '{}'".format(login))
  38. if not login or not password:
  39. return ""
  40. with closing(socket.socket(
  41. socket.AF_UNIX,
  42. socket.SOCK_STREAM)
  43. ) as sock:
  44. try:
  45. sock.settimeout(self.timeout)
  46. sock.connect(self.socket)
  47. buf = bytes()
  48. supported_mechs = []
  49. done = False
  50. seen_part = [0, 0, 0]
  51. # Upon the initial connection we only care about the
  52. # handshake, which is usually just around 100 bytes long,
  53. # e.g.
  54. #
  55. # VERSION 1 2
  56. # MECH PLAIN plaintext
  57. # SPID 22901
  58. # CUID 1
  59. # COOKIE 2dbe4116a30fb4b8a8719f4448420af7
  60. # DONE
  61. #
  62. # Hence, we try to read just once with a buffer big
  63. # enough to hold all of it.
  64. buf = sock.recv(1024)
  65. while b'\n' in buf and not done:
  66. line, buf = buf.split(b'\n', 1)
  67. parts = line.split(b'\t')
  68. first, parts = parts[0], parts[1:]
  69. if first == b'VERSION':
  70. if seen_part[0]:
  71. logger.warning(
  72. "Server presented multiple VERSION "
  73. "tokens, ignoring"
  74. )
  75. continue
  76. version = parts
  77. logger.debug("Dovecot server version: '{}'".format(
  78. (b'.'.join(version)).decode()
  79. ))
  80. if int(version[0]) != 1:
  81. logger.fatal(
  82. "Only Dovecot 1.x versions are supported!"
  83. )
  84. return ""
  85. seen_part[0] += 1
  86. elif first == b'MECH':
  87. supported_mechs.append(parts[0])
  88. seen_part[1] += 1
  89. elif first == b'DONE':
  90. seen_part[2] += 1
  91. if not (seen_part[0] and seen_part[1]):
  92. logger.fatal(
  93. "An unexpected end of the server "
  94. "handshake received!"
  95. )
  96. return ""
  97. done = True
  98. if not done:
  99. logger.fatal("Encountered a broken server handshake!")
  100. return ""
  101. logger.debug(
  102. "Supported auth methods: '{}'"
  103. .format((b"', '".join(supported_mechs)).decode())
  104. )
  105. if b'PLAIN' not in supported_mechs:
  106. logger.info(
  107. "Authentication method 'PLAIN' is not supported, "
  108. "but is required!"
  109. )
  110. return ""
  111. # Handshake
  112. logger.debug("Sending auth handshake")
  113. sock.send(b'VERSION\t1\t1\n')
  114. sock.send(b'CPID\t%u\n' % os.getpid())
  115. request_id = next(self.request_id_gen)
  116. logger.debug(
  117. "Authenticating with request id: '{}'"
  118. .format(request_id)
  119. )
  120. sock.send(
  121. b'AUTH\t%u\tPLAIN\tservice=radicale\tresp=%b\n' %
  122. (
  123. request_id, base64.b64encode(
  124. b'\0%b\0%b' %
  125. (login.encode(), password.encode())
  126. )
  127. )
  128. )
  129. logger.debug("Processing auth response")
  130. buf = sock.recv(1024)
  131. line = buf.split(b'\n', 1)[0]
  132. parts = line.split(b'\t')[:2]
  133. resp, reply_id, params = (
  134. parts[0], int(parts[1]),
  135. dict(part.split('=', 1) for part in parts[2:])
  136. )
  137. logger.debug(
  138. "Auth response: result='{}', id='{}', parameters={}"
  139. .format(resp.decode(), reply_id, params)
  140. )
  141. if request_id != reply_id:
  142. logger.fatal(
  143. "Unexpected reply ID {} received (expected {})"
  144. .format(
  145. reply_id, request_id
  146. )
  147. )
  148. return ""
  149. if resp == b'OK':
  150. return login
  151. except socket.error as e:
  152. logger.fatal(
  153. "Failed to communicate with Dovecot socket %r: %s" %
  154. (self.socket, e)
  155. )
  156. return ""