--- 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) 方案将解决此类问题。