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()