10个版本

0.1.9 2023年9月15日
0.1.8 2023年9月14日

#1039 in Web编程

MIT 许可证

79KB
1.5K SLoC

沙发Pub:一个最小化功能的活动Pub服务器

$ sofapub
A minimally functional ActivityPub implementation

Usage: sofapub <COMMAND>

Commands:
  setup
  server
  client
  get
  post
  webfinger
  help       Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

今晚我想到,如果能有一个工具来正确签署远程活动Pub服务器的请求,以便我可以评估它们的响应并构建有效的接口,那该多好啊。这个项目就是从那个想法开始的。最初的可行原型是在我坐在沙发上的时候构建的。

TLS

我使用certbot手动生成我的测试域名(sofa.jdt.dev)的证书。这是我用过的命令。

certbot certonly --manual -d sofa.jdt.dev --agree-tos --preferred-challenges dns-01 --server https://acme-v02.api.letsencrypt.org/directory --register-unsafely-without-email --rsa-key-size 4096 --config-dir certbot/config --work-dir certbot/work --logs-dir certbot/logs

显然,您需要更改自己的域名。您需要准备好更新DNS设置以添加验证TXT记录到Let's Encrypt。

网络

您需要在您的服务器上将端口443/tcp转发到端口8086(我选择默认值)上。您还需要适当地配置DNS,以便将域名指向您转发的外部地址。

安装

您可以下载此仓库并使用cargo buildcargo run --bin sofapub,就像您想要的。此包也存在于https://crates.io上,因此您可以直接使用cargo install sofapub进行安装。这将还下载和编译最新版本以进行更新。

设置

这是我使用的设置命令(根据您的需求进行调整)。这将创建RSA证书和基本配置。

sofapub setup \
    --username justin \
    --display-name "Justin Thomas" \
    --summary "This is my test account" \
    --domain sofa.jdt.dev \
    --tls-private-key-path /opt/certbot/config/live/sofa.jdt.dev/privkey.pem \
    --tls-certificate-path /opt/certbot/config/live/sofa.jdt.dev/fullchain.pem

如果您省略了--tls-private-key-path--tls-certificate-path,则服务器将正常工作,但您需要在其他地方处理证书并将连接代理到本地机器上的HTTP端口8086/tcp。如果您使用像ngrok这样的工具公开服务,这可能很有用(我计划在某个时候测试和记录这种情况)。

配置存储在 $HOME/.sofapub 中,并将创建一个合适的目录结构以支持服务器的运行。模板将被写入到 $HOME/.sofapub/templates

操作

RUST_LOG=debug sofapub server 将以调试日志启动 SofaPub 服务器。

发送到 inbox 的消息将被捕获在 ~/.sofapub/data/inbox 文件夹中,以服务器生成的 UUID 文件名。以下是我来自 infosec.exchange 的用户发起的 Follow 请求的示例

$ RUST_LOG=debug sofapub server

[2023-09-02T01:05:32Z DEBUG server] igniting Configuration
[2023-09-02T01:05:32Z INFO  rocket::launch] 🔧 Configured for debug.
[2023-09-02T01:05:32Z INFO  rocket::launch::_] address: 0.0.0.0
[2023-09-02T01:05:32Z INFO  rocket::launch::_] port: 8086
[2023-09-02T01:05:32Z INFO  rocket::launch::_] workers: 16
[2023-09-02T01:05:32Z INFO  rocket::launch::_] max blocking threads: 512
[2023-09-02T01:05:32Z INFO  rocket::launch::_] ident: Rocket
[2023-09-02T01:05:32Z INFO  rocket::launch::_] IP header: X-Real-IP
[2023-09-02T01:05:32Z INFO  rocket::launch::_] limits: bytes = 8KiB, data-form = 2MiB, file = 1MiB, form = 32KiB, json = 1MiB, msgpack = 1MiB, string = 8KiB
[2023-09-02T01:05:32Z INFO  rocket::launch::_] temp dir: /var/folders/z6/y2vfsg3j739fbx4hwzf_m7700000gn/T/
[2023-09-02T01:05:32Z INFO  rocket::launch::_] http/2: true
[2023-09-02T01:05:32Z INFO  rocket::launch::_] keep-alive: 5s
[2023-09-02T01:05:32Z INFO  rocket::launch::_] tls: enabled w/o mtls
[2023-09-02T01:05:32Z INFO  rocket::launch::_] shutdown: ctrlc = true, force = true, signals = [SIGTERM], grace = 2s, mercy = 3s
[2023-09-02T01:05:32Z INFO  rocket::launch::_] log level: normal
[2023-09-02T01:05:32Z INFO  rocket::launch::_] cli colors: true
[2023-09-02T01:05:32Z INFO  rocket::launch] 📬 Routes:
[2023-09-02T01:05:32Z INFO  rocket::launch::_] (inbox_post) POST /inbox
[2023-09-02T01:05:32Z INFO  rocket::launch::_] (profile) GET /<handle>
[2023-09-02T01:05:32Z INFO  rocket::launch::_] (activity_pub) GET /users/<_username>
[2023-09-02T01:05:32Z INFO  rocket::launch::_] (webfinger) GET /.well-known/webfinger?<resource> application/jrd+json
[2023-09-02T01:05:32Z INFO  rocket::launch] 📡 Fairings:
[2023-09-02T01:05:32Z INFO  rocket::launch::_] Shield (liftoff, response, singleton)
[2023-09-02T01:05:32Z INFO  rocket::launch::_] SofaPub Configuration (ignite)
[2023-09-02T01:05:32Z INFO  rocket::shield::shield] 🛡 Shield:
[2023-09-02T01:05:32Z INFO  rocket::shield::shield::_] X-Frame-Options: SAMEORIGIN
[2023-09-02T01:05:32Z INFO  rocket::shield::shield::_] X-Content-Type-Options: nosniff
[2023-09-02T01:05:32Z INFO  rocket::shield::shield::_] Permissions-Policy: interest-cohort=()
[2023-09-02T01:05:32Z WARN  rocket::launch] 🚀 Rocket has launched from https://0.0.0.0:8086
[2023-09-02T01:05:39Z DEBUG rustls::server::hs] decided upon suite TLS13_AES_256_GCM_SHA384
[2023-09-02T01:05:39Z INFO  rocket::server] POST /inbox application/activity+json:
[2023-09-02T01:05:39Z INFO  rocket::server::_] Matched: (inbox_post) POST /inbox
[2023-09-02T01:05:39Z INFO  rocket::server::_] Outcome: Success
[2023-09-02T01:05:39Z INFO  rocket::server::_] Response succeeded.
^C[2023-09-02T01:05:43Z WARN  rocket::server] Received SIGINT. Requesting shutdown.
[2023-09-02T01:05:43Z INFO  rocket::server] Shutdown requested. Waiting for pending I/O...
[2023-09-02T01:05:43Z DEBUG rustls::conn] Sending warning alert CloseNotify
[2023-09-02T01:05:43Z INFO  rocket::server] Graceful shutdown completed successfully.

$ ls -la ~/.sofapub/data/inbox/
total 24
drwxr-xr-x@ 6 justin  staff  192 Sep  1 18:05 .
drwxr-xr-x@ 7 justin  staff  224 Sep  1 17:55 ..
-rw-r--r--@ 1 justin  staff  227 Sep  1 18:03 1b7a3159-dcac-49f8-b687-b1defa06ff79.json
-rw-r--r--@ 1 justin  staff  360 Sep  1 18:05 f7c0ca70-49a9-4901-8928-f7bae48b8d1b.json

$ cat ~/.sofapub/data/inbox/*.json | jq
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "actor": "https://infosec.exchange/users/jdt",
  "id": "https://infosec.exchange/a9941fe3-1051-490c-8cb8-8793a1a9bcf3",
  "object": "https://sofa.jdt.dev/users/justin",
  "type": "Follow"
}
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "actor": "https://infosec.exchange/users/jdt",
  "id": "https://infosec.exchange/users/jdt#follows/2830232/undo",
  "object": {
    "actor": "https://infosec.exchange/users/jdt",
    "id": "https://infosec.exchange/a9941fe3-1051-490c-8cb8-8793a1a9bcf3",
    "object": "https://sofa.jdt.dev/users/justin",
    "type": "Follow"
  },
  "type": "Undo"
}

实际上,我之前就发起了 Follow,上面的日志中显示的是撤销操作。不过,你可以看到这两条消息都被 SofaPub 捕获供你审查。

客户端使用

sofapub 二进制文件中内置了一些客户端功能。随着我进度的发展,我会显著扩展这些功能。目前,你可以这样发出 FollowUndo 动作(用于 Follow 操作)

$ sofapub client follow \
  --id https://infosec.exchange/users/jdt \
  --inbox https://infosec.exchange/users/jdt/inbox
ACTIVITY ID: https://sofa.jdt.dev/objects/4720e9f7-40d8-4c77-bfd4-42e59dc4f962
POST DELIVERED

$ sofapub client follow \
  --id https://infosec.exchange/users/jdt \
  --inbox https://infosec.exchange/users/jdt/inbox \
  --undo https://sofa.jdt.dev/objects/4720e9f7-40d8-4c77-bfd4-42e59dc4f962
ACTIVITY ID: https://sofa.jdt.dev/objects/0b3e0473-2d4a-4e21-b5ae-7fe3ddbe949f
POST DELIVERED

上面显示的是我对我来自 infosec.exchange 的用户 @jdt@infosec.exchange 的用户发出 Follow 命令。该命令发出一个 ACTIVITY ID,然后我使用它来 Undo 那个 Follow。我还在这里的 inbox 中找到了来自 https://infosec.exchangeAccept 活动

{
    "@context":"https://www.w3.org/ns/activitystreams",
    "actor":"https://infosec.exchange/users/jdt",
    "id":"https://infosec.exchange/users/jdt#accepts/follows/2842669",
    "object":{
      "actor":"https://sofa.jdt.dev/profile",
      "id":"https://sofa.jdt.dev/objects/4720e9f7-40d8-4c77-bfd4-42e59dc4f962",
      "object":"https://infosec.exchange/users/jdt",
      "type":"Follow"
    },
    "type":"Accept"
}

目前,用于向 /followers 提供响应的 followers.jsonFollowUndo 命令上会立即更新。我应该调整 Follow 的这一部分,以等待 Accept

使用简单获取功能进行演示

假设你的 SofaPub 已经启动并运行,并且你可以成功地从另一个实例查询你的用户,你应该可以使用包含的工具以有趣的方式查询和交互 ActivityPub 实例。以下是一个示例,我使用 sofapub webfingersofapub get 来从需要请求签名的所有操作的服务器(https://firefish.social)请求配置文件数据。

首先,我使用包含的 webfinger 工具检索我在 Firefish 的用户的 WebFinger 记录。

$ sofapub webfinger @[email protected] | jq
{
  "subject": "acct:[email protected]",
  "links": [
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://firefish.social/users/9j54zro9qetnjhza"
    },
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://firefish.social/@jdt"
    },
    {
      "rel": "http://ostatus.org/schema/1.0/subscribe",
      "template": "https://firefish.social/authorize-follow?acct={uri}"
    }
  ]
}

接下来,我使用类型为 application/activity+jsonself href 值来尝试使用 curl 获取记录。这在大多数默认不需要请求签名的 Mastodon 系统上都会工作。

$ curl -H "Accept: application/activity+json" https://firefish.social/users/9j54zro9qetnjhza
Unauthorized

糟糕。不用担心,这里我通过 sofapub get 处理请求,它使用我的配置中的私有 RSA 密钥对请求进行签名。我的 sofapub server正在运行,以便目标服务器可以检索我的公钥以验证签名。

$ sofapub get https://firefish.social/users/9j54zro9qetnjhza | jq
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
      "movedToUri": "as:movedTo",
      "sensitive": "as:sensitive",
      "Hashtag": "as:Hashtag",
      "quoteUri": "fedibird:quoteUri",
      "quoteUrl": "as:quoteUrl",
      "toot": "http://joinmastodon.org/ns#",
      "Emoji": "toot:Emoji",
      "featured": "toot:featured",
      "discoverable": "toot:discoverable",
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "misskey": "https://misskey-hub.net/ns#",
      "_misskey_content": "misskey:_misskey_content",
      "_misskey_quote": "misskey:_misskey_quote",
      "_misskey_reaction": "misskey:_misskey_reaction",
      "_misskey_votes": "misskey:_misskey_votes",
      "_misskey_talk": "misskey:_misskey_talk",
      "isCat": "misskey:isCat",
      "fedibird": "http://fedibird.com/ns#",
      "vcard": "http://www.w3.org/2006/vcard/ns#"
    }
  ],
  "type": "Person",
  "id": "https://firefish.social/users/9j54zro9qetnjhza",
  "inbox": "https://firefish.social/users/9j54zro9qetnjhza/inbox",
  "outbox": "https://firefish.social/users/9j54zro9qetnjhza/outbox",
  "followers": "https://firefish.social/users/9j54zro9qetnjhza/followers",
  "following": "https://firefish.social/users/9j54zro9qetnjhza/following",
  "featured": "https://firefish.social/users/9j54zro9qetnjhza/collections/featured",
  "sharedInbox": "https://firefish.social/inbox",
  "endpoints": {
    "sharedInbox": "https://firefish.social/inbox"
  },
  "url": "https://firefish.social/@jdt",
  "preferredUsername": "jdt",
  "name": null,
  "summary": null,
  "icon": null,
  "image": null,
  "tag": [],
  "manuallyApprovesFollowers": false,
  "discoverable": true,
  "publicKey": {
    "id": "https://firefish.social/users/9j54zro9qetnjhza#main-key",
    "type": "Key",
    "owner": "https://firefish.social/users/9j54zro9qetnjhza",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyzcWqxqXMH+tsPhIIZhU\nc9kHQHN+quayOAw+FtdX7bNmo+fY2Bndy5wRHymdcF/fFIXxCfeN6aO0FqBsPCrt\nO7XBsRkHi4LPSaZN730q+Q/FZmf6SVy943WWf8LgXOkt2VjJRO52w0seGrPR1/Dd\nB/6rFTDOVWUUyASL8+E1X2yQJ/veHRFrwpLPwYnfjJypCzhd2z3++y1PzjeHwygE\nHJx7EIcmFsiw7F+xDkEY4RWA/vV7bTajsij1P+DRkJJN+eNoK8y58Oxx2hf20tko\nf4cFnuawyLCRquixNlTHNqHxXR87nLEMP4rZrjuOjf5aIG7kxbyBnNuPtZ8ASrb/\nyprlsWkhkOY22K+XwvTWGDyo8Fduxh5ntWUB97fV1gDzD2NoN2bhXA4giUGCbo5V\nTj2Sbgsvk/DrF21whQjCJvVCThZwKfX7hZaaTWljNE1UEOTyS16WM+1i/ZWlOE5V\nQ7IbgImC+0rsbE9XeQaBJK5OhrOgO1nUeQkR4DwSaSORWuLf5xewJ6ZYxT474M+l\nytmrvCJFLUriDqFk8zjr6gon7fLt2yKagLaEU5DXduJQRMkpJ4hajpuZE2YNQwGj\n+ZNtpc6+OYfSbyxo2D/+HfVf1emQ1tSKo/w0chLzbokpIEFuR/4pmg1Etr5XG3XY\nP2nwPARDmUE/dzJ9Avq8Qy8CAwEAAQ==\n-----END PUBLIC KEY-----\n"
  },
  "isCat": false
}

依赖关系

~42–77MB
~1.5M SLoC