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 .dotdict import DotDict, LowerDotDict, DefaultDotDict, MultiDotDict, JsonEncoder
from .misc import *
from .cache import LruCache, TtlCache
from .cache import CacheDecorator, LruCache, TtlCache
from .connection import Connection
from .http_urllib_client import HttpUrllibClient, HttpUrllibResponse

View file

@ -1,8 +1,9 @@
import re
import json, re
from datetime import datetime
from collections import OrderedDict
from functools import wraps
from functools import update_wrapper
from hashlib import sha1
from . import DotDict
@ -43,17 +44,24 @@ def parse_ttl(ttl):
class BaseCache(OrderedDict):
_get = OrderedDict.get
_items = OrderedDict.items
def __init__(self, maxsize=1024, ttl=None):
self.ttl = parse_ttl(ttl)
self.maxsize = maxsize
self.set = self.store
self.deco = lambda *args: CacheDecorator(self, *args)
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})'
def items(self):
return [[k, v.data] for k,v in self._items()]
def get(self, key):
while len(self) >= self.maxsize and self.maxsize > 0:
self.popitem(last=False)
@ -109,24 +117,29 @@ class BaseCache(OrderedDict):
self[key]['timestamp'] = timestamp + self.ttl
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
def decorator(function, key, arg=0):
@wraps(function)
def wrapper(*args, **kwargs):
cached = self.fetch(key)
## This doesn't work for some reason
def CacheDecorator(cache):
def decorator(func):
def wrapper(cls, *args, **kwargs):
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
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 wrapper
return decorator
class TtlCache(BaseCache):

View file

@ -95,7 +95,7 @@ def catch_kb_interrupt(function, *args, **kwargs):
'''
try:
function(*args, **kwargs)
return function(*args, **kwargs)
except KeyboardInterrupt:
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):
return super().hash(password)
return self.hasher.hash(password)
def verify(self, passhash: str, password: str):

View file

@ -3,18 +3,14 @@ from setuptools import setup, find_namespace_packages
requires = [
'argon2-cffi==20.1.0',
'pillow==8.2.0',
'pycryptodome==3.10.1',
'sanic==21.2.4',
'Sanic-Cors==1.0.0'
'argon2-cffi==20.1.0'
]
setup(
name="IzzyLib HTTP Server",
name="IzzyLib Password Hasher",
version='0.6.0',
packages=find_namespace_packages(include=['izzylib.http_server']),
packages=find_namespace_packages(include=['izzylib.hasher']),
python_requires='>=3.7.0',
install_requires=requires,
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,
parse_signature,
fetch_actor,
fetch_webfinger_account,
fetch_instance,
fetch_nodeinfo,
fetch_webfinger_account,
set_requests_client,
generate_rsa_key
)
@ -26,8 +27,9 @@ __all__ = [
'HttpRequestsResponse',
'SigningError',
'fetch_actor',
'fetch_webfinger_account',
'fetch_instance',
'fetch_nodeinfo',
'fetch_webfinger_account',
'generate_rsa_key',
'parse_signature',
'set_requests_client',

View file

@ -46,10 +46,6 @@ class HttpRequestsClient(object):
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('host', request.host)
request.add_header('date', datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'))
@ -63,7 +59,7 @@ class HttpRequestsClient(object):
'keyId': keyid,
'algorithm': 'rsa-sha256',
'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()]
@ -83,6 +79,12 @@ class HttpRequestsClient(object):
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):
resp = self.request(url, *args, **kwargs)
@ -130,18 +132,15 @@ class HttpRequestsClient(object):
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):
def __init__(self, client, url, data=None, headers={}, query={}, method='get'):
parsed = urlparse(url)
self.args = [url]
self.kwargs = {'params': query}
self.kwargs = DotDict({'params': query})
self.method = method.lower()
self.client = client
self.path = parsed.path
self.host = parsed.netloc
new_headers = client.headers.copy()
new_headers.update(headers)
@ -151,11 +150,34 @@ class HttpRequestsRequest(object):
if not parsed_headers.get('user-agent'):
parsed_headers['user-agent'] = client.agent
self.kwargs['headers'] = new_headers
self.kwargs['headers'] = DotDict(new_headers)
self.kwargs['data'] = data
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):
@ -191,7 +213,11 @@ class HttpRequestsResponse(object):
@cached_property
def json(self):
return DotDict(self.body)
try:
return DotDict(self.text)
except:
return DotDict(self.body)
@cached_property
@ -371,6 +397,25 @@ def fetch_actor(url):
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)
def fetch_webfinger_account(handle, domain):
if not Client:

View file

@ -220,7 +220,7 @@ class SqlSession(object):
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)
if row:
@ -232,11 +232,13 @@ class SqlSession(object):
if getattr(table, 'timestamp', None) and not kwargs.get('timestamp'):
kwargs['timestamp'] = datetime.now()
res = self.execute(table.insert().values(**kwargs))
#return self.fetch(table_name, **kwargs)
self.execute(table.insert().values(**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:
rowid = row.id
table = row._table_name
@ -247,6 +249,9 @@ class SqlSession(object):
tclass = self.table[table]
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):
if row: