182 lines
4.8 KiB
Python
182 lines
4.8 KiB
Python
import asyncio
|
|
import aiohttp
|
|
import json
|
|
import logging
|
|
import binascii
|
|
import base64
|
|
import traceback
|
|
|
|
from urllib.parse import urlparse
|
|
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, script_path
|
|
from . import database as db
|
|
|
|
|
|
# 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'
|
|
]
|
|
|
|
auth_paths = [
|
|
'/@',
|
|
'/users'
|
|
]
|
|
|
|
|
|
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:
|
|
async with aiohttp.request(reqtype, f'https://{MASTOCONFIG["domain"]}/{path}{query}', headers=headers, data=post) as resp:
|
|
if resp.status not in [200, 202]:
|
|
logging.warning(f'Recieved error {resp.status} from Mastodon')
|
|
json_error(504, f'Failed to forward request. Recieved error {resp.status} from Mastodon')
|
|
|
|
data = await resp.read()
|
|
|
|
raise aiohttp.web.HTTPOk(body=data, content_type=resp.content_type)
|
|
|
|
except ClientConnectorError:
|
|
return json_error(504, f'Failed to connect to Mastodon')
|
|
|
|
|
|
async def http_redirect(app, handler):
|
|
async def redirect_handler(request):
|
|
headers = {'Host': MASTOCONFIG["domain"]}
|
|
json_req = request.headers.get('Accept') == 'application/json'
|
|
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
|
|
|
|
if json_req:
|
|
headers.update({'Accept': 'application/json'})
|
|
|
|
try:
|
|
data = await request.json()
|
|
|
|
except Exception as e:
|
|
#logging.warning(f'failed to grab data: {e}')
|
|
data = None
|
|
|
|
await passthrough(request.path, 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
|
|
json_req = request.headers.get('Accept') == 'application/json'
|
|
|
|
if any(map(request.path.startswith, auth_paths)) and not user_check(request.path):
|
|
if json_req or request.path.endswith('.json'):
|
|
if 'signature' in request.headers:
|
|
data = await request.json()
|
|
print(json.dumps(data, indent=' '))
|
|
|
|
if 'actor' not in data:
|
|
raise json_error(401, 'signature check failed, no actor in message')
|
|
|
|
actor = data["actor"]
|
|
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')
|
|
|
|
else:
|
|
raise json_error(401, 'Missing signature')
|
|
|
|
else:
|
|
auth_username = 'admin'
|
|
auth_password = 'doubleheck'
|
|
auth_realm = 'Nope'
|
|
|
|
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)
|
|
|
|
return (await handler(request))
|
|
return http_signatures_handler
|
|
|
|
|
|
async def http_filter(app, handler):
|
|
async def http_filter_handler(request):
|
|
data = await request.json()
|
|
actor = data.get('actor')
|
|
|
|
if [agent for agent in blocked_agents if agent in request.headers.get('User-Agent', '').lower()]:
|
|
raise HTTPTeapot(body='418 This teapot kills fascists', content_type='text/plain')
|
|
|
|
if db.ban_check(actor)
|
|
raise json_error(403, 'Forbidden')
|
|
|
|
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
|
|
|
|
|
|
__all__ = ['http_signatures_middleware', 'http_auth_middleware', 'http_filter_middleware', 'http_trailing_slash']
|