235 lines
6.7 KiB
Python
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']
|