2022-05-06 07:04:51 +00:00
|
|
|
import logging
|
|
|
|
import subprocess
|
|
|
|
import traceback
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
from . import __version__, misc
|
2022-05-06 07:04:51 +00:00
|
|
|
from .http_debug import STATS
|
2022-11-09 10:58:35 +00:00
|
|
|
from .misc import Message, Response, WKNodeinfo
|
2022-05-06 07:04:51 +00:00
|
|
|
from .processors import run_processor
|
|
|
|
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
routes = []
|
|
|
|
|
2022-11-09 10:58:35 +00:00
|
|
|
try:
|
|
|
|
commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii')
|
|
|
|
version = f'{__version__} {commit_label}'
|
|
|
|
|
|
|
|
except:
|
|
|
|
version = __version__
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
|
|
|
|
def register_route(method, path):
|
|
|
|
def wrapper(func):
|
|
|
|
routes.append([method, path, func])
|
|
|
|
return func
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
@register_route('GET', '/')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def home(request):
|
2022-11-07 12:54:32 +00:00
|
|
|
targets = '<br>'.join(request.app.database.hostnames)
|
|
|
|
note = request.app.config.note
|
|
|
|
count = len(request.app.database.hostnames)
|
|
|
|
host = request.app.config.host
|
|
|
|
|
|
|
|
text = f"""
|
2022-05-06 07:04:51 +00:00
|
|
|
<html><head>
|
|
|
|
<title>ActivityPub Relay at {host}</title>
|
|
|
|
<style>
|
|
|
|
p {{ color: #FFFFFF; font-family: monospace, arial; font-size: 100%; }}
|
|
|
|
body {{ background-color: #000000; }}
|
|
|
|
a {{ color: #26F; }}
|
|
|
|
a:visited {{ color: #46C; }}
|
|
|
|
a:hover {{ color: #8AF; }}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p>This is an Activity Relay for fediverse instances.</p>
|
|
|
|
<p>{note}</p>
|
|
|
|
<p>You may subscribe to this relay with the address: <a href="https://{host}/actor">https://{host}/actor</a></p>
|
|
|
|
<p>To host your own relay, you may download the code at this address: <a href="https://git.pleroma.social/pleroma/relay">https://git.pleroma.social/pleroma/relay</a></p>
|
|
|
|
<br><p>List of {count} registered instances:<br>{targets}</p>
|
2022-11-07 12:54:32 +00:00
|
|
|
</body></html>"""
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(text, ctype='html')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
@register_route('GET', '/inbox')
|
|
|
|
@register_route('GET', '/actor')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def actor(request):
|
2022-11-07 10:30:13 +00:00
|
|
|
data = Message.new_actor(
|
2022-11-07 12:54:32 +00:00
|
|
|
host = request.app.config.host,
|
|
|
|
pubkey = request.app.database.pubkey
|
2022-11-07 10:30:13 +00:00
|
|
|
)
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(data, ctype='activity')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
@register_route('POST', '/inbox')
|
|
|
|
@register_route('POST', '/actor')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def inbox(request):
|
2022-11-07 12:54:32 +00:00
|
|
|
config = request.app.config
|
|
|
|
database = request.app.database
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if missing signature header
|
|
|
|
if 'signature' not in request.headers:
|
|
|
|
logging.verbose('Actor missing signature header')
|
|
|
|
raise HTTPUnauthorized(body='missing signature')
|
|
|
|
|
|
|
|
## read message and get actor id and domain
|
|
|
|
try:
|
2022-11-07 10:30:13 +00:00
|
|
|
data = await request.json(loads=Message.new_from_json)
|
|
|
|
|
2022-11-16 14:12:23 +00:00
|
|
|
## reject if there is no actor in the message
|
2022-11-07 10:30:13 +00:00
|
|
|
if 'actor' not in data:
|
2022-11-16 14:12:23 +00:00
|
|
|
logging.verbose('actor not in data')
|
|
|
|
return Response.new_error(400, 'no actor in message', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
logging.verbose('Failed to parse inbox message')
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(400, 'failed to parse message', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-16 15:31:21 +00:00
|
|
|
software = await misc.fetch_nodeinfo(data.domain)
|
2022-11-07 10:30:13 +00:00
|
|
|
actor = await misc.request(data.actorid)
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if actor is empty
|
|
|
|
if not actor:
|
2022-11-07 10:30:13 +00:00
|
|
|
logging.verbose(f'Failed to fetch actor: {data.actorid}')
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(400, 'failed to fetch actor', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if the actor isn't whitelisted while the whiltelist is enabled
|
2022-11-07 10:30:13 +00:00
|
|
|
elif config.whitelist_enabled and not config.is_whitelisted(data.domain):
|
|
|
|
logging.verbose(f'Rejected actor for not being in the whitelist: {data.actorid}')
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(403, 'access denied', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if actor is banned
|
2022-11-07 12:54:32 +00:00
|
|
|
if request.app['config'].is_banned(data.domain):
|
2022-11-07 10:30:13 +00:00
|
|
|
logging.verbose(f'Ignored request from banned actor: {data.actorid}')
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(403, 'access denied', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if software used by actor is banned
|
2022-11-16 15:31:21 +00:00
|
|
|
if config.is_banned_software(software):
|
|
|
|
logging.verbose(f'Rejected actor for using specific software: {software}')
|
|
|
|
return Response.new_error(403, 'access denied', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if the signature is invalid
|
2022-11-07 10:30:13 +00:00
|
|
|
if not (await misc.validate_signature(data.actorid, request)):
|
|
|
|
logging.verbose(f'signature validation failed for: {data.actorid}')
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(401, 'signature check failed', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
## reject if activity type isn't 'Follow' and the actor isn't following
|
2022-11-07 10:30:13 +00:00
|
|
|
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}')
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(401, 'access denied', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
logging.debug(f">> payload {data}")
|
|
|
|
|
2022-11-07 10:30:13 +00:00
|
|
|
await run_processor(request, actor, data, software)
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(status=202)
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
@register_route('GET', '/.well-known/webfinger')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def webfinger(request):
|
|
|
|
subject = request.query['resource']
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
if subject != f'acct:relay@{request.app.config.host}':
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new_error(404, 'user not found', 'json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
data = {
|
|
|
|
'subject': subject,
|
2022-11-07 12:54:32 +00:00
|
|
|
'aliases': [request.app.config.actor],
|
2022-05-06 07:04:51 +00:00
|
|
|
'links': [
|
2022-11-07 12:54:32 +00:00
|
|
|
{'href': request.app.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\"'}
|
2022-05-06 07:04:51 +00:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(data, ctype='json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-07 13:18:25 +00:00
|
|
|
@register_route('GET', '/nodeinfo/{version:\d.\d\.json}')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def nodeinfo_2_0(request):
|
2022-11-09 11:11:16 +00:00
|
|
|
niversion = request.match_info['version'][:3]
|
2022-05-06 07:04:51 +00:00
|
|
|
data = {
|
2022-11-09 11:11:16 +00:00
|
|
|
'openRegistrations': not request.app.config.whitelist_enabled,
|
2022-05-06 07:04:51 +00:00
|
|
|
'protocols': ['activitypub'],
|
|
|
|
'services': {
|
|
|
|
'inbound': [],
|
|
|
|
'outbound': []
|
|
|
|
},
|
|
|
|
'software': {
|
|
|
|
'name': 'activityrelay',
|
2022-06-06 12:37:08 +00:00
|
|
|
'version': version
|
2022-05-06 07:04:51 +00:00
|
|
|
},
|
|
|
|
'usage': {
|
|
|
|
'localPosts': 0,
|
|
|
|
'users': {
|
|
|
|
'total': 1
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'metadata': {
|
2022-11-07 12:54:32 +00:00
|
|
|
'peers': request.app.database.hostnames
|
2022-05-06 07:04:51 +00:00
|
|
|
},
|
2022-11-09 11:11:16 +00:00
|
|
|
'version': niversion
|
2022-05-06 07:04:51 +00:00
|
|
|
}
|
|
|
|
|
2022-11-07 13:18:25 +00:00
|
|
|
if version == '2.1':
|
|
|
|
data['software']['repository'] = 'https://git.pleroma.social/pleroma/relay'
|
|
|
|
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(data, ctype='json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
@register_route('GET', '/.well-known/nodeinfo')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def nodeinfo_wellknown(request):
|
2022-11-07 13:18:25 +00:00
|
|
|
data = WKNodeinfo.new(
|
|
|
|
v20 = f'https://{request.app.config.host}/nodeinfo/2.0.json',
|
|
|
|
v21 = f'https://{request.app.config.host}/nodeinfo/2.1.json'
|
|
|
|
)
|
|
|
|
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(data, ctype='json')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-07 12:54:32 +00:00
|
|
|
@register_route('GET', '/stats')
|
2022-05-06 07:04:51 +00:00
|
|
|
async def stats(request):
|
2022-11-09 10:58:35 +00:00
|
|
|
return Response.new(STATS, ctype='json')
|