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
class RelayDatabase:
class RelayDatabase(dict):
def __init__(self, config):
dict.__init__(self, {
'relay-list': {},
'private-key': None,
'version': 1
})
self.config = config
self.data = None
self.PRIVKEY = None
@ -25,26 +30,23 @@ class RelayDatabase:
@property
def privkey(self):
try:
return self.data['private-key']
except KeyError:
return False
return self['private-key']
@property
def hostnames(self):
return [urlparse(inbox).hostname for inbox in self.inboxes]
return tuple(self['relay-list'].keys())
@property
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):
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):
@ -52,14 +54,31 @@ class RelayDatabase:
try:
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:
self.data['private-key'] = key.get('privateKey')
if self['version'] == None:
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
except FileNotFoundError:
@ -69,14 +88,6 @@ class RelayDatabase:
if self.config.db.stat().st_size > 0:
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:
logging.info("No actor keys present, generating 4096-bit RSA keypair.")
self.generate_key()
@ -90,34 +101,57 @@ class RelayDatabase:
def save(self):
with self.config.db.open('w') as fd:
data = {
'relay-list': self.inboxes,
'private-key': self.privkey
}
json.dump(data, fd, indent=4)
json.dump(self, fd, indent=4)
def get_inbox(self, domain):
def get_inbox(self, domain, fail=False):
if domain.startswith('http'):
domain = urlparse(domain).hostname
for inbox in self.inboxes:
if domain == urlparse(inbox).hostname:
return inbox
if domain not in self['relay-list']:
if fail:
raise KeyError(domain)
return
return self['relay-list'][domain]
def add_inbox(self, inbox):
assert inbox.startswith('https')
assert not self.get_inbox(inbox)
def add_inbox(self, inbox, followid=None, fail=False):
assert inbox.startswith('https'), 'Inbox must be a url'
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):
inbox = self.get_inbox(inbox_url)
def del_inbox(self, domain, followid=None, fail=False):
data = self.get_inbox(domain, fail=True)
if not inbox:
raise KeyError(inbox_url)
if not data['followid'] or not followid or data['followid'] == followid:
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'):
actor = f'https://{actor}/actor'
if not database.get_inbox(actor):
return click.echo(f'Error: Not following actor: {actor}')
database.del_inbox(actor)
if database.del_inbox(actor):
database.save()
run_in_loop(misc.unfollow_remote_actor, actor)
click.echo(f'Sent unfollow message to: {actor}')
return click.echo(f'Sent unfollow message to: {actor}')
return click.echo(f'Error: Not following actor: {actor}')
@cli_inbox.command('add')
@ -121,17 +119,14 @@ def cli_inbox_add(inbox):
if not inbox.startswith('http'):
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):
click.echo(f'Error: Refusing to add banned inbox: {inbox}')
return
return click.echo(f'Error: Refusing to add banned inbox: {inbox}')
database.add_inbox(inbox)
if database.add_inbox(inbox):
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')
@ -140,14 +135,17 @@ def cli_inbox_remove(inbox):
'Remove an inbox from the 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}')
return
database.del_inbox(dbinbox)
database.del_inbox(dbinbox['domain'])
database.save()
click.echo(f'Removed inbox from the database: {inbox}')
@ -174,13 +172,14 @@ def cli_instance_ban(target):
config = app['config']
database = app['database']
inbox = database.get_inbox(target)
if target.startswith('http'):
target = urlparse(target).hostname
if config.ban_instance(target):
config.save()
if inbox:
database.del_inbox(inbox)
if database.del_inbox(target):
database.save()
click.echo(f'Banned instance: {target}')
@ -321,15 +320,14 @@ def cli_whitelist_remove(instance):
config = app['config']
database = app['database']
inbox = database.get_inbox(instance)
if not config.del_whitelist(instance):
return click.echo(f'Instance not in the whitelist: {instance}')
config.save()
if inbox and config.whitelist_enabled:
database.del_inbox(inbox)
if config.whitelist_enabled:
if database.del_inbox(inbox):
database.save()
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)
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 session.request(method, uri, headers=headers, data=data) as resp:
## aiohttp has been known to leak if the response hasn't been read,

View file

@ -60,10 +60,12 @@ async def handle_follow(actor, data, request):
database = app['database']
inbox = misc.get_actor_inbox(actor)
dbinbox = database.get_inbox(inbox)
if inbox not in database.inboxes:
database.add_inbox(inbox)
if not database.add_inbox(inbox, data['id']):
database.set_followid(inbox, data['id'])
database.save()
asyncio.ensure_future(misc.follow_remote_actor(actor['id']))
message = {
@ -92,12 +94,11 @@ async def handle_undo(actor, data, request):
return await handle_forward(actor, data, request)
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
database.del_inbox(inbox)
database.save()
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>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>
</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(
status = 200,
@ -89,11 +89,11 @@ async def inbox(request):
actor_id = data['actor']
actor_domain = urlparse(actor_id).hostname
## reject if there is no actor in the message
except KeyError:
logging.verbose('actor not in data')
raise HTTPUnauthorized(body='no actor in message')
## reject if there is no actor in the message
except:
traceback.print_exc()
logging.verbose('Failed to parse inbox message')