Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reflect changes in pysasl 0.5 #72

Merged
merged 1 commit into from
Oct 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions pymap/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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`
Expand Down Expand Up @@ -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]:
Expand All @@ -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

Expand Down
25 changes: 14 additions & 11 deletions pymap/imap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions pymap/imap/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
22 changes: 13 additions & 9 deletions pymap/sieve/manage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import asyncio
import binascii
import logging
import re
from argparse import ArgumentParser
Expand All @@ -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, \
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -284,7 +288,7 @@ async def _do_starttls(self) -> Response:
protocol.connection_made(new_transport)
self._print('%d <->| %s', b'<TLS handshake>')
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:
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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]
Expand Down
1 change: 0 additions & 1 deletion test/server/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

class FakeArgs(Namespace):
debug = True
insecure_login = True
demo_data = 'pymap.backend.dict'
demo_user = 'testuser'
demo_password = 'testpass'
Expand Down
1 change: 0 additions & 1 deletion test/server/test_managesieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down