paws/paws/middleware.py
2020-01-17 08:22:59 -05:00

235 lines
6.7 KiB
Python

import asyncio
import aiohttp
import json
import logging
import binascii
import base64
import traceback
from urllib.parse import urlparse, quote_plus
from aiohttp_jinja2 import render_template as render
from aiohttp.http_exceptions import *
from aiohttp.client_exceptions import *
from .signature import validate, pass_hash
from .functions import error, user_check
from .config import MASTOCONFIG, PAWSCONFIG, VERSION, script_path
from .database import pawsdb, query, trans, ban_check, user_ban_check, banned_user_check
# I'm a little teapot :3
class HTTPTeapot(aiohttp.web.HTTPError):
status_code = 418
blocked_agents = [
'gabsocial',
'kiwifarms',
'fedichive',
'liveview',
'freespeech',
'shitposter.club',
'baraag',
'gameliberty',
'neckbeard',
'soapbox'
]
auth_paths = [
'/@',
'/users'
]
def parse_sig(signature, short=False):
for line in signature.split(','):
if 'keyid' in line.lower():
actor = line.split('=')[1].split('#')[0].replace('"', '')
return urlparse(actor).netloc if short else actor
def parse_ua(agent):
if not agent:
return
ua1 = agent.split('https://')
if len(ua1) < 2:
logging.warning(f'No url in user-agent: {agent}')
return 'unknown'
if 'Mastodon' in agent:
ua2 = ua1[1].split('/')
elif 'Pleroma' in agent:
ua2 = ua1[1].split(' <')
elif 'Misskey' in agent or 'BarksharkRelay' in agent:
ua2 = ua1[1].split(')')
elif 'Friendica' in agent or 'microblog.pub' in agent:
ua2 = ua1[1]
else:
logging.warning(f'Unhandled user-agent: {agent}')
return 'unknown'
if len(ua2) > 1:
logging.debug(f'domain: {ua2}')
return ua2[0]
logging.warning(f'Invalid user-agent: {ua2}')
async def raise_auth_error(request, auth_realm):
raise aiohttp.web.HTTPUnauthorized(
headers={aiohttp.hdrs.WWW_AUTHENTICATE: f'Basic realm={auth_realm}'},
body=open(f'{script_path}/templates/unauthorized.html').read(),
content_type='text/html'
)
async def passthrough(path, headers, post=None, query=None):
reqtype = 'POST' if post else 'GET'
url = urlparse(path).path
querydata = query if query else ''
try:
# I don't think I need this, but I'll leave it here just in case
#async with aiohttp.request(reqtype, f'https://{MASTOCONFIG["domain"]}/{path}{query}', headers=headers, data=post) as resp:
async with aiohttp.request(reqtype, f'http://localhost:3000/{path}{query}', headers=headers, data=post) as resp:
data = await resp.read()
if resp.status not in [200, 202]:
print(data)
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 error(504, f'Failed to connect to Mastodon')
async def http_redirect(app, handler):
async def redirect_handler(request):
if not request.path.startswith('/paws'):
querydata = request.query
rawquery = '?'
if len(querydata) > 0:
for var in querydata:
if rawquery == '?':
rawquery += f'{var}={querydata[var]}'
else:
rawquery += f'&{var}={querydata[var]}'
query = rawquery if rawquery != '' else None
try:
data = await request.json()
except Exception as e:
#logging.warning(f'failed to grab data: {e}')
data = None
await passthrough(request.path, request.headers, post=data, query=query)
return (await handler(request))
return redirect_handler
async def http_signatures(app, handler):
async def http_signatures_handler(request):
request['validated'] = False
if any(map(request.path.startswith, auth_paths)) and request.method != 'POST':
if 'json' in request.headers.get('Accept', '') or request.path.endswith('.json'):
request['reqtype'] = 'json'
if not user_check(request.path) and not MASTOCONFIG['auth_fetch']:
signature = request.headers.get('signature', '')
actor = parse_sig(signature)
if not signature:
logging.warning('missing signature')
raise error(401, 'Missing signature')
if not (await validate(actor, request)):
logging.info(f'Signature validation failed for: {actor}')
raise error(401, 'signature check failed, signature did not match key')
elif not request.path.startswith('/paws/'):
request['reqtype'] = 'html'
token = request.cookies.get('paws_token')
access_user = pawsdb.users.get(query.token == token)
if not token or not access_user:
return aiohttp.web.HTTPFound(f'/paws/login?msg=InvalidToken&redir={quote_plus(request.path)}')
split_path = request.path.split('/')
user = split_path[1].replace('@', '') if request.path.startswith('/@') else split_path[2]
if user_ban_check(user.lower(), (access_user['handle'].lower(), access_user['instance'])):
return render('pages/error.html', request, {'msg': 'Access Denied', 'code': '403'}, status=403)
return (await handler(request))
return http_signatures_handler
async def http_filter(app, handler):
async def http_filter_handler(request):
if request.get('reqtype') == 'json':
signature = request.headers.get('signature').lower()
ua = request.headers.get('user-agent').lower()
if not user_check(request.path):
sig_domain = parse_sig(signature, short=True)
ua_domain = parse_ua(ua)
domain = ua_domain if not sig_domain else sig_domain
token = request.cookies.get('paws_token')
user_data = pawsdb.users.get(query.token == token)
user = (user_data['handle'], user_data['instance'])
full_user = f"{user_data['handle']}@{user_data['instance']}"
if not 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 ban_check(domain):
logging.info(f'Blocked instance: {domain}')
raise error(403, 'Forbidden')
if banned_user_check(user):
logging.info(f'Blocked user: {domain}')
return render('pages/error.html', request, {'msg': 'Access Denied', 'code': '403'}, status=403)
return (await handler(request))
return http_filter_handler
# Fucking trailing slashes
async def http_trailing_slash(app, handler):
async def http_trailing_slash_handler(request):
if request.path != '/' and request.path.endswith('/'):
return aiohttp.web.HTTPFound(request.path[:-1])
return (await handler(request))
return http_trailing_slash_handler
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']