docs-matrix-spec/locales/zh-Hans/server-server-api.md
2025-04-20 16:13:37 +08:00

762 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "服务器-服务器 API"
weight: 20
type: docs
---
{{< boxes/warning >}}
本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 <a href="https://codeberg.org/wholetrans/docs-matrix-spec">Forgejo 存储库</a> 打开 Issue、提交 Pull Request 或<a href="mailto:errata@wholetrans.org">邮件联系</a>我们提出改进建议和参与翻译与核对。
{{< /boxes/warning >}}
Matrix 家服务器homeservers之间通过联邦 API亦称为服务器-服务器 API进行通信。家服务器利用这些 API 实时互投消息、检索彼此的历史消息,并查询关于对方服务器上用户的资料和在线状态等信息。
这些 API 通过各服务器之间的 HTTPS 请求实现。HTTPS 请求在 TLS 传输层和 HTTP 层的 Authorization 头内均需使用公钥签名进行强认证。
家服务器之间主要有三种通信方式:
持久化数据单元PDU
这些事件会从一个家服务器广播到加入同一房间(由房间 ID 标识)的其他任意服务器。它们会被持久化用于记录房间消息和状态的历史。
类似电子邮件PDU 的原始服务器负责将该事件传递给目标服务器。然而PDU 使用原始服务器的私钥签名,因此可以通过第三方服务器进行传递。
短暂数据单元EDU
这些事件在家服务器对之间点对点推送。它们不会被持久化,也不是房间历史的一部分,接收的家服务器也无需回复。
查询请求Queries
这是由一方发起、向另一方发送 HTTPS GET 请求以获取某些信息,并由对方应答的单次请求/响应交互。不会被持久化,也不包含任何长期历史,仅请求查询发起瞬间的快照状态。
EDU 和 PDU 进一步被封装在一个称为事务Transaction的信封中通过 HTTPS PUT 请求从源服务器传送至目标家服务器。
## API 标准
Matrix 服务器-服务器通信的强制基准是通过 HTTPS API 交换 JSON 对象。未来可能会指定更高效的传输方式作为可选扩展。
所有的 `POST``PUT` 端点要求请求服务器在请求体中提供一个可能为空的JSON 对象。请求服务器应为所有带有 JSON 请求体的请求提供 `Content-Type: application/json` 头,但不是强制性的。
同理,本规范中的所有端点要求目标服务器返回一个 JSON 对象。服务器必须在所有 JSON 响应中包含 `Content-Type: application/json` 头。
所有请求和响应中的 JSON 数据都必须使用 UTF-8 编码。
### TLS
服务器-服务器通信必须通过 HTTPS 实现。
目标服务器必须提供由已知证书机构签署的 TLS 证书。
请求服务器最终负责确定信任的证书机构,强烈建议依赖操作系统的判断。服务器可以为管理员提供覆盖信任机构列表的方法。服务器还可以针对白名单中的域名或网段跳过证书验证,用于测试或在其他地方完成验证(如 `.onion` 地址)的网络环境下。
在发起请求时,服务器应尽可能遵守 SNI服务器名称指示发送期望证书的 SNI除非证书预期是 IP 地址IP 地址不支持 SNI不应发送
建议服务器利用 [证书透明计划](https://www.certificate-transparency.org/)。
### 不支持的端点
若收到对不支持(或未知)端点的请求,服务器必须返回 404 `M_UNRECOGNIZED` 错误。
同样405 `M_UNRECOGNIZED` 错误用于指示对已知端点的不支持 HTTP 方法。
## 服务器发现
### 解析服务器名称
每个 Matrix 家服务器通过一个包含主机名和可选端口的服务器名称唯一标识,详见 [语法说明](/appendices#server-name)。如适用,委托服务器名采用相同语法。
服务器名需解析为可连接的 IP 地址与端口,解析过程中涉及不同证书和 `Host` 头的设置。整体流程如下:
1. 如果主机名是 IP 字面量,则应直接使用该 IP 与指定端口(未指定则为 8448。目标服务器必须呈现对应 IP 地址的有效证书。请求中的 `Host` 头应设为服务器名称(若含端口也需带端口)。
2. 若主机名不是 IP 字面量,且服务器名称中包含明确端口,需通过 CNAME、AAAA 或 A 记录解析主机名为 IP 地址;请求将发至解析得到的 IP 和端口,`Host` 头为原始服务器名称(含端口)。目标服务器必须呈现该主机名的有效证书。
3. 若主机名非 IP 字面量,无明确端口,则向 `https://<hostname>/.well-known/matrix/server` 发起常规 HTTPS 请求,期望返回本节后续定义的模式。须跟随 30x 跳转,但需避免重定向循环。`/.well-known` 端点的响应(无论成功与否)应由请求服务器进行缓存。服务器应遵守响应内的缓存控制头,如无则使用合理默认值(建议 24 小时)。另外应限制响应最大缓存时间,建议为 48 小时。错误建议缓存最多一小时,并对重复失败采用指数退避。`/.well-known` 返回的响应模式详见本节后续。若响应无效JSON 无效、字段缺失、返回非 200 等)则跳转到步骤 4。若响应有效解析 `m.server` 字段(格式 `<delegated_hostname>[:<delegated_port>]`)并按如下处理:
1.`<delegated_hostname>` 为 IP 字面量,则用该 IP 和 `<delegated_port>`(未提供则 8448。目标服务器必须有对应 IP 的有效 TLS 证书。请求 `Host` 头为该 IP若含端口亦包含端口
2.`<delegated_hostname>` 非 IP 字面量,且 `<delegated_port>` 存在,查找其 CNAME、AAAA 或 A 记录,得出 IP连同 `<delegated_port>` 使用。请求 `Host` 头为 `<delegated_hostname>:<delegated_port>`。目标服务器需有 `<delegated_hostname>` 的有效证书。
3. {{% added-in v="1.8" %}} 若 `<delegated_hostname>` 不是 IP 字面量,且未指定 `<delegated_port>`,则查找 `_matrix-fed._tcp.<delegated_hostname>` 的 SRV 记录,可能带来新的主机名(需 AAAA 或 A 记录解析)及端口。请求应发往解析出的 IP 与端口,`Host` 头为 `<delegated_hostname>`。目标服务器需有 `<delegated_hostname>` 的有效证书。
4. **[已废弃]** 若 `<delegated_hostname>` 不是 IP 字面量,未指定 `<delegated_port>`,且找不到 `_matrix-fed._tcp.<delegated_hostname>` SRV 记录,则查 `_matrix._tcp.<delegated_hostname>`,同样可能得到主机名和端口。请求应发往解析到的 IP 和端口,`Host` 头为 `<delegated_hostname>`。目标服务器需有 `<delegated_hostname>` 的有效证书。
5. 若未找到 SRV 记录,通过 CNAME、AAAA 或 A 记录解析 IP之后用 8448 端口发请求,`Host` 头为 `<delegated_hostname>`。目标服务器须有 `<delegated_hostname>` 的有效证书。
4. {{% added-in v="1.8" %}} 若 `/.well-known` 请求返回错误,则尝试解析 `_matrix-fed._tcp.<hostname>` 的 SRV 记录,或得主机名和端口。请求发往解析到的 IP 与端口,`Host` 头为 `<hostname>`,目标服务器需有 `<hostname>` 的有效证书。
5. **[已废弃]** 若 `/.well-known` 请求错误且找不到 `_matrix-fed._tcp.<hostname>` SRV 记录,则解析 `_matrix._tcp.<hostname>` SRV 记录,同样可能获主机名和端口。请求发往解析到的 IP 和端口,`Host` 头为 `<hostname>`,目标服务器需有 `<hostname>` 的有效证书。
6.`/.well-known` 返回错误,且未找到 SRV 记录,则用 CNAME、AAAA、A 记录解析 IP发往 8448 端口,`Host` 头为 `<hostname>`,目标服务器需有 `<hostname>` 有效证书。
{{% boxes/note %}}
我们强制要求 SRV 委托使用 `<hostname>` 而非 `<delegated_hostname>` 的原因:
1. DNS 并不安全(并非所有域名都部署 DNSSEC因此委托目标必须通过 TLS 证明自己是 `<hostname>` 的合法代理。
2. 与 [RFC6125](https://datatracker.ietf.org/doc/html/rfc6125#section-6.2.1) 以及 XMPP 等其他使用 SRV 记录的应用保持一致。
{{% /boxes/note %}}
{{% boxes/note %}}
注意,根据 [RFC2782](https://www.rfc-editor.org/rfc/rfc2782.html) 要求SRV 记录的目标不能是 CNAME
> the name MUST NOT be an alias (in the sense of RFC 1034 or RFC 2181)
{{% /boxes/note %}}
{{% boxes/note %}}
步骤 3.4 与 5 已废弃,因为采用了 IANA 未注册的服务名,未来可能被规范移除。鼓励服务器管理员优先使用 `.well-known`,不要依赖任何形式的 SRV 记录。
关于 8448 端口与 `matrix-fed` 的 IANA 注册见 [此处](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=matrix-fed)。
{{% /boxes/note %}}
{{% http-api spec="server-server" api="wellknown" %}}
### 服务器实现
{{% http-api spec="server-server" api="version" %}}
### 获取服务器公钥
{{% boxes/note %}}
曾经存在“版本 1”密钥交换已因意义不大被规范移除。可在 [历史草案](https://github.com/matrix-org/matrix-doc/blob/51faf8ed2e4a63d4cfd6d23183698ed169956cc0/specification/server_server_api.rst#232version-1) 查阅。
{{% /boxes/note %}}
每个家服务器通过 `/_matrix/key/v2/server` 发布自身公钥。家服务器可直接请求 `/_matrix/key/v2/server` 获取公钥,也可借助中间公证服务器通过 `/_matrix/key/v2/query/{serverName}` API 查询。公证服务器会代表其他服务器查询目标服务器的 `/_matrix/key/v2/server` API然后用自己的密钥为响应签名。服务器可同时查询多个公证服务器确保它们返回的公钥一致。
该方法借鉴了 [Perspectives Project](https://web.archive.org/web/20170702024706/https://perspectives-project.org/),但增加了 NACL 密钥并采用 JSON 而非 XML。其优势是不依赖单一信任根每台服务器可自由选择信任哪些公证且能通过交叉查询验证密钥一致性。
#### 公钥发布
家服务器在 `/_matrix/key/v2/server` 以 JSON 对象发布其签名密钥。响应包含一组 `verify_keys`,用于签名联邦请求以及事件。还包含一组仅可用于事件签名的 `old_verify_keys`
{{% http-api spec="server-server" api="keys_server" %}}
#### 通过其他服务器查询密钥
服务器可通过公证服务器查询目标服务器公钥。公证服务器可能也是别的家服务器。其会使用 `/_matrix/key/v2/server` 从目标服务器取得密钥,并在响应前对结果再签名。
对于离线或无法提供密钥的服务器,公证服务器可利用缓存返回密钥。为防止 DNS 欺骗,可同时向多台服务器查询密钥。
{{% http-api spec="server-server" api="keys_query" %}}
## 认证
### 请求认证
家服务器发起的每个 HTTP 请求都需使用公钥数字签名进行认证。请求方法、目标和体被封装进 JSON 对象后签名,签名采用 JSON 签名算法,最终以 `X-Matrix` 认证方式添加至 Authorization 头。注意target 字段需包含以 `/_matrix/...` 为首的全路径(包括 `?` 和各参数),不含前导 `https:` 与目标服务器主机名。
步骤 1签名 JSON
```
{
"method": "POST",
"uri": "/target",
"origin": "origin.hs.example.com",
"destination": "destination.hs.example.com",
"content": <JSON-parsed request body>,
"signatures": {
"origin.hs.example.com": {
"ed25519:key1": "ABCDEF..."
}
}
}
```
上例中的服务器名称是相关家服务器的服务器名,不受 [服务器名称解析](#resolving-server-names) 中委托影响,始终用委托前的名称。此规则在后续签名流程中也适用。
步骤 2添加 Authorization 头:
POST /target HTTP/1.1
Authorization: X-Matrix origin="origin.hs.example.com",destination="destination.hs.example.com",key="ed25519:key1",sig="ABCDEF..."
Content-Type: application/json
<JSON-encoded request body>
Python 示例代码:
```py
def authorization_headers(origin_name, origin_signing_key,
destination_name, request_method, request_target,
content=None):
request_json = {
"method": request_method,
"uri": request_target,
"origin": origin_name,
"destination": destination_name,
}
if content is not None:
# 假定内容已为解析好的 JSON
request_json["content"] = content
signed_json = sign_json(request_json, origin_name, origin_signing_key)
authorization_headers = []
for key, sig in signed_json["signatures"][origin_name].items():
authorization_headers.append(bytes(
"X-Matrix origin=\"%s\",destination=\"%s\",key=\"%s\",sig=\"%s\"" % (
origin_name, destination_name, key, sig,
)
))
return ("Authorization", authorization_headers[0])
```
Authorization 头格式见 [RFC 9110 第 11.4 节](https://datatracker.ietf.org/doc/html/rfc9110#section-11.4)。简言之,头以授权机制 `X-Matrix` 开始,后跟一或多空格,再跟一组用逗号隔开的 name=value 参数对。各参数对两侧允许有零个或多个空格及制表符。名称大小写不敏感顺序无关。value 含非法 token 字符时必须加引号,若合法可省略引号。用引号的 value 可包含反斜杠转义字符。解析时须将转义字符还原。
为兼容旧服务器,发送端应:
- 只在 `X-Matrix` 后加一个空格;
- 只用小写参数名;
- 避免值中含反斜杠;
- 避免参数对间有额外空白字符。
兼容旧服务器的接收端应允许参数值中含冒号,无需加引号。
可用的授权参数包含:
- `origin`:发送服务器的服务器名,与上文 JSON 的 `origin` 字段一致。
- `destination`{{% added-in v="1.3" %}} 接收服务器名,与 JSON 的 `destination` 字段一致。为兼容旧服务器,允许无此参数,但必须始终发送;若有且值与接收服务器名不符,接收端须以 401 Unauthorized 拒绝请求。
- `key`:用于签名请求的发送服务器密钥 ID含算法名
- `signature`:步骤 1 中 JSON 的签名。
未知参数应被忽略。
{{% boxes/note %}}
{{% changed-in v="1.11" %}}
本节原引用了 [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1) 和 [RFC 7230](https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2),已被 RFC 9110 替代,但相关内容未变。
{{% /boxes/note %}}
### 响应认证
响应通过 TLS 服务器证书认证。家服务器不应在确认已认证对方服务器前发送请求,以防消息泄漏。
### 客户端 TLS 证书
因如 Matrix 这样的 HTTP 服务常部署在负责 TLS 的负载均衡器之后,因此建议在 HTTP 层而非 TLS 层进行请求认证,这样 TLS 客户端证书难以校验。
家服务器可提供 TLS 客户端证书,接收方可校验是否与原始服务器证书一致。
## 事务
家服务器之间通过事务消息Transaction交换 EDU 和 PDU。这些事务以 JSON 对象编码,通过 HTTP PUT 请求传输。事务仅对交换的两台服务器有意义,不具备全局意义。
事务有限制:每个最多可含 50 个 PDU 和 100 个 EDU。
{{% http-api spec="server-server" api="transactions" %}}
## PDUs
每个 PDU 包含一个房间事件,原服务器希望将其发送至目标服务器。
PDU 中的 `prev_events` 字段标识事件的“父事件”并通过将事件链接为有向无环图DAG在房间内建立事件的部分顺序。发送服务器应填充所有自己尚未看到其子事件的事件从而表明此事件紧随所有已知事件之后。
例如,设房间事件形成下述 DAG。新事件应以 `E4``E6` 作为 `prev_events`,因为两者尚无子事件:
E1
^
|
E2 <--- E5
^ ^
| |
E3 E6
^
|
E4
完整的 PDU 模式见 [房间版本规范](/rooms)
### PDU 接收时的校验
服务器接收到远端事件时必须确保该事件
1. 是一个有效事件否则直接丢弃有效事件必须含有 `room_id`并符合该 [房间版本](/rooms) 的事件格式
2. 签名校验通过否则丢弃
3. 哈希校验通过否则事件被裁剪redacted后再继续处理
4. 基于认证事件auth events的授权规则校验通过否则拒绝
5. 基于事件前状态的授权规则校验通过否则拒绝
6. 基于房间当前状态的授权规则校验通过否则软失败”(soft failed)。
各项校验及失败处理详述如下
关于事件需要包含哪些哈希及签名及其计算详见 [事件签名](#signing-events)
#### 定义
所需权限等级Required Power Level
每类事件类型对应一个 *权限等级*由当前 [`m.room.power_levels`](/client-server-api/#mroompower_levels) 事件指定若事件类型在 `events` 块未显式列出则根据是否为状态事件分别使用 `state_default` `events_default`
邀请/踢出/封禁/撤回等级Invite Level, Kick Level, Ban Level, Redact Level
由当前 [`m.room.power_levels`](/client-server-api/#mroompower_levels) 状态内的 `invite``kick``ban``redact` 指定邀请默认为 0踢出封禁撤回均默认为 50
目标用户Target User
对于 [`m.room.member`](/client-server-api/#mroommember) 状态事件由事件的 `state_key` 指定的用户
{{% boxes/warning %}}
部分 [房间版本](/rooms) 允许权限等级为字符串仅为向后兼容家服务器应合理防止用户发送带字符串值权限事件如直接拒绝 API 请求且默认权限值绝不可为字符串
详情见 [房间版本规范](/rooms)
{{% /boxes/warning %}}
#### 授权规则
授权与状态有关单个事件需多次用不同状态集校验具体规则和适用算法由房间版本决定详细内容见 [房间版本规范](/rooms)
##### 认证事件选择Auth events selection
PDU `auth_events` 字段标识允许发起发送的事件集房间中的 `m.room.create` 事件无 `auth_events`其他事件应按照以下房间状态子集选取
- `m.room.create` 事件
- 当前的 `m.room.power_levels` 事件如有
- 发送方当前的 `m.room.member` 事件如有
- 若类型为 `m.room.member`
- 目标用户当前的 `m.room.member` 事件如有
- `membership` `join``invite` `knock`则为当前 `m.room.join_rules`
- `membership` `invite` `content` `third_party_invite`则加入当前 `m.room.third_party_invite` 事件 `state_key` 匹配 `content.third_party_invite.signed.token`
- `content.join_authorised_via_users_server` 存在 [房间版本支持受限房间](/rooms/#feature-matrix)则加入 `state_key` 匹配 `content.join_authorised_via_users_server` `m.room.member` 事件
#### 拒绝Rejection
被拒绝事件不应下发给客户端也不可作为新事件的前序事件后续如有其他服务器发出的引用被拒绝事件的新事件只要其授权校验通过也可被接受授权校验状态正常更新除针对被拒绝事件若为状态事件不更新
若事务中的事件被拒绝不应以错误码响应整个事务请求
{{% boxes/note %}}
这意味着某些被拒绝事件依然可出现在房间事件 DAG
{{% /boxes/note %}}
{{% boxes/note %}}
区别于裁剪事件redacted event裁剪事件依然可影响房间状态取消内容的 `join` 事件依然会使用户视为已加入房间
{{% /boxes/note %}}
#### 软失败Soft failure
{{% boxes/rationale %}}
为防止用户通过指向旧 DAG 分支的事件规避封禁或其他权限限制例如被封禁用户通过发送引用被封禁前分支的事件继续发言此类事件本身合法不应单纯以拒绝处理因为无法区分延迟事件和规避事件因此此类事件需正常参与状态解析与联邦协议但服务器可选择不将其下发至客户端
通常服务器会发现此类事件基于当前状态无法授权即综合所有前沿节点的解析状态此时服务器可选择不通知客户端
这样可阻止恶意服务器向客户端投递规避事件因最终用户不会看到例如
A
/
B
`B` 为用户 `X` 的封禁事件 `X` 试图通过发送事件 `C` 修改话题以规避封禁
A
/ \
B C
若服务器先见到 `B` 后见到 `C`应对 `C` 软失败即不通知客户端也不再引用 `C`
若后来有服务器发送同时引用 `B` `C` 的事件 `D`如其先见到了 `C` 后到的 `B`
A
/ \
B C
\ /
D
`D` 可正常处理前提授权通过)。`D` 处的状态可能包含 `C`客户端应当收到包含 `C` 的新状态。(*注意*实际取决于具体状态解析算法对应 `C` `B` 优先。)
若所有服务器都先收到 `B`所有对 `C` 软失败则后续新事件 `D'` 不再引用 `C`
A
/ \
B C
|
D'
{{% /boxes/rationale %}}
通过联邦收到新事件后应在基于事件自身状态校验通过后再以房间当前状态校验不通过时则软失败”。
软失败事件不应下发至客户端也不应被新事件引用或加入到前沿事件集中其余处理同常规事件
{{% boxes/note %}}
如有其他事件引用该软失败事件则其可照常参与状态解析状态解析算法须防止此机制下恶意事件注入房间状态
{{% /boxes/note %}}
{{% boxes/note %}}
软失败的状态事件如在状态解析中被选为当前状态客户端应常规方式收到该事件例如在 sync 响应的 `state` 部分推送)。
{{% /boxes/note %}}
{{% boxes/note %}}
若联邦请求需返回软失败事件 `/event/<event_id>`应正常返回`/backfill` `/get_missing_events` 仅当请求中包含引用该软失败事件的事件时才会返回
{{% /boxes/note %}}
#### 检索事件授权信息
家服务器可能缺失事件授权信息或需从其他服务器验证授权链通过以下 API 获取所需信息
{{% http-api spec="server-server" api="event_auth" %}}
## EDUs
EDU 相比 PDU 没有事件 ID房间 ID 前序事件列表通常用于非持久化数据如用户在线状态正在输入提示等
{{% definition path="api/server-server/definitions/edu_with_example" %}}
## 房间状态解析
*状态* `(event_type, state_key)` `event_id` 的映射每个房间初始状态为空每有状态事件加入即更新房间状态
若每个事件只有一个 `prev_event`其后状态唯一若事件图分支合并可能存在不同状态此时需用 *状态解析算法* 决定合并结果
如下事件图顶部为最早的 E0
E0
|
E1
/ \
E2 E4
| |
E3 |
\ /
E5
E3 E4 都是 `m.room.name` 事件E5 处房间名称如何确定
状态解析算法由房间版本决定详见 [房间版本规范](/rooms)
## 回溯填充与缺失事件获取
家服务器加入房间后会收到所有在房间内其他家服务器产生的事件因此近期历史不会丢失用户可通过 `/messages` 客户端 API 端点查历史如倒退到加入房间前其服务器本地无历史
为此联邦 API 提供类似 `/messages` 的服务器-服务器历史获取接口 `/backfill`
如需历史家服务器可选一已存有最早历史用户的家服务器发起 `/backfill` 请求
类似回溯服务器可能缺失某些事件可通过 `/get_missing_events` 获取缺失事件
{{% http-api spec="server-server" api="backfill" %}}
## 检索事件
在某些情况下家服务器可能缺失特定事件或无法简单通过回溯获取的房间信息相关 API 允许家服务器获取指定时间点的事件及状态
{{% http-api spec="server-server" api="events" %}}
## 加入房间
当新用户想加入本服务器已知的房间时服务器可直接检查房间状态判断能否加入若可以则生成签名并发出新的 `m.room.member` 状态事件加入用户若服务器尚未知该房间则需通过更长的多阶段握手流程先选定一个已在该房间的远程家服务器协助加入即远程加入握手
握手涉及三个角色发起加入用户的家服务器(“加入服务器”)、托管用户请求别名的目录服务器以及房间中已有成员所属家服务器(“常驻服务器”)。
概述如下加入服务器先向目录服务器查询别名获取房间 ID 及加入候选服务器继而请求其中一个常驻服务器查询房间信息再用这些资料构建并签名 `m.room.member` 加入事件最终发往常驻服务器
概念上为三个家服务器角色实际通常目录服务器本身即为房间成员实际流程也常只有两台服务器参与
```
+---------+ +---------------+ +-----------------+ +-----------------+
| Client | | JoiningServer | | DirectoryServer | | ResidentServer |
+---------+ +---------------+ +-----------------+ +-----------------+
| | | |
| join request | | |
|---------------------->| | |
| | | |
| | directory request | |
| |---------------------------->| |
| | | |
| | directory response | |
| |<----------------------------| |
| | | |
| | make_join request | |
| |------------------------------------------------>|
| | | |
| | |make_join response |
| |<------------------------------------------------|
| | | |
| | send_join request | |
| |------------------------------------------------>|
| | | |
| | |send_join response |
| |<------------------------------------------------|
| | | |
| join response | | |
|<----------------------| | |
| | | |
```
第一步通常通过目录服务器的 [`/query/directory`](/server-server-api/#get_matrixfederationv1querydirectory) 端点查询房间 ID 与加入候选服务器若为被邀请后加入则可直接选用邀请事件源服务器为候选提高效率但需考虑邀请服务器可能已不再是房间成员因此失败时须回退到通常流程
获得房间 ID 和候选服务器后加入服务器选一常驻服务器通过 `GET /make_join` 获取房间事件模板常驻服务器返回填充事件各项所需信息
加入服务器需补充完善 `origin``origin_server_ts``event_id`然后签名
最后加入服务器通过 `PUT /send_join` 将新事件送往常驻服务器
常驻服务器为事件加签接受并将其写入房间事件图并将新事件及房间全状态含刚签出的事件发送给房间内其他服务器
{{% http-api spec="server-server" api="joins-v1" %}}
{{% http-api spec="server-server" api="joins-v2" %}}
### 受限房间
受限房间详细描述见 [客户端-服务器 API](/client-server-api/#restricted-rooms)仅在 [支持受限加入的房间版本](/rooms/#feature-matrix) 下启用
处理请求加入受限房间时常驻服务器需确保加入服务器满足 `m.room.join_rules` 所定义的至少一项条件若无条件或者无条件符合所需模式则视为全部校验失败
校验条件失败时`/make_join` `/send_join` 应返回 400 `M_UNABLE_TO_AUTHORISE_JOIN`通常因无法获知所需房间的状态信息)。
若加入用户满足某些条件但常驻服务器自身不满足生成 `join_authorised_via_users_server` 所需的权限则返回 400 `M_UNABLE_TO_GRANT_JOIN`表明需换服务器尝试
所有条件均未满足时常驻服务器返回 403 `M_FORBIDDEN`
## 敲门加入房间 {#knocking-rooms}
房间可通过 join rules 允许敲门允许时用户可请求加入房间即被邀请)。本地已在房间服务器可直接发送敲门事件否则需如 [加入房间](/server-server-api/#joining-rooms) 一样通过握手流程让远端协助发送
敲门握手同加入握手基本一致区别在于角色变为敲门服务器”,API 包括 `/make_knock` `/send_knock`
服务器间敲门可通过离开房间取消见下述邀请拒绝相关说明
{{% http-api spec="server-server" api="knocks" %}}
## 房间邀请
同一服务器用户间发起邀请时服务器可直接签署并跳过此处流程跨服务器邀请时必须向被邀服务器请求事件签名和校验
邀请事件同样用于通知之前的敲门请求被接受因此接收服务器应准备好将此前敲门事件与邀请事件关联即使邀请未直接引用敲门)。
{{% http-api spec="server-server" api="invites-v1" %}}
{{% http-api spec="server-server" api="invites-v2" %}}
## 离开房间(拒绝邀请)
家服务器可主动发送 `m.room.member` 事件令用户离开房间拒绝本地邀请或撤销敲门针对其他家服务器发出的远程邀请或敲门由于图谱中未参与需采用特殊方式拒绝邀请直接先加入再离开并不可取因为客户端会认为用户先接受邀请再主动退出这和拒绝邀请有本质区别
[加入房间](#joining-rooms) 握手类似发起离开的服务器需先向常驻服务器发 `/make_leave`拒绝邀请时常驻服务器可为邀请发起方收到 `/make_leave` 模板事件后发起服务器签名并设置自己的 `event_id`通过 `/send_leave` 发送给常驻服务器常驻服务器再下发给房间内其他服务器
{{% http-api spec="server-server" api="leaving-v1" %}}
{{% http-api spec="server-server" api="leaving-v2" %}}
## 第三方邀请
{{% boxes/note %}}
有关第三方邀请的更多信息请见 [客户端-服务器 API](/client-server-api) 的对应模块
{{% /boxes/note %}}
用户欲邀请不知道 Matrix ID 的用户进房间时可使用第三方标识如邮箱或手机号发起邀请
此标识及其与 Matrix ID 的绑定由实现 [身份服务 API](/identity-service-api) 的身份服务器验证
### 第三方标识已有绑定时
若标识已绑定 Matrix ID身份服务器查询会返回邀请将作为普通 `m.room.member` 事件处理
### 第三方标识尚无绑定时
若标识尚未绑定 Matrix ID则邀请服务器将请求身份服务器存储并待有人绑定该标识后推送邀请服务器还需在房间发 `m.room.third_party_invite` 事件写入显示名令牌及身份服务器返回的公钥
当某个 Matrix ID 绑定此标识后身份服务器会按 [邀请存储](/identity-service-api#invitation-storage) 的说明 POST 至对应家服务器
每次邀请家服务器会创建携带特殊 `third_party_invite` 节点的 `m.room.member` 事件内含令牌及签名对象
如接收家服务器已在房间可直接授权发事件否则需向房间家服务器发授权请求
{{% http-api spec="server-server" api="third_party_invite" %}}
#### 邀请校验
家服务器收到带 `third_party_invite` 对象的 `m.room.member` 邀请事件后须在无需第三方服务器的情况下验证受邀 Matrix ID 与标识已有验证关系
需从房间状态取出 `m.room.third_party_invite` 事件 `state_key` `m.room.member` 事件内容内 `third_party_invite` 里的 `token` 字段匹配获得身份服务器提供的公钥
用该公钥校验 `m.room.member` 事件 `content.third_party_invite.signed` 对象的签名保证创建邀请事件的确为拥有此第三方标识的用户
鉴于该签名对象仅能在绑定标识与 Matrix ID 时由身份服务器发送一次且内含指明 Matrix ID 及令牌因此能保证为真实所有者
## 公共房间目录
为配合 [客户端-服务器 API](/client-server-api) 的房间目录家服务器需要可从远端查询目标服务器的公共房间请求目标服务器的 `/publicRooms` 端点即可
{{% http-api spec="server-server" api="public_rooms" %}}
## 空间Spaces
为配合 [客户端-服务器 API Spaces 模块](/client-server-api/#spaces)家服务器需可从远端服务器查询空间信息
{{% http-api spec="server-server" api="space_hierarchy" %}}
## 正在输入通知
当服务器用户发正在输入通知时需将该通知同步到房间内其他服务器令其用户获得同样状态接收服务器应确保用户确已在房间且为发送方服务器的用户
{{% definition path="api/server-server/definitions/event-schemas/m.typing" %}}
## 在线状态Presence
服务器 API 的在线状态完全基于下述 EDU 交换不涉及 PDU 或联邦查询
服务器应仅为对方感兴趣的用户发送在线状态更新例如对方正与本地用户共处一房间
{{% definition path="api/server-server/definitions/event-schemas/m.presence" %}}
## 回执
回执为 EDU用于指示对某事件的操作标记”。目前仅支持已读回执“(read receipt表示用户已读到事件处)。
本用户自己发的事件不必发送读回执因发事件即视为已读
{{% definition path="api/server-server/definitions/event-schemas/m.receipt" %}}
## 信息查询
查询是指向家服务器检索资源如用户或房间的信息常与客户端向客户端-服务器 API 发起请求配合实现
可进行多种查询下文先是通用查询端点后跟具体类型
{{% http-api spec="server-server" api="query" %}}
## OpenID
第三方服务可用由 <span class="title-ref">客户端-服务器 API</span> 预生成的访问令牌交换获取用户信息。"OpenID" 可用于验证用户身份而无需授予账户全部访问权限。
由 OpenID API 生成的访问令牌仅对 OpenID API 有效,在其他用途无效。
{{% http-api spec="server-server" api="openid" %}}
## 设备管理
用户设备详情需高效公开并及时更新,以保证端到端加密可靠,让用户知晓房间内涉及的设备;同时设备间消息需要筛选并分发。下述内容补充 [客户端-服务器 API 设备管理模块](/client-server-api#device-management)。
Matrix 目前采用自定义的发布/订阅机制同步用户设备列表(联邦同步)。服务器首次获取远端用户设备列表时,通过 `/user/keys/query` 接口结果填本地缓存。后续通过 `m.device_list_update` EDU 增量更新。每次新 EDU 针对给定用户的一台设备(含唯一 `stream_id`),并在 `prev_id` 字段指向增量更新参考。为便于多实例并发,`prev_id` 可包含所有当前尚未被引用的 key若依次发送一条记录`prev_id` 仅有一个。
这样形成了 `m.device_list_update` EDU 的有向无环图,指明更新某用户设备列表前必须已接收哪些 EDU。若引用了本地未知的 `prev_id`,服务器需重新调用 `/user/keys/query` API 后继续处理。响应返回 `stream_id`,供后续同步使用。
{{% http-api spec="server-server" api="user_devices" %}}
{{% definition path="api/server-server/definitions/event-schemas/m.device_list_update" %}}
## 端到端加密
本节补充 [客户端-服务器 API 端到端加密模块](/client-server-api#end-to-end-encryption)。详细加密流程可见该模块。
此处 API 主要用于代客户端通过联邦转发请求,并原样转发响应。
{{% http-api spec="server-server" api="user_keys" %}}
{{% definition path="api/server-server/definitions/event-schemas/m.signing_key_update" %}}
## 发送到设备消息
发送到设备的消息通过 `m.direct_to_device` EDU 实现,不涉及 PDU 或联邦查询。
每条“发给设备”消息须以以下 EDU 发送到目标服务器:
{{% definition path="api/server-server/definitions/event-schemas/m.direct_to_device" %}}
## 内容仓库
事件附件(图片、文件等)通过 [客户端-服务器 API 的内容仓库](/client-server-api/#content-repository) 上传至家服务器。当服务器需获取远端服务器存储的媒体数据时,需从远端下载。
服务器必须基于 [Matrix 内容 URI](/client-server-api/#matrix-content-mxc-uris) 的服务地址(格式 `mxc://{ServerName}/{MediaID}`),始终应从 `{ServerName}` 服务器下载,利用下述端点。
{{% changed-in v="1.11" %}} 之前推荐使用 [/client-server-api/#content-repository](/client-server-api/#content-repository) 内描述的 `/_matrix/media/*` 端点,如今这些端点已废弃,新端点需认证。服务器(而非用户)无法提供所需访问令牌。因此服务器应优先尝试新端点,遇到 404 `M_UNRECOGNIZED` 时再尝试废弃端点,并确保设置 `allow_remote``false`
{{% http-api spec="server-server" api="content_repository" %}}
## 服务器访问控制列表ACL
服务器 ACL 及其用途详见 [客户端-服务器 API 的服务器 ACL 部分](/client-server-api#server-access-control-lists-acls-for-rooms)。
远端服务器发起请求时,必须验证其是否有权限访问指定房间。被拒绝的服务器必须以 403 HTTP 状态码及 `errcode: M_FORBIDDEN` 响应。
以下端点前缀必须受保护:
- `/_matrix/federation/v1/make_join`
- `/_matrix/federation/v1/make_leave`
- `/_matrix/federation/v1/send_join`
- `/_matrix/federation/v2/send_join`
- `/_matrix/federation/v1/send_leave`
- `/_matrix/federation/v2/send_leave`
- `/_matrix/federation/v1/invite`
- `/_matrix/federation/v2/invite`
- `/_matrix/federation/v1/make_knock`
- `/_matrix/federation/v1/send_knock`
- `/_matrix/federation/v1/state`
- `/_matrix/federation/v1/state_ids`
- `/_matrix/federation/v1/backfill`
- `/_matrix/federation/v1/event_auth`
- `/_matrix/federation/v1/get_missing_events`
此外,[`/_matrix/federation/v1/send/{txnId}`](#put_matrixfederationv1sendtxnid) 端点必须按以下规则保护:
- 对所有 PDU 逐个应用 ACL若发送服务器被拒绝访问 `room_id` 房间,则应忽略该 PDU并在各自事件 ID 对应响应项中注明错误。
- 所有房间相关的 EDU 也须应用 ACL
- 对 [输入状态通知 (`m.typing`)](#typing-notifications),发送服务器被拒绝访问相应 `room_id` 时忽略其 EDU。
- 对 [已读回执 (`m.receipt`)](#receipts),若发送服务器被拒绝访问某房间,则对应该房间的所有回执均应忽略。
## 事件签名
事件签名过程受事件被裁剪redact带来的复杂性影响。
### 为出站事件添加哈希和签名
签名前,需先计算事件的*内容哈希*content hash使用 [Unpadded Base64](/appendices#unpadded-base64) 编码,放入事件 `hashes.sha256` 字段。
随后,执行*裁剪*redaction算法见 [裁剪规则](/client-server-api#redactions)),再用 [JSON 签名](/appendices#signing-json) 算法及服务器签名密钥签名,生成的签名再拷贝回原事件对象。
签名事件范例见 [房间版本规范](/rooms)。
### 校验接收事件的哈希和签名
服务器收到联邦事件后,应立即校验哈希及签名。
首查签名,先裁剪事件,然后按 [校验签名流程](/appendices#checking-for-a-signature) 检查签名(可接受完整或已裁剪事件)。
期望签名包括:
- `sender` 服务器(如为第三方邀请则例外。否则 sender 要与第三方邀请吻合,而实际发事件或为不同服务器)。
- 若为房间版本 1/2还包括事件 ID 创建方服务器。其他房间版本事件 ID 不在联邦传递,因此无需额外签名。
签名正确后,计算期望的内容哈希。`hashes` 字段的内容解码后与期望值比对。
如哈希校验失败,表明只收到裁剪后事件,故直接用裁剪结果。
### 计算事件引用哈希Reference Hash
*引用哈希*reference hash覆盖事件重要字段包括内容哈希。部分房间版本用于事件标识符具体见[房间版本规范](/rooms)。计算过程如下:
1. 事件经过裁剪算法处理。
2. 移除 `signatures``unsigned` 字段。
3. 转为 [规范化 JSON](/appendices#canonical-json)。
4. 计算 sha256 哈希。
### 计算事件内容哈希Content Hash
*内容哈希* 覆盖原始未裁剪的完整事件。计算步骤:
1. 移除已有的 `unsigned``signatures``hashes` 字段。
2. 用 [规范化 JSON](/appendices#canonical-json) 编码后做 SHA-256 哈希。
### 示例代码
```py
def hash_and_sign_event(event_object, signing_key, signing_name):
# 首先计算事件内容哈希
content_hash = compute_content_hash(event_object)
event_object["hashes"] = {"sha256": encode_unpadded_base64(content_hash)}
# 裁剪事件,移除非必要字段
stripped_object = strip_non_essential_keys(event_object)
# 对裁剪后的 JSON 做签名(只签关键字段和哈希)
signed_object = sign_json(stripped_object, signing_key, signing_name)
# 把签名从裁剪后事件拷贝回原事件
event_object["signatures"] = signed_object["signatures"]
def compute_content_hash(event_object):
# 复制事件对象
event_object = dict(event_object)
# "unsigned" 字段可由他服务器更改,需排除
event_object.pop("unsigned", None)
# 签名相关依赖当下 "hashes",因此须排除
event_object.pop("signatures", None)
event_object.pop("hashes", None)
# 编码为规范化 JSON 取得一致字节输出
event_json_bytes = encode_canonical_json(event_object)
return hashlib.sha256(event_json_bytes)
```
## 安全注意事项
当域名所有权变更,接手人可冒充前任所有者接收消息(类似邮件)并请求其他服务器转发历史。未来,如 [MSC1228](https://github.com/matrix-org/matrix-spec-proposals/issues/1228) 方案将解决此类问题。