mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 14:49:50 +00:00
init
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
STORAGE_BASE = Path('storage/dp')
|
||||
THUMB_SIZE = (21, 21)
|
||||
MSN_SIZE = (96, 96)
|
||||
1
|
||||
def compute_md5(data: bytes) -> str:
|
||||
return hashlib.md5(data).hexdigest()[:16]
|
||||
|
||||
def get_img_type(data: bytes) -> Optional[str]:
|
||||
if data[:6] in (b'GIF87a', b'GIF89a'):
|
||||
return 'gif'
|
||||
if data[:2] == b'\xff\xd8':
|
||||
return 'jpeg'
|
||||
if data[:8] == b'\x89PNG\r\n\x1a\n':
|
||||
return 'png'
|
||||
return None
|
||||
|
||||
def get_avatar_dir(uuid: str) -> Path:
|
||||
return STORAGE_BASE / uuid
|
||||
|
||||
def store_avatar_file(uuid: str, data: bytes, ext: str) -> str:
|
||||
from PIL import Image
|
||||
|
||||
md5 = compute_md5(data)
|
||||
avatar_dir = get_avatar_dir(uuid)
|
||||
avatar_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
full_path = avatar_dir / f'{md5}.{ext}'
|
||||
thumb_path = avatar_dir / f'{md5}_thumb.png'
|
||||
msn_path = avatar_dir / f'{md5}_msn.png'
|
||||
|
||||
if not full_path.exists():
|
||||
full_path.write_bytes(data)
|
||||
|
||||
if not thumb_path.exists() or not msn_path.exists():
|
||||
try:
|
||||
img = Image.open(BytesIO(data))
|
||||
if not thumb_path.exists():
|
||||
img.resize(THUMB_SIZE).save(str(thumb_path))
|
||||
if not msn_path.exists():
|
||||
img.resize(MSN_SIZE).save(str(msn_path))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return md5
|
||||
|
||||
|
||||
def find_avatar_file(
|
||||
uuid: str,
|
||||
md5: str,
|
||||
*,
|
||||
small: bool = False,
|
||||
msn: bool = False,
|
||||
) -> Optional[Path]:
|
||||
avatar_dir = get_avatar_dir(uuid)
|
||||
if not avatar_dir.is_dir():
|
||||
return None
|
||||
|
||||
if small:
|
||||
thumb = avatar_dir / f'{md5}_thumb.png'
|
||||
if thumb.is_file():
|
||||
return thumb
|
||||
|
||||
elif msn:
|
||||
msn_file = avatar_dir / f'{md5}_msn.png'
|
||||
if msn_file.is_file():
|
||||
return msn_file
|
||||
|
||||
for p in avatar_dir.iterdir():
|
||||
if p.is_file() and p.stem == md5:
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_avatar_data(
|
||||
uuid: str,
|
||||
md5: Optional[str],
|
||||
*,
|
||||
small: bool = False,
|
||||
msn: bool = False,
|
||||
) -> Optional[Tuple[bytes, str]]:
|
||||
if not md5:
|
||||
return None
|
||||
|
||||
path = find_avatar_file(uuid, md5, small=small, msn=msn)
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = path.read_bytes()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
ext = path.suffix.lstrip('.').lower() or 'png'
|
||||
mime_map = {'jpg': 'jpeg', 'jpeg': 'jpeg', 'png': 'png', 'gif': 'gif'}
|
||||
content_type = 'image/' + mime_map.get(ext, ext)
|
||||
return data, content_type
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import hashlib, secrets, base64, binascii
|
||||
from typing import Dict, Optional, Tuple, Any, List, Type
|
||||
|
||||
from argon2 import PasswordHasher as Argon2Impl
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
|
||||
HASHERS: Dict[str, Type['Hasher']] = {}
|
||||
|
||||
class Hasher:
|
||||
algorithm = 'unknown'
|
||||
separator = '$'
|
||||
|
||||
@classmethod
|
||||
def encode(cls, password: str, *stuff: Any, salt: Optional[str] = None, identifier: Optional[str] = None) -> str:
|
||||
assert password is not None
|
||||
if salt is None:
|
||||
salt = gen_salt()
|
||||
assert cls.separator not in salt
|
||||
|
||||
if identifier is None:
|
||||
(hash_bytes, other_stuff) = cls._encode_impl(password, *stuff, salt=salt)
|
||||
else:
|
||||
(hash_bytes, other_stuff) = cls._encode_impl(password, *stuff, salt=salt, identifier=identifier)
|
||||
|
||||
hash = base64.b64encode(hash_bytes).decode('ascii').strip()
|
||||
return cls.separator.join([cls.algorithm] + other_stuff + [salt, hash])
|
||||
|
||||
@classmethod
|
||||
def _encode_impl(cls, password: str, *stuff: Any, salt: str, identifier: Optional[str] = None) -> Tuple[bytes, List[str]]:
|
||||
raise NotImplementedError('Hasher._encode_impl')
|
||||
|
||||
@classmethod
|
||||
def extract_salt(cls, encoded: str) -> Optional[str]:
|
||||
return encoded.split(cls.separator)[-2]
|
||||
|
||||
@classmethod
|
||||
def extract_hash(cls, encoded: str) -> bytes:
|
||||
hash = encoded.split(cls.separator)[-1]
|
||||
return base64.b64decode(hash)
|
||||
|
||||
@classmethod
|
||||
def verify(cls, password: str, encoded: str) -> bool:
|
||||
try:
|
||||
(algorithm, *stuff, salt, _) = encoded.split(cls.separator)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
try:
|
||||
hasher = HASHERS[algorithm]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
assert algorithm == hasher.algorithm
|
||||
encoded_2 = hasher.encode(password, *stuff, salt=salt)
|
||||
return secrets.compare_digest(encoded, encoded_2)
|
||||
|
||||
class Argon2PasswordHasher(Hasher):
|
||||
algorithm = 'argon2id'
|
||||
impl = Argon2Impl()
|
||||
|
||||
@classmethod
|
||||
def _encode_impl(cls, password: str, *stuff: Any, salt: str) -> Tuple[bytes, List[str]]:
|
||||
assert not stuff
|
||||
hash_str = cls.impl.hash(password + salt)
|
||||
hash_bytes = hash_str.encode('utf-8')
|
||||
return hash_bytes, []
|
||||
|
||||
@classmethod
|
||||
def verify(cls, password: str, encoded: str) -> bool | str:
|
||||
try:
|
||||
parts = encoded.split(cls.separator)
|
||||
if len(parts) != 3 or parts[0] != cls.algorithm:
|
||||
raise ValueError("Not an argon2 hash")
|
||||
_, salt, b64hash = parts
|
||||
hash_str = base64.b64decode(b64hash).decode('utf-8')
|
||||
cls.impl.verify(hash_str, password + salt)
|
||||
return True
|
||||
except (ValueError, VerifyMismatchError, binascii.Error):
|
||||
# PBKDF2 fallback, migrate hash to Argon2
|
||||
try:
|
||||
parts = encoded.split(cls.separator)
|
||||
alg = parts[0]
|
||||
if alg != PBKDF2PasswordHasher.algorithm:
|
||||
return False
|
||||
_, *stuff, salt, _ = parts
|
||||
if PBKDF2PasswordHasher.verify(password, encoded):
|
||||
return 'MIGRATEMEPLSTHX'
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
class PBKDF2PasswordHasher(Hasher):
|
||||
algorithm = 'pbkdf2_sha256'
|
||||
iterations = 24000
|
||||
|
||||
@classmethod
|
||||
def _encode_impl(cls, password: str, *stuff: Any, salt: str) -> Tuple[bytes, List[str]]:
|
||||
assert len(stuff) <= 1
|
||||
iterations: Optional[int] = (stuff[0] if stuff else None)
|
||||
if iterations is None:
|
||||
iterations = cls.iterations
|
||||
iterations = int(iterations)
|
||||
hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), iterations, None)
|
||||
return hash, [str(iterations)]
|
||||
|
||||
class MD5PasswordHasher(Hasher):
|
||||
algorithm = 'md5'
|
||||
digest = hashlib.md5
|
||||
|
||||
@classmethod
|
||||
def _encode_impl(cls, password: str, *stuff: Any, salt: str, identifier: Optional[str] = None) -> Tuple[bytes, List[str]]:
|
||||
assert not stuff
|
||||
assert salt is not None
|
||||
md5 = hashlib.md5()
|
||||
|
||||
if identifier is None:
|
||||
md5.update((salt + password).encode('utf-8'))
|
||||
else:
|
||||
md5.update((salt + password + identifier).encode('utf-8'))
|
||||
|
||||
return md5.digest(), []
|
||||
|
||||
# maybe we could find a better way to do this? it works for now though
|
||||
@classmethod
|
||||
def encode_aim5(cls, password: str, *, salt: str) -> str:
|
||||
md5_pw_raw = hashlib.md5(password.encode('utf-8')).digest()
|
||||
md5 = hashlib.md5()
|
||||
md5.update(salt.encode('utf-8'))
|
||||
md5.update(md5_pw_raw)
|
||||
md5.update(b'AOL Instant Messenger (SM)')
|
||||
hash_b64 = base64.b64encode(md5.digest()).decode('ascii').strip()
|
||||
return cls.separator.join([cls.algorithm, salt, hash_b64])
|
||||
|
||||
@classmethod
|
||||
def verify_hash(cls, hash_1: str, encoded: str) -> bool:
|
||||
try:
|
||||
(_, _, hash) = encoded.split(cls.separator)
|
||||
except ValueError:
|
||||
return False
|
||||
hash = binascii.hexlify(base64.b64decode(hash)).decode('ascii')
|
||||
return secrets.compare_digest(hash_1, hash)
|
||||
|
||||
class MD5CryptPasswordHasher(Hasher):
|
||||
algorithm = 'md5crypt'
|
||||
|
||||
@classmethod
|
||||
def _encode_impl(cls, password: str, *stuff: Any, salt: str) -> Tuple[bytes, List[str]]:
|
||||
from util.unixmd5crypt import unix_md5_crypt
|
||||
assert not stuff
|
||||
return unix_md5_crypt(password, salt), []
|
||||
|
||||
@classmethod
|
||||
def encode(cls, password: str, *stuff: Any, salt: Optional[str] = None) -> str:
|
||||
assert not stuff
|
||||
assert salt is not None
|
||||
if salt[:3] == '$1$':
|
||||
salt = salt[3:]
|
||||
salt = salt[:8]
|
||||
return super().encode(password, salt=salt)
|
||||
|
||||
class SHA1PasswordHasher(Hasher):
|
||||
algorithm = 'sha1'
|
||||
encoding = 'utf-16le'
|
||||
|
||||
@classmethod
|
||||
def encode(cls, password: str, *stuff: Any) -> str:
|
||||
assert password is not None
|
||||
|
||||
(hash_bytes, other_stuff) = cls._encode_impl(password, *stuff)
|
||||
|
||||
hash = (base64.b64encode(hash_bytes)
|
||||
.decode('ascii')
|
||||
.strip())
|
||||
|
||||
return cls.separator.join([cls.algorithm] + other_stuff + [hash])
|
||||
|
||||
@classmethod
|
||||
def _encode_impl(cls, password: str, *stuff: Any) -> Tuple[bytes, List[str]]:
|
||||
assert not stuff
|
||||
|
||||
sha1 = hashlib.sha1()
|
||||
sha1.update(password.encode(cls.encoding))
|
||||
|
||||
return sha1.digest(), []
|
||||
|
||||
@classmethod
|
||||
def extract_salt(cls, encoded: str) -> Optional[str]:
|
||||
raise NotImplementedError('SHA1PasswordHasher.extract_salt')
|
||||
|
||||
@classmethod
|
||||
def verify(cls, password: str, encoded: str) -> bool:
|
||||
try:
|
||||
(algorithm, *stuff, _) = encoded.split(cls.separator)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
try:
|
||||
hasher = HASHERS[algorithm]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
assert algorithm == hasher.algorithm
|
||||
encoded_2 = hasher.encode(password, *stuff)
|
||||
return secrets.compare_digest(encoded, encoded_2)
|
||||
|
||||
HASHERS[Argon2PasswordHasher.algorithm] = Argon2PasswordHasher
|
||||
HASHERS[PBKDF2PasswordHasher.algorithm] = PBKDF2PasswordHasher
|
||||
HASHERS[MD5PasswordHasher.algorithm] = MD5PasswordHasher
|
||||
HASHERS[MD5CryptPasswordHasher.algorithm] = MD5CryptPasswordHasher
|
||||
HASHERS[SHA1PasswordHasher.algorithm] = SHA1PasswordHasher
|
||||
|
||||
def gen_salt(length: int = 15) -> str:
|
||||
return secrets.token_hex(length)[:length]
|
||||
|
||||
hasher = Argon2PasswordHasher
|
||||
hasher_pbkdf2 = PBKDF2PasswordHasher
|
||||
hasher_md5 = MD5PasswordHasher
|
||||
hasher_md5crypt = MD5CryptPasswordHasher
|
||||
hasher_sha1 = SHA1PasswordHasher
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import Any
|
||||
import json
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
class JSONType(types.TypeDecorator): # type: ignore
|
||||
impl = types.TEXT
|
||||
|
||||
def load_dialect_impl(self, dialect: Any) -> Any:
|
||||
if dialect.name == 'postgresql':
|
||||
t = postgresql.JSON()
|
||||
else:
|
||||
t = types.TEXT()
|
||||
return dialect.type_descriptor(t)
|
||||
|
||||
def process_bind_param(self, value: Any, dialect: Any) -> Any:
|
||||
if value is None or dialect.name == 'postgresql':
|
||||
return value
|
||||
return json.dumps(value)
|
||||
|
||||
def process_result_value(self, value: Any, dialect: Any) -> Any:
|
||||
if value is None or dialect.name == 'postgresql':
|
||||
return value
|
||||
return json.loads(value)
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
from typing import FrozenSet, Any, Iterable, Optional, TypeVar, List, Dict, Tuple, Generic, TYPE_CHECKING
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import asyncio, functools, itertools, traceback, ssl, jinja2, settings, sys, platform
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from aiohttp import web
|
||||
from pathlib import Path
|
||||
|
||||
EMPTY_SET: FrozenSet[Any] = frozenset()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
VoidTaskType = asyncio.Task[None]
|
||||
else:
|
||||
VoidTaskType = Any
|
||||
|
||||
def gen_uuid() -> str:
|
||||
return str(uuid4())
|
||||
|
||||
T = TypeVar('T')
|
||||
def first_in_iterable(iterable: Iterable[T]) -> Optional[T]:
|
||||
for x in iterable: return x
|
||||
return None
|
||||
|
||||
def last_in_iterable(iterable: Iterable[T]) -> Optional[T]:
|
||||
last = None
|
||||
|
||||
for x in iterable:
|
||||
last = x
|
||||
return last
|
||||
|
||||
def generate_random_string(chars: int) -> bytes:
|
||||
import random, string
|
||||
|
||||
result = ''.join(random.choice(string.ascii_letters) for i in range(chars))
|
||||
return result.encode()
|
||||
|
||||
class Runner(metaclass = ABCMeta):
|
||||
__slots__ = ('host', 'port', 'ssl_context', 'ssl_only', 'service')
|
||||
|
||||
host: str
|
||||
port: int
|
||||
ssl_context: Optional[ssl.SSLContext]
|
||||
ssl_only: bool
|
||||
service: str
|
||||
|
||||
def __init__(self, host: str, port: int, *, ssl_context: Optional[ssl.SSLContext] = None, ssl_only: bool = False, service: str) -> None:
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ssl_context = ssl_context
|
||||
self.ssl_only = ssl_only
|
||||
self.service = service
|
||||
|
||||
@abstractmethod
|
||||
def create_servers(self, loop: asyncio.AbstractEventLoop) -> List[Any]: pass
|
||||
|
||||
def teardown(self, loop: asyncio.AbstractEventLoop) -> Any:
|
||||
pass
|
||||
|
||||
class ProtocolRunner(Runner):
|
||||
__slots__ = ('_protocol')
|
||||
|
||||
_protocol: Any
|
||||
|
||||
def __init__(
|
||||
self, host: str, port: int, protocol: Any, *, args: Optional[List[Any]] = None,
|
||||
ssl_context: Optional[ssl.SSLContext] = None, ssl_only: bool = False, service: str
|
||||
) -> None:
|
||||
super().__init__(host, port, ssl_context = ssl_context, ssl_only = ssl_only, service = service)
|
||||
if args:
|
||||
protocol = functools.partial(protocol, *args)
|
||||
self._protocol = protocol
|
||||
|
||||
def create_servers(self, loop: asyncio.AbstractEventLoop) -> List[Any]:
|
||||
return [loop.create_server(self._protocol, self.host, self.port, ssl = self.ssl_context)]
|
||||
|
||||
class AIOHTTPRunner(Runner):
|
||||
__slots__ = ('app', '_handler')
|
||||
|
||||
app: Any
|
||||
_handler: Optional[Any]
|
||||
|
||||
def __init__(self, host: str, port: int, app: Any, *, ssl_context: Optional[ssl.SSLContext] = None, ssl_only: bool = False, service: str) -> None:
|
||||
super().__init__(host, port, ssl_context = ssl_context, ssl_only = ssl_only, service = service)
|
||||
self.app = app
|
||||
self._handler = None
|
||||
|
||||
def create_servers(self, loop: asyncio.AbstractEventLoop) -> List[Any]:
|
||||
assert self._handler is None
|
||||
self._handler = self.app.make_handler(loop = loop)
|
||||
loop.run_until_complete(self.app.startup())
|
||||
|
||||
ret = []
|
||||
if not self.ssl_only:
|
||||
ret.append(loop.create_server(self._handler, self.host, self.port, ssl = None))
|
||||
if self.ssl_context is not None:
|
||||
ret.append(loop.create_server(self._handler, self.host, (self.port if self.ssl_only else 443), ssl = self.ssl_context))
|
||||
return ret
|
||||
|
||||
def teardown(self, loop: asyncio.AbstractEventLoop) -> None:
|
||||
handler = self._handler
|
||||
assert handler is not None
|
||||
self._handler = None
|
||||
loop.run_until_complete(self.app.shutdown())
|
||||
loop.run_until_complete(handler.shutdown(60))
|
||||
loop.run_until_complete(self.app.cleanup())
|
||||
|
||||
class Logger:
|
||||
__slots__ = ('prefix', '_log')
|
||||
|
||||
prefix: str
|
||||
_log: bool
|
||||
|
||||
def __init__(self, prefix: str, obj: object) -> None:
|
||||
import settings
|
||||
self.prefix = '[{}] ({:06x})'.format(prefix, hash(obj) % 0xFFFFFF)
|
||||
|
||||
def debug(self, *args: Any) -> None:
|
||||
if settings.DEBUG:
|
||||
print(self.prefix, '<Debug>', *args)
|
||||
|
||||
def info(self, *args: Any) -> None:
|
||||
print(self.prefix, '<Info>', *args)
|
||||
|
||||
def error(self, exc: Exception) -> None:
|
||||
trace = traceback.print_exception(type(exc), exc, exc.__traceback__)
|
||||
print (self.prefix, '<Error>', trace)
|
||||
|
||||
def log_connect(self) -> None:
|
||||
self.debug("Connected!")
|
||||
|
||||
def log_disconnect(self) -> None:
|
||||
self.debug("Disconnected!")
|
||||
|
||||
def run_loop(loop: asyncio.AbstractEventLoop, runners: List[Runner]) -> None:
|
||||
|
||||
for runner in runners:
|
||||
print("[{}] Started service on {}:{}".format(runner.service, runner.host, runner.port))
|
||||
|
||||
foos = itertools.chain(*(
|
||||
runner.create_servers(loop) for runner in runners
|
||||
))
|
||||
servers = loop.run_until_complete(asyncio.gather(*foos))
|
||||
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
finally:
|
||||
for server in servers:
|
||||
server.close()
|
||||
loop.run_until_complete(asyncio.gather(*(
|
||||
server.wait_closed() for server in servers
|
||||
)))
|
||||
for runner in runners:
|
||||
runner.teardown(loop)
|
||||
server_temp_cleanup()
|
||||
loop.close()
|
||||
|
||||
def add_to_jinja_env(app: web.Application, prefix: str, tmpl_dir: str, *, globals: Optional[Dict[str, Any]] = None) -> None:
|
||||
jinja_env = app['jinja_env']
|
||||
jinja_env.loader.mapping[prefix] = jinja2.FileSystemLoader(tmpl_dir)
|
||||
if globals:
|
||||
jinja_env.globals.update(globals)
|
||||
|
||||
def arbitrary_decode(d: bytes) -> str:
|
||||
if not d: return ''
|
||||
|
||||
return ''.join(map(chr, [b for b in d]))
|
||||
|
||||
def arbitrary_encode(s: str) -> bytes:
|
||||
return bytes([ord(c) for c in s])
|
||||
|
||||
def date_format(d: Optional[datetime]) -> Optional[str]:
|
||||
if d is None:
|
||||
return None
|
||||
d_iso = '{}{}'.format(
|
||||
d.isoformat()[0:19], 'Z',
|
||||
)
|
||||
return d_iso
|
||||
|
||||
def server_temp_cleanup() -> None:
|
||||
# For now, just clean up stuff in the Yahoo! HTTP file transfer storage folder
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
path = Path('storage/file')
|
||||
if not path.exists():
|
||||
return
|
||||
for file_dir in path.iterdir():
|
||||
shutil.rmtree(str(file_dir), ignore_errors = True)
|
||||
|
||||
|
||||
def _get_avatar_path(uuid: str) -> Path:
|
||||
return Path('storage/dp') / uuid[0:1] / uuid[0:2]
|
||||
|
||||
K = TypeVar('K')
|
||||
V = TypeVar('V')
|
||||
class DefaultDict(Dict[K, V]):
|
||||
_default: V
|
||||
|
||||
def __init__(self, default: V, mapping: Dict[K, V]) -> None:
|
||||
super().__init__(mapping)
|
||||
self._default = default
|
||||
|
||||
def __getitem__(self, key: K) -> V:
|
||||
v = super().__getitem__(key)
|
||||
if v is None:
|
||||
v = self._default
|
||||
return v
|
||||
|
||||
class MultiDict(Generic[K, V]):
|
||||
_impl: List[Tuple[K, V]]
|
||||
|
||||
def __init__(self, data: Optional[Iterable[Tuple[K, V]]] = None) -> None:
|
||||
super().__init__()
|
||||
self._impl = ([] if data is None else list(data))
|
||||
|
||||
def __contains__(self, key: K) -> bool:
|
||||
for d in self._impl:
|
||||
if d[0] == key: return True
|
||||
return False
|
||||
|
||||
def add(self, key: K, value: V) -> None:
|
||||
self._impl.append((key, value))
|
||||
|
||||
def get(self, key: K) -> Optional[V]:
|
||||
for d in self._impl:
|
||||
if d[0] == key: return d[1]
|
||||
return None
|
||||
|
||||
def getall(self, key: K) -> Optional[Iterable[V]]:
|
||||
values = [] # type: List[V]
|
||||
for d in self._impl:
|
||||
if d[0] == key:
|
||||
values.append(d[1])
|
||||
return values if values else None
|
||||
|
||||
def items(self) -> Iterable[Tuple[K, V]]:
|
||||
return self._impl
|
||||
@@ -0,0 +1,159 @@
|
||||
# NOTICE:
|
||||
#
|
||||
# This script has been modified to work on Python 3 (e.g. hashlib, workarounds
|
||||
# for ord() and byte strings). All credit is given where credit is due. :)
|
||||
|
||||
#########################################################
|
||||
# md5crypt.py
|
||||
#
|
||||
# 0423.2000 by michal wallace http://www.sabren.com/
|
||||
# based on perl's Crypt::PasswdMD5 by Luis Munoz (lem@cantv.net)
|
||||
# based on /usr/src/libcrypt/crypt.c from FreeBSD 2.2.5-RELEASE
|
||||
#
|
||||
# MANY THANKS TO
|
||||
#
|
||||
# Carey Evans - http://home.clear.net.nz/pages/c.evans/
|
||||
# Dennis Marti - http://users.starpower.net/marti1/
|
||||
#
|
||||
# For the patches that got this thing working!
|
||||
#
|
||||
#########################################################
|
||||
"""md5crypt.py - Provides interoperable MD5-based crypt() function
|
||||
|
||||
SYNOPSIS
|
||||
|
||||
import md5crypt.py
|
||||
|
||||
cryptedpassword = md5crypt.md5crypt(password, salt);
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
unix_md5_crypt() provides a crypt()-compatible interface to the
|
||||
rather new MD5-based crypt() function found in modern operating systems.
|
||||
It's based on the implementation found on FreeBSD 2.2.[56]-RELEASE and
|
||||
contains the following license in it:
|
||||
|
||||
"THE BEER-WARE LICENSE" (Revision 42):
|
||||
<phk@login.dknet.dk> wrote this file. As long as you retain this notice you
|
||||
can do whatever you want with this stuff. If we meet some day, and you think
|
||||
this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
|
||||
|
||||
apache_md5_crypt() provides a function compatible with Apache's
|
||||
.htpasswd files. This was contributed by Bryan Hart <bryan@eai.com>.
|
||||
|
||||
"""
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
MAGIC = '$1$' # Magic string
|
||||
ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
def unix_md5_crypt(pw: str, salt: str) -> bytes:
|
||||
# Take care of the magic string if present
|
||||
if salt[:len(MAGIC)] == MAGIC:
|
||||
salt = salt[len(MAGIC):]
|
||||
|
||||
# salt can have up to 8 characters:
|
||||
salt = salt.split('$', 1)[0]
|
||||
salt = salt[:8]
|
||||
|
||||
ctx = md5()
|
||||
ctx.update(pw.encode())
|
||||
ctx.update(MAGIC.encode())
|
||||
ctx.update(salt.encode())
|
||||
|
||||
tmp = md5()
|
||||
tmp.update(pw.encode())
|
||||
tmp.update(salt.encode())
|
||||
tmp.update(pw.encode())
|
||||
final = tmp.digest()
|
||||
|
||||
for pl in range(len(pw),0,-16):
|
||||
if pl > 16:
|
||||
ctx.update(final[:16])
|
||||
else:
|
||||
ctx.update(final[:pl])
|
||||
|
||||
# Now the 'weird' xform (??)
|
||||
|
||||
i = len(pw)
|
||||
while i:
|
||||
if i & 1:
|
||||
#if ($i & 1) { $ctx->add(pack("C", 0)); }
|
||||
ctx.update(b'\x00')
|
||||
else:
|
||||
ctx.update(pw[0].encode())
|
||||
i = i >> 1
|
||||
|
||||
final = ctx.digest()
|
||||
|
||||
# The following is supposed to make
|
||||
# things run slower.
|
||||
|
||||
# my question: WTF???
|
||||
|
||||
for i in range(1000):
|
||||
ctx1 = md5()
|
||||
if i & 1:
|
||||
ctx1.update(pw.encode())
|
||||
else:
|
||||
ctx1.update(final[:16])
|
||||
|
||||
if i % 3:
|
||||
ctx1.update(salt.encode())
|
||||
|
||||
if i % 7:
|
||||
ctx1.update(pw.encode())
|
||||
|
||||
if i & 1:
|
||||
ctx1.update(final[:16])
|
||||
else:
|
||||
ctx1.update(pw.encode())
|
||||
|
||||
final = ctx1.digest()
|
||||
|
||||
# Final xform
|
||||
|
||||
passwd = ''
|
||||
|
||||
passwd = passwd + to64(
|
||||
(final[0] << 16)
|
||||
| (final[6] << 8)
|
||||
| (final[12]), 4
|
||||
)
|
||||
|
||||
passwd = passwd + to64(
|
||||
(final[1] << 16)
|
||||
| (final[7] << 8)
|
||||
| (final[13]), 4
|
||||
)
|
||||
|
||||
passwd = passwd + to64(
|
||||
(final[2] << 16)
|
||||
| (final[8] << 8)
|
||||
| (final[14]), 4
|
||||
)
|
||||
|
||||
passwd = passwd + to64(
|
||||
(final[3] << 16)
|
||||
| (final[9] << 8)
|
||||
| (final[15]), 4
|
||||
)
|
||||
|
||||
passwd = passwd + to64(
|
||||
(final[4] << 16)
|
||||
| (final[10] << 8)
|
||||
| (final[5]), 4
|
||||
)
|
||||
|
||||
passwd = passwd + to64(final[11], 2)
|
||||
|
||||
return (MAGIC + salt + '$' + passwd).encode('utf-8')
|
||||
|
||||
def to64(v: int, n: int) -> str:
|
||||
ret = ''
|
||||
while (n - 1 >= 0):
|
||||
n = n - 1
|
||||
ret = ret + ITOA64[v & 0x3f]
|
||||
v = v >> 6
|
||||
return ret
|
||||
Reference in New Issue
Block a user