a lot of changes
This commit is contained in:
parent
62e045fb66
commit
de081b0463
|
@ -2,3 +2,5 @@
|
|||
Uncia Relay by Zoey Mae
|
||||
https://git.barkshark.xyz/izaliamae/uncia
|
||||
'''
|
||||
import sys, os
|
||||
sys.path.append(f'{os.getcwd()}/modules')
|
||||
|
|
|
@ -72,7 +72,7 @@ except:
|
|||
|
||||
dbconfig = {
|
||||
'host': env.get('DBHOST'),
|
||||
'port': env.get('DBPORT', 54321),
|
||||
'port': int(env.get('DBPORT', 5432)),
|
||||
'user': env.get('DBUSER', env.get('USER')),
|
||||
'pass': env.get('DBPASS'),
|
||||
'name': env.get('DBNAME', 'uncia'),
|
||||
|
|
|
@ -101,7 +101,7 @@ def setup(db=None):
|
|||
'whitelist': False,
|
||||
'block_relays': True,
|
||||
'require_approval': True,
|
||||
'notifications': False,
|
||||
'notification': False,
|
||||
'log_level': 'INFO',
|
||||
'development': False,
|
||||
'setup': False
|
||||
|
|
|
@ -4,7 +4,8 @@ from Crypto.PublicKey import RSA
|
|||
|
||||
from . import *
|
||||
from . import bool_check as bcheck, dbcache
|
||||
from ..log import logging
|
||||
from ..Lib.IzzyLib import logging
|
||||
from ..functions import DotDict
|
||||
|
||||
Hash = HashContext()
|
||||
Hash.setsalt()
|
||||
|
@ -35,17 +36,20 @@ def rsa_key(actor, db=None, cached=True):
|
|||
actor_key = db.insert('keys', new_key)
|
||||
db.end()
|
||||
|
||||
actor_key = DotDict(actor_key)
|
||||
actor_key.update({
|
||||
'PRIVKEY': RSA.importKey(actor_key['privkey']),
|
||||
'PUBKEY': RSA.importKey(actor_key['pubkey'])
|
||||
})
|
||||
|
||||
actor_key.size = actor_key.PRIVKEY.size_in_bytes()/2
|
||||
|
||||
dbcache.key.store(actor, actor_key)
|
||||
return actor_key
|
||||
|
||||
|
||||
@connection
|
||||
def config(data, cache=True, db=None):
|
||||
def config(data, default=None, cache=True, db=None):
|
||||
if len(dbcache.config.keys()) < 1 and cache:
|
||||
update_config()
|
||||
|
||||
|
@ -91,7 +95,7 @@ def config(data, cache=True, db=None):
|
|||
if row:
|
||||
value = bcheck(row['value'])
|
||||
dbcache.config[data] = value
|
||||
return value
|
||||
return value if value != None else default
|
||||
|
||||
|
||||
@connection
|
||||
|
@ -239,4 +243,4 @@ else:
|
|||
|
||||
# Set log level from config
|
||||
log_level = config('log_level')
|
||||
logging.setLevel(log_level)
|
||||
logging.setLevel(log_level if log_level else 'INFO')
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import pg
|
||||
import ujson as json
|
||||
import pg, json
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
@ -108,10 +107,15 @@ def add_retry(msgid, inbox, data, headers, db=None):
|
|||
if row:
|
||||
return True
|
||||
|
||||
domain_retries = get.retries({'inbox': inbox})
|
||||
|
||||
if len(domain_retries) >= 500:
|
||||
return
|
||||
|
||||
data = {
|
||||
'inbox': inbox,
|
||||
'data': json.dumps(data, escape_forward_slashes=False),
|
||||
'headers': json.dumps(headers, escape_forward_slashes=False),
|
||||
'data': json.dumps(data),
|
||||
'headers': json.dumps(headers),
|
||||
'msgid': msgid,
|
||||
'timestamp': datetime.now().timestamp()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import traceback
|
|||
from sanic import response
|
||||
|
||||
from .log import logging
|
||||
from .templates import error
|
||||
|
||||
|
||||
def logstr(request, status, e=False):
|
||||
|
@ -13,24 +14,20 @@ def logstr(request, status, e=False):
|
|||
|
||||
|
||||
def not_found(request, exception):
|
||||
from .templates import error
|
||||
return error(request, f'Not found: {request.path}', 404)
|
||||
|
||||
|
||||
def method_not_supported(request, exception):
|
||||
from .templates import error
|
||||
return error(request, f'Invalid method: {request.method}', 405)
|
||||
|
||||
|
||||
def server_error(request, exception):
|
||||
from .templates import error
|
||||
logstr(request, 500, e=exception)
|
||||
msg = 'OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!'
|
||||
return error(request, msg, 500)
|
||||
|
||||
|
||||
def no_template(request, exception):
|
||||
from .templates import error
|
||||
logstr(request, 500, e=exception)
|
||||
msg = 'I\'m a dumbass and forgot to create a template for this page'
|
||||
return error(request, msg, 500)
|
||||
|
|
|
@ -194,7 +194,7 @@
|
|||
%tr
|
||||
%td{'class': 'col1 instance'}
|
||||
-if user.domain != 'any'
|
||||
%a{'href': 'https://{{user.domain}}/user/{{user.user}}', 'target': '_new'}
|
||||
%a{'href': 'https://{{user.domain}}/users/{{user.user}}', 'target': '_new'}
|
||||
{{user.user}}@{{user.domain}}
|
||||
|
||||
-else
|
||||
|
|
|
@ -20,9 +20,67 @@ from .config import script_path, stor_path, version, pyv
|
|||
httpclient = urllib3.PoolManager(num_pools=100, timeout=urllib3.Timeout(connect=15, read=15))
|
||||
|
||||
|
||||
def format_urls(urls):
|
||||
from .messages import fetch
|
||||
def defhead():
|
||||
from .database import get
|
||||
|
||||
host = get.config('host')
|
||||
data = {
|
||||
'User-Agent': f'python/{pyv[0]}.{pyv[1]}.{pyv[2]} (UnciaRelay/{version}; +https://{host})',
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def fetch(url, cached=True, signed=False, new_headers={}):
|
||||
from .signatures import SignHeaders
|
||||
from .database import get
|
||||
cached_data = cache.url.fetch(url)
|
||||
host = get.config('host')
|
||||
|
||||
if cached and cached_data:
|
||||
logging.debug(f'Returning cached data for {url}')
|
||||
return cached_data
|
||||
|
||||
headers = defhead()
|
||||
headers.update(new_headers)
|
||||
headers.update({'Accept': 'application/json'})
|
||||
|
||||
if signed:
|
||||
headers = SignHeaders(headers, 'default', f'https://{host}/actor#main-key', url, 'get')
|
||||
|
||||
try:
|
||||
logging.debug(f'Fetching new data for {url}')
|
||||
response = httpclient.request('GET', url, headers=headers)
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f'Failed to fetch {url}')
|
||||
logging.debug(e)
|
||||
return
|
||||
|
||||
if response.data == b'':
|
||||
logging.debug(f'Received blank data while fetching url: {url}')
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
|
||||
if cached:
|
||||
logging.debug(f'Caching {url}')
|
||||
cache.url.store(url, data)
|
||||
|
||||
if data.get('error'):
|
||||
return
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f'Failed to load data: {response.data}')
|
||||
logging.debug(e)
|
||||
return
|
||||
|
||||
|
||||
|
||||
def format_urls(urls):
|
||||
actor = urls.get('actor')
|
||||
inbox = urls.get('inbox')
|
||||
domain = urls.get('domain')
|
||||
|
@ -67,7 +125,6 @@ def get_id(data):
|
|||
|
||||
|
||||
def get_user(user):
|
||||
from .messages import fetch
|
||||
username, domain = user.split('@')
|
||||
webfinger = fetch(f'https://{domain}/.well-known/webfinger?resource=acct:{user}')
|
||||
|
||||
|
@ -87,7 +144,6 @@ def get_user(user):
|
|||
|
||||
|
||||
def get_post_user(data):
|
||||
from .messages import fetch
|
||||
if data['type'] in ['Follow', 'Undo']:
|
||||
return
|
||||
|
||||
|
@ -121,7 +177,7 @@ def get_post_user(data):
|
|||
|
||||
|
||||
|
||||
def format_date(timestamp):
|
||||
def format_date(timestamp=None):
|
||||
if timestamp:
|
||||
date = datetime.fromtimestamp(timestamp)
|
||||
|
||||
|
@ -222,6 +278,49 @@ class LRUCache(OrderedDict):
|
|||
return None
|
||||
|
||||
|
||||
class DotDict(dict):
|
||||
__setattr__ = dict.__setitem__
|
||||
__delattr__ = dict.__delitem__
|
||||
|
||||
def __init__(self, value=None, **kwargs):
|
||||
super().__init__()
|
||||
|
||||
if value.__class__ == str:
|
||||
self.FromJson(value)
|
||||
|
||||
elif value.__class__ in [dict, DotDict]:
|
||||
self.update(value)
|
||||
|
||||
elif value:
|
||||
raise TypeError('The value must be a JSON string, dict, or another DotDict object, not', value.__class__)
|
||||
|
||||
if kwargs:
|
||||
self.update(kwargs)
|
||||
|
||||
|
||||
def __getattr__(self, value, default=None):
|
||||
val = self.get(value, default) if default else self[value]
|
||||
|
||||
return DotDict(val) if type(val) == dict else val
|
||||
|
||||
|
||||
def ToJson(self, **kwargs):
|
||||
return self.__str__(**kwargs)
|
||||
|
||||
|
||||
def FromJson(self, string):
|
||||
data = json.loads(string)
|
||||
self.update(data)
|
||||
|
||||
|
||||
def AsDict(self):
|
||||
return {k: v for k, v in self.items() if not k.startswith('__')}
|
||||
|
||||
|
||||
def __parse_item(self, data):
|
||||
return DotDict(data) if type(data) == dict else data
|
||||
|
||||
|
||||
cache_size = 2048
|
||||
|
||||
class cache:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import sys
|
||||
import logging as logger
|
||||
import logging.config as logconf
|
||||
|
||||
from os import environ as env
|
||||
from datetime import datetime
|
||||
|
||||
from .Lib.IzzyLib import logging
|
||||
|
||||
|
||||
# Custom logger
|
||||
class Log():
|
||||
|
@ -131,6 +131,6 @@ LOG = dict(
|
|||
},
|
||||
)
|
||||
|
||||
logconf.dictConfig(LOG)
|
||||
#logconf.dictConfig(LOG)
|
||||
|
||||
logging = Log()
|
||||
#logging = Log()
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import asyncio, threading, uuid, ujson as json
|
||||
import urllib3
|
||||
import asyncio, threading, uuid, traceback, json, base64
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .log import logging
|
||||
from .functions import format_date, get_id, cache, httpclient, get_user, get_inbox
|
||||
from .signatures import sign_headers
|
||||
import urllib3
|
||||
|
||||
from .Lib.IzzyLib import logging
|
||||
from .functions import format_date, get_id, cache, httpclient, get_user, get_inbox, fetch, defhead
|
||||
from .signatures import SignHeaders, SignHeaders, SignBody
|
||||
from .database import get, put
|
||||
from .config import version, pyv
|
||||
|
||||
|
@ -13,15 +14,6 @@ from .config import version, pyv
|
|||
host = get.config('host')
|
||||
|
||||
|
||||
def defhead():
|
||||
host = get.config('host')
|
||||
data = {
|
||||
'User-Agent': f'python/{pyv[0]}.{pyv[1]}.{pyv[2]} (UnciaRelay/{version}; +https://{host})',
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def accept(followid, urls):
|
||||
actor_url = urls['actor']
|
||||
inbox = urls['inbox']
|
||||
|
@ -44,15 +36,11 @@ def accept(followid, urls):
|
|||
'id': f'https://{host}/activities/{UUID}',
|
||||
}
|
||||
|
||||
headers = {
|
||||
'date': format_date(None)
|
||||
}
|
||||
|
||||
if push(inbox, body, headers):
|
||||
if push(inbox, body):
|
||||
put.inbox('add', urls)
|
||||
|
||||
if not get.config('require_approval'):
|
||||
thread = threading.Thread(target=notification, args=(domain))
|
||||
if not get.config('require_approval') and get.config('notification'):
|
||||
thread = threading.Thread(target=notification, args=[domain])
|
||||
thread.start()
|
||||
|
||||
return True
|
||||
|
@ -70,19 +58,11 @@ def announce(obj_id, inbox):
|
|||
'id': activity_id
|
||||
}
|
||||
|
||||
headers = {
|
||||
'date': format_date(None)
|
||||
}
|
||||
|
||||
push_inboxes(message, headers, origin=inbox)
|
||||
push_inboxes(message, origin=inbox)
|
||||
|
||||
|
||||
def forward(data, inbox):
|
||||
headers = {
|
||||
'date': format_date(None)
|
||||
}
|
||||
|
||||
push_inboxes(data, headers, origin=inbox)
|
||||
push_inboxes(data, origin=inbox)
|
||||
|
||||
|
||||
def paws(url):
|
||||
|
@ -95,14 +75,10 @@ def paws(url):
|
|||
"object": f"https://{host}/paws/lorge"
|
||||
}
|
||||
|
||||
headers = {
|
||||
'date': format_date(None)
|
||||
}
|
||||
|
||||
push(url, data, headers)
|
||||
push(url, data)
|
||||
|
||||
|
||||
def notification(domain, *args):
|
||||
def notification(domain):
|
||||
acct = get.config('admin')
|
||||
admin_user, admin_domain = acct.split('@')
|
||||
admin_uuid = uuid.uuid4()
|
||||
|
@ -145,7 +121,7 @@ def notification(domain, *args):
|
|||
if not get.config('require_approval'):
|
||||
admin_message['object']['content'] = f'<p><a href=\"https://{domain}/about\">{domain}</a> has joined the relay</p>'
|
||||
|
||||
return True if push(inbox, admin_message, None) else False
|
||||
return True if push(inbox, admin_message) else False
|
||||
|
||||
|
||||
### End of messages
|
||||
|
@ -199,7 +175,7 @@ def run_retries(inbox=None, msgid=None):
|
|||
thread.start()
|
||||
|
||||
|
||||
def push_inboxes(data, headers, origin=None):
|
||||
def push_inboxes(data, headers={}, origin=None):
|
||||
object_id = get_id(data)
|
||||
object_domain = urlparse(object_id).netloc
|
||||
threads = []
|
||||
|
@ -209,40 +185,40 @@ def push_inboxes(data, headers, origin=None):
|
|||
domain = inbox['domain']
|
||||
|
||||
if domain != object_domain and inbox_url != origin:
|
||||
threads.append(threading.Thread(target=push, args=(inbox_url, data, headers, 'retry')))
|
||||
threads.append(threading.Thread(target=push, args=(inbox_url, data, headers, True)))
|
||||
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
|
||||
|
||||
def push(inbox, data, headers, *args):
|
||||
def push(inbox, data, headers={}, retry=False):
|
||||
logging.debug(f'Sending message to {inbox}')
|
||||
body = json.dumps(data, escape_forward_slashes=False).encode('utf-8')
|
||||
body = json.dumps(data)
|
||||
url = get_id(data)
|
||||
|
||||
orig_head = {} if not headers else headers.copy()
|
||||
|
||||
if not headers:
|
||||
headers = {}
|
||||
posthost = urlparse(inbox).netloc
|
||||
orig_head = headers.copy()
|
||||
|
||||
headers.update(defhead())
|
||||
|
||||
if 'Content-Type' not in headers:
|
||||
headers.update({'Content-Type': 'application/activity+json'})
|
||||
headers['content-type'] = 'application/activity+json'
|
||||
headers['digest'] = f'SHA-256={SignBody(body)}'
|
||||
|
||||
if headers.get('signature'):
|
||||
del headers['signature']
|
||||
|
||||
headers = sign_headers(headers, 'default', f'https://{host}/actor#main-key', f'get {urlparse(inbox).path}')
|
||||
headers = SignHeaders(headers, 'default', f'https://{host}/actor#main-key', inbox, 'post')
|
||||
|
||||
try:
|
||||
response = httpclient.request('POST', inbox, body=body, headers=headers)
|
||||
respdata = response.data.decode()
|
||||
|
||||
if response.status not in [200, 202]:
|
||||
logging.verbose(f'Failed to push to {inbox}: Error {response.status}')
|
||||
logging.debug(f'Response: {response.data.decode()}')
|
||||
|
||||
if response.status not in [401, 403]:
|
||||
if len(respdata) < 200:
|
||||
logging.debug(f'Response from {posthost}: {respdata}')
|
||||
|
||||
if response.status not in [403]:
|
||||
put.add_retry(url, inbox, data, orig_head)
|
||||
|
||||
else:
|
||||
|
@ -253,49 +229,3 @@ def push(inbox, data, headers, *args):
|
|||
except Exception as e:
|
||||
logging.verbose(f'Connection error when pushing to {inbox}: {e}')
|
||||
put.add_retry(url, inbox, data, orig_head)
|
||||
|
||||
|
||||
def fetch(url, cached=True, signed=False):
|
||||
from .signatures import sign_headers
|
||||
|
||||
cached_data = cache.url.fetch(url)
|
||||
host = get.config('host')
|
||||
|
||||
if cached and cached_data:
|
||||
logging.debug(f'Returning cached data for {url}')
|
||||
return cached_data
|
||||
|
||||
headers = defhead()
|
||||
headers.update({'Accept': 'application/json'})
|
||||
|
||||
headers = sign_headers(headers, 'default', f'https://{host}/actor#main-key', f'get {urlparse(url).path}') if signed else headers
|
||||
|
||||
try:
|
||||
logging.debug(f'Fetching new data for {url}')
|
||||
response = httpclient.request('GET', url, headers=headers)
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f'Failed to fetch {url}')
|
||||
logging.debug(e)
|
||||
return
|
||||
|
||||
if response.data == b'':
|
||||
logging.debug(f'Received blank data while fetching url: {url}')
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
|
||||
if cached:
|
||||
logging.debug(f'Caching {url}')
|
||||
cache.url.store(url, data)
|
||||
|
||||
if data.get('error'):
|
||||
return
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f'Failed to load data: {response.data}')
|
||||
logging.debug(e)
|
||||
return
|
||||
|
|
|
@ -4,7 +4,7 @@ from sanic import response
|
|||
|
||||
from .log import logging
|
||||
from .templates import error
|
||||
from .signatures import validate
|
||||
from .signatures import ValidateRequest
|
||||
from .views import Login
|
||||
from .database import get, put
|
||||
from .config import version
|
||||
|
@ -14,24 +14,25 @@ async def access_log(request, response):
|
|||
response.headers['Server'] = f'Uncia/{version}'
|
||||
response.headers['Trans'] = 'Rights'
|
||||
|
||||
addr = request.headers.get('x-forwarded-for', request.remote_addr)
|
||||
uagent = request.headers.get('user-agent')
|
||||
logging.info(f'{request.remote_addr} {request.method} {request.path} {response.status} "{uagent}"')
|
||||
logging.info(f'{addr} {request.method} {request.path} {response.status} "{uagent}"')
|
||||
|
||||
|
||||
async def query_post_dict(request):
|
||||
request['query'] = {}
|
||||
request['form'] = {}
|
||||
request.ctx.query = {}
|
||||
request.ctx.form = {}
|
||||
|
||||
for k, v in request.query_args:
|
||||
request['query'].update({k: v})
|
||||
request.ctx.query.update({k: v})
|
||||
|
||||
for k, v in request.form.items():
|
||||
request['form'].update({k: v[0]})
|
||||
request.ctx.form.update({k: v[0]})
|
||||
|
||||
|
||||
async def authentication(request):
|
||||
if request.path == '/inbox':
|
||||
valid = validate(request)
|
||||
valid = ValidateRequest(request)
|
||||
data = request.json
|
||||
|
||||
if valid == False and data.get('type') == 'Delete':
|
||||
|
|
|
@ -13,6 +13,7 @@ from .functions import get_inbox, cache, get_id, get_post_user, format_urls
|
|||
def relay_announce(data, actor, urls):
|
||||
object_id = get_id(data)
|
||||
inbox = urls['inbox']
|
||||
instance = urls['domain']
|
||||
|
||||
if not object_id:
|
||||
logging.debug(f'Can\'t find object id')
|
||||
|
@ -26,8 +27,11 @@ def relay_announce(data, actor, urls):
|
|||
if username:
|
||||
user, domain = username
|
||||
|
||||
if get.domainban(domain):
|
||||
logging.info(f'Rejected post from banned instance: {domain} from {instance}')
|
||||
|
||||
if get.userban(user, domain):
|
||||
logging.info(f'Rejected banned user: {user}@{domain}')
|
||||
logging.info(f'Rejected post from banned user: {user}@{domain} from {instance}')
|
||||
return
|
||||
|
||||
announce(object_id, inbox)
|
||||
|
@ -62,8 +66,9 @@ def relay_follow(data, actor, urls):
|
|||
if get.config('require_approval'):
|
||||
put.request('add', urls, followid=followid)
|
||||
|
||||
thread = threading.Thread(target=notification, args=[domain])
|
||||
thread.start()
|
||||
if get.config('notification'):
|
||||
thread = threading.Thread(target=notification, args=[domain])
|
||||
thread.start()
|
||||
|
||||
return
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from .templates import build_templates
|
|||
from . import errors, views, middleware as mw
|
||||
|
||||
|
||||
app = Sanic(log_config=LOG)
|
||||
app = Sanic()
|
||||
app.config.FORWARDED_SECRET = fwsecret
|
||||
|
||||
# Register middlewares
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import json
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
|
||||
import httpsig
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA, SHA256, SHA512
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
|
||||
from .log import logging
|
||||
from .functions import cache, format_date
|
||||
from .functions import cache, format_date, fetch
|
||||
from .database import get
|
||||
|
||||
|
||||
|
@ -18,23 +22,27 @@ HASHES = {
|
|||
}
|
||||
|
||||
|
||||
def parse_sig(sig):
|
||||
if not sig:
|
||||
logging.warning('Missing signature header')
|
||||
def ParseSig(headers):
|
||||
sig_header = headers.get('signature')
|
||||
|
||||
if not sig_header:
|
||||
logging.verbose('Missing signature header')
|
||||
return
|
||||
|
||||
parts = {'headers': 'date'}
|
||||
split_sig = sig_header.split(',')
|
||||
signature = {}
|
||||
|
||||
for part in sig.strip().split(','):
|
||||
k, v = part.replace('"', '').split('=', maxsplit=1)
|
||||
for part in split_sig:
|
||||
key, value = part.split('=', 1)
|
||||
signature[key.lower()] = value.replace('"', '')
|
||||
|
||||
if k == 'headers':
|
||||
parts['headers'] = v.split()
|
||||
if not signature.get('headers'):
|
||||
logging.verbose('Missing headers section in signature')
|
||||
return
|
||||
|
||||
else:
|
||||
parts[k] = v
|
||||
signature['headers'] = signature['headers'].split()
|
||||
|
||||
return parts
|
||||
return signature
|
||||
|
||||
|
||||
def build_sigstring(request, used_headers, target=None):
|
||||
|
@ -61,87 +69,81 @@ def build_sigstring(request, used_headers, target=None):
|
|||
return string
|
||||
|
||||
|
||||
def sign_sigstring(sigstring, key, hashalg='SHA256'):
|
||||
cached_data = cache.sig.fetch(sigstring)
|
||||
def SignBody(body):
|
||||
bodyhash = cache.sig.fetch(body)
|
||||
|
||||
if cached_data:
|
||||
logging.info('Returning cache sigstring')
|
||||
return cached_data
|
||||
if not bodyhash:
|
||||
h = SHA256.new(body.encode('utf-8'))
|
||||
bodyhash = b64encode(h.digest()).decode('utf-8')
|
||||
cache.sig[body] = bodyhash
|
||||
|
||||
sign_key = get.rsa_key(key)
|
||||
return bodyhash
|
||||
|
||||
if not sign_key:
|
||||
|
||||
|
||||
def ValidateSignature(headers, method, path):
|
||||
headers = {k.lower(): v for k,v in headers.items()}
|
||||
signature = ParseSig(headers)
|
||||
|
||||
actor_data = fetch(signature['keyid'])
|
||||
logging.debug(actor_data)
|
||||
|
||||
try:
|
||||
pubkey = actor_data['publicKey']['publicKeyPem']
|
||||
|
||||
except Exception as e:
|
||||
logging.verbose(f'Failed to get public key for actor {signature["keyid"]}')
|
||||
return
|
||||
|
||||
pkcs = PKCS1_v1_5.new(sign_key['PRIVKEY'])
|
||||
h = HASHES[hashalg.lower()].new()
|
||||
h.update(sigstring.encode('ascii'))
|
||||
valid = httpsig.HeaderVerifier(headers, pubkey, signature['headers'], method, path, sign_header='signature').verify()
|
||||
|
||||
sigdata = b64encode(pkcs.sign(h)).decode('ascii')
|
||||
cache.sig.store(sigstring, sigdata)
|
||||
if not valid:
|
||||
if not isinstance(valid, tuple):
|
||||
logging.verbose('Signature validation failed for unknown actor')
|
||||
logging.verbose(valid)
|
||||
|
||||
return sigdata
|
||||
else:
|
||||
logging.verbose(f'Signature validation failed for actor: {valid[1]}')
|
||||
|
||||
|
||||
def sign_headers(headers, key, key_id, target):
|
||||
headers = {k.lower(): v for k, v in headers.items()}
|
||||
|
||||
headers['date'] = format_date(None)
|
||||
|
||||
used_headers = headers.keys()
|
||||
sigstring = build_sigstring(headers, used_headers, target=target)
|
||||
signed_sigstring = sign_sigstring(sigstring, key)
|
||||
|
||||
sig = {
|
||||
'keyId': key_id,
|
||||
'algorithm': 'rsa-sha256',
|
||||
'headers': ' '.join(used_headers),
|
||||
'signature': signed_sigstring
|
||||
}
|
||||
|
||||
sig_header = ['{}="{}"'.format(k, v) for k, v in sig.items()]
|
||||
headers['signature'] = ','.join(sig_header)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def validate(request):
|
||||
from .messages import fetch
|
||||
data = request.json
|
||||
actor = data.get('actor')
|
||||
sig = parse_sig(request.headers.get('signature'))
|
||||
|
||||
if not actor:
|
||||
logging.debug('Missing actor')
|
||||
return
|
||||
|
||||
if not sig:
|
||||
logging.debug('Missing signature')
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def ValidateRequest(request):
|
||||
'''
|
||||
Validates the headers in a Sanic or Aiohttp request (other frameworks may be supported)
|
||||
See ValidateSignature for 'client' and 'agent' usage
|
||||
'''
|
||||
return ValidateSignature(request.headers, request.method, request.path)
|
||||
|
||||
|
||||
def SignHeaders(headers, key, keyid, url, method='get'):
|
||||
if headers.get('date'):
|
||||
del headers['date']
|
||||
|
||||
actor_key = get.rsa_key(key)
|
||||
|
||||
if not actor_key:
|
||||
logging.error('Could not find signing key:', key)
|
||||
return
|
||||
|
||||
actor_data = fetch(actor)
|
||||
privkey = actor_key['privkey']
|
||||
RSAkey = RSA.import_key(privkey)
|
||||
key_size = int(RSAkey.size_in_bytes()/2)
|
||||
logging.debug('Signing key size:', key_size)
|
||||
|
||||
if not actor_data:
|
||||
logging.debug('Missing actor data')
|
||||
return False
|
||||
parsed_url = urlparse(url)
|
||||
|
||||
if not actor_data.get('publicKey') or not actor_data['publicKey'].get('publicKeyPem'):
|
||||
logging.debug(f'Missing pubkey')
|
||||
return
|
||||
raw_headers = {'date': format_date(), 'host': parsed_url.netloc, '(request-target)': ' '.join([method.lower(), parsed_url.path])}
|
||||
raw_headers.update(dict(headers))
|
||||
header_keys = raw_headers.keys()
|
||||
|
||||
actor_key = actor_data['publicKey']['publicKeyPem']
|
||||
pkcs = PKCS1_v1_5.new(RSA.importKey(actor_data['publicKey']['publicKeyPem']))
|
||||
signer = httpsig.HeaderSigner(keyid, privkey, f'rsa-sha{key_size}', headers=header_keys, sign_header='signature')
|
||||
new_headers = signer.sign(raw_headers, parsed_url.netloc, method, parsed_url.path)
|
||||
logging.debug('Signed headers:', new_headers)
|
||||
|
||||
sigstring = build_sigstring(request, sig['headers'])
|
||||
logging.debug(f'Signing string: {sigstring}')
|
||||
del new_headers['(request-target)']
|
||||
|
||||
signalg, hashalg = sig['algorithm'].split('-')
|
||||
sigdata = b64decode(sig['signature'])
|
||||
|
||||
h = HASHES[hashalg].new()
|
||||
h.update(sigstring.encode('ascii'))
|
||||
result = pkcs.verify(h, sigdata)
|
||||
|
||||
logging.debug(f'Sig verification result: {result}')
|
||||
|
||||
return result
|
||||
return new_headers
|
||||
|
|
|
@ -54,7 +54,6 @@ class Inbox(HTTPMethodView):
|
|||
return response.text('Failed to fetch actor', status=400)
|
||||
|
||||
domain = urlparse(actor_url).netloc
|
||||
|
||||
urls = {
|
||||
'inbox': inbox,
|
||||
'actor': actor_url,
|
||||
|
@ -187,7 +186,7 @@ class WellknownNodeinfo(HTTPMethodView):
|
|||
|
||||
class WellknowWebfinger(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
res = request['query'].get('resource')
|
||||
res = request.ctx.query.get('resource')
|
||||
|
||||
if not res or res != f'acct:relay@{host}':
|
||||
data = {}
|
||||
|
@ -229,7 +228,7 @@ class Faq(HTTPMethodView):
|
|||
|
||||
class Admin(HTTPMethodView):
|
||||
async def get(self, request, *args, action=None, msg=None, **kwargs):
|
||||
page = kwargs.get('page', request['query'].get('page', 'instances'))
|
||||
page = kwargs.get('page', request.ctx.query.get('page', 'instances'))
|
||||
|
||||
if action:
|
||||
return error(request, f'Not found: {request.path}', 404)
|
||||
|
@ -252,8 +251,8 @@ class Admin(HTTPMethodView):
|
|||
|
||||
async def post(self, request, action=''):
|
||||
action = re.sub(r'[^a-z]+', '', action.lower())
|
||||
data = request['form']
|
||||
page = request['form'].get('page', 'instances')
|
||||
data = request.ctx.form
|
||||
page = data.get('page', 'instances')
|
||||
|
||||
msg = admin.run(action, data)
|
||||
|
||||
|
@ -280,7 +279,7 @@ class Account(HTTPMethodView):
|
|||
|
||||
async def post(self, request, action=''):
|
||||
action = re.sub(r'[^a-z]+', '', action.lower())
|
||||
password = request['form'].get('password')
|
||||
password = request.ctx.form.get('password')
|
||||
token = request.cookies.get('token')
|
||||
token_data = get.token(token)
|
||||
user = get.user(token_data['userid'])
|
||||
|
@ -300,8 +299,8 @@ class Account(HTTPMethodView):
|
|||
return resp
|
||||
|
||||
if action == 'password':
|
||||
pass1 = request['form'].get('newpass1')
|
||||
pass2 = request['form'].get('newpass2')
|
||||
pass1 = request.ctx.form.get('newpass1')
|
||||
pass2 = request.ctx.form.get('newpass2')
|
||||
|
||||
if pass1 != pass2:
|
||||
return await self.get(request, msg='New passwords do not match')
|
||||
|
@ -315,7 +314,7 @@ class Account(HTTPMethodView):
|
|||
return await self.get(request, msg='Updated password')
|
||||
|
||||
if action == 'name':
|
||||
dispname = request['form'].get('displayname')
|
||||
dispname = request.ctx.form.get('displayname')
|
||||
|
||||
if not dispname:
|
||||
return await self.get(request, msg='Missing new display name')
|
||||
|
@ -327,7 +326,7 @@ class Account(HTTPMethodView):
|
|||
return await self.get(request, msg='Failed to update display name')
|
||||
|
||||
if action == 'token':
|
||||
form_token = request['form'].get('token')
|
||||
form_token = request.ctx.form.get('token')
|
||||
|
||||
if not form_token:
|
||||
return await self.get(request, msg='Failed to provide token to delete')
|
||||
|
@ -359,14 +358,14 @@ class Login(HTTPMethodView):
|
|||
async def get(self, request, msg=None):
|
||||
data = {
|
||||
'msg': msg,
|
||||
'code': request['query'].get('code')
|
||||
'code': request.ctx.query.get('code')
|
||||
}
|
||||
|
||||
return render('login.html', request, data)
|
||||
|
||||
async def post(self, request):
|
||||
username = request['form'].get('username')
|
||||
password = request['form'].get('password')
|
||||
username = request.ctx.form.get('username')
|
||||
password = request.ctx.form.get('password')
|
||||
|
||||
if None in [username, password]:
|
||||
return await self.get(request, msg='Missing username or password')
|
||||
|
@ -405,13 +404,13 @@ class Register(HTTPMethodView):
|
|||
async def get(self, request, msg=None):
|
||||
data = {
|
||||
'msg': msg,
|
||||
'code': request['query'].get('code')
|
||||
'code': request.ctx.query.get('code')
|
||||
}
|
||||
|
||||
return render('register.html', request, data)
|
||||
|
||||
async def post(self, request):
|
||||
data = request['form']
|
||||
data = request.ctx.form
|
||||
keys = ['username', 'password', 'password2', 'code']
|
||||
|
||||
for key in keys:
|
||||
|
@ -465,12 +464,12 @@ class Robots(HTTPMethodView):
|
|||
|
||||
class Setup(HTTPMethodView):
|
||||
async def get(self, request, *args, msg=None, **kwargs):
|
||||
data = {'code': request['query'].get('code'), 'msg': msg}
|
||||
data = {'code': request.ctx.query.get('code'), 'msg': msg}
|
||||
|
||||
return render('setup.html', request, data)
|
||||
|
||||
async def post(self, request, action=''):
|
||||
data = request['form']
|
||||
data = request.ctx.form
|
||||
|
||||
if data.get('code') != get.auth_code:
|
||||
return await self.get(request, msg='Invalid auth code')
|
||||
|
|
Loading…
Reference in a new issue