diff --git a/relay/database.py b/relay/database.py index 6e71f56..bc08414 100644 --- a/relay/database.py +++ b/relay/database.py @@ -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 diff --git a/relay/manage.py b/relay/manage.py index 4aae7c7..fb76236 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -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}') + if database.del_inbox(actor): + database.save() + run_in_loop(misc.unfollow_remote_actor, actor) + return click.echo(f'Sent unfollow message to: {actor}') - 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'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) - database.save() - click.echo(f'Added inbox to the database: {inbox}') + if database.add_inbox(inbox): + database.save() + 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,16 +320,15 @@ 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) - database.save() + if config.whitelist_enabled: + if database.del_inbox(inbox): + database.save() click.echo(f'Removed instance from the whitelist: {instance}') diff --git a/relay/misc.py b/relay/misc.py index 139192d..9161f53 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -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, diff --git a/relay/processors.py b/relay/processors.py index 9377b65..b53ace6 100644 --- a/relay/processors.py +++ b/relay/processors.py @@ -60,11 +60,13 @@ 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'])) + + asyncio.ensure_future(misc.follow_remote_actor(actor['id'])) message = { "@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) 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']) diff --git a/relay/views.py b/relay/views.py index 6eac9d7..a5da8ff 100644 --- a/relay/views.py +++ b/relay/views.py @@ -37,7 +37,7 @@ a:hover {{ color: #8AF; }}
You may subscribe to this relay with the address: https://{host}/actor
To host your own relay, you may download the code at this address: https://git.pleroma.social/pleroma/relay
List of {count} registered instances:
{targets}