10个版本
0.1.9 | 2023年9月15日 |
---|---|
0.1.8 | 2023年9月14日 |
#1039 in Web编程
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 build
和cargo 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
二进制文件中内置了一些客户端功能。随着我进度的发展,我会显著扩展这些功能。目前,你可以这样发出 Follow
和 Undo
动作(用于 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.exchange 的 Accept
活动
{
"@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.json
在 Follow
和 Undo
命令上会立即更新。我应该调整 Follow
的这一部分,以等待 Accept
。
使用简单获取功能进行演示
假设你的 SofaPub 已经启动并运行,并且你可以成功地从另一个实例查询你的用户,你应该可以使用包含的工具以有趣的方式查询和交互 ActivityPub 实例。以下是一个示例,我使用 sofapub webfinger
和 sofapub 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+json
的 self
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