---
title: "服务器-服务器 API"
weight: 20
type: docs
---
{{< boxes/warning >}}
本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 Forgejo 存储库 打开 Issue、提交 Pull Request 或邮件联系我们提出改进建议和参与翻译与核对。
{{< /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:///.well-known/matrix/server` 发起常规 HTTPS 请求,期望返回本节后续定义的模式。须跟随 30x 跳转,但需避免重定向循环。`/.well-known` 端点的响应(无论成功与否)应由请求服务器进行缓存。服务器应遵守响应内的缓存控制头,如无则使用合理默认值(建议 24 小时)。另外应限制响应最大缓存时间,建议为 48 小时。错误建议缓存最多一小时,并对重复失败采用指数退避。`/.well-known` 返回的响应模式详见本节后续。若响应无效(JSON 无效、字段缺失、返回非 200 等)则跳转到步骤 4。若响应有效,解析 `m.server` 字段(格式 `[:]`)并按如下处理:
1. 若 `` 为 IP 字面量,则用该 IP 和 ``(未提供则 8448)。目标服务器必须有对应 IP 的有效 TLS 证书。请求 `Host` 头为该 IP(若含端口亦包含端口)。
2. 若 `` 非 IP 字面量,且 `` 存在,查找其 CNAME、AAAA 或 A 记录,得出 IP,连同 `` 使用。请求 `Host` 头为 `:`。目标服务器需有 `` 的有效证书。
3. {{% added-in v="1.8" %}} 若 `` 不是 IP 字面量,且未指定 ``,则查找 `_matrix-fed._tcp.` 的 SRV 记录,可能带来新的主机名(需 AAAA 或 A 记录解析)及端口。请求应发往解析出的 IP 与端口,`Host` 头为 ``。目标服务器需有 `` 的有效证书。
4. **[已废弃]** 若 `` 不是 IP 字面量,未指定 ``,且找不到 `_matrix-fed._tcp.` SRV 记录,则查 `_matrix._tcp.`,同样可能得到主机名和端口。请求应发往解析到的 IP 和端口,`Host` 头为 ``。目标服务器需有 `` 的有效证书。
5. 若未找到 SRV 记录,通过 CNAME、AAAA 或 A 记录解析 IP,之后用 8448 端口发请求,`Host` 头为 ``。目标服务器须有 `` 的有效证书。
4. {{% added-in v="1.8" %}} 若 `/.well-known` 请求返回错误,则尝试解析 `_matrix-fed._tcp.` 的 SRV 记录,或得主机名和端口。请求发往解析到的 IP 与端口,`Host` 头为 ``,目标服务器需有 `` 的有效证书。
5. **[已废弃]** 若 `/.well-known` 请求错误且找不到 `_matrix-fed._tcp.` SRV 记录,则解析 `_matrix._tcp.` SRV 记录,同样可能获主机名和端口。请求发往解析到的 IP 和端口,`Host` 头为 ``,目标服务器需有 `` 的有效证书。
6. 若 `/.well-known` 返回错误,且未找到 SRV 记录,则用 CNAME、AAAA、A 记录解析 IP,发往 8448 端口,`Host` 头为 ``,目标服务器需有 `` 有效证书。
{{% boxes/note %}}
我们强制要求 SRV 委托使用 `` 而非 `` 的原因:
1. DNS 并不安全(并非所有域名都部署 DNSSEC),因此委托目标必须通过 TLS 证明自己是 `` 的合法代理。
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": ,
"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
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/`),应正常返回。`/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
第三方服务可用由 客户端-服务器 API 预生成的访问令牌交换获取用户信息。"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) 方案将解决此类问题。