prevent old unfollows from booting instances

This commit is contained in:
Izalia Mae 2022-11-05 22:15:37 -04:00
parent 4d121adaa2
commit dcb7980c50
5 changed files with 115 additions and 76 deletions

View file

@ -6,10 +6,15 @@ from Crypto.PublicKey import RSA
from urllib.parse import urlparse from urllib.parse import urlparse
class RelayDatabase: class RelayDatabase(dict):
def __init__(self, config): def __init__(self, config):
dict.__init__(self, {
'relay-list': {},
'private-key': None,
'version': 1
})
self.config = config self.config = config
self.data = None
self.PRIVKEY = None self.PRIVKEY = None
@ -25,26 +30,23 @@ class RelayDatabase:
@property @property
def privkey(self): def privkey(self):
try: return self['private-key']
return self.data['private-key']
except KeyError:
return False
@property @property
def hostnames(self): def hostnames(self):
return [urlparse(inbox).hostname for inbox in self.inboxes] return tuple(self['relay-list'].keys())
@property @property
def inboxes(self): def inboxes(self):
return self.data.get('relay-list', []) return tuple(data['inbox'] for data in self['relay-list'].values())
return self['relay-list']
def generate_key(self): def generate_key(self):
self.PRIVKEY = RSA.generate(4096) self.PRIVKEY = RSA.generate(4096)
self.data['private-key'] = self.PRIVKEY.exportKey('PEM').decode('utf-8') self['private-key'] = self.PRIVKEY.exportKey('PEM').decode('utf-8')
def load(self): def load(self):
@ -52,14 +54,31 @@ class RelayDatabase:
try: try:
with self.config.db.open() as fd: with self.config.db.open() as fd:
self.data = json.load(fd) data = json.load(fd)
key = self.data.pop('actorKeys', None) self['version'] = data.get('version', None)
self['private-key'] = data.get('private-key')
if key: if self['version'] == None:
self.data['private-key'] = key.get('privateKey') self['version'] = 1
if 'actorKeys' in data:
self['private-key'] = data['actorKeys']['privateKey']
for item in data.get('relay-list', []):
domain = urlparse(item).hostname
self['relay-list'][domain] = {
'inbox': item,
'followid': None
}
else:
self['relay-list'] = data.get('relay-list', {})
for domain in self['relay-list'].keys():
if self.config.is_banned(domain) or (self.config.whitelist_enabled and not self.config.is_whitelisted(domain)):
self.del_inbox(domain)
self.data.pop('actors', None)
new_db = False new_db = False
except FileNotFoundError: except FileNotFoundError:
@ -69,14 +88,6 @@ class RelayDatabase:
if self.config.db.stat().st_size > 0: if self.config.db.stat().st_size > 0:
raise e from None raise e from None
if not self.data:
logging.info('No database was found. Making a new one.')
self.data = {}
for inbox in self.inboxes:
if self.config.is_banned(inbox) or (self.config.whitelist_enabled and not self.config.is_whitelisted(inbox)):
self.del_inbox(inbox)
if not self.privkey: if not self.privkey:
logging.info("No actor keys present, generating 4096-bit RSA keypair.") logging.info("No actor keys present, generating 4096-bit RSA keypair.")
self.generate_key() self.generate_key()
@ -90,34 +101,57 @@ class RelayDatabase:
def save(self): def save(self):
with self.config.db.open('w') as fd: with self.config.db.open('w') as fd:
data = { json.dump(self, fd, indent=4)
'relay-list': self.inboxes,
'private-key': self.privkey
}
json.dump(data, fd, indent=4)
def get_inbox(self, domain): def get_inbox(self, domain, fail=False):
if domain.startswith('http'): if domain.startswith('http'):
domain = urlparse(domain).hostname domain = urlparse(domain).hostname
for inbox in self.inboxes: if domain not in self['relay-list']:
if domain == urlparse(inbox).hostname: if fail:
return inbox raise KeyError(domain)
return
return self['relay-list'][domain]
def add_inbox(self, inbox): def add_inbox(self, inbox, followid=None, fail=False):
assert inbox.startswith('https') assert inbox.startswith('https'), 'Inbox must be a url'
assert not self.get_inbox(inbox) domain = urlparse(inbox).hostname
self.data['relay-list'].append(inbox) if self.get_inbox(domain):
if fail:
raise KeyError(domain)
return False
self['relay-list'][domain] = {
'domain': domain,
'inbox': inbox,
'followid': followid
}
logging.verbose(f'Added inbox to database: {inbox}')
return self['relay-list'][domain]
def del_inbox(self, inbox_url): def del_inbox(self, domain, followid=None, fail=False):
inbox = self.get_inbox(inbox_url) data = self.get_inbox(domain, fail=True)
if not inbox: if not data['followid'] or not followid or data['followid'] == followid:
raise KeyError(inbox_url) del self['relay-list'][data['domain']]
logging.verbose(f'Removed inbox from database: {data["inbox"]}')
return True
self.data['relay-list'].remove(inbox) if fail:
raise ValueError('Follow IDs do not match')
logging.debug(f'Follow ID does not match: db = {data["followid"]}, object = {followid}')
return False
def set_followid(self, domain, followid):
data = self.get_inbox(domain, fail=True)
data['followid'] = followid

View file

@ -100,14 +100,12 @@ def cli_inbox_unfollow(actor):
if not actor.startswith('http'): if not actor.startswith('http'):
actor = f'https://{actor}/actor' actor = f'https://{actor}/actor'
if not database.get_inbox(actor): if database.del_inbox(actor):
return click.echo(f'Error: Not following actor: {actor}') database.save()
run_in_loop(misc.unfollow_remote_actor, actor)
return click.echo(f'Sent unfollow message to: {actor}')
database.del_inbox(actor) return click.echo(f'Error: Not following actor: {actor}')
database.save()
run_in_loop(misc.unfollow_remote_actor, actor)
click.echo(f'Sent unfollow message to: {actor}')
@cli_inbox.command('add') @cli_inbox.command('add')
@ -121,17 +119,14 @@ def cli_inbox_add(inbox):
if not inbox.startswith('http'): if not inbox.startswith('http'):
inbox = f'https://{inbox}/inbox' inbox = f'https://{inbox}/inbox'
if database.get_inbox(inbox):
click.echo(f'Error: Inbox already in database: {inbox}')
return
if config.is_banned(inbox): if config.is_banned(inbox):
click.echo(f'Error: Refusing to add banned inbox: {inbox}') return click.echo(f'Error: Refusing to add banned inbox: {inbox}')
return
database.add_inbox(inbox) if database.add_inbox(inbox):
database.save() database.save()
click.echo(f'Added inbox to the database: {inbox}') return click.echo(f'Added inbox to the database: {inbox}')
click.echo(f'Error: Inbox already in database: {inbox}')
@cli_inbox.command('remove') @cli_inbox.command('remove')
@ -140,14 +135,17 @@ def cli_inbox_remove(inbox):
'Remove an inbox from the database' 'Remove an inbox from the database'
database = app['database'] database = app['database']
dbinbox = database.get_inbox(inbox)
if not dbinbox: try:
dbinbox = database.get_inbox(inbox, fail=True)
except KeyError:
click.echo(f'Error: Inbox does not exist: {inbox}') click.echo(f'Error: Inbox does not exist: {inbox}')
return return
database.del_inbox(dbinbox) database.del_inbox(dbinbox['domain'])
database.save() database.save()
click.echo(f'Removed inbox from the database: {inbox}') click.echo(f'Removed inbox from the database: {inbox}')
@ -174,13 +172,14 @@ def cli_instance_ban(target):
config = app['config'] config = app['config']
database = app['database'] database = app['database']
inbox = database.get_inbox(target)
if target.startswith('http'):
target = urlparse(target).hostname
if config.ban_instance(target): if config.ban_instance(target):
config.save() config.save()
if inbox: if database.del_inbox(target):
database.del_inbox(inbox)
database.save() database.save()
click.echo(f'Banned instance: {target}') click.echo(f'Banned instance: {target}')
@ -321,16 +320,15 @@ def cli_whitelist_remove(instance):
config = app['config'] config = app['config']
database = app['database'] database = app['database']
inbox = database.get_inbox(instance)
if not config.del_whitelist(instance): if not config.del_whitelist(instance):
return click.echo(f'Instance not in the whitelist: {instance}') return click.echo(f'Instance not in the whitelist: {instance}')
config.save() config.save()
if inbox and config.whitelist_enabled: if config.whitelist_enabled:
database.del_inbox(inbox) if database.del_inbox(inbox):
database.save() database.save()
click.echo(f'Removed instance from the whitelist: {instance}') click.echo(f'Removed instance from the whitelist: {instance}')

View file

@ -255,6 +255,12 @@ async def request(uri, data=None, force=False, sign_headers=True, activity=True)
headers.update(signing_headers) headers.update(signing_headers)
try: try:
if data:
logging.verbose(f'Sending "{action}" to inbox: {uri}')
else:
logging.verbose(f'Sending GET request to url: {uri}')
async with ClientSession(trace_configs=http_debug()) as session, app['semaphore']: async with ClientSession(trace_configs=http_debug()) as session, app['semaphore']:
async with session.request(method, uri, headers=headers, data=data) as resp: async with session.request(method, uri, headers=headers, data=data) as resp:
## aiohttp has been known to leak if the response hasn't been read, ## aiohttp has been known to leak if the response hasn't been read,

View file

@ -60,11 +60,13 @@ async def handle_follow(actor, data, request):
database = app['database'] database = app['database']
inbox = misc.get_actor_inbox(actor) inbox = misc.get_actor_inbox(actor)
dbinbox = database.get_inbox(inbox)
if inbox not in database.inboxes: if not database.add_inbox(inbox, data['id']):
database.add_inbox(inbox) database.set_followid(inbox, data['id'])
database.save() database.save()
asyncio.ensure_future(misc.follow_remote_actor(actor['id']))
asyncio.ensure_future(misc.follow_remote_actor(actor['id']))
message = { message = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
@ -92,12 +94,11 @@ async def handle_undo(actor, data, request):
return await handle_forward(actor, data, request) return await handle_forward(actor, data, request)
database = app['database'] database = app['database']
inbox = database.get_inbox(actor['id']) objectid = misc.distill_object_id(data)
if not inbox: if not database.del_inbox(actor['id'], objectid):
return return
database.del_inbox(inbox)
database.save() database.save()
await misc.unfollow_remote_actor(actor['id']) await misc.unfollow_remote_actor(actor['id'])

View file

@ -37,7 +37,7 @@ a:hover {{ color: #8AF; }}
<p>You may subscribe to this relay with the address: <a href="https://{host}/actor">https://{host}/actor</a></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> <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> <br><p>List of {count} registered instances:<br>{targets}</p>
</body></html>""".format(host=request.host, note=app['config'].note, targets=targets, count=len(app['database'].inboxes)) </body></html>""".format(host=request.host, note=app['config'].note, targets=targets, count=len(app['database'].hostnames))
return Response( return Response(
status = 200, status = 200,
@ -89,11 +89,11 @@ async def inbox(request):
actor_id = data['actor'] actor_id = data['actor']
actor_domain = urlparse(actor_id).hostname actor_domain = urlparse(actor_id).hostname
## 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') raise HTTPUnauthorized(body='no actor in message')
## reject if there is no actor in the message
except: except:
traceback.print_exc() traceback.print_exc()
logging.verbose('Failed to parse inbox message') logging.verbose('Failed to parse inbox message')