Add latest working code

This commit is contained in:
Anthony Wang 2023-01-18 06:23:36 +00:00
parent f463134952
commit a2e773ebd1
No known key found for this signature in database
GPG key ID: 42A5B952E6DD8D38
16 changed files with 95 additions and 224 deletions

View file

@ -2,11 +2,9 @@
Fuwuqi (fúwùqì or 服务器 means "server" in Chinese) is a useless C2S ActivityPub server for "extremely hardcore" ActivityPub enthusiasts. Craft your own exquisite WebFinger response! Customize your actor object exactly like you want! Hack and extend the 100-line Python server code!
If that sounds like a world of pain, close this webpage now... OK, got that out of the way. Time for some fun!
## Configuration
First, clone this repo on your server.
First, clone this repo on your server and your client device.
Now, generate an RSA keypair on your client device:
```bash
@ -14,22 +12,24 @@ openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
```
Rename `users/test.jsonld` to your username and pop it open in your favorite text editor. You should change `0.exozy.me` to match your server's domain name, `test` to your username, and the `publicKeyPem` field to the public key you just generated.
On the server, rename `users/test.jsonld` to your username and pop it open in your favorite text editor. You should change `0.exozy.me` to match your server's domain name, `test` to your username, and the `publicKeyPem` field to the public key you just generated.
Onward! Now open `.well-known/webfinger` in your editor, and modify it similarly.
Onward! Now open `.well-known/webfinger` in your editor, and modify it similarly. Finally, open `server.py` and update the domain, obviously.
That wasn't so bad, was it? (sobbing sounds)
## Usage
Alright, time for the real deal. Start up `python main.py` on your server. If you want to customize the port or whatever, just change the source code.
Alright, time for the real deal. Start up `python server.py` on your server. If you want to customize the port or whatever, just change the source code.
Now on your client device, open up your favorite C2S ActivityPub client... oh wait... there aren't any! Welp, you'll just have to settle for reading the [AP spec](https://www.w3.org/TR/activitypub/), writing some homemade JSON, and figuring out how `python client.py` works.
Now on your client device, open up your favorite C2S ActivityPub client... oh wait... there aren't any! Welp, you'll just have to settle for reading the [AP spec](https://www.w3.org/TR/activitypub/), writing some homemade JSON, and figuring out how `python client.py` works. That's rough, buddy. You'll have to learn how to manually write `Note`s, manually accept follow requests, and more! You can find some examples in this repo. HTTP signatures are generated client-side, for no good reason other than it works. If you want to view your unread messages, just `curl` your inbox. Easy as that.
Like and subscribe and enjoy your new "extremely hardcore" ActivityPub server!!! 🎉😎🚀🙃🥳
Enjoy your new "extremely hardcore" ActivityPub server!!! 🎉😎🚀🙃🥳
## Resources
- https://www.w3.org/TR/activitypub/
- https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
- https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
- https://docs.joinmastodon.org/spec/
- https://socialhub.activitypub.rocks/t/python-mastodon-server-post-with-http-signature/2757

12
accept.jsonld Normal file
View file

@ -0,0 +1,12 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Accept",
"actor": "https://0.exozy.me/users/test.jsonld",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://social.exozy.me/b363f127-6422-4566-b8f1-878aa33b0e1c",
"type": "Follow",
"actor": "https://social.exozy.me/users/a",
"object": "https://0.exozy.me/users/test.jsonld"
}
}

View file

@ -1,14 +0,0 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://0.exozy.me/users/test.outbox/hello-world2",
"type": "Create",
"actor": "https://0.exozy.me/users/test.jsonld",
"object": {
"id": "https://0.exozy.me/users/test.statuses/hello-world2",
"type": "Note",
"attributedTo": "https://0.exozy.me/users/test.jsonld",
"inReplyTo": "https://social.exozy.me/@a/109707513227348721",
"content": "Hello from fuwuqi! 2",
"to": "https://www.w3.org/ns/activitystreams#Public"
}
}

View file

@ -3,16 +3,17 @@ from cryptography.hazmat.primitives.asymmetric import padding
from base64 import b64encode
from email.utils import formatdate
from requests import post
from sys import argv
date = formatdate(usegmt=True)
with open('activity.jsonld', 'rb') as f:
with open(argv[1], 'rb') as f:
activity = f.read()
digester = hashes.Hash(hashes.SHA256())
digester.update(activity)
digest = b64encode(digester.finalize()).decode()
message = f'(request-target): post /users/a/inbox\nhost: social.exozy.me\ndate: {date}\ndigest: SHA-256={digest}'
message = f'date: {date}\ndigest: SHA-256={digest}'
with open('private.pem', 'rb') as f:
privkey = serialization.load_pem_private_key(f.read(), None)
@ -22,11 +23,9 @@ signature = b64encode(privkey.sign(
padding.PKCS1v15(),
hashes.SHA256()
)).decode()
header = f'keyId="https://0.exozy.me/users/test.jsonld#main-key",headers="(request-target) host date digest",signature="{signature}"'
header = f'keyId="https://0.exozy.me/users/test.jsonld#main-key",headers="date digest",signature="{signature}"'
resp = post('http://localhost:4200/users/test.outbox', headers={
'(request-target)': 'post /users/a/inbox',
'Host': 'social.exozy.me',
resp = post('https://0.exozy.me/users/test.outbox', headers={
'Date': date,
'Digest': f'SHA-256={digest}',
'Signature': header,

14
create.jsonld Normal file
View file

@ -0,0 +1,14 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://0.exozy.me/users/test.outbox/hello-world2",
"type": "Create",
"actor": "https://0.exozy.me/users/test.jsonld",
"object": {
"id": "https://0.exozy.me/users/test.statuses/hello-world2",
"type": "Note",
"attributedTo": "https://0.exozy.me/users/test.jsonld",
"inReplyTo": "https://social.exozy.me/@a/109707513227348721",
"content": "Hello from fuwuqi! 2",
"to": "https://www.w3.org/ns/activitystreams#Public"
}
}

7
follow.jsonld Normal file
View file

@ -0,0 +1,7 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://0.exozy.me/users/test.outbox/follow",
"type": "Follow",
"actor": "https://0.exozy.me/users/test.jsonld",
"object": "https://social.exozy.me/users/a"
}

View file

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCc+fpJFDUlPCoU
wUmrsGDExb/hX8dhKMc9u0ov/ePBc5OY4tTPjCvghvUjCIqmyC7PS7JfUuNt7epD
OpoJm7mF2gaj64mxqfPrICgtjARN8gUiOpCvcWggZDFgkMQuSFI7yG1s4oqhGJUq
yMPnHhBwSRmQsBp5/xZWHTywSoTd8Li4uY6VJq/JxswuZqH1R2GWMVxpVJDuqjH0
cW8oXZ6LgGY3ZZ0wZnwk3bSdzQ7GhmkGkwCtGoXSSuMiSEDn/0K8hkRQs9AKFgvo
ByYtLSP/JPETzZ4UwEG/J6DwgIxHNxsKg36FWe/dxv/v4HA12hCNmq3REb+GFulz
xEq35g71AgMBAAECggEACqnGcS67ZMyfSn/4xP4QYhLRc89MSBLQ5KYeJirCd70l
97riMY5i2qKARhbJ8wYNJqK3OqzIQIrqoLzQguTo/NPbI0kYNjvbvbXrqhsP8yAv
bhb5W27s36ppWkHAjyj1gRRz2R4obu9bDqggT/Olso2cpsyiTSA2qwyFt1n7MueQ
ag/lSYAAnjRfnxnGiBwy+wURxZH+Y9uBhb9E8MZ+W+sUc7l8RDUfYPN9d7NmxfdV
x6x+jk+OLpYotj1v6aTfdwBCqpdt2ulMZexTCfjwAIU/XI2uhqaO1dzKI+Ssr/WY
j8R7LXV94pI1ZXdwAJ/8aWGllAhj1hpnhacjnr7JAQKBgQDZPj5zJVk7TBoRxQVw
VCfmGwPwAkfmGGqn9g41VlKtyG+JhWQ7aAs0gMFwq6QGDEBloAiXQw7z033ZSNTP
p61QCePq6fLGEgdsfgjGotEbZ2XDZdB6fdzK+sKJLQtXr8FVrywyNmlX0bk8IRSs
hnJXYco/VTLeDiQePO2bjm0UdQKBgQC4+0hR/se6AHgN9449ruHLSOCcC+Y6WQJ1
BM+ydk7OnnfDmxzXQMbzri5kwDTBw+Y01avbRkpMZbHtWip9QWL96Kf8D6OUoX6d
KJuheKsuJUKmXTPF8YyJiY+KXmx1yz7hu8X/SN955s4BAttIZGQvSIFpy+izzQWc
FghcplvAgQKBgQCIkuIN37AOYFSPUU6PBMkkl11NWRG8bSM4Pq9GBuPpjvXX/f06
f7lzo3J5E98FUlR1zzs3ZRgUX6RhorDvb1m81Mrtl3Bh51m1cjKwNhHB6aoHQo3j
RBc3oJgGR0Q3Ny4TYRIm6yAk7ptGWwG1SLy/hKHyWOymvzsjq2gxgEPBNQKBgEyM
6KvOBPdLVGNrS/jo01Yd7Z2GKxuAVEz61bzjys8ksylGmpPVob+cGGTnSa3aFP1O
Y1VV7E9bUluIEcdN9NpgmovsKOTMRCpjcKxM1II/NyrDrTZANMmCHN3FH5tLpdUi
sNhpXtoCksPGW9rEeNU8axnOIZmuwaCLWaCF07iBAoGAX494Slca2EnERTtcX9jq
SM/srMTz+cm5zNA3npMj1eZ7zNglD5tZapc3f5OErrDdZz5wlmIo/eheQxb8TMQW
F5goYHwpq6bghHlbAxK2I353s8Q8WriLTvAYouHfEqd14AS42OXD44CBwU0V5rRV
MmlfOVBGqPVHLQK/tOebZIw=
-----END PRIVATE KEY-----

View file

@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnPn6SRQ1JTwqFMFJq7Bg
xMW/4V/HYSjHPbtKL/3jwXOTmOLUz4wr4Ib1IwiKpsguz0uyX1Ljbe3qQzqaCZu5
hdoGo+uJsanz6yAoLYwETfIFIjqQr3FoIGQxYJDELkhSO8htbOKKoRiVKsjD5x4Q
cEkZkLAaef8WVh08sEqE3fC4uLmOlSavycbMLmah9UdhljFcaVSQ7qox9HFvKF2e
i4BmN2WdMGZ8JN20nc0OxoZpBpMArRqF0krjIkhA5/9CvIZEULPQChYL6AcmLS0j
/yTxE82eFMBBvyeg8ICMRzcbCoN+hVnv3cb/7+BwNdoQjZqt0RG/hhbpc8RKt+YO
9QIDAQAB
-----END PUBLIC KEY-----

View file

@ -9,6 +9,9 @@ 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)
@ -18,6 +21,20 @@ def collection_append(file, item):
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, 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']))
@ -26,20 +43,21 @@ class fuwuqi(SimpleHTTPRequestHandler):
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)
actorfile = f'users/{quote_plus(keyid)}'
if not isfile(actorfile):
with open(actorfile, 'w') as f:
f.write(get(keyid).text)
with open(actorfile) as f:
pubkeypem = load(f)['publicKey']['publicKeyPem'].encode('utf8')
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'
@ -61,27 +79,34 @@ class fuwuqi(SimpleHTTPRequestHandler):
self.send_response(401)
return
username = search('^/users/(.*)\.(in|out)box$', self.path).group(1)
if self.path.endswith('inbox'):
# S2S
id = activity['id'].split('/')[-1]
with open(f'users/{username}/{id}', 'w') as f:
dump(activity, f)
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)
print(self.headers)
print(body)
resp = post('https://social.exozy.me/users/a/inbox', headers=self.headers, data=body)
# 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()

View file

@ -1,24 +0,0 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://0.exozy.me/users/test.jsonld",
"type": "Person",
"preferredUsername": "test",
"name": "Billiam Wender",
"inbox": "https://0.exozy.me/users/test.inbox",
"outbox": "https://0.exozy.me/users/test.outbox",
"followers": "https://0.exozy.me/users/test.followers",
"following": "https://0.exozy.me/users/test.following",
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://0.exozy.me/users/test.png"
},
"publicKey": {
"id": "https://0.exozy.me/users/test.jsonld#main-key",
"owner": "https://0.exozy.me/users/test.jsonld",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnPn6SRQ1JTwqFMFJq7Bg\nxMW/4V/HYSjHPbtKL/3jwXOTmOLUz4wr4Ib1IwiKpsguz0uyX1Ljbe3qQzqaCZu5\nhdoGo+uJsanz6yAoLYwETfIFIjqQr3FoIGQxYJDELkhSO8htbOKKoRiVKsjD5x4Q\ncEkZkLAaef8WVh08sEqE3fC4uLmOlSavycbMLmah9UdhljFcaVSQ7qox9HFvKF2e\ni4BmN2WdMGZ8JN20nc0OxoZpBpMArRqF0krjIkhA5/9CvIZEULPQChYL6AcmLS0j\n/yTxE82eFMBBvyeg8ICMRzcbCoN+hVnv3cb/7+BwNdoQjZqt0RG/hhbpc8RKt+YO\n9QIDAQAB\n-----END PUBLIC KEY-----\n"
}
}

View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","toot":"http://joinmastodon.org/ns#","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"},"alsoKnownAs":{"@id":"as:alsoKnownAs","@type":"@id"},"movedTo":{"@id":"as:movedTo","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","discoverable":"toot:discoverable","Device":"toot:Device","Ed25519Signature":"toot:Ed25519Signature","Ed25519Key":"toot:Ed25519Key","Curve25519Key":"toot:Curve25519Key","EncryptedMessage":"toot:EncryptedMessage","publicKeyBase64":"toot:publicKeyBase64","deviceId":"toot:deviceId","claim":{"@type":"@id","@id":"toot:claim"},"fingerprintKey":{"@type":"@id","@id":"toot:fingerprintKey"},"identityKey":{"@type":"@id","@id":"toot:identityKey"},"devices":{"@type":"@id","@id":"toot:devices"},"messageFranking":"toot:messageFranking","messageType":"toot:messageType","cipherText":"toot:cipherText","suspended":"toot:suspended","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://social.exozy.me/users/a","type":"Person","following":"https://social.exozy.me/users/a/following","followers":"https://social.exozy.me/users/a/followers","inbox":"https://social.exozy.me/users/a/inbox","outbox":"https://social.exozy.me/users/a/outbox","featured":"https://social.exozy.me/users/a/collections/featured","featuredTags":"https://social.exozy.me/users/a/collections/tags","preferredUsername":"a","name":"","summary":"\u003cp\u003e(+ (My username) (Ordinal n) (! off) (Element 39) (Wide area network) (Prefix for billion))\u003c/p\u003e\u003cp\u003eBoosts interesting things and sometimes transcribes special patterns generated by my biological neural networks into UTF-8 encoded strings.\u003c/p\u003e\u003cp\u003eCurrently oscillating between hacking ActivityPub federation into Forgejo/Gitea, pondering about the ForgeFed spec, wasting time, and sleeping (with 1/3 probability!).\u003c/p\u003e\u003cp\u003eWill happily answer any questions about forge federation.\u003c/p\u003e","url":"https://social.exozy.me/@a","manuallyApprovesFollowers":false,"discoverable":true,"published":"2022-09-07T00:00:00Z","devices":"https://social.exozy.me/users/a/collections/devices","alsoKnownAs":["https://social.exozy.me/users/ta180m"],"publicKey":{"id":"https://social.exozy.me/users/a#main-key","owner":"https://social.exozy.me/users/a","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrHXYuP4RKjac7h0woPA\nmBeETGogZ2IPUbDhA4DliUUHbUGYIK3XeIA4iywpnTbuHxE7L2PqMYNvfYMjQ+HS\nlJfSgYLB5mFxvzNcKqaUfAZazx3RzHLxftxt4DxWhEtS+vVd4RsxU2uvCOU8nN7o\nvk40GaYrgAms/7sapAjqbn6ngclVtVOBm0QhCG9cfg4QZoIIp98wpj/7kWZHxl8Y\nKJKN4G66FP+WAPIiLO/EmBqB9jcTQM2UMIGol4+616zFbmrow4KFCxZhiap+doP2\n8SjZSlA4Fhlk4qtHvQRjPlx2u/gvyUc7gQoa3PTb64rdDw1ahGvLTQKB6uVAXpDN\n2wIDAQAB\n-----END PUBLIC KEY-----\n"},"tag":[],"attachment":[{"type":"PropertyValue","name":"Website","value":"\u003ca href=\"https://a.exozy.me\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003ea.exozy.me\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Code","value":"\u003ca href=\"https://git.exozy.me/a\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egit.exozy.me/a\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"exozyme","value":"\u003ca href=\"https://exozy.me\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003eexozy.me\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Also me","value":"\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.mit.edu/@xy\" class=\"u-url mention\"\u003e@\u003cspan\u003exy@mastodon.mit.edu\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e"}],"endpoints":{"sharedInbox":"https://social.exozy.me/inbox"},"icon":{"type":"Image","mediaType":"image/gif","url":"https://social.exozy.me/system/accounts/avatars/108/958/469/936/988/491/original/c16834437aa85a2a.gif"},"image":{"type":"Image","mediaType":"image/jpeg","url":"https://social.exozy.me/system/accounts/headers/108/958/469/936/988/491/original/521f64b30b55553d.jpg"}}

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [],
}
{"@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "totalItems": 3, "orderedItems": ["https://social.exozy.me/users/a", "https://social.exozy.me/users/a", "https://social.exozy.me/users/a"]}

View file

@ -1,6 +1 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [],
}
{"@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "totalItems": 2, "orderedItems": ["https://social.exozy.me/users/a", "https://social.exozy.me/users/a"]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long