create Response class

This commit is contained in:
Izalia Mae 2022-11-09 05:58:35 -05:00
parent 6af9c8e6fe
commit ff95a3033d
2 changed files with 75 additions and 36 deletions

View file

@ -10,6 +10,7 @@ from Crypto.Hash import SHA, SHA256, SHA512
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5 from Crypto.Signature import PKCS1_v1_5
from aiohttp import ClientSession from aiohttp import ClientSession
from aiohttp.web import Response as AiohttpResponse
from datetime import datetime from datetime import datetime
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from urllib.parse import urlparse from urllib.parse import urlparse
@ -26,6 +27,13 @@ HASHES = {
'sha512': SHA512 'sha512': SHA512
} }
MIMETYPES = {
'activity': 'application/activity+json',
'html': 'text/html',
'json': 'application/json',
'plain': 'text/plain'
}
NODEINFO_NS = { NODEINFO_NS = {
'20': 'http://nodeinfo.diaspora.software/ns/schema/2.0', '20': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
'21': 'http://nodeinfo.diaspora.software/ns/schema/2.1' '21': 'http://nodeinfo.diaspora.software/ns/schema/2.1'
@ -166,12 +174,12 @@ async def request(uri, data=None, force=False, sign_headers=True, activity=True)
method = 'POST' if data else 'GET' method = 'POST' if data else 'GET'
action = data.get('type') if data else None action = data.get('type') if data else None
headers = { headers = {
'Accept': 'application/activity+json, application/json;q=0.9', 'Accept': f'{MIMETYPES["activity"]}, {MIMETYPES["json"]};q=0.9',
'User-Agent': 'ActivityRelay', 'User-Agent': 'ActivityRelay',
} }
if data: if data:
headers['Content-Type'] = 'application/activity+json' if activity else 'application/json' headers['Content-Type'] = MIMETYPES['activity' if activity else 'json']
if sign_headers: if sign_headers:
signing_headers = { signing_headers = {
@ -219,10 +227,10 @@ async def request(uri, data=None, force=False, sign_headers=True, activity=True)
return logging.verbose(f'Received error when sending {action} to {uri}: {resp.status} {resp_data}') return logging.verbose(f'Received error when sending {action} to {uri}: {resp.status} {resp_data}')
if resp.content_type == 'application/activity+json': if resp.content_type == MIMETYPES['activity']:
resp_data = await resp.json(loads=Message.new_from_json) resp_data = await resp.json(loads=Message.new_from_json)
elif resp.content_type == 'application/json': elif resp.content_type == MIMETYPES['json']:
resp_data = await resp.json(loads=DotDict.new_from_json) resp_data = await resp.json(loads=DotDict.new_from_json)
else: else:
@ -453,6 +461,45 @@ class Message(DotDict):
return self.object return self.object
class Response(AiohttpResponse):
@classmethod
def new(cls, body='', status=200, headers=None, ctype='text'):
kwargs = {
'status': status,
'headers': headers,
'content_type': MIMETYPES[ctype]
}
if isinstance(body, bytes):
kwargs['body'] = body
elif isinstance(body, dict) and ctype in {'json', 'activity'}:
kwargs['text'] = json.dumps(body)
else:
kwargs['text'] = body
return cls(**kwargs)
@classmethod
def new_error(cls, status, body, ctype='plain'):
if ctype == 'json':
body = json.dumps({'status': status, 'error': body})
return cls(body=body, status=status, ctype=ctype)
@property
def location(self):
return self.headers.get('Location')
@location.setter
def location(self, value):
self.headers['Location'] = value
class WKNodeinfo(DotDict): class WKNodeinfo(DotDict):
@classmethod @classmethod
def new(cls, v20, v21): def new(cls, v20, v21):

View file

@ -2,16 +2,21 @@ import logging
import subprocess import subprocess
import traceback import traceback
from aiohttp.web import HTTPForbidden, HTTPUnauthorized, Response, json_response, route
from . import __version__, misc from . import __version__, misc
from .http_debug import STATS from .http_debug import STATS
from .misc import Message, WKNodeinfo from .misc import Message, Response, WKNodeinfo
from .processors import run_processor from .processors import run_processor
routes = [] routes = []
try:
commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii')
version = f'{__version__} {commit_label}'
except:
version = __version__
def register_route(method, path): def register_route(method, path):
def wrapper(func): def wrapper(func):
@ -21,14 +26,6 @@ def register_route(method, path):
return wrapper return wrapper
try:
commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii')
version = f'{__version__} {commit_label}'
except:
version = __version__
@register_route('GET', '/') @register_route('GET', '/')
async def home(request): async def home(request):
targets = '<br>'.join(request.app.database.hostnames) targets = '<br>'.join(request.app.database.hostnames)
@ -55,12 +52,7 @@ a:hover {{ color: #8AF; }}
<br><p>List of {count} registered instances:<br>{targets}</p> <br><p>List of {count} registered instances:<br>{targets}</p>
</body></html>""" </body></html>"""
return Response( return Response.new(text, ctype='html')
status = 200,
content_type = 'text/html',
charset = 'utf-8',
text = text
)
@register_route('GET', '/inbox') @register_route('GET', '/inbox')
@ -71,7 +63,7 @@ async def actor(request):
pubkey = request.app.database.pubkey pubkey = request.app.database.pubkey
) )
return json_response(data, content_type='application/activity+json') return Response.new(data, ctype='activity')
@register_route('POST', '/inbox') @register_route('POST', '/inbox')
@ -95,29 +87,29 @@ async def inbox(request):
## reject if there is no actor in the message ## reject if there is no actor in the message
except KeyError: except KeyError:
logging.verbose('actor not in data') logging.verbose('actor not in data')
raise HTTPUnauthorized(body='no actor in message') return Response.new_error(400, 'no actor in message', 'json')
except: except:
traceback.print_exc() traceback.print_exc()
logging.verbose('Failed to parse inbox message') logging.verbose('Failed to parse inbox message')
raise HTTPUnauthorized(body='failed to parse message') return Response.new_error(400, 'failed to parse message', 'json')
actor = await misc.request(data.actorid) actor = await misc.request(data.actorid)
## reject if actor is empty ## reject if actor is empty
if not actor: if not actor:
logging.verbose(f'Failed to fetch actor: {data.actorid}') logging.verbose(f'Failed to fetch actor: {data.actorid}')
raise HTTPUnauthorized('failed to fetch actor') return Response.new_error(400, 'failed to fetch actor', 'json')
## reject if the actor isn't whitelisted while the whiltelist is enabled ## reject if the actor isn't whitelisted while the whiltelist is enabled
elif config.whitelist_enabled and not config.is_whitelisted(data.domain): elif config.whitelist_enabled and not config.is_whitelisted(data.domain):
logging.verbose(f'Rejected actor for not being in the whitelist: {data.actorid}') logging.verbose(f'Rejected actor for not being in the whitelist: {data.actorid}')
raise HTTPForbidden(body='access denied') return Response.new_error(403, 'access denied', 'json')
## reject if actor is banned ## reject if actor is banned
if request.app['config'].is_banned(data.domain): if request.app['config'].is_banned(data.domain):
logging.verbose(f'Ignored request from banned actor: {data.actorid}') logging.verbose(f'Ignored request from banned actor: {data.actorid}')
raise HTTPForbidden(body='access denied') return Response.new_error(403, 'access denied', 'json')
## reject if software used by actor is banned ## reject if software used by actor is banned
if len(config.blocked_software): if len(config.blocked_software):
@ -125,22 +117,22 @@ async def inbox(request):
if config.is_banned_software(software): if config.is_banned_software(software):
logging.verbose(f'Rejected actor for using specific software: {software}') logging.verbose(f'Rejected actor for using specific software: {software}')
raise HTTPForbidden(body='access denied') return Response.new_error(403, 'access denied', 'json')
## reject if the signature is invalid ## reject if the signature is invalid
if not (await misc.validate_signature(data.actorid, request)): if not (await misc.validate_signature(data.actorid, request)):
logging.verbose(f'signature validation failed for: {data.actorid}') logging.verbose(f'signature validation failed for: {data.actorid}')
raise HTTPUnauthorized(body='signature check failed, signature did not match key') return Response.new_error(401, 'signature check failed', 'json')
## reject if activity type isn't 'Follow' and the actor isn't following ## reject if activity type isn't 'Follow' and the actor isn't following
if data['type'] != 'Follow' and not database.get_inbox(data.domain): if data['type'] != 'Follow' and not database.get_inbox(data.domain):
logging.verbose(f'Rejected actor for trying to post while not following: {data.actorid}') logging.verbose(f'Rejected actor for trying to post while not following: {data.actorid}')
raise HTTPUnauthorized(body='access denied') return Response.new_error(401, 'access denied', 'json')
logging.debug(f">> payload {data}") logging.debug(f">> payload {data}")
await run_processor(request, actor, data, software) await run_processor(request, actor, data, software)
return Response(body=b'{}', content_type='application/activity+json') return Response.new(status=202)
@register_route('GET', '/.well-known/webfinger') @register_route('GET', '/.well-known/webfinger')
@ -148,7 +140,7 @@ async def webfinger(request):
subject = request.query['resource'] subject = request.query['resource']
if subject != f'acct:relay@{request.app.config.host}': if subject != f'acct:relay@{request.app.config.host}':
return json_response({'error': 'user not found'}, status=404) return Response.new_error(404, 'user not found', 'json')
data = { data = {
'subject': subject, 'subject': subject,
@ -159,7 +151,7 @@ async def webfinger(request):
] ]
} }
return json_response(data) return Response.new(data, ctype='json')
@register_route('GET', '/nodeinfo/{version:\d.\d\.json}') @register_route('GET', '/nodeinfo/{version:\d.\d\.json}')
@ -191,7 +183,7 @@ async def nodeinfo_2_0(request):
if version == '2.1': if version == '2.1':
data['software']['repository'] = 'https://git.pleroma.social/pleroma/relay' data['software']['repository'] = 'https://git.pleroma.social/pleroma/relay'
return json_response(data) return Response.new(data, ctype='json')
@register_route('GET', '/.well-known/nodeinfo') @register_route('GET', '/.well-known/nodeinfo')
@ -201,9 +193,9 @@ async def nodeinfo_wellknown(request):
v21 = f'https://{request.app.config.host}/nodeinfo/2.1.json' v21 = f'https://{request.app.config.host}/nodeinfo/2.1.json'
) )
return json_response(data) return Response.new(data, ctype='json')
@register_route('GET', '/stats') @register_route('GET', '/stats')
async def stats(request): async def stats(request):
return json_response(STATS) return Response.new(STATS, ctype='json')