forked from mirrors/fuwuqi
138 lines
5.5 KiB
Python
138 lines
5.5 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(username, file, item):
|
|
with open(f'users/{username}.{file}') as f:
|
|
collection = load(f)
|
|
collection['orderedItems'].append(item)
|
|
collection['totalItems'] += 1
|
|
with open(f'users/{username}.{file}', 'w') as f:
|
|
dump(collection, f)
|
|
|
|
|
|
def collection_pop(username, file, item):
|
|
with open(f'users/{username}.{file}') as f:
|
|
collection = load(f)
|
|
collection['orderedItems'].pop(item)
|
|
collection['totalItems'] -= 1
|
|
with open(f'users/{username}.{file}', 'w') as f:
|
|
dump(collection, f)
|
|
|
|
|
|
def iri_to_actor(iri):
|
|
if domain in iri:
|
|
username = search(f'^{domain}/users/(.*?)$', iri.removesuffix('#main-key')).group(1)
|
|
actorfile = f'users/{username}'
|
|
else:
|
|
actorfile = f'users/{quote_plus(iri.removesuffix("#main-key"))}'
|
|
if not isfile(actorfile):
|
|
with open(actorfile, 'w') as f:
|
|
resp = get(iri, headers={'Accept': 'application/activity+json'})
|
|
print(resp)
|
|
print(resp.text)
|
|
f.write(resp.text)
|
|
with open(actorfile) as f:
|
|
return load(f)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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 signer public key
|
|
signer = iri_to_actor(search('keyId="(.*?)"', self.headers['Signature']).group(1))
|
|
pubkeypem = signer['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
|
|
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']):
|
|
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'])
|
|
elif activity['type'] == 'Undo' and activity['object']['type'] == 'Follow' and \
|
|
activity['actor'] == activity['object']['actor']:
|
|
# Unfollow request
|
|
collection_remove(username, 'followers', activity['actor'])
|
|
elif self.path.endswith('outbox'):
|
|
# C2S
|
|
collection_append(username, 'outbox', activity)
|
|
# Clients responsible for addressing activity
|
|
for to in activity['to']:
|
|
if 'followers' in to or to == 'https://www.w3.org/ns/activitystreams#Public':
|
|
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
|
|
if activity['type'] == 'Create':
|
|
# Post
|
|
id = activity['object']['id'].split('/')[-1]
|
|
with open(f'users/{username}.statuses/{id}', 'w') as f:
|
|
dump(activity['object'], f)
|
|
elif activity['type'] == 'Accept':
|
|
# Accept follow request
|
|
collection_append(username, 'followers', activity['object']['actor'])
|
|
elif activity['type'] == 'Like':
|
|
# Like post
|
|
collection_append(username, 'liked', activity['object'])
|
|
elif activity['type'] == 'Undo':
|
|
if activity['object']['type'] == 'Follow':
|
|
# Unfollow request
|
|
collection_remove(username, 'following', activity['object']['object'])
|
|
elif activity['object']['type'] == 'Like':
|
|
# Unlike post
|
|
collection_remove(username, 'liked', activity['object']['object'])
|
|
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
|
|
|
|
ThreadingHTTPServer(('localhost', 4200), fuwuqi).serve_forever()
|