fuwuqi/server.py

139 lines
5.5 KiB
Python
Raw Normal View History

2023-01-18 05:01:59 +00:00
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
2023-01-18 06:23:36 +00:00
domain = 'https://0.exozy.me'
2023-01-18 20:00:54 +00:00
def collection_append(username, file, item):
with open(f'users/{username}.{file}') as f:
2023-01-18 05:01:59 +00:00
collection = load(f)
collection['orderedItems'].append(item)
2023-01-18 19:48:11 +00:00
collection['totalItems'] += 1
2023-01-18 20:00:54 +00:00
with open(f'users/{username}.{file}', 'w') as f:
dump(collection, f)
2023-01-18 19:48:11 +00:00
2023-01-18 20:00:54 +00:00
def collection_pop(username, file, item):
with open(f'users/{username}.{file}') as f:
2023-01-18 19:48:11 +00:00
collection = load(f)
collection['orderedItems'].pop(item)
collection['totalItems'] -= 1
2023-01-18 20:00:54 +00:00
with open(f'users/{username}.{file}', 'w') as f:
dump(collection, f)
2023-01-18 05:01:59 +00:00
2023-01-18 06:23:36 +00:00
def iri_to_actor(iri):
if domain in iri:
2023-01-18 20:06:17 +00:00
username = search(f'^{domain}/users/(.*?)$', iri.removesuffix('#main-key')).group(1)
actorfile = f'users/{username}'
2023-01-18 06:23:36 +00:00
else:
2023-01-18 19:48:11 +00:00
actorfile = f'users/{quote_plus(iri.removesuffix("#main-key"))}'
2023-01-18 06:23:36 +00:00
if not isfile(actorfile):
with open(actorfile, 'w') as f:
2023-01-18 19:48:11 +00:00
resp = get(iri, headers={'Accept': 'application/activity+json'})
2023-01-18 20:00:54 +00:00
print(resp)
print(resp.text)
2023-01-18 06:23:36 +00:00
f.write(resp.text)
with open(actorfile) as f:
return load(f)
2023-01-18 19:48:11 +00:00
def send(to, headers, body):
actor = iri_to_actor(to)
headers['Host'] = to.split('/')[2]
resp = post(actor['inbox'], headers=headers, data=body)
print(resp)
print(resp.text)
2023-01-18 05:01:59 +00:00
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)
2023-01-18 06:23:36 +00:00
username = search('^/users/(.*)\.(in|out)box$', self.path).group(1)
# Get signer public key
signer = iri_to_actor(search('keyId="(.*?)"', self.headers['Signature']).group(1))
pubkeypem = signer['publicKey']['publicKeyPem'].encode('utf8')
2023-01-18 06:23:36 +00:00
pubkey = serialization.load_pem_public_key(pubkeypem, None)
2023-01-18 05:01:59 +00:00
# Assemble headers
headers = search('headers="(.*?)"', self.headers['Signature']).group(1)
message = ''
for header in headers.split():
2023-01-18 06:23:36 +00:00
if header == '(request-target)':
headerval = f'post {self.path}'
else:
headerval = self.headers[header]
2023-01-18 05:01:59 +00:00
message += f'{header}: {headerval}\n'
# Verify HTTP signature
2023-01-18 20:06:17 +00:00
signature = search('signature="(.*?)"', self.headers['Signature']).group(1)
pubkey.verify(b64decode(signature), message[:-1].encode('utf8'), padding.PKCS1v15(), hashes.SHA256())
2023-01-18 05:01:59 +00:00
2023-02-06 20:28:25 +00:00
# Make sure activity doer matches HTTP signature
if ('actor' in activity and activity['actor'] != signer['id']) or \
('attributedTo' in activity and activity['attributedTo'] != signer['id']) or \
('attributedTo' in activity['object'] and activity['object']['attributedTo'] != signer['id']):
2023-01-18 05:01:59 +00:00
self.send_response(401)
return
if self.path.endswith('inbox'):
# S2S
collection_append(username, 'inbox', activity)
if activity['type'] == 'Accept' and activity['actor'] == activity['object']['object']:
# Follow request accepted
collection_append(username, 'following', activity['actor'])
2023-01-19 01:55:44 +00:00
elif activity['type'] == 'Undo' and activity['object']['type'] == 'Follow' and \
activity['actor'] == activity['object']['actor']:
# Unfollow request
collection_remove(username, 'followers', activity['actor'])
2023-01-18 05:01:59 +00:00
elif self.path.endswith('outbox'):
# C2S
collection_append(username, 'outbox', activity)
2023-01-18 19:48:11 +00:00
# Clients responsible for addressing activity
for to in activity['to']:
2023-01-18 19:52:31 +00:00
if 'followers' in to or to == 'https://www.w3.org/ns/activitystreams#Public':
2023-01-18 19:48:11 +00:00
with open(f'users/{username}.followers') as f:
for follower in load(f)['orderedItems']:
send(follower, self.headers, body)
else:
send(to, self.headers, body)
# Process activity
2023-01-18 05:01:59 +00:00
if activity['type'] == 'Create':
2023-01-18 19:48:11 +00:00
# Post
id = activity['object']['id'].split('/')[-1]
2023-01-18 05:01:59 +00:00
with open(f'users/{username}.statuses/{id}', 'w') as f:
dump(activity['object'], f)
2023-01-18 19:48:11 +00:00
elif activity['type'] == 'Accept':
# Accept follow request
2023-01-18 20:00:54 +00:00
collection_append(username, 'followers', activity['object']['actor'])
2023-01-18 19:48:11 +00:00
elif activity['type'] == 'Like':
# Like post
2023-01-18 20:00:54 +00:00
collection_append(username, 'liked', activity['object'])
2023-01-18 19:48:11 +00:00
elif activity['type'] == 'Undo':
if activity['object']['type'] == 'Follow':
# Unfollow request
2023-01-18 20:00:54 +00:00
collection_remove(username, 'following', activity['object']['object'])
2023-01-18 19:48:11 +00:00
elif activity['object']['type'] == 'Like':
# Unlike post
2023-01-18 20:00:54 +00:00
collection_remove(username, 'liked', activity['object']['object'])
2023-01-18 05:01:59 +00:00
self.send_response(200)
2023-01-18 06:23:36 +00:00
self.end_headers()
2023-01-18 05:01:59 +00:00
ThreadingHTTPServer(('localhost', 4200), fuwuqi).serve_forever()