aboutsummaryrefslogtreecommitdiff
path: root/server.py
blob: cbd56023ae7a79677278f3fe0f46a9b9ca5ba1cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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 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) or \
           ('attributedTo' in activity and activity['attributedTo'] != actor) or \
           ('actor' in activity['object'] and activity['object']['actor'] != actor) or \
           ('attributedTo' in activity['object'] and activity['object']['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)
            # 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'] == 'Follow':
                # Follow request
                collection_append(username, 'following', activity['object'])
            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()