Skip to content

Latest commit

 

History

History
280 lines (202 loc) · 10.1 KB

README.md

File metadata and controls

280 lines (202 loc) · 10.1 KB

How to run a ChactivityServer

Dependencies

To run this project, you will need openssl-devel and rust on your system. Then, a working configuration for your http server and for the project will be necessary.

Nginx

This is an example of configuration for NGINX (with certbot for https-certificates)

server {
    server_name example.tld; # managed by Certbot
    root /var/www/example/public/;

    location /inbox {
        proxy_pass http://localhost:8080;
    }

    location /users {
        proxy_pass http://localhost:8080;
    }

    location /.well-known/webfinger {
        proxy_pass http://localhost:8080;
    }

    location / {
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }


    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/example-tld/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example-tld/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

Configuration

config.json can be used as a starting point to configure the project. .keys/privkey.pem and .keys/publickey.pem are used to sign post requests between instances. I use the private key generated by certbot (and do a static link). To get a public key:

openssl rsa -in /etc/letsencrypt/live/example/privkey.pem -outform PEM -pubout -out /var/www/example/chactivityserver/.keys/pubkey.pem
  • auto_follow_back will automatically follow people following the instance.

How it works?

Preferred links

This project mostly uses 3 standards:

Profile discovery

There is mostly 3 different aspects. Profile discovery, announcing articles, retrieving articles. The first step is profile discovery. Any chactivity server uses by default a single account (chef) and the discovery is done via the WebFinger protocol.

Webfinger

To discover a profile, servers MUST do a request to example.tld/.well-known/webfinger?resource=acct:[email protected]. This will return a JSON response containing the link to the profile (example.tld/users/chef)

Profile

example.tld/users/chef is accepting GET requests with the header: Accept: application/activity+json. And is returning a JSON response containing the profile, links to the outbox, followers, inbox and linked inbox (which is the same as the inbox, as there is only one user).

Outbox

example.tld/users/chef/outbox is accepting GET requests with the header: Accept: application/activity+json. The outbox contains all articles already written. So that any server can get all past interactions. The outbox is divided in pages of 12 articles. A page can be accessed by passing page=1 to the request (or any other number). Page=1 is the first page. The last page is announced in the outbox description (page=0 or no page parameter).

Finally, the outbox content is cached in the cache directory. This cache is automatically invalidated when a new recipe in /content/recettes is detected (we store the current number of articles and the last modification date in .cache/date_file.txt).

However, this is not enough to have incoming articles on remote instances. The server MUST announce new articles by posting to followers inbox.

Posting events

Http-Signatures

Sending events to other instances MUST include a HTTP-Signature. This signature is generated by:

  1. Using the keys provided in the configuration (a classic RSA key)
  2. It uses the rsa-sha256 algorithm to sign requests. This is done via a fork of http-sig
  3. This adds a new header to the signed requests like:
Signature: keyId="rsa-key-1",algorithm="hs2019",
     created=1402170695, expires=1402170995,
     headers="(request-target) (created) (expires)
       host date digest content-length",
     signature="Base64(RSA-SHA256(signing string))"

Posting on the Fediverse

When a new article is detected, the cache is refreshed and articles are transformed into a JSON:

{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        {
            "ostatus": "http://ostatus.org#",
            "atomUri": "ostatus:atomUri",
            "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
            "conversation": "ostatus:conversation",
            "sensitive": "as:sensitive",
            "toot": "http://joinmastodon.org/ns#",
            "votersCount": "toot:votersCount"
        }
    ],
    "id": format!("https://{}/recettes/{}", self.config.domain, filename_without_extension),
    "type": "Create",
    "actor": format!("https://{}/users/{}", self.config.domain, self.config.user),
    "published": published,
    "to": [
        "https://www.w3.org/ns/activitystreams#Public"
    ],
    "cc": [
        format!("https://{}/users/{}/followers", self.config.domain, self.config.user),
    ],
    "object": {
        "id": format!("https://{}/recettes/{}", self.config.domain, filename_without_extension),
        "type": "Article",
        "summary": null,
        "inReplyTo": null,
        "published": published,
        "url": format!("https://{}/recettes/{}", self.config.domain, filename_without_extension),
        "attributedTo": format!("https://{}/users/{}", self.config.domain, self.config.user),
        "to": [
            "https://www.w3.org/ns/activitystreams#Public"
        ],
        "cc": [
            format!("https://{}/users/{}/followers", self.config.domain, self.config.user),
        ],
        "sensitive": false,
        "atomUri": format!("https://{}/recettes/{}", self.config.domain, filename_without_extension),
        "content": markdown,
        "name": match_title.get(1).map_or("Chalut!", |m| m.as_str()).to_owned(),
        "mediaType": String::from("text/markdown"),
        "attachment": attachments,
        "tag": tags_value,
        "license": self.config.license
    }
}

attachments: are the list of images in the article (checked in config.img_dir/recipe-name/) tags are retrieved from the article if the list in the config is empty.

This JSON is sent to each followers inbox via signed requests. If two followers got a shared inbox, only one POST will be done.

Followers

Followers are cached in .cache/followers and available via example.tld/users/chef/followers.

A new follower is added to the list by receiving a Follow activity in the inbox. Then, an Accept activity is sent to the follower inbox.

Mechanism is described there.

Other activities

Liking or boosting recipes

Likes and boosts are cached in .cache/likes.json and available via example.tld/users/chef/likes with the following parameters:

  • object the current pathname (e.g. /recettes/profiteroles-chocolat)
  • wanted_type like or boost

This is shown in the website via a fetch() request done in layouts/_default/single.html

Receiving articles

From other instances

It will be incoming articles automatically received when they will update their cache. The content will appear when hugo will be executed. For the format, cf Posting on the Fediverse.

From Mastodon

It's possible to convert toots to articles in cha-cuit. Toots are Note objects in the ActivityPub protocol. The following format will be used:

---
title: Note["summary"]
date: SystemTime.now
tags: [Note["tags"]]
author: Note["actor"]
thumbnail: (Optional)Note["attachment"][0]
---

Note["content"]

(Optional) Gallery of Notes["attachment"]

Note["id"]

So, a CW will be used as a title, the gallery will be up to 4 images and #chacuit must be specified.

e.g.:

Mastodon

Cha-Cuit

Signature verification

Every incoming POST request in the inbox MUST include a signature.

The mechanism is fully documented there and the code uses a fork of PassFort/http-signatures#18. Some other steps are also documented here.

Basically, 3 steps are done:

  1. The Digest header is verified: base64(sha256(body)).
  2. The Date should be from a request less than 12 hours old.
  3. The Signature header is verified:

e.g. Signature: keyId="https://my-example.com/actor#main-key",headers="(request-target) host date digest",signature="Y2FiYW...IxNGRiZDk4ZA=="

keyId will be fetched and we will retrieve result['publicKey']['publicKeyPem']. Then we will build the string to verify (e.g. signedString):

(request-target): post /users/chef/inbox
host: cha-cu.it
date: Fri, 13 Jan 2023 00:24:25 GMT
digest: SHA-256=0ipv+6eNelzhIkYlUeJkSVz+mAuFPOlABHgWP4UdY1Y=
content-type: application/activity+json

that will be something like: PublicKey.verify(signedString, base64.decode(Signature["signature"])).

Following instances

There is 2 ways to get incoming articles from the Fediverse:

  1. Following users. This is configurable via config.json (auto_follow_back and manual_follow_list which can contains actors that will be automatically followed during cache invalidation)
  2. Following other instances via instances.txt.

Mentions

New comments on article are not shown (at least for now), but if somebody answer to a post (e.g. via commenting it on mastodon), the comment will be saved in .cache/mentions. So that you can do whatever you want with the comments.

Blocking instances

To block a user or an instance, simply add it to the file configured in block_list in the config. At the next update, they will be added to the block list, all previous content hidden, new content will be discarded and the blocked user will be un-followed.