mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-10 02:17:59 +00:00
prevent old unfollows from booting instances
This commit is contained in:
parent
4d121adaa2
commit
dcb7980c50
|
@ -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
|
||||||
|
|
|
@ -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.del_inbox(actor)
|
|
||||||
database.save()
|
database.save()
|
||||||
|
|
||||||
run_in_loop(misc.unfollow_remote_actor, actor)
|
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')
|
@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,15 +320,14 @@ 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}')
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -60,10 +60,12 @@ 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 = {
|
||||||
|
@ -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'])
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue