fuwuqi/server.py
2023-01-18 06:25:31 +00:00

113 lines
4.1 KiB
Python

from base64 import b64decode
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from json import dump, load, loads
from re import search
from requests import get, post
from os.path import isfile
from urllib.parse import quote_plus
domain = 'https://0.exozy.me'
def collection_append(file, item):
with open(file) as f:
collection = load(f)
collection['totalItems'] += 1
collection['orderedItems'].append(item)
with open(file, 'w') as f:
dump(collection, f)
def iri_to_actor(iri):
if domain in iri:
name = search(f'^{domain}/users/(.*?)#main-key$', iri).group(1)
actorfile = f'users/{name}'
else:
actorfile = f'users/{quote_plus(iri)}'
if not isfile(actorfile):
with open(actorfile, 'w') as f:
resp = get(iri.removesuffix('#main-key'), headers={'Accept': 'application/activity+json'})
f.write(resp.text)
with open(actorfile) as f:
return load(f)
class fuwuqi(SimpleHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers['Content-Length']))
activity = loads(body)
print(activity)
print(self.headers)
print(self.path)
username = search('^/users/(.*)\.(in|out)box$', self.path).group(1)
# Get actor public key
keyid = search('keyId="(.*?)"', self.headers['Signature']).group(1)
actor = iri_to_actor(keyid)
pubkeypem = actor['publicKey']['publicKeyPem'].encode('utf8')
pubkey = serialization.load_pem_public_key(pubkeypem, None)
# Assemble headers
headers = search('headers="(.*?)"', self.headers['Signature']).group(1)
message = ''
for header in headers.split():
if header == '(request-target)':
headerval = f'post {self.path}'
else:
headerval = self.headers[header]
message += f'{header}: {headerval}\n'
# Verify HTTP signature
signature = search('signature="(.*?)"', self.headers['Signature']).group(1)
pubkey.verify(
b64decode(signature),
message[:-1].encode('utf8'),
padding.PKCS1v15(),
hashes.SHA256()
)
# Make sure activity doer matches HTTP signature
actor = keyid.removesuffix('#main-key')
if 'actor' in activity and activity['actor'] != actor:
self.send_response(401)
return
if 'attributedTo' in activity and activity['attributedTo'] != actor:
self.send_response(401)
return
if self.path.endswith('inbox'):
# S2S
collection_append(f'users/{username}.inbox', activity)
elif self.path.endswith('outbox'):
# C2S
collection_append(f'users/{username}.outbox', activity)
if activity['type'] == 'Create':
id = activity['id'].split('/')[-1]
with open(f'users/{username}.statuses/{id}', 'w') as f:
dump(activity['object'], f)
# Send to followers
with open(f'users/{username}.following') as f:
for followed in load(f)['orderedItems']:
actor = iri_to_actor(followed)
self.headers['Host'] = followed.split('/')[2]
resp = post(actor['inbox'], headers=self.headers, data=body)
print(resp)
print(resp.text)
elif activity['type'] == 'Follow':
object = iri_to_actor(activity['object'])
self.headers['Host'] = activity['object'].split('/')[2]
resp = post(object['inbox'], headers=self.headers, data=body)
print(resp)
print(resp.text)
collection_append(f'users/{username}.following', activity['object'])
self.send_response(200)
self.end_headers()
ThreadingHTTPServer(('localhost', 4200), fuwuqi).serve_forever()