From 46489746c5e3aaf7207317a68ae9966d9668c408 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Wed, 15 Jan 2020 08:56:27 -0500 Subject: [PATCH] basic oauth support --- README.md | 2 +- paws/config.py | 2 +- paws/database.py | 14 +++-- paws/functions.py | 56 ++++++++++++++++++- paws/middleware.py | 91 +++++++++++++------------------ paws/routes.py | 37 ++++++++++++- paws/templates/unauthorized.html | 24 --------- paws/views.py | 93 ++++++++++++++++++++++++++++++-- requirements.txt | 4 +- 9 files changed, 229 insertions(+), 94 deletions(-) delete mode 100644 paws/templates/unauthorized.html diff --git a/README.md b/README.md index 049d009..43f864f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ rewrite { rewrite { if_op or if {path} starts_with /@ - if {path} starts_with /authorize + if {path} starts_with /paws to {path} /auth/{path} } diff --git a/paws/config.py b/paws/config.py index 2e208e1..27b9c39 100644 --- a/paws/config.py +++ b/paws/config.py @@ -9,7 +9,7 @@ from envbash import load_envbash from .functions import bool_check -VERSION = '0.1' +VERSION = '0.2-beta' full_path = abspath(sys.executable) if getattr(sys, 'frozen', False) else abspath(__file__) script_path = getattr(sys, '_MEIPASS', dirname(abspath(__file__))) diff --git a/paws/database.py b/paws/database.py index 1d033ba..c0ee826 100644 --- a/paws/database.py +++ b/paws/database.py @@ -2,7 +2,7 @@ import sys from DBUtils.PooledPg import PooledPg as DB from datetime import datetime -from tinydb import TinyDB, Query +from tinydb import TinyDB, Query, where from tinydb_smartcache import SmartCacheTable from tinyrecord import transaction as trans from tldextract import extract @@ -23,14 +23,12 @@ def jsondb(): db.table_class = SmartCacheTable - tables = { - 'bans': db.table('bans'), - 'follows': db.table('follows'), - 'users': db.table('users'), - 'domains': db.table('domains') - } + class table: + bans = db.table('bans') + follows = db.table('follows') + users = db.table('users') - return tables + return table def pgdb(): diff --git a/paws/functions.py b/paws/functions.py index 29c0344..4f0b404 100644 --- a/paws/functions.py +++ b/paws/functions.py @@ -3,6 +3,9 @@ import re import json import logging +from colour import Color +from aiohttp_jinja2 import render_template as render + error_codes = { 400: 'BadRequest', @@ -26,7 +29,7 @@ def bool_check(value): return value -def json_error(code, error): +def error(code, error): error_body = json.dumps({'error': error}) cont_type = 'application/json' @@ -56,3 +59,54 @@ def user_check(path): return False + +class color: + def __init__(self): + self.check = lambda color: Color(f'#{str(color)}' if re.search(r'^(?:[0-9a-fA-F]{3}){1,2}$', color) else color) + + def multi(self, multiplier): + if multiplier >= 1: + return 1 + + elif multiplier <= 0: + return 0 + + return multiplier + + def lighten(self, color, multiplier): + col = self.check(color) + col.luminance += ((1 - col.luminance) * self.multi(multiplier)) + + return col.hex_l + + def darken(self, color, multiplier): + col = self.check(color) + col.luminance -= (col.luminance * self.multi(multiplier)) + + return col.hex_l + + + def saturate(self, color, multiplier): + col = self.check(color) + col.saturation += ((1 - col.saturation) * self.multi(multiplier)) + + return col.hex_l + + + def desaturate(self, color, multiplier): + col = self.check(color) + col.saturation -= (col.saturation * self.multi(multiplier)) + + return col.hex_l + + + def rgba(self, color, transparency): + col = self.check(color) + + red = col.red*255 + green = col.green*255 + blue = col.blue*255 + trans = self.multi(transparency) + + return f'rgba({red:0.2f}, {green:0.2f}, {blue:0.2f}, {trans:0.2f})' + diff --git a/paws/middleware.py b/paws/middleware.py index 3b47a16..1395453 100644 --- a/paws/middleware.py +++ b/paws/middleware.py @@ -6,14 +6,14 @@ import binascii import base64 import traceback -from urllib.parse import urlparse +from urllib.parse import urlparse, quote_plus from aiohttp.http_exceptions import * from aiohttp.client_exceptions import * from .signature import validate, pass_hash -from .functions import json_error, user_check -from .config import MASTOCONFIG, PAWSCONFIG, script_path -from . import database as db +from .functions import error, user_check +from .config import MASTOCONFIG, PAWSCONFIG, VERSION, script_path +from .database import pawsdb, query, trans, ban_check # I'm a little teapot :3 @@ -99,40 +99,41 @@ async def passthrough(path, headers, post=None, query=None): if resp.status not in [200, 202]: print(data) - logging.warning(f'Recieved error {resp.status} from Mastodon') - json_error(resp.status, f'Failed to forward request. Recieved error {resp.status} from Mastodon') + logging.debug(f'Recieved error {resp.status} from Mastodon') + error(resp.status, f'Failed to forward request. Recieved error {resp.status} from Mastodon') raise aiohttp.web.HTTPOk(body=data, content_type=resp.content_type) except ClientConnectorError: traceback.print_exc() - return json_error(504, f'Failed to connect to Mastodon') + return error(504, f'Failed to connect to Mastodon') async def http_redirect(app, handler): async def redirect_handler(request): - querydata = request.query + if not request.path.startswith('/paws'): + querydata = request.query - rawquery = '?' + rawquery = '?' - if len(querydata) > 0: - for var in querydata: - if rawquery == '?': - rawquery += f'{var}={querydata[var]}' + if len(querydata) > 0: + for var in querydata: + if rawquery == '?': + rawquery += f'{var}={querydata[var]}' - else: - rawquery += f'&{var}={querydata[var]}' + else: + rawquery += f'&{var}={querydata[var]}' - query = rawquery if rawquery != '' else None + query = rawquery if rawquery != '' else None - try: - data = await request.json() + try: + data = await request.json() - except Exception as e: - #logging.warning(f'failed to grab data: {e}') - data = None + except Exception as e: + #logging.warning(f'failed to grab data: {e}') + data = None - await passthrough(request.path, request.headers, post=data, query=query) + await passthrough(request.path, request.headers, post=data, query=query) return (await handler(request)) return redirect_handler @@ -152,40 +153,19 @@ async def http_signatures(app, handler): if not signature: logging.warning('missing signature') - raise json_error(401, 'Missing signature') + raise error(401, 'Missing signature') if not (await validate(actor, request)): logging.info(f'Signature validation failed for: {actor}') - raise json_error(401, 'signature check failed, signature did not match key') + raise error(401, 'signature check failed, signature did not match key') - else: + elif not request.path.startswith('/paws/'): request['reqtype'] = 'html' - auth_username = PAWSCONFIG['user'] - auth_password = PAWSCONFIG['pass'] - auth_realm = 'Nope' + token = request.cookies.get('paws_token') - auth_header = request.headers.get(aiohttp.hdrs.AUTHORIZATION) - - if auth_header == None or not auth_header.startswith('Basic '): - return await raise_auth_error(request, auth_realm) - - try: - secret = auth_header[6:].encode('utf-8') - auth_decoded = base64.decodebytes(secret).decode('utf-8') - - except (UnicodeDecodeError, UnicodeEncodeError, binascii.Error): - await raise_auth_error(request) - - credentials = auth_decoded.split(':') - - if len(credentials) != 2: - await raise_auth_error(request, auth_realm) - - username, password = credentials - - if username != auth_username or password != auth_password: - await raise_auth_error(request, auth_realm) + if not token or not pawsdb.users.get(query.token == token): + return aiohttp.web.HTTPFound(f'/paws/login?redir={quote_plus(request.path)}') return (await handler(request)) return http_signatures_handler @@ -203,15 +183,15 @@ async def http_filter(app, handler): domain = ua_domain if not sig_domain else sig_domain if not domain: - raise json_error(401, 'Can\'t find instance domain') + raise error(401, 'Can\'t find instance domain') if [agent for agent in blocked_agents if agent in ua]: logging.info(f'Blocked garbage: {domain}') raise HTTPTeapot(body='418 This teapot kills fascists', content_type='text/plain') - if db.ban_check(domain): + if ban_check(domain): logging.info(f'Blocked instance: {domain}') - raise json_error(403, 'Forbidden') + raise error(403, 'Forbidden') return (await handler(request)) return http_filter_handler @@ -227,4 +207,9 @@ async def http_trailing_slash(app, handler): return http_trailing_slash_handler -__all__ = ['http_signatures_middleware', 'http_auth_middleware', 'http_filter_middleware', 'http_trailing_slash'] +async def http_server_header(request, response): + response.headers['SErver'] = f'PAWS/{VERSION}' + response.headers['trans_rights'] = 'are human rights' + + +__all__ = ['http_signatures_middleware', 'http_auth_middleware', 'http_filter_middleware', 'http_trailing_slash', 'http_server_header'] diff --git a/paws/routes.py b/paws/routes.py index 804e8a3..f1f3019 100644 --- a/paws/routes.py +++ b/paws/routes.py @@ -2,11 +2,14 @@ import os import sys import asyncio import aiohttp +import aiohttp_jinja2 +import jinja2 from ipaddress import ip_address as address from urllib.parse import urlparse -from .config import PAWSCONFIG, VERSION, script_path, logging +from .config import PAWSCONFIG, MASTOCONFIG, VERSION, script_path, logging +from .functions import color from . import middleware @@ -19,10 +22,40 @@ def webserver(): middleware.http_redirect ]) + web.on_response_prepare.append(middleware.http_server_header) + web.add_routes([ - aiohttp.web.route('GET', '/authorize', views.authorize), + aiohttp.web.route('GET', '/paws/login', views.get_login), + aiohttp.web.route('POST', '/paws/login', views.post_login), + aiohttp.web.route('GET', '/paws/logout', views.get_logout), + aiohttp.web.route('GET', '/paws/auth', views.get_auth), + aiohttp.web.route('GET', '/paws/style.css', views.get_style) ]) + async def global_vars(request): + return { + 'VERSION': VERSION, + 'len': len, + 'request': request, + 'domain': MASTOCONFIG['domain'], + 'urlparse': urlparse, + 'lighten': color().lighten, + 'darken': color().darken, + 'saturate': color().saturate, + 'desaturate': color().desaturate, + 'rgba': color().rgba + } + + + aiohttp_jinja2.setup( + web, + loader=jinja2.FileSystemLoader(f'{script_path}/templates'), + autoescape=jinja2.select_autoescape(['html', 'css']), + context_processors=[global_vars], + lstrip_blocks=True, + trim_blocks=True + ) + return web diff --git a/paws/templates/unauthorized.html b/paws/templates/unauthorized.html deleted file mode 100644 index 43a4e15..0000000 --- a/paws/templates/unauthorized.html +++ /dev/null @@ -1,24 +0,0 @@ - - - Nope.mov - - - - - - diff --git a/paws/views.py b/paws/views.py index 9f262a4..1bc6b8f 100644 --- a/paws/views.py +++ b/paws/views.py @@ -1,5 +1,92 @@ import aiohttp +import random + +from aiohttp_jinja2 import render_template as render +from urllib.parse import quote_plus, unquote_plus + +from .database import pawsdb, trans, query, where +from .functions import error +from .oauth import create_app, login + + +async def get_login(request): + parms = request.rel_url.query + redir = parms.get('redir') + numid = random.randint(1*1000000, 10*1000000-1) + + return render('pages/login.html', request, {'redir': redir, 'numid': numid}) + + +async def post_login(request): + data = await request.post() + domain = data.get('domain') + redir = data.get('redir') + numid = data.get('numid') + + if domain in ['', None]: + return render('pages/login.html', request, {'msg': 'Missing domain'}) + + appid, appsecret, redir_url = create_app(domain) + + with trans(pawsdb.users) as tr: + tr.insert({ + 'handle': data['numid'], + 'domain': data['domain'], + 'icon': None, + 'appid': appid, + 'appsecret': appsecret, + 'token': None + }) + + response = aiohttp.web.HTTPFound(redir_url) + response.set_cookie('paws_numid', numid, max_age=60*60*24*14, path='/paws') + + if redir not in ['', None]: + response.set_cookie('paws_redir', redir, max_age=60*60*24*14, path='/paws') + + return response + + +async def get_auth(request): + parms = request.rel_url.query + cookies = request.cookies + redir = cookies.get('paws_redir') + numid = cookies.get('paws_numid') + code = parms.get('code') + + if None in [numid, code]: + response = render('pages/error.html', request, {'msg': 'Missing temporary userid or auth code', 'code': 500}, status=500) + + user = pawsdb.users.get(query.handle == str(numid)) + token, userinfo = login(user, code) + + with trans(pawsdb.users) as tr: + tr.update({'handle': userinfo['username'], 'icon': userinfo['avatar_static'], 'token': token}, where('handle') == numid) + + print(user) + + response = aiohttp.web.HTTPFound(redir) + response.set_cookie('paws_token', token, max_age=60*60*24*14) + response.del_cookie('paws_redir', path='/paws') + response.del_cookie('paws_numid', path='/paws') + return response + + +async def get_logout(request): + token = request.cookies.get('paws_token') + + with trans(pawsdb.users) as tr: + tr.remove(where('token') == token) + + response = render('pages/login.html', request, {'msg': 'Logged out'}) + response.del_cookie('token') + + return response + + +async def get_style(request): + response = render('color.css', request, {}) + response.headers['Content-Type'] = 'text/css' + + return response -async def authorize(request): - data = {['heck']} - return aiohttp.web.json_response(data) diff --git a/requirements.txt b/requirements.txt index ecfd344..32f6f85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,6 @@ pycryptodome tldextract envbash ipaddress - +mastodon.py +aiohttp_jinja2 +colour