use new request properties and only fetch nodeinfo on follow

This commit is contained in:
Izalia Mae 2022-11-18 16:41:14 -05:00
parent 4a8a8da740
commit ba9f2718aa
2 changed files with 84 additions and 83 deletions

View file

@ -6,92 +6,97 @@ from uuid import uuid4
from . import misc from . import misc
async def handle_relay(request, actor, data, nodeinfo): async def handle_relay(request):
if data.objectid in request.app.cache.objects: if request.message.objectid in request.cache.objects:
logging.verbose(f'already relayed {data.objectid}') logging.verbose(f'already relayed {request.message.objectid}')
return return
logging.verbose(f'Relaying post from {data.actorid}') logging.verbose(f'Relaying post from {request.message.actorid}')
message = misc.Message.new_announce( message = misc.Message.new_announce(
host = request.app.config.host, host = request.config.host,
object = data.objectid object = request.message.objectid
) )
logging.debug(f'>> relay: {message}') logging.debug(f'>> relay: {message}')
inboxes = misc.distill_inboxes(actor, data.objectid) inboxes = misc.distill_inboxes(request.actor, request.message.objectid)
futures = [misc.request(inbox, data=message) for inbox in inboxes] futures = [misc.request(inbox, data=message) for inbox in inboxes]
asyncio.ensure_future(asyncio.gather(*futures)) asyncio.ensure_future(asyncio.gather(*futures))
request.app.cache.objects[data.objectid] = message.id request.cache.objects[request.message.objectid] = message.id
async def handle_forward(request, actor, data, nodeinfo): async def handle_forward(request):
if data.id in request.app.cache.objects: if request.message.id in request.cache.objects:
logging.verbose(f'already forwarded {data.id}') logging.verbose(f'already forwarded {request.message.id}')
return return
message = misc.Message.new_announce( message = misc.Message.new_announce(
host = request.app.config.host, host = request.config.host,
object = data object = request.message
) )
logging.verbose(f'Forwarding post from {actor.id}') logging.verbose(f'Forwarding post from {request.actor.id}')
logging.debug(f'>> Relay {data}') logging.debug(f'>> Relay {request.message}')
inboxes = misc.distill_inboxes(actor, data.id) inboxes = misc.distill_inboxes(request.actor, request.message.id)
futures = [misc.request(inbox, data=message) for inbox in inboxes] futures = [misc.request(inbox, data=message) for inbox in inboxes]
asyncio.ensure_future(asyncio.gather(*futures)) asyncio.ensure_future(asyncio.gather(*futures))
request.app.cache.objects[data.id] = message.id request.cache.objects[request.message.id] = message.id
async def handle_follow(request, actor, data, nodeinfo): async def handle_follow(request):
if not request.app.database.add_inbox(actor.shared_inbox, data.id): nodeinfo = await misc.fetch_nodeinfo(request.actor.domain)
request.app.database.set_followid(actor.id, data.id) software = nodeinfo.swname if nodeinfo else None
request.app.database.save() ## reject if software used by actor is banned
if request.config.is_banned_software(software):
return logging.verbose(f'Rejected follow from actor for using specific software: actor={request.actor.id}, software={software}')
request.database.add_inbox(request.actor.shared_inbox, request.message.id, software)
request.database.save()
await misc.request( await misc.request(
actor.shared_inbox, request.actor.shared_inbox,
misc.Message.new_response( misc.Message.new_response(
host = request.app.config.host, host = request.config.host,
actor = actor.id, actor = request.actor.id,
followid = data.id, followid = request.message.id,
accept = True accept = True
) )
) )
# Are Akkoma and Pleroma the only two that expect a follow back? # Are Akkoma and Pleroma the only two that expect a follow back?
# Ignoring only Mastodon for now # Ignoring only Mastodon for now
if nodeinfo.swname != 'mastodon': if software != 'mastodon':
await misc.request( await misc.request(
actor.shared_inbox, request.actor.shared_inbox,
misc.Message.new_follow( misc.Message.new_follow(
host = request.app.config.host, host = request.config.host,
actor = actor.id actor = request.actor.id
) )
) )
async def handle_undo(request, actor, data, nodeinfo): async def handle_undo(request):
## If the object is not a Follow, forward it ## If the object is not a Follow, forward it
if data['object']['type'] != 'Follow': if request.message.object.type != 'Follow':
return await handle_forward(request, actor, data, nodeinfo) return await handle_forward(request)
if not request.app.database.del_inbox(actor.domain, data.id): if not request.database.del_inbox(request.actor.domain, request.message.id):
return return
request.app.database.save() request.database.save()
message = misc.Message.new_unfollow( message = misc.Message.new_unfollow(
host = request.app.config.host, host = request.config.host,
actor = actor.id, actor = request.actor.id,
follow = data follow = request.message
) )
await misc.request(actor.shared_inbox, message) await misc.request(request.actor.shared_inbox, message)
processors = { processors = {
@ -104,9 +109,9 @@ processors = {
} }
async def run_processor(request, actor, data, nodeinfo): async def run_processor(request):
if data.type not in processors: if request.message.type not in processors:
return return
logging.verbose(f'New "{data.type}" from actor: {actor.id}') logging.verbose(f'New "{request.message.type}" from actor: {request.actor.id}')
return await processors[data.type](request, actor, data, nodeinfo) return await processors[request.message.type](request)

View file

@ -33,10 +33,10 @@ def register_route(method, path):
@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.database.hostnames)
note = request.app.config.note note = request.config.note
count = len(request.app.database.hostnames) count = len(request.database.hostnames)
host = request.app.config.host host = request.config.host
text = f""" text = f"""
<html><head> <html><head>
@ -64,8 +64,8 @@ a:hover {{ color: #8AF; }}
@register_route('GET', '/actor') @register_route('GET', '/actor')
async def actor(request): async def actor(request):
data = Message.new_actor( data = Message.new_actor(
host = request.app.config.host, host = request.config.host,
pubkey = request.app.database.pubkey pubkey = request.database.pubkey
) )
return Response.new(data, ctype='activity') return Response.new(data, ctype='activity')
@ -74,67 +74,63 @@ async def actor(request):
@register_route('POST', '/inbox') @register_route('POST', '/inbox')
@register_route('POST', '/actor') @register_route('POST', '/actor')
async def inbox(request): async def inbox(request):
config = request.app.config config = request.config
database = request.app.database database = request.database
## reject if missing signature header ## reject if missing signature header
try: if not request.signature:
signature = DotDict.new_from_signature(request.headers['signature'])
except KeyError:
logging.verbose('Actor missing signature header') logging.verbose('Actor missing signature header')
raise HTTPUnauthorized(body='missing signature') raise HTTPUnauthorized(body='missing signature')
## read message
try: try:
data = await request.json(loads=Message.new_from_json) request['message'] = await request.json(loads=Message.new_from_json)
## reject if there is no message
if not request.message:
logging.verbose('empty message')
return Response.new_error(400, 'missing message', 'json')
## reject if there is no actor in the message ## reject if there is no actor in the message
if 'actor' not in data: if 'actor' not in request.message:
logging.verbose('actor not in data') logging.verbose('actor not in message')
return Response.new_error(400, 'no actor in message', 'json') return Response.new_error(400, 'no actor in message', 'json')
except: except:
## this code should hopefully never get called
traceback.print_exc() traceback.print_exc()
logging.verbose('Failed to parse inbox message') logging.verbose('Failed to parse inbox message')
return Response.new_error(400, 'failed to parse message', 'json') return Response.new_error(400, 'failed to parse message', 'json')
actor = await misc.request(signature.keyid) request['actor'] = await misc.request(request.signature.keyid)
nodeinfo = await misc.fetch_nodeinfo(actor.domain)
## reject if actor is empty ## reject if actor is empty
if not actor: if not request.actor:
logging.verbose(f'Failed to fetch actor: {actor.id}') logging.verbose(f'Failed to fetch actor: {request.actor.id}')
return Response.new_error(400, 'failed to fetch actor', 'json') 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(actor.domain): if config.whitelist_enabled and not config.is_whitelisted(request.actor.domain):
logging.verbose(f'Rejected actor for not being in the whitelist: {actor.id}') logging.verbose(f'Rejected actor for not being in the whitelist: {request.actor.id}')
return Response.new_error(403, 'access denied', 'json') return Response.new_error(403, 'access denied', 'json')
## reject if actor is banned ## reject if actor is banned
if request.app['config'].is_banned(actor.domain): if request.config.is_banned(request.actor.domain):
logging.verbose(f'Ignored request from banned actor: {actor.id}') logging.verbose(f'Ignored request from banned actor: {actor.id}')
return Response.new_error(403, 'access denied', 'json') return Response.new_error(403, 'access denied', 'json')
## reject if software used by actor is banned
if config.is_banned_software(nodeinfo.swname):
logging.verbose(f'Rejected actor for using specific software: {nodeinfo.swname}')
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(actor, signature, request)): if not (await misc.validate_signature(request.actor, request.signature, request)):
logging.verbose(f'signature validation failed for: {actor.id}') logging.verbose(f'signature validation failed for: {actor.id}')
return Response.new_error(401, 'signature check failed', 'json') 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(actor.domain): if request.message.type != 'Follow' and not database.get_inbox(request.actor.domain):
logging.verbose(f'Rejected actor for trying to post while not following: {actor.id}') logging.verbose(f'Rejected actor for trying to post while not following: {request.actor.id}')
return Response.new_error(401, 'access denied', 'json') return Response.new_error(401, 'access denied', 'json')
logging.debug(f">> payload {data}") logging.debug(f">> payload {request.message.to_json(4)}")
await run_processor(request, actor, data, nodeinfo) await run_processor(request)
return Response.new(status=202) return Response.new(status=202)
@ -146,15 +142,15 @@ async def webfinger(request):
except KeyError: except KeyError:
return Response.new_error(400, 'missing \'resource\' query key', 'json') return Response.new_error(400, 'missing \'resource\' query key', 'json')
if subject != f'acct:relay@{request.app.config.host}': if subject != f'acct:relay@{request.config.host}':
return Response.new_error(404, 'user not found', 'json') return Response.new_error(404, 'user not found', 'json')
data = { data = {
'subject': subject, 'subject': subject,
'aliases': [request.app.config.actor], 'aliases': [request.config.actor],
'links': [ 'links': [
{'href': request.app.config.actor, 'rel': 'self', 'type': 'application/activity+json'}, {'href': request.config.actor, 'rel': 'self', 'type': 'application/activity+json'},
{'href': request.app.config.actor, 'rel': 'self', 'type': 'application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"'} {'href': request.config.actor, 'rel': 'self', 'type': 'application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"'}
] ]
} }
@ -165,7 +161,7 @@ async def webfinger(request):
async def nodeinfo_2_0(request): async def nodeinfo_2_0(request):
niversion = request.match_info['version'][:3] niversion = request.match_info['version'][:3]
data = { data = {
'openRegistrations': not request.app.config.whitelist_enabled, 'openRegistrations': not request.config.whitelist_enabled,
'protocols': ['activitypub'], 'protocols': ['activitypub'],
'services': { 'services': {
'inbound': [], 'inbound': [],
@ -182,7 +178,7 @@ async def nodeinfo_2_0(request):
} }
}, },
'metadata': { 'metadata': {
'peers': request.app.database.hostnames 'peers': request.database.hostnames
}, },
'version': niversion 'version': niversion
} }
@ -196,8 +192,8 @@ async def nodeinfo_2_0(request):
@register_route('GET', '/.well-known/nodeinfo') @register_route('GET', '/.well-known/nodeinfo')
async def nodeinfo_wellknown(request): async def nodeinfo_wellknown(request):
data = WKNodeinfo.new( data = WKNodeinfo.new(
v20 = f'https://{request.app.config.host}/nodeinfo/2.0.json', v20 = f'https://{request.config.host}/nodeinfo/2.0.json',
v21 = f'https://{request.app.config.host}/nodeinfo/2.1.json' v21 = f'https://{request.config.host}/nodeinfo/2.1.json'
) )
return Response.new(data, ctype='json') return Response.new(data, ctype='json')