Skip to content

Commit

Permalink
Merge pull request #90 from icgood/cpu
Browse files Browse the repository at this point in the history
Use a thread pool for auth hashing
  • Loading branch information
icgood committed Aug 4, 2020
2 parents 3f28b12 + 0b27584 commit 93bf669
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 6 deletions.
2 changes: 1 addition & 1 deletion pymap/backend/redis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ async def _check_user(self, redis: Redis,
data = await self._get_user(redis, user)
if data is None:
raise InvalidAuth()
data.check_password(credentials)
await data.check_password(credentials)
if user != credentials.identity:
raise InvalidAuth(authorization=True)
return credentials.identity
Expand Down
13 changes: 13 additions & 0 deletions pymap/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from abc import abstractmethod, ABCMeta
from argparse import Namespace
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor
from ssl import SSLContext
from typing import Any, TypeVar, Type, Union, Optional, Iterable, Iterator, \
Sequence, Mapping, Dict
Expand Down Expand Up @@ -97,6 +98,8 @@ class IMAPConfig(metaclass=ABCMeta):
``key_file`` and an SSL context will be created.
starttls_enabled: True if opportunistic TLS should be supported.
hash_context: The hash to use for passwords.
cpu_subsystem: The subsystem to use for CPU-heavy operations,
defaulting to a small thread pool.
preauth_credentials: If given, clients will pre-authenticate on
connection using these credentials.
proxy_protocol: The PROXY protocol implementation to use.
Expand All @@ -123,6 +126,7 @@ def __init__(self, args: Namespace, *,
preauth_credentials: AuthenticationCredentials = None,
proxy_protocol: ProxyProtocol = None,
hash_context: HashInterface = None,
cpu_subsystem: Subsystem = None,
max_append_len: Optional[int] = 1000000000,
bad_command_limit: Optional[int] = 5,
disable_search_keys: Iterable[bytes] = None,
Expand All @@ -138,6 +142,8 @@ def __init__(self, args: Namespace, *,
self.disable_search_keys: Final = disable_search_keys or []
self.hash_context: Final = hash_context or \
get_hash(passlib_config=args.passlib_cfg)
self.cpu_subsystem: Final = cpu_subsystem or \
self._get_cpu_subsystem()
self._ssl_context = ssl_context or self._load_certs(extra)
self._tls_enabled = tls_enabled
self._preauth_credentials = preauth_credentials
Expand Down Expand Up @@ -188,6 +194,13 @@ def backend_capability(self) -> BackendCapability:
"""
...

@classmethod
def _get_cpu_subsystem(cls) -> Subsystem:
cpu_count = os.cpu_count() or 1
cpus_minus_one = max(1, cpu_count - 1)
executor = ThreadPoolExecutor(max_workers=cpus_minus_one)
return Subsystem.for_threading(executor)

@classmethod
def _load_certs(cls, extra: Mapping[str, Any]) -> SSLContext:
cert_file: Optional[str] = extra.get('cert_file')
Expand Down
15 changes: 11 additions & 4 deletions pymap/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Any, Optional, Mapping, Dict
from typing_extensions import Final

from pysasl import AuthenticationCredentials
from pysasl import AuthenticationCredentials, HashInterface

from .config import IMAPConfig
from .exceptions import InvalidAuth
Expand Down Expand Up @@ -61,7 +61,7 @@ def to_dict(self) -> Mapping[str, Any]:
data['password'] = self.password
return data

def check_password(self, creds: AuthenticationCredentials) -> None:
async def check_password(self, creds: AuthenticationCredentials) -> None:
"""Check the given credentials against the known password comparison
data. If the known data used a hash, then the equivalent hash of the
provided secret is compared.
Expand All @@ -73,8 +73,15 @@ def check_password(self, creds: AuthenticationCredentials) -> None:
:class:`~pymap.exceptions.InvalidAuth`
"""
config = self.config
if self.password is None:
raise InvalidAuth()
elif not creds.check_secret(self.password, hash=config.hash_context):
hash_context = self.config.hash_context
cpu_subsystem = self.config.cpu_subsystem
fut = self._check_secret(creds, self.password, hash_context)
if not await cpu_subsystem.execute(fut):
raise InvalidAuth()

async def _check_secret(self, creds: AuthenticationCredentials,
password: str,
hash_context: HashInterface) -> bool:
return creds.check_secret(password, hash=hash_context)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
license = f.read()

setup(name='pymap',
version='0.19.0',
version='0.20.0',
author='Ian Good',
author_email='icgood@gmail.com',
description='Lightweight, asynchronous IMAP serving in Python.',
Expand Down

0 comments on commit 93bf669

Please sign in to comment.