11 KiB
A minimal ActivityPub server.
Based on the specification: https://www.w3.org/TR/activitypub
Also: https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tutorial.txt
https://blog.dereferenced.org/what-is-ocap-and-why-should-i-care
https://alexcastano.com/what-is-activity-pub
This project is currently pre alpha and not recommended for any real world uses.
Goals
- A minimal ActivityPub server, comparable to an email MTA.
- AGPLv3+
- Server-to-server and client-to-server protocols supported.
- Implemented in a common language (Python 3)
- Keyword filtering.
- Being able to define roles and skills, similar to the Pursuance project.
- Sharings collection, similar to the gnusocial sharings plugin
- Resistant to flooding, hellthreads, etc.
- Support content warnings, reporting and blocking.
- http signatures and basic auth.
- Compatible with http (onion addresses), https and dat.
- Minimal dependencies.
- Capabilities based security
- Data minimization principle. Configurable post expiry time.
- Commandline interface. If there's a GUI it should be a separate project.
- Designed for intermittent connectivity. Assume network disruptions.
- Suitable for single board computers.
Object capabilities workflow
This is one proposed way that OCAP could work.
- Works from person to person, not instance to instance.
- Produces negligible additional network traffic
- Works in the same way between people on different instances or the same instance
- People can alter what their followers can do on an individual basis
- Leverages the existing follow request mechanism
Default capabilities are initially set up when a follow request is made. The Accept activity sent back from a follow request can be received by any instance. A capabilities accept activity is attached to the follow accept.
Alice
|
V
Follow Request
|
V
Bob
|
V
Create/store default Capabilities
for Alice
|
V
Follow Accept + default Capabilities
|
V
Alice
|
V
Store Granted Capabilities
The default capabilities could be any preferred policy of the instance. They could be no capabilities at all, read only or full access to everything.
Example Follow request from Alice to Bob:
{'actor': 'http://alicedomain.net/users/alice',
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
'id': 'http://alicedomain.net/users/alice/statuses/1562507338839876',
'object': 'http://bobdomain.net/users/bob',
'published': '2019-07-07T13:48:58Z',
'to': ['http://bobdomain.net/users/bob'],
'type': 'Follow'}
Follow Accept from Bob to Alice with attached capabilities.
{'actor': 'http://bobdomain.net/users/bob',
'capabilities': {'actor': 'http://bobdomain.net/users/bob',
'capability': ['inbox:write', 'objects:read'],
'id': 'http://bobdomain.net/caps/alice@alicedomain.net#rOYtHApyr4ZWDUgEE1KqjhTe0kI3T2wJ',
'scope': 'http://alicedomain.net/users/alice',
'type': 'Capability'},
'cc': [],
'object': {'actor': 'http://alicedomain.net/users/alice',
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
'id': 'http://alicedomain.net/users/alice/statuses/1562507338839876',
'object': 'http://bobdomain.net/users/bob',
'published': '2019-07-07T13:48:58Z',
'to': ['http://bobdomain.net/users/bob'],
'type': 'Follow'},
'to': ['http://alicedomain.net/users/alice'],
'type': 'Accept'}
When posts are subsequently sent from the following instance (server-to-server) they should have the corresponding capability id string attached within the Create wrapper. To handle the shared inbox scenario this should be a list rather than a single string. In the above example that would be ['http://bobdomain.net/caps/alice@alicedomain.net#rOYtHApyr4ZWDUgEE1KqjhTe0kI3T2wJ']. It should contain a random token which is hard to guess by brute force methods.
Alice
|
V
Send Post
Attach id from Stored Capabilities
granted by Bob
|
V
Bob
|
V
http signature check
|
V
Check Capability id matches
stored capabilities
|
V
Match stored capability scope
against actor on received post
|
V
Check that stored capability
contains inbox:write, etc
|
V
Any other checks
|
V
Accept incoming post
Subsequently Bob could change the stored capabilities for Alice in their database, giving the new object a different id. This could be sent back to Alice as an Update activity with attached capability.
Bob can send this to Alice, altering capability to now include inbox:noreply. Notice that the random token at the end of the id has changed, so that Alice can't continue to use the old capabilities.
{'actor': 'http://bobdomain.net/users/bob',
'cc': [],
'object': {'actor': 'http://bobdomain.net/users/bob',
'capability': ['inbox:write', 'objects:read', 'inbox:noreply'],
'id': 'http://bobdomain.net/caps/alice@alicedomain.net#53nwZhHipNFCNwrJ2sgE8GPx13SnV23X',
'scope': 'http://alicedomain.net/users/alice',
'type': 'Capability'},
'to': ['http://alicedomain.net/users/alice'],
'type': 'Update'}
Alice then receives this and updates her capabilities granted by Bob to:
{'actor': 'http://bobdomain.net/users/bob',
'capability': ['inbox:write', 'objects:read', 'inbox:noreply'],
'id': 'http://bobdomain.net/caps/alice@alicedomain.net#53nwZhHipNFCNwrJ2sgE8GPx13SnV23X',
'scope': 'http://alicedomain.net/users/alice',
'type': 'Capability'}
If she sets her system to somehow ignore the update then if capabilities are strictly enforced she will no longer be able to send messages to Bob's inbox.
Object capabilities can be strictly enforced by adding the --ocap option when running the server. The only activities which it is not enforced upon are Follow and Accept. Anyone can create a follow request or accept updated capabilities.
Object capabilities in the shared inbox scenario
Shared inboxes are obviously essential for any kind of scalability, otherwise there would be vast amounts of duplicated messages being dumped onto the intertubes like a big truck.
With the shared inbox instead of sending from Alice to 500 of her fans on a different instance - repeatedly sending the same message to individual inboxes - a single message is sent to its shared inbox (which has its own special account called 'inbox') and it then decides how to distribute that. If a list of capability ids is attached to the message which gets sent to the shared inbox then the receiving server can use that.
Some capabilities
inbox:write - follower can post anything to your inbox
inbox:noreply - follower can't reply to your posts
inbox:nolike - follower can't like your posts
inbox:nopics - follower can't post image links
inbox:noannounce - follower can't send repeats (announce activities) to your inbox
inbox:cw - follower can't post to your inbox unless they include a content warning
Object capabilities adversaries
If Eve subsequently learns what the capabilities id is for Alice by somehow intercepting the traffic (eg. suppose she works for Eveflare) then she can't gain the capabilities of Alice due to the scope parameter against which the actors of incoming posts are checked.
Eve could create a post pretending to be from Alice's domain, but the http signature check would fail due to her not having Alice's keys.
The only scenarios in which Eve might triumph would be if she could also do DNS highjacking and:
- Bob isn't storing Alice's public key and looks it up repeatedly
- Alice and Bob's instances are foolishly configured to perform blind key rotation such that her being in the middle is indistinguishable from expected key changes
Even if Eve has an account on Alice's instance this won't help her very much unless she can get write access to the database.
Install
sudo pacman -S tor python-pip python-pysocks python-pycryptodome python-beautifulsoup4
sudo pip install commentjson
Running Tests
To run the unit tests:
python3 epicyon.py --tests
To run the network tests. These simulate instances exchanging messages.
python3 epicyon.py --testsnetwork
Viewing Public Posts
To view the public posts for a person:
python3 epicyon.py --posts nickname@domain
If you want to view the raw json:
python3 epicyon.py --postsraw nickname@domain
Account Management
To add a new account:
python3 epicyon.py --addaccount nickname@domain --password [yourpassword]
To remove an account (be careful!):
python3 epicyon.py --rmaccount nickname@domain
To change the password for an account:
python3 epicyon.py --changepassword nickname@domain newpassword
Running the Server
To run with defaults:
python3 epicyon.py
In a browser of choice (but not Tor browser) you can then navigate to:
http://localhost:8085/users/admin
If it's working then you should see the json actor for the default admin account.
For a more realistic installation you can run on a defined domain and port:
python3 epicyon.py --domain [name] --port 8000 --https
You will need to proxy port 8000 through your web server and set up CA certificates as needed.
By default data will be stored in the directory in which you run the server, but you can also specify a directory:
python3 epicyon.py --domain [name] --port 8000 --https --path [data directory]
By default the server will federate with any others. You can limit this to a well-defined list with the --federate option.
python3 epicyon.py --domain [name] --port 8000 --https --federate domain1.net domain2.org domain3.co.uk
Following other accounts
With your server running you can then follow other accounts with:
python3 epicyon.py --nickname [yournick] --domain [name] --follow othernick@domain
You may or may not need to use the --port, --https and --tor options, depending upon how your server was set up.