2022-11-18 19:10:39 +00:00
|
|
|
import asyncio
|
2022-05-06 07:04:51 +00:00
|
|
|
import json
|
2018-08-10 19:59:46 +00:00
|
|
|
import logging
|
2022-05-06 07:04:51 +00:00
|
|
|
import traceback
|
2018-08-10 19:59:46 +00:00
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
from urllib.parse import urlparse
|
2018-08-10 19:59:46 +00:00
|
|
|
|
2022-11-18 19:10:39 +00:00
|
|
|
from .misc import fetch_nodeinfo
|
|
|
|
|
2018-08-10 19:59:46 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
class RelayDatabase(dict):
|
2022-05-06 07:04:51 +00:00
|
|
|
def __init__(self, config):
|
2022-11-06 02:15:37 +00:00
|
|
|
dict.__init__(self, {
|
|
|
|
'relay-list': {},
|
|
|
|
'private-key': None,
|
2022-11-09 09:54:46 +00:00
|
|
|
'follow-requests': {},
|
2022-11-06 02:15:37 +00:00
|
|
|
'version': 1
|
|
|
|
})
|
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
self.config = config
|
|
|
|
self.PRIVKEY = None
|
2019-09-30 10:45:46 +00:00
|
|
|
|
2018-08-10 19:59:46 +00:00
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
@property
|
|
|
|
def PUBKEY(self):
|
|
|
|
return self.PRIVKEY.publickey()
|
2019-09-30 10:45:46 +00:00
|
|
|
|
2019-05-21 16:29:55 +00:00
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
@property
|
|
|
|
def pubkey(self):
|
|
|
|
return self.PUBKEY.exportKey('PEM').decode('utf-8')
|
2020-11-22 05:50:57 +00:00
|
|
|
|
2018-08-10 19:59:46 +00:00
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
@property
|
|
|
|
def privkey(self):
|
2022-11-06 02:15:37 +00:00
|
|
|
return self['private-key']
|
2018-08-10 19:59:46 +00:00
|
|
|
|
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
@property
|
|
|
|
def hostnames(self):
|
2022-11-06 02:15:37 +00:00
|
|
|
return tuple(self['relay-list'].keys())
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
def inboxes(self):
|
2022-11-06 02:15:37 +00:00
|
|
|
return tuple(data['inbox'] for data in self['relay-list'].values())
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
def generate_key(self):
|
|
|
|
self.PRIVKEY = RSA.generate(4096)
|
2022-11-06 02:15:37 +00:00
|
|
|
self['private-key'] = self.PRIVKEY.exportKey('PEM').decode('utf-8')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
def load(self):
|
|
|
|
new_db = True
|
|
|
|
|
|
|
|
try:
|
|
|
|
with self.config.db.open() as fd:
|
2022-11-06 02:15:37 +00:00
|
|
|
data = json.load(fd)
|
|
|
|
|
|
|
|
self['version'] = data.get('version', None)
|
|
|
|
self['private-key'] = data.get('private-key')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
if self['version'] == None:
|
|
|
|
self['version'] = 1
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
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', {})
|
|
|
|
|
2022-11-18 19:10:39 +00:00
|
|
|
for domain, instance in self['relay-list'].items():
|
2022-11-06 02:15:37 +00:00
|
|
|
if self.config.is_banned(domain) or (self.config.whitelist_enabled and not self.config.is_whitelisted(domain)):
|
|
|
|
self.del_inbox(domain)
|
2022-11-18 19:10:39 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
if not instance.get('software'):
|
|
|
|
nodeinfo = asyncio.run(fetch_nodeinfo(domain))
|
|
|
|
|
|
|
|
if not nodeinfo:
|
|
|
|
continue
|
|
|
|
|
|
|
|
instance['software'] = nodeinfo.swname
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
new_db = False
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
except json.decoder.JSONDecodeError as e:
|
|
|
|
if self.config.db.stat().st_size > 0:
|
|
|
|
raise e from None
|
|
|
|
|
|
|
|
if not self.privkey:
|
|
|
|
logging.info("No actor keys present, generating 4096-bit RSA keypair.")
|
|
|
|
self.generate_key()
|
|
|
|
|
|
|
|
else:
|
|
|
|
self.PRIVKEY = RSA.importKey(self.privkey)
|
|
|
|
|
|
|
|
self.save()
|
|
|
|
return not new_db
|
|
|
|
|
|
|
|
|
|
|
|
def save(self):
|
|
|
|
with self.config.db.open('w') as fd:
|
2022-11-06 02:15:37 +00:00
|
|
|
json.dump(self, fd, indent=4)
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
def get_inbox(self, domain, fail=False):
|
2022-05-06 07:04:51 +00:00
|
|
|
if domain.startswith('http'):
|
|
|
|
domain = urlparse(domain).hostname
|
|
|
|
|
2022-11-18 19:36:30 +00:00
|
|
|
inbox = self['relay-list'].get(domain)
|
2022-11-06 02:15:37 +00:00
|
|
|
|
2022-11-18 19:36:30 +00:00
|
|
|
if inbox:
|
|
|
|
return inbox
|
2022-11-06 02:15:37 +00:00
|
|
|
|
2022-11-18 19:36:30 +00:00
|
|
|
if fail:
|
|
|
|
raise KeyError(domain)
|
2022-11-06 02:15:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
def add_inbox(self, inbox, followid=None, fail=False):
|
|
|
|
assert inbox.startswith('https'), 'Inbox must be a url'
|
|
|
|
domain = urlparse(inbox).hostname
|
|
|
|
|
|
|
|
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]
|
2022-05-06 07:04:51 +00:00
|
|
|
|
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
def del_inbox(self, domain, followid=None, fail=False):
|
2022-11-07 14:53:04 +00:00
|
|
|
data = self.get_inbox(domain, fail=False)
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
if fail:
|
|
|
|
raise KeyError(domain)
|
|
|
|
|
|
|
|
return False
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
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
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
if fail:
|
|
|
|
raise ValueError('Follow IDs do not match')
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
logging.debug(f'Follow ID does not match: db = {data["followid"]}, object = {followid}')
|
|
|
|
return False
|
2022-05-06 22:08:55 +00:00
|
|
|
|
2022-05-06 07:04:51 +00:00
|
|
|
|
2022-11-06 02:15:37 +00:00
|
|
|
def set_followid(self, domain, followid):
|
|
|
|
data = self.get_inbox(domain, fail=True)
|
|
|
|
data['followid'] = followid
|
2022-11-09 09:54:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_request(self, domain, fail=True):
|
|
|
|
if domain.startswith('http'):
|
|
|
|
domain = urlparse(domain).hostname
|
|
|
|
|
|
|
|
try:
|
|
|
|
return self['follow-requests'][domain]
|
|
|
|
|
|
|
|
except KeyError as e:
|
|
|
|
if fail:
|
|
|
|
raise e
|
|
|
|
|
|
|
|
|
|
|
|
def add_request(self, actor, inbox, followid):
|
|
|
|
domain = urlparse(inbox).hostname
|
|
|
|
|
|
|
|
try:
|
|
|
|
request = self.get_request(domain)
|
|
|
|
request['followid'] = followid
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
self['follow-requests'][domain] = {
|
|
|
|
'actor': actor,
|
|
|
|
'inbox': inbox,
|
|
|
|
'followid': followid
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def del_request(self, domain):
|
|
|
|
if domain.startswith('http'):
|
|
|
|
domain = urlparse(inbox).hostname
|
|
|
|
|
|
|
|
del self['follow-requests'][domain]
|