diff --git a/IzzyLib/http_server.py b/IzzyLib/http_server.py index 5a467c4..51baeb9 100644 --- a/IzzyLib/http_server.py +++ b/IzzyLib/http_server.py @@ -4,6 +4,7 @@ import logging as pylog from jinja2.exceptions import TemplateNotFound from multidict import CIMultiDict from multiprocessing import cpu_count, current_process +from sanic.views import HTTPMethodView from urllib.parse import parse_qsl, urlparse from . import http, logging @@ -30,12 +31,11 @@ class HttpServer(sanic.Sanic): self.port = int(port) self.workers = int(kwargs.get('workers', cpu_count())) self.sig_handler = kwargs.get('sig_handler') - self.ctx = DotDict() super().__init__(name, request_class=kwargs.get('request_class', HttpRequest)) - #for log in ['sanic.root', 'sanic.access']: - #pylog.getLogger(log).setLevel(pylog.CRITICAL) + for log in ['sanic.root', 'sanic.access']: + pylog.getLogger(log).setLevel(pylog.ERROR) self.template = Template( kwargs.get('tpl_search', []), @@ -56,6 +56,11 @@ class HttpServer(sanic.Sanic): signal.signal(signal.SIGTERM, self.finish) + ## Sanic spits out a warning, so this is the workaround to stop it + def __setattr__(self, key, value): + object.__setattr__(self, key, value) + + def add_method_route(self, method, *routes): for route in routes: self.add_route(method.as_view(), route) @@ -88,8 +93,10 @@ class HttpServer(sanic.Sanic): if self.sig_handler: self.sig_handler() + print('stopping.....') self.stop() logging.info('Bye! :3') + sys.exit() class HttpRequest(sanic.request.Request): diff --git a/IzzyLib/misc.py b/IzzyLib/misc.py index 33200fc..1abcdb9 100644 --- a/IzzyLib/misc.py +++ b/IzzyLib/misc.py @@ -1,5 +1,5 @@ '''Miscellaneous functions''' -import hashlib, random, string, sys, os, json, socket, time +import hashlib, random, string, sys, os, json, statistics, socket, time, timeit from os import environ as env from datetime import datetime @@ -11,8 +11,9 @@ from shutil import copyfile, rmtree from . import logging try: - from passlib.hash import argon2 + import argon2 except ImportError: + logging.verbose('argon2-cffi not installed. PasswordHasher class disabled') argon2 = None @@ -191,6 +192,47 @@ def PrintMethods(object, include_underscore=False): print(line) +def TimeFunction(func, *args, passes=1, use_gc=True, **kwargs): + options = [ + lambda: func(*args, **kwargs) + ] + + if use_gc: + options.append('gc.enable()') + + timer = timeit.Timer(*options) + + if passes > 1: + return timer.repeat(passes, 1) + + return timer.timeit(1) + + +def TimeFunctionPPrint(func, *args, passes=5, use_gc=True, floatlen=3, **kwargs): + parse_time = lambda num: f'{round(num, floatlen)}s' + times = [] + + for idx in range(0, passes): + passtime = TimeFunction(func, *args, **kwargs, passes=1, use_gc=use_gc) + times.append(passtime) + print(f'Pass {idx+1}: {parse_time(passtime)}') + + average = statistics.fmean(times) + + print('-----------------') + print(f'Average: {parse_time(average)}') + print(f'Total: {parse_time(sum(times))}') + + + +def TimePassHasher(string='hecking heck', passes=3, iterations=[2,4,8,16,32,64,96]): + for iteration in iterations: + print('\nTesting hash iterations:', iteration) + hasher = PasswordHasher(iterations=iteration) + strhash = hasher.hash(string) + TimeFunctionPPrint(hasher.verify, strhash, string, passes=passes) + + class Connection(socket.socket): def __init__(self, address='127.0.0.1', port=8080, tcp=True): super().__init__(socket.AF_INET, socket.SOCK_STREAM if tcp else socket.SOCK_DGRAM) @@ -216,6 +258,9 @@ class Connection(socket.socket): class DotDict(dict): + dict_ignore_types = ['basecache', 'lrucache', 'ttlcache'] + + def __init__(self, value=None, **kwargs): '''Python dictionary, but variables can be set/get via attributes @@ -233,7 +278,7 @@ class DotDict(dict): if isinstance(value, (str, bytes)): self.from_json(value) - elif isinstance(value, dict) or isinstance(value, list): + elif value.__class__.__name__.lower() not in self.dict_ignore_types and isinstance(value, dict): self.update(value) elif value: @@ -252,7 +297,7 @@ class DotDict(dict): def __setitem__(self, k, v): - if isinstance(v, dict): + if v.__class__.__name__.lower() not in self.dict_ignore_types and isinstance(v, dict): v = DotDict(v) super().__setitem__(k, v) @@ -309,7 +354,7 @@ class LowerDotDict(DotDict): def __setattr__(self, key, value): - return super().__setattr(self, key.lower(), value) + return super().__setattr__(key.lower(), value) def update(self, data): @@ -359,6 +404,17 @@ class Path(object): return mode if type(mode) == oct else eval(f'0o{mode}') + def append(self, text, new=True): + path = str(self.__path) + text + + if new: + return Path(path) + + self.__path = Pathlib(path) + + return self + + def size(self): return self.__path.stat().st_size @@ -549,45 +605,70 @@ class JsonEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, obj) -class PasswordHash(object): - def __init__(self, salt=None, rounds=8, bsize=50, threads=os.cpu_count(), length=64): - if type(salt) == Path: - if salt.exists(): - with salt.open() as fd: - self.salt = fd.read() +class PasswordHasher(DotDict): + ## The defaults can usually be used, except for `iterations`. That should be tweaked on each machine + ## You can use the TimeFunctionPPrint command above to test this - else: - newsalt = RandomGen(40) - - with salt.open('w') as fd: - fd.write(newsalt) - - self.salt = newsalt - - else: - self.salt = salt or RandomGen(40) - - self.rounds = rounds - self.bsize = bsize * 1024 - self.threads = threads - self.length = length + aliases = { + 'iterations': 'time_cost', + 'memory': 'memory_cost', + 'threads': 'parallelism' + } - def hash(self, password): - return argon2.using( - salt = self.salt.encode('UTF-8'), - rounds = self.rounds, - memory_cost = self.bsize, - max_threads = self.threads, - digest_size = self.length - ).hash(password) + def __init__(self, **kwargs): + if not argon2: + raise ValueError('password hashing disabled') + + super().__init__({ + 'time_cost': 16, + 'memory_cost': 100 * 1024, + 'parallelism': os.cpu_count(), + 'encoding': 'utf-8', + 'type': argon2.Type.ID, + }) + + self.hasher = None + self.update(kwargs) + self.setup() - def verify(self, password, passhash): - return argon2.using( - salt = self.salt.encode('UTF-8'), - rounds = self.rounds, - memory_cost = self.bsize, - max_threads = self.threads, - digest_size = self.length - ).verify(password, passhash) + def get_config(self, key): + key = self.aliases.get(key, key) + + self[key] + return self.get(key) / 1024 if key == 'memory_cost' else self.get(key) + + + def set_config(self, key, value): + key = self.aliases.get(key, key) + self[key] = value * 1024 if key == 'memory_cost' else value + self.setup() + + + def setup(self): + self.hasher = argon2.PasswordHasher(**self) + + + def hash(self, password: str): + return self.hasher.hash(password) + + + def verify(self, passhash: str, password: str): + try: + return self.hasher.verify(passhash, password) + + except argon2.exceptions.VerifyMismatchError: + return False + + + def iteration_test(self, string='hecking heck', passes=3, iterations=[8,16,24,32,40,48,56,64]): + original_iter = self.get_config('iterations') + + for iteration in iterations: + self.set_config('iterations', iteration) + print('\nTesting hash iterations:', iteration) + + TimeFunctionPPrint(self.verify, self.hash(string), string, passes=passes) + + self.set_config('iterations', original_iter)