From e5d2faa7102d07ce3d5af59e3c5e9c51e173c14a Mon Sep 17 00:00:00 2001 From: Ian Good Date: Sat, 19 Oct 2019 13:52:41 +0000 Subject: [PATCH] Reflect changes in pysasl 0.5 --- pymap/config.py | 22 ++++++++++------------ pymap/imap/__init__.py | 25 ++++++++++++++----------- pymap/imap/state.py | 4 ++-- pymap/sieve/manage/__init__.py | 22 +++++++++++++--------- setup.cfg | 4 ++-- test/server/base.py | 1 - test/server/test_managesieve.py | 1 - 7 files changed, 41 insertions(+), 38 deletions(-) diff --git a/pymap/config.py b/pymap/config.py index d8f508f3..8638165c 100644 --- a/pymap/config.py +++ b/pymap/config.py @@ -94,7 +94,7 @@ class IMAPConfig(metaclass=ABCMeta): Alternatively, you can pass extra arguments ``cert_file`` and ``key_file`` and an SSL context will be created. starttls_enabled: True if opportunistic TLS should be supported. - reject_insecure_auth: True if authentication mechanisms that transmit + secure_auth: True if authentication mechanisms that transmit credentials in cleartext should be rejected on non-encrypted transports. preauth_credentials: If given, clients will pre-authenticate on @@ -119,8 +119,8 @@ def __init__(self, args: Namespace, *, debug: bool = False, subsystem: Subsystem = None, ssl_context: SSLContext = None, - starttls_enabled: bool = True, - reject_insecure_auth: bool = True, + tls_enabled: bool = True, + secure_auth: bool = True, preauth_credentials: AuthenticationCredentials = None, max_append_len: Optional[int] = 1000000000, bad_command_limit: Optional[int] = 5, @@ -138,8 +138,7 @@ def __init__(self, args: Namespace, *, self.disable_search_keys: Final = disable_search_keys or [] self.max_idle_wait: Final = max_idle_wait self._ssl_context = ssl_context or self._load_certs(extra) - self._starttls_enabled = starttls_enabled - self._reject_insecure_auth = reject_insecure_auth + self._tls_enabled = tls_enabled self._preauth_credentials = preauth_credentials self._max_append_len = max_append_len @@ -166,9 +165,8 @@ def from_args(cls: Type[ConfigT], args: Namespace) -> ConfigT: """ parsed_args = cls.parse_args(args) return cls(args, host=args.host, port=args.port, debug=args.debug, - reject_insecure_auth=not args.insecure_login, cert_file=args.cert, key_file=args.key, - **parsed_args) + tls_enabled=args.tls, **parsed_args) def apply_context(self) -> None: """Apply the configured settings to any :mod:`~pymap.context` @@ -211,14 +209,14 @@ def commands(self) -> Commands: @property def initial_auth(self) -> SASLAuth: - if self._reject_insecure_auth: + if self._tls_enabled: return SASLAuth([]) else: - return self.insecure_auth + return self.tls_auth @property - def insecure_auth(self) -> SASLAuth: - return SASLAuth.plaintext() + def tls_auth(self) -> SASLAuth: + return SASLAuth.defaults() @property def preauth_credentials(self) -> Optional[AuthenticationCredentials]: @@ -244,7 +242,7 @@ def login_capability(self) -> Sequence[bytes]: @property def initial_capability(self) -> Sequence[bytes]: ret = [b'LITERAL+'] - if self._starttls_enabled: + if self._tls_enabled: ret.append(b'STARTTLS') return ret diff --git a/pymap/imap/__init__.py b/pymap/imap/__init__.py index 1e595673..f6fb36b1 100644 --- a/pymap/imap/__init__.py +++ b/pymap/imap/__init__.py @@ -12,7 +12,7 @@ from base64 import b64encode, b64decode from contextlib import closing, AsyncExitStack from ssl import SSLContext -from typing import TypeVar, Iterable, Sequence, List, Optional, Awaitable +from typing import TypeVar, Optional, Iterable, Sequence, List, Awaitable from pymap.concurrent import Event from pymap.config import IMAPConfig @@ -31,7 +31,7 @@ from pymap.parsing.state import ParsingState, ParsingInterrupt, \ ExpectContinuation from pymap.sockets import InheritedSockets, SocketInfo -from pysasl import ServerChallenge, AuthenticationError, \ +from pysasl import ServerChallenge, ChallengeResponse, AuthenticationError, \ AuthenticationCredentials from .state import ConnectionState @@ -66,8 +66,10 @@ def add_arguments(cls, parser: ArgumentParser) -> None: group.add_argument('--systemd-sockets', action='store_const', dest='inherited_sockets', const='systemd', help='use systemd inherited sockets') - group.add_argument('--insecure-login', action='store_true', - help='allow plaintext login without TLS') + else: + parser.set_defaults(inherited_sockets=None) + group.add_argument('--no-tls', dest='tls', action='store_false', + help='disable TLS') @classmethod async def start(cls, backend: BackendInterface, @@ -202,22 +204,23 @@ async def authenticate(self, state: ConnectionState, mech_name: bytes) \ mech = state.auth.get_server(mech_name) if not mech: return None - responses: List[ServerChallenge] = [] + responses: List[ChallengeResponse] = [] while True: try: creds, final = mech.server_attempt(responses) except ServerChallenge as chal: - chal_bytes = b64encode(chal.get_challenge()) + chal_bytes = b64encode(chal.data) cont = ResponseContinuation(chal_bytes) await self.write_response(cont) resp_bytes = bytes(await self.read_continuation(0)) - try: - chal.set_response(b64decode(resp_bytes)) - except binascii.Error as exc: - raise AuthenticationError(exc) if resp_bytes.rstrip(b'\r\n') == b'*': raise AuthenticationError('Authentication canceled.') - responses.append(chal) + try: + resp_dec = b64decode(resp_bytes) + except binascii.Error as exc: + raise AuthenticationError() from exc + else: + responses.append(ChallengeResponse(chal.data, resp_dec)) else: if final is not None: cont = ResponseContinuation(b64encode(final)) diff --git a/pymap/imap/state.py b/pymap/imap/state.py index 4a2afd30..4c4df790 100644 --- a/pymap/imap/state.py +++ b/pymap/imap/state.py @@ -105,7 +105,7 @@ async def do_greeting(self) -> CommandResponse: if preauth_creds: self._session = await self._login(preauth_creds) elif socket_info.get().from_localhost: - self.auth = self.config.insecure_auth + self.auth = self.config.tls_auth resp_cls = ResponsePreAuth if preauth_creds else ResponseOk return resp_cls(b'*', self.config.greeting, self.capability) @@ -134,7 +134,7 @@ async def do_starttls(self, cmd: StartTLSCommand) -> _CommandRet: self._capability.remove(b'STARTTLS') except ValueError: raise NotSupportedError('STARTTLS not available.') - self.auth = self.config.insecure_auth + self.auth = self.config.tls_auth return ResponseOk(cmd.tag, b'Ready to handshake.'), None async def do_capability(self, cmd: CapabilityCommand) -> _CommandRet: diff --git a/pymap/sieve/manage/__init__.py b/pymap/sieve/manage/__init__.py index fca04b0f..2f92ab56 100644 --- a/pymap/sieve/manage/__init__.py +++ b/pymap/sieve/manage/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import binascii import logging import re from argparse import ArgumentParser @@ -21,7 +22,7 @@ from pymap.parsing.exceptions import NotParseable from pymap.parsing.primitives import String from pymap.sockets import SocketInfo -from pysasl import ServerChallenge, AuthenticationError, \ +from pysasl import ServerChallenge, ChallengeResponse, AuthenticationError, \ AuthenticationCredentials from .command import Command, NoOpCommand, LogoutCommand, CapabilityCommand, \ @@ -221,26 +222,29 @@ async def _do_authenticate(self, cmd: AuthenticateCommand) -> Response: mech = self.auth.get_server(cmd.mech_name) if not mech: return Response(Condition.NO, text='Invalid SASL mechanism.') - responses: List[ServerChallenge] = [] + responses: List[ChallengeResponse] = [] if cmd.initial_data is not None: - chal = ServerChallenge(b'') - chal.set_response(b64decode(cmd.initial_data)) - responses.append(chal) + resp_dec = b64decode(cmd.initial_data) + responses.append(ChallengeResponse(b'', resp_dec)) while True: try: creds, final = mech.server_attempt(responses) except ServerChallenge as chal: - chal_bytes = b64encode(chal.get_challenge()) + chal_bytes = b64encode(chal.data) chal_str = String.build(chal_bytes) chal_str.write(self.writer) self.writer.write(b'\r\n') await self.writer.drain() resp_bytes = await self._read_data() resp_str, _ = String.parse(resp_bytes, self.params) - chal.set_response(b64decode(resp_str.value)) if resp_str.value == b'*': raise AuthenticationError('Authentication cancelled.') - responses.append(chal) + try: + resp_dec = b64decode(resp_str.value) + except binascii.Error as exc: + raise AuthenticationError() from exc + else: + responses.append(ChallengeResponse(chal.data, resp_dec)) except AuthenticationError as exc: return Response(Condition.NO, text=str(exc)) else: @@ -284,7 +288,7 @@ async def _do_starttls(self) -> Response: protocol.connection_made(new_transport) self._print('%d <->| %s', b'') self._offer_starttls = False - self.auth = self.config.insecure_auth + self.auth = self.config.tls_auth return CapabilitiesResponse(self.capabilities) async def run(self) -> None: diff --git a/setup.cfg b/setup.cfg index b6bd7f5c..03945bda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pymap -version = 0.13.2 +version = 0.13.3 author = Ian Good author_email = icgood@gmail.com description = Lightweight, asynchronous IMAP serving in Python. @@ -23,7 +23,7 @@ python_requires = ~=3.7 include_package_data = True packages = find: install_requires = - pysasl + pysasl >= 0.5.0 typing-extensions [options.extras_require] diff --git a/test/server/base.py b/test/server/base.py index 5c4a1029..713a535b 100644 --- a/test/server/base.py +++ b/test/server/base.py @@ -15,7 +15,6 @@ class FakeArgs(Namespace): debug = True - insecure_login = True demo_data = 'pymap.backend.dict' demo_user = 'testuser' demo_password = 'testpass' diff --git a/test/server/test_managesieve.py b/test/server/test_managesieve.py index 6b0d7295..bbb7a99c 100644 --- a/test/server/test_managesieve.py +++ b/test/server/test_managesieve.py @@ -13,7 +13,6 @@ def _push_capabilities(self, transport): b'"IMPLEMENTATION" "pymap managesieve', (br'.*?', ), b'"\r\n' b'"SASL" "PLAIN LOGIN"\r\n' b'"SIEVE" "fileinto reject envelope body"\r\n' - b'"STARTTLS"\r\n' b'"UNAUTHENTICATE"\r\n' b'"VERSION" "1.0"\r\n' b'OK\r\n')