diaspora* 0.9.1.0 联邦与 REST API 集成指南

diaspora* 0.9.1.0 进阶实测:WebFinger/hCard/Salmon 联邦三链路与双 pod TLS 端到端中继闭环实录,OAuth2 scope 白名单、REST API 端点与 OIDC 集成全链路,附七条实测踩坑。

diaspora*diaspora*Fediverse联邦平台RubyRails2026/11/02

diaspora* 0.9.1.0 联邦与 REST API 集成指南

本文是 diaspora* 专栏的进阶篇,基于 0.9.1.0 双 pod 联邦端到端实测与 REST API 全链路实测。前置认知见 《diaspora* 介绍与选型指南》,单 pod 部署见 《diaspora* Docker Compose 部署指南》,日常维护见 《diaspora* 日常运维手册》。文中命令与回显均为 2026-11-02 逐字实录;pod 域名以 pod.example.com / pod-b.example.com 占位,示例管理员账号 admin@pod.example.com / q1w2e3r4。

一、环境信息

1.1 双 pod 实测拓扑

项pod Apod B
compose 项目diaspora-0.9.1.0-stackdiaspora-b-0.9.1.0
镜像diaspora-app:0.9.1.0(同一自建镜像)同左
URLhttps://pod.example.com/(宿主 8143:443)https://pod-b.example.com/(宿主 8144:443)
数据库diaspora_productiondiaspora_b_production(DATABASE_NAME 区分)
网络diaspora-0910-stack_diaspora(subnet 10.89.68.0/24)external 加入 pod A 网络,别名 pod-b.example.com

两 pod 各自独立 web/sidekiq/db/redis 四容器;通过 compose 网络别名让容器内 DNS 直接解析对方域名,模拟真实跨域名联邦。

1.2 联邦栈版本

联邦层由 diaspora_federation gem 1.2.0(+ diaspora_federation-rails 引擎)实现,源码 Gemfile.lock 可查;nodeinfo 实测 protocols: ["diaspora"]——diaspora* 走自有协议,不与 ActivityPub 直接互通(选型背景见认知篇)。

二、联邦协议原理

2.1 发现链(Discovery)

远程实体定位固定三步,全部走 HTTPS:

  1. WebFinger:GET https://<pod域名>/.well-known/webfinger?resource=acct:<user>@<pod域名> → 返回 hcard/salmon/seed/profile-page/atom 链接集合(实测 200,链接为 pod 绝对 URL);
  2. hCard:GET /hcard/users/:guid → 公开档案 + RSA 公钥(验签基础);
  3. 实体抓取:GET /fetch/:type/:guid → 返回 Salmon MagicEnvelope(base64url 内嵌签名 XML)。

2.2 投递链(Delivery)

  • 推送:发送侧由 sidekiq 作业(Workers::SendPrivate、Workers::Public 等)把签名 XML POST 到接收侧 POST /receive/public 或 POST /receive/users/:guid;
  • 补拉:接收侧遇到未知实体时按 2.1 第 3 步回源抓取;
  • 订阅:GET /public/:username.atom 提供公开 Atom 流。

联邦引擎路由逐字(diaspora_federation-rails 1.2.0 config/routes.rb):

POST /receive/public
POST /receive/users/:guid
GET  /fetch/:type/:guid
GET  /.well-known/webfinger
GET  /hcard/users/:guid

2.3 TLS 是联邦前提(源码实锤)

diaspora_federation 1.2.0 discovery.rb:webfinger 永远先请求 https://<domain>/.well-known/webfinger,仅当 DiasporaFederation.webfinger_http_fallback 为 true 才回退 http;而应用 initializer 写死:

config.webfinger_http_fallback = Rails.env == "development"

结论:生产 http-only 的 pod 之间无法联邦(实测 pod B 解析 pod A 后 443 拒连 → DiscoveryError)。官方文档同样要求 TLS。

三、双 pod 联邦环境搭建

3.1 证书(自签 CA,实测命令逐字)

openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
  -keyout ca.key -out ca.crt -subj "/CN=S8 Diaspora Test CA"
for h in pod.example.com pod-b.example.com; do
  openssl req -newkey rsa:2048 -nodes -keyout $h.key -out $h.csr -subj "/CN=$h"
  printf "subjectAltName=DNS:%s\n" $h > san.ext
  openssl x509 -req -in $h.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -days 825 -sha256 -extfile san.ext -out $h.crt
done
# 联邦出站信任库 = 测试CA + 系统CA(两 pod 共用)
cat ca.crt /etc/ssl/certs/ca-certificates.crt > ca-combined.pem

3.2 pod A 切换 https

compose 环境变量(puma 原生 ssl:// bind,无需 nginx):

ENVIRONMENT_URL: "https://pod.example.com/"
ENVIRONMENT_REQUIRE_SSL: "true"
SERVER_LISTEN: "ssl://0.0.0.0:443?key=/diaspora/config/certs/pod.example.com.key&cert=/diaspora/config/certs/pod.example.com.crt"
ENVIRONMENT_CERTIFICATE_AUTHORITIES: "/diaspora/config/certs/ca-combined.pem"

端口 8143:443,挂载 ./data/certs:/diaspora/config/certs:ro。web 启动日志实测:

Rack::SSL is enabled
* Listening on ssl://0.0.0.0:443?key=/diaspora/config/certs/pod.example.com.key&cert=...

官方口径:http→https 改协议无后果;pod 域名上线后不可更改(它固化在每个 diaspora_id 与已签名实体里)。

3.3 pod B(docker-compose-b.yml 关键差异)

同镜像、独立 db-b/redis-b/数据目录,与 pod A 的四处差异:

name: diaspora-b-0.9.1.0
services:
  web:
    ports: ["8144:443"]
    environment:
      ENVIRONMENT_URL: "https://pod-b.example.com/"
      DATABASE_NAME: diaspora_b_production
      SERVER_LISTEN: "ssl://0.0.0.0:443?key=/diaspora/config/certs/pod-b.example.com.key&cert=/diaspora/config/certs/pod-b.example.com.crt"
      # ENVIRONMENT_CERTIFICATE_AUTHORITIES 同 pod A
    networks:
      diaspora:
        aliases: [pod-b.example.com]
networks:
  diaspora:
    external: true
    name: diaspora-0910-stack_diaspora   # 复用 pod A 网络

sidekiq/db/redis 服务同理(sidekiq 必须同样挂 certs 卷,见 6.1)。pod B 注册 admin@pod-b.example.com 并 Role.add_admin 提权(流程与 pod A 完全一致,见部署篇 4.8)。

四、联邦实测记录

4.1 跨 pod 用户发现

在 pod B 容器内按 diaspora_id 抓取 pod A 用户:

$ docker compose -f docker-compose-b.yml exec -T web bin/rails runner /dev/stdin <<'RUBY'
p = Person.find_or_fetch_by_identifier("admin@pod.example.com")
puts "FETCHED handle=#{p.diaspora_handle} guid=#{p.guid} pod=#{p.pod.slice(:host,:ssl,:status)} public_key?=#{!p.public_key.blank?}"
RUBY
FETCHED handle=admin@pod.example.com guid=2ab71dd0b885013fc1726ec7298bc224 pod={"host"=>"pod.example.com","ssl"=>true,"status"=>"unchecked"} public_key?=true

链路走 https://pod.example.com/.well-known/webfinger → hcard → 实体抓取,别名 DNS 命中 pod A 容器 443,TLS 以 ca-combined.pem 校验通过。

4.2 pod B 关注 pod A(Contact 模型坑)

必须用 User#share_with:Contact 模型没有 aspect/aspect_id 字段,面向关系走 AspectMembership(has_many :aspects, through: :aspect_memberships),直接给 Contact 塞 aspect_id 会报错:

u = User.first
person = Person.find_or_fetch_by_identifier("admin@pod.example.com")
aspect = u.aspects.first
u.share_with(person, aspect)

pod B 侧结果:sharing=false receiving=true(我发起了分享,对方尚未回关)。share_with 触发 pod B sidekiq 的 SendPrivate/SendContact 作业推送 pod A。

4.3 pod A 自动接受回关

PODA sees remote="admin@pod-b.example.com" guid=d95abc90b887013ff28672cd1b5135e7
PODA contact sharing=true receiving=false

pod A 默认 interactions.following=accept(自动接受关注),于是 pod A 侧 sharing=true——双向通道打通。

4.4 帖子中继闭环(核心成果)

pod A 用 REST API 发公开帖:

{"body": "Federation relay test from pod A", "public": true}

→ 201,guid=617b4990b889013fe328121320dd5457。8 秒后在 pod B 查库:

PODB RECEIVED: guid=617b4990b889013fe328121320dd5457 text=Federation relay test from pod A author=admin@pod.example.com

跨 pod Salmon 推送闭环成立:pod A sidekiq 出站投递 → pod B /receive/... 入站 → 验签入库,guid 全链一致。

4.5 实体抓取端点(type 命名坑)

GET /fetch/StatusMessage/<guid>   → 500 InvalidEntityName
GET /fetch/status_message/<guid>  → 200 Salmon MagicEnvelope XML

/fetch/:type/:guid 的 type 必须是 snake_case 实体名(ENTITY_NAME_REGEX 校验),返回体是 base64url 内嵌签名 XML 的 MagicEnvelope。

五、REST API 集成

5.1 认证模型链

0.9.0.0 起 API 为官方支持面(文档:https://diaspora.github.io/api-documentation/)。源码模型链:

OAuthApplication(client_id/secret)
  └─ Authorization(scopes 白名单校验,绑定 user+application)
       └─ OAuthAccessToken(token = SecureRandom.hex(32),expires_at = 24h)

Authorization::SCOPES 白名单逐字(app/models/api/openid_connect/authorization.rb):

contacts:modify  contacts:read  conversations  email  interactions
name  nickname  notifications  openid  picture
private:modify  private:read  profile  profile:modify  profile:read_private
public:modify  public:read  sub  tags:modify  tags:read

请求以 Authorization: Bearer <token> 携带;控制器按端点声明所需 scope(require_access_token %w[...]),不足返回 403 insufficient_scope,token 无效返回 401。

5.2 实测造 token(rails runner 脚本)

生产应走 OAuth 授权流程(用户在 /api/applications 注册应用 + 授权码换 token);服务端自动化测试可直接建链:

app = OAuthApplication.create!(name: "s8-test", redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
                               scopes: Api::OpenidConnect::Authorization::SCOPES)
u = User.first
auth = Authorization.create!(user: u, o_auth_application: app,
                             scopes: Api::OpenidConnect::Authorization::SCOPES)
tok = auth.o_auth_access_tokens.create!
puts "CLIENT_ID=#{app.uid} TOKEN=#{tok.token}"

实测输出:CLIENT_ID=83997982f890b31eaad80342f51f54fe 同型 32 位 hex;TOKEN 为 64 位 hex(b79084e5e1638c3f...),expires_at 24 小时。

5.3 端点实测(pod A,https://localhost:8143)

请求scope 要求实测
GET /api/v1/streams/main?limit=1contacts:read + private:read200,JSON 数组含本地与远程帖
GET /api/v1/posts/:guidpublic:read200,帖子 JSON(含 author diaspora_id)
POST /api/v1/postspublic:modify201,guid=02f39260b887013fc1726ec7298bc224
GET /api/v1/userprofile403 insufficient_scope(见 5.4)
任意端点 + 错误 token—401

发帖 payload 与网页表单不同构,这是集成最常见的坑:

curl -sk -X POST https://localhost:8143/api/v1/posts \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"body": "Hello from API", "public": true}'

控制器 normalized_create_params 把 body 映射为 status_message.text,public 必填(params.require(:public)),可选 aspects/location/poll/photos(photo 需先经上传接口且属当前用户 pending 状态)。

5.4 scope 粒度陷阱

GET /api/v1/user(当前用户档案)要求 profile scope(users_controller.rb)——它不在「public/private 读写」常见组合里,漏配时 403 且错误体明确 insufficient_scope。集成时按最小权限申请:只读公开流给 public:read,机器人发帖给 public:modify,需要档案再加 profile。

5.5 OIDC 发现端点

GET /.well-known/openid-configuration → 200,含 authorization_endpoint / token_endpoint / jwks_uri 等,第三方应用可走标准 OIDC 登录。注意:0.9.1.0 正是修复 OIDC 实现中恶意客户端注册触发 pod 内网请求(SSRF)漏洞的安全版本,低于 0.9.1.0 的 pod 接入第三方 OIDC 应用前应先升级。

六、集成踩坑与建议(实测排障实录)

  1. sidekiq 忘挂 certs 卷 → 出站联邦全停:pod A 切 https 后 sidekiq 因 ENVIRONMENT_CERTIFICATE_AUTHORITIES 指向不存在的路径直接 FATAL 崩溃循环,web 一切正常、帖子「发出去了」但远端永远收不到——积压作业滞留 Redis。补 - ./data/certs:/diaspora/config/certs:ro 重启后积压立刻消费。排查口诀:入站看 web,出站看 sidekiq。
  2. http-only 无法联邦:见 2.3,别在测试环境浪费时间,直接上 TLS。
  3. API 与网页表单参数不同构:网页 status_message[text] 嵌套 vs API body 扁平,混用会得到 422 Failed to create the post。
  4. token 24h 过期:长驻集成用 refresh_token 机制或定期重建授权;测试脚本造 token 时记住有效期。
  5. 域名不可迁移:联邦身份(diaspora_id、签名实体、hcard URL)全部固化域名,pod 上线前确定域名。
  6. rails runner 多语句经 docker compose exec 拆词:报 Please specify a valid ruby command;用 /dev/stdin + heredoc 喂脚本文件(本文 4.1/5.2 均此法)。
  7. /fetch 实体名 snake_case:StatusMessage 会 500 InvalidEntityName。

七、参考链接

diaspora* 0.9.1.0 联邦与 REST API 集成指南 - Quzuo