This commit is contained in:
Izalia Mae 2021-06-17 23:05:55 -04:00
parent 00a5b03995
commit b4615af139
11 changed files with 105 additions and 376 deletions

View file

@ -17,7 +17,7 @@ izzylog = logging.logger['IzzyLib']
from .path import Path from .path import Path
from .dotdict import DotDict, LowerDotDict, DefaultDotDict, MultiDotDict, JsonEncoder from .dotdict import DotDict, LowerDotDict, DefaultDotDict, MultiDotDict, JsonEncoder
from .misc import * from .misc import *
from .cache import LruCache, TtlCache from .cache import CacheDecorator, LruCache, TtlCache
from .connection import Connection from .connection import Connection
from .http_urllib_client import HttpUrllibClient, HttpUrllibResponse from .http_urllib_client import HttpUrllibClient, HttpUrllibResponse

View file

@ -1,8 +1,9 @@
import re import json, re
from datetime import datetime from datetime import datetime
from collections import OrderedDict from collections import OrderedDict
from functools import wraps from functools import update_wrapper
from hashlib import sha1
from . import DotDict from . import DotDict
@ -43,17 +44,24 @@ def parse_ttl(ttl):
class BaseCache(OrderedDict): class BaseCache(OrderedDict):
_get = OrderedDict.get _get = OrderedDict.get
_items = OrderedDict.items
def __init__(self, maxsize=1024, ttl=None): def __init__(self, maxsize=1024, ttl=None):
self.ttl = parse_ttl(ttl) self.ttl = parse_ttl(ttl)
self.maxsize = maxsize self.maxsize = maxsize
self.set = self.store self.set = self.store
self.deco = lambda *args: CacheDecorator(self, *args)
def __str__(self): def __str__(self):
data = ', '.join([f'{k}="{v["data"]}"' for k,v in self.items()]) data = ', '.join([f'{k}="{v}"' for k,v in self.items()])
return f'BaseCache({data})' return f'BaseCache({data})'
def items(self):
return [[k, v.data] for k,v in self._items()]
def get(self, key): def get(self, key):
while len(self) >= self.maxsize and self.maxsize > 0: while len(self) >= self.maxsize and self.maxsize > 0:
self.popitem(last=False) self.popitem(last=False)
@ -109,24 +117,29 @@ class BaseCache(OrderedDict):
self[key]['timestamp'] = timestamp + self.ttl self[key]['timestamp'] = timestamp + self.ttl
self.move_to_end(key) self.move_to_end(key)
return self[key].data return item.data
## Was gonna use this for db stuff, but I need to plan it out better ## This doesn't work for some reason
def decorator(function, key, arg=0): def CacheDecorator(cache):
@wraps(function) def decorator(func):
def wrapper(*args, **kwargs): def wrapper(cls, *args, **kwargs):
cached = self.fetch(key) key = sha1(json.dumps(args).encode() + json.dumps(kwargs).encode()).hexdigest()
cached = cache.fetch(key)
print(cached)
if cached: if cached != None:
print('Returning cached value:', cache)
return cached return cached
result = function(*args, **kwargs) result = func(cls, *args, **kwargs)
self.store(key, args[arg] if type(arg) == int else kwargs[arg]) cache.store(key, result)
return result return result
return wrapper return wrapper
return decorator
class TtlCache(BaseCache): class TtlCache(BaseCache):

View file

@ -95,7 +95,7 @@ def catch_kb_interrupt(function, *args, **kwargs):
''' '''
try: try:
function(*args, **kwargs) return function(*args, **kwargs)
except KeyboardInterrupt: except KeyboardInterrupt:
izzylog.verbose('Bye! UvU') izzylog.verbose('Bye! UvU')

View file

@ -0,0 +1 @@
from .hasher import PasswordHasher

View file

@ -54,7 +54,7 @@ class PasswordHasher:
def hash(self, password: str): def hash(self, password: str):
return super().hash(password) return self.hasher.hash(password)
def verify(self, passhash: str, password: str): def verify(self, passhash: str, password: str):

View file

@ -3,18 +3,14 @@ from setuptools import setup, find_namespace_packages
requires = [ requires = [
'argon2-cffi==20.1.0', 'argon2-cffi==20.1.0'
'pillow==8.2.0',
'pycryptodome==3.10.1',
'sanic==21.2.4',
'Sanic-Cors==1.0.0'
] ]
setup( setup(
name="IzzyLib HTTP Server", name="IzzyLib Password Hasher",
version='0.6.0', version='0.6.0',
packages=find_namespace_packages(include=['izzylib.http_server']), packages=find_namespace_packages(include=['izzylib.hasher']),
python_requires='>=3.7.0', python_requires='>=3.7.0',
install_requires=requires, install_requires=requires,
include_package_data=False, include_package_data=False,

View file

@ -1,2 +0,0 @@
from .hasher import PasswordHasher
from .server import HttpServer, HttpServerRequest, HttpServerResponse

View file

@ -1,331 +0,0 @@
# probably gonna remove this since I'll be using my asgi framework
import multiprocessing, sanic, signal, traceback
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, izzylog
from .misc import DotDict, DefaultDict, LowerDotDict
from .template import Template
log_path_ignore = [
'/media',
'/static'
]
log_ext_ignore = [
'js', 'ttf', 'woff2',
'ac3', 'aiff', 'flac', 'm4a', 'mp3', 'ogg', 'wav', 'wma',
'apng', 'ico', 'jpeg', 'jpg', 'png', 'svg',
'divx', 'mov', 'mp4', 'webm', 'wmv'
]
class HttpServer(sanic.Sanic):
def __init__(self, name='sanic', host='0.0.0.0', port='4080', **kwargs):
self.host = host
self.port = int(port)
self.workers = int(kwargs.get('workers', cpu_count()))
self.sig_handler = kwargs.get('sig_handler')
super().__init__(name, request_class=kwargs.get('request_class', HttpServerRequest))
for log in ['sanic.root', 'sanic.access']:
pylog.getLogger(log).setLevel(pylog.ERROR)
self.template = Template(
kwargs.get('tpl_search', []),
kwargs.get('tpl_globals', {}),
kwargs.get('tpl_context'),
kwargs.get('tpl_autoescape', True)
)
self.template.addEnv('app', self)
self.error_handler.add(TemplateNotFound, NoTemplateError)
self.error_handler.add(Exception, kwargs.get('error_handler', GenericError))
self.register_middleware(MiddlewareAccessLog, attach_to='response')
signal.signal(signal.SIGHUP, self.finish)
signal.signal(signal.SIGINT, self.finish)
signal.signal(signal.SIGQUIT, self.finish)
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)
def add_method_routes(self, routes: list):
for route in routes:
self.add_method_route(*route)
def start(self):
options = {
'host': self.host,
'port': self.port,
'workers': self.workers,
'access_log': False,
'debug': False
}
msg = f'Starting {self.name} at {self.host}:{self.port}'
if self.workers > 1:
msg += f' with {self.workers} workers'
izzylog.info(msg)
self.run(**options)
def finish(self):
if self.sig_handler:
self.sig_handler()
print('stopping.....')
self.stop()
izzylog.info('Bye! :3')
sys.exit()
class HttpServerRequest(sanic.request.Request):
def __init__(self, url_bytes, headers, version, method, transport, app):
super().__init__(url_bytes, headers, version, method, transport, app)
self.Headers = HttpHeaders(headers)
self.Data = HttpData(self)
self.template = self.app.template
self.__setup_defaults()
self.__parse_path()
#if self.paths.media:
#return
self.__parse_signature()
self.Run()
def Run(self):
pass
def response(self, tpl, *args, **kwargs):
return self.template.response(self, tpl, *args, **kwargs)
def alldata(self):
return self.__combine_dicts(self.content.json, self.data.query, self.data.form)
def verify(self, actor=None):
self.ap.valid = http.VerifyHeaders(self.headers, self.method, self.path, actor, self.body)
return self.ap.valid
def __combine_dicts(self, *dicts):
data = DotDict()
for item in dicts:
data.update(item)
return data
def __setup_defaults(self):
self.paths = DotDict({'media': False, 'json': False, 'ap': False, 'cookie': False})
self.ap = DotDict({'valid': False, 'signature': {}, 'actor': None, 'inbox': None, 'domain': None})
def __parse_path(self):
self.paths.media = any(map(self.path.startswith, log_path_ignore)) or any(map(self.path.startswith, log_ext_ignore))
self.paths.json = self.__json_check()
def __parse_signature(self):
sig = self.headers.getone('signature', None)
if sig:
self.ap.signature = http.ParseSig(sig)
if self.ap.signature:
self.ap.actor = self.ap.signature.get('keyid', '').split('#', 1)[0]
self.ap.domain = urlparse(self.ap.actor).netloc
def __json_check(self):
if self.path.endswith('.json'):
return True
accept = self.headers.getone('Accept', None)
if accept:
mimes = [v.strip() for v in accept.split(',')]
if any(mime in ['application/json', 'application/activity+json'] for mime in mimes):
return True
return False
class HttpServerResponse:
Text = sanic.response.text
Html = sanic.response.html
Json = sanic.response.json
Redir = sanic.response.redirect
def Css(*args, headers={}, **kwargs):
ReplaceHeader(headers, 'content-type', 'text/css')
return sanic.response.text(*args, headers=headers, **kwargs)
def Js(*args, headers={}, **kwargs):
ReplaceHeader(headers, 'content-type', 'application/javascript')
return sanic.response.text(*args, headers=headers, **kwargs)
def Ap(*args, headers={}, **kwargs):
ReplaceHeader(headers, 'content-type', 'application/activity+json')
return sanic.response.json(*args, headers=headers, **kwargs)
def Jrd(*args, headers={}, **kwargs):
ReplaceHeader(headers, 'content-type', 'application/jrd+json')
return sanic.response.json(*args, headers=headers, **kwargs)
class HttpHeaders(LowerDotDict):
def __init__(self, headers):
super().__init__()
for k,v in headers.items():
if not self.get(k):
self[k] = []
self[k].append(v)
def getone(self, key, default=None):
value = self.get(key)
if not value:
return default
return value[0]
def getall(self, key, default=[]):
return self.get(key.lower(), default)
class HttpData(object):
def __init__(self, request):
self.request = request
@property
def combined(self):
return DotDict(**self.form.asDict(), **self.query.asDict(), **self.json.asDict())
@property
def query(self):
data = {k: v for k,v in parse_qsl(self.request.query_string)}
return DotDict(data)
@property
def form(self):
data = {k: v[0] for k,v in self.request.form.items()}
return DotDict(data)
@property
def files(self):
return DotDict({k:v[0] for k,v in self.request.files.items()})
### body functions
@property
def raw(self):
try:
return self.request.body
except Exception as e:
izzylog.verbose('IzzyLib.http_server.Data.raw: failed to get body')
izzylog.debug(f'{e.__class__.__name__}: {e}')
return b''
@property
def text(self):
try:
return self.raw.decode()
except Exception as e:
izzylog.verbose('IzzyLib.http_server.Data.text: failed to get body')
izzylog.debug(f'{e.__class__.__name__}: {e}')
return ''
@property
def json(self):
try:
return DotDict(self.text)
except Exception as e:
izzylog.verbose('IzzyLib.http_server.Data.json: failed to get body')
izzylog.debug(f'{e.__class__.__name__}: {e}')
data = '{}'
return {}
async def MiddlewareAccessLog(request, response):
if request.paths.media:
return
uagent = request.headers.get('user-agent')
address = request.headers.get('x-real-ip', request.forwarded.get('for', request.remote_addr))
izzylog.info(f'({multiprocessing.current_process().name}) {address} {request.method} {request.path} {response.status} "{uagent}"')
def GenericError(request, exception):
try:
status = exception.status_code
except:
status = 500
if status not in range(200, 499):
traceback.print_exc()
msg = f'{exception.__class__.__name__}: {str(exception)}'
if request.paths.json:
return sanic.response.json({'error': {'status': status, 'message': msg}})
try:
return request.response('server_error.haml', status=status, context={'status': str(status), 'error': msg})
except TemplateNotFound:
return sanic.response.text(f'Error {status}: {msg}')
def NoTemplateError(request, exception):
izzylog.error('TEMPLATE_ERROR:', f'{exception.__class__.__name__}: {str(exception)}')
return sanic.response.html('I\'m a dumbass and forgot to create a template for this page', 500)
def ReplaceHeader(headers, key, value):
for k,v in headers.items():
if k.lower() == header.lower():
del headers[k]

View file

@ -7,8 +7,9 @@ from .client import (
verify_headers, verify_headers,
parse_signature, parse_signature,
fetch_actor, fetch_actor,
fetch_webfinger_account, fetch_instance,
fetch_nodeinfo, fetch_nodeinfo,
fetch_webfinger_account,
set_requests_client, set_requests_client,
generate_rsa_key generate_rsa_key
) )
@ -26,8 +27,9 @@ __all__ = [
'HttpRequestsResponse', 'HttpRequestsResponse',
'SigningError', 'SigningError',
'fetch_actor', 'fetch_actor',
'fetch_webfinger_account', 'fetch_instance',
'fetch_nodeinfo', 'fetch_nodeinfo',
'fetch_webfinger_account',
'generate_rsa_key', 'generate_rsa_key',
'parse_signature', 'parse_signature',
'set_requests_client', 'set_requests_client',

View file

@ -46,10 +46,6 @@ class HttpRequestsClient(object):
def __sign_request(self, request, privkey, keyid): def __sign_request(self, request, privkey, keyid):
if not crypto_enabled:
izzylog.error('Crypto functions disabled')
return
request.add_header('(request-target)', f'{request.method.lower()} {request.path}') request.add_header('(request-target)', f'{request.method.lower()} {request.path}')
request.add_header('host', request.host) request.add_header('host', request.host)
request.add_header('date', datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')) request.add_header('date', datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'))
@ -63,7 +59,7 @@ class HttpRequestsClient(object):
'keyId': keyid, 'keyId': keyid,
'algorithm': 'rsa-sha256', 'algorithm': 'rsa-sha256',
'headers': ' '.join([k.lower() for k in request.headers.keys()]), 'headers': ' '.join([k.lower() for k in request.headers.keys()]),
'signature': b64encode(PkcsHeaders(privkey, request.headers)).decode('UTF-8') 'signature': b64encode(sign_pkcs_headers(privkey, request.headers)).decode('UTF-8')
} }
sig_items = [f'{k}="{v}"' for k,v in sig.items()] sig_items = [f'{k}="{v}"' for k,v in sig.items()]
@ -83,6 +79,12 @@ class HttpRequestsClient(object):
return HttpRequestsResponse(request.send()) return HttpRequestsResponse(request.send())
def signed_request(self, privkey, keyid, *args, **kwargs):
request = HttpRequestsRequest(self, *args, **kwargs)
self.__sign_request(request, privkey, keyid)
return HttpRequestsResponse(request.send())
def download(self, url, filepath, *args, filename=None, **kwargs): def download(self, url, filepath, *args, filename=None, **kwargs):
resp = self.request(url, *args, **kwargs) resp = self.request(url, *args, **kwargs)
@ -130,18 +132,15 @@ class HttpRequestsClient(object):
return self.request(*args, headers=headers, **kwargs) return self.request(*args, headers=headers, **kwargs)
def signed_request(self, privkey, keyid, *args, **kwargs):
request = HttpRequestsRequest(self, *args, **kwargs)
self.__sign_request(request, privkey, keyid)
return HttpRequestsResponse(request.send())
class HttpRequestsRequest(object): class HttpRequestsRequest(object):
def __init__(self, client, url, data=None, headers={}, query={}, method='get'): def __init__(self, client, url, data=None, headers={}, query={}, method='get'):
parsed = urlparse(url)
self.args = [url] self.args = [url]
self.kwargs = {'params': query} self.kwargs = DotDict({'params': query})
self.method = method.lower() self.method = method.lower()
self.client = client self.client = client
self.path = parsed.path
self.host = parsed.netloc
new_headers = client.headers.copy() new_headers = client.headers.copy()
new_headers.update(headers) new_headers.update(headers)
@ -151,11 +150,34 @@ class HttpRequestsRequest(object):
if not parsed_headers.get('user-agent'): if not parsed_headers.get('user-agent'):
parsed_headers['user-agent'] = client.agent parsed_headers['user-agent'] = client.agent
self.kwargs['headers'] = new_headers self.kwargs['headers'] = DotDict(new_headers)
self.kwargs['data'] = data self.kwargs['data'] = data
if client.proxy.enabled: if client.proxy.enabled:
self.kwargs['proxies'] = {self.proxy.ptype: f'{self.proxy.ptype}://{self.proxy.host}:{self.proxy.port}'} self.kwargs['proxies'] = DotDict({self.proxy.ptype: f'{self.proxy.ptype}://{self.proxy.host}:{self.proxy.port}'})
@property
def body(self):
return self.kwargs.data
@body.setter
def body(self, data):
self.kwargs.data = data
@property
def headers(self):
return self.kwargs.headers
def add_header(self, key, value):
self.kwargs.headers[key] = value
def remove_header(self, key):
self.kwargs.headers.pop(key, None)
def send(self): def send(self):
@ -191,7 +213,11 @@ class HttpRequestsResponse(object):
@cached_property @cached_property
def json(self): def json(self):
return DotDict(self.body) try:
return DotDict(self.text)
except:
return DotDict(self.body)
@cached_property @cached_property
@ -371,6 +397,25 @@ def fetch_actor(url):
return actor return actor
@lru_cache(maxsize=512)
def fetch_instance(domain):
if not Client:
raise ValueError('Please set global client with "SetRequestsClient(client)"')
headers = {'Accept': 'application/json'}
resp = Client.request(f'https://{domain}/api/v1/instance', headers=headers)
try:
return resp.json
except json.decoder.JSONDecodeError:
return
except Exception as e:
izzylog.debug(f'HTTP {resp.status}: {resp.body}')
raise e from None
@lru_cache(maxsize=512) @lru_cache(maxsize=512)
def fetch_webfinger_account(handle, domain): def fetch_webfinger_account(handle, domain):
if not Client: if not Client:

View file

@ -220,7 +220,7 @@ class SqlSession(object):
return self.fetch(*args, single=False, **kwargs) return self.fetch(*args, single=False, **kwargs)
def insert(self, table_name, **kwargs): def insert(self, table_name, return_row=False, **kwargs):
row = self.fetch(table_name, **kwargs) row = self.fetch(table_name, **kwargs)
if row: if row:
@ -232,11 +232,13 @@ class SqlSession(object):
if getattr(table, 'timestamp', None) and not kwargs.get('timestamp'): if getattr(table, 'timestamp', None) and not kwargs.get('timestamp'):
kwargs['timestamp'] = datetime.now() kwargs['timestamp'] = datetime.now()
res = self.execute(table.insert().values(**kwargs)) self.execute(table.insert().values(**kwargs))
#return self.fetch(table_name, **kwargs)
if return_row:
return self.fetch(table_name, **kwargs)
def update(self, table=None, rowid=None, row=None, **data): def update(self, table=None, rowid=None, row=None, return_row=False, **data):
if row: if row:
rowid = row.id rowid = row.id
table = row._table_name table = row._table_name
@ -247,6 +249,9 @@ class SqlSession(object):
tclass = self.table[table] tclass = self.table[table]
self.execute(tclass.update().where(tclass.c.id == rowid).values(**data)) self.execute(tclass.update().where(tclass.c.id == rowid).values(**data))
if return_row:
return self.fetch(table, id=rowid)
def remove(self, table=None, rowid=None, row=None): def remove(self, table=None, rowid=None, row=None):
if row: if row: