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

42 KiB
Raw Blame History

title weight type
服务器-服务器 API 20 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 对象。未来可能会指定更高效的传输方式作为可选扩展。

所有的 POSTPUT 端点要求请求服务器在请求体中提供一个可能为空的JSON 对象。请求服务器应为所有带有 JSON 请求体的请求提供 Content-Type: application/json 头,但不是强制性的。

同理,本规范中的所有端点要求目标服务器返回一个 JSON 对象。服务器必须在所有 JSON 响应中包含 Content-Type: application/json 头。

所有请求和响应中的 JSON 数据都必须使用 UTF-8 编码。

TLS

服务器-服务器通信必须通过 HTTPS 实现。

目标服务器必须提供由已知证书机构签署的 TLS 证书。

请求服务器最终负责确定信任的证书机构,强烈建议依赖操作系统的判断。服务器可以为管理员提供覆盖信任机构列表的方法。服务器还可以针对白名单中的域名或网段跳过证书验证,用于测试或在其他地方完成验证(如 .onion 地址)的网络环境下。

在发起请求时,服务器应尽可能遵守 SNI服务器名称指示发送期望证书的 SNI除非证书预期是 IP 地址IP 地址不支持 SNI不应发送

建议服务器利用 证书透明计划

不支持的端点

若收到对不支持(或未知)端点的请求,服务器必须返回 404 M_UNRECOGNIZED 错误。

同样405 M_UNRECOGNIZED 错误用于指示对已知端点的不支持 HTTP 方法。

服务器发现

解析服务器名称

每个 Matrix 家服务器通过一个包含主机名和可选端口的服务器名称唯一标识,详见 语法说明。如适用,委托服务器名采用相同语法。

服务器名需解析为可连接的 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 以及 XMPP 等其他使用 SRV 记录的应用保持一致。 {{% /boxes/note %}}

{{% boxes/note %}} 注意,根据 RFC2782 要求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 注册见 此处。 {{% /boxes/note %}}

{{% http-api spec="server-server" api="wellknown" %}}

服务器实现

{{% http-api spec="server-server" api="version" %}}

获取服务器公钥

{{% boxes/note %}} 曾经存在“版本 1”密钥交换已因意义不大被规范移除。可在 历史草案 查阅。 {{% /boxes/note %}}

每个家服务器通过 /_matrix/key/v2/server 发布自身公钥。家服务器可直接请求 /_matrix/key/v2/server 获取公钥,也可借助中间公证服务器通过 /_matrix/key/v2/query/{serverName} API 查询。公证服务器会代表其他服务器查询目标服务器的 /_matrix/key/v2/server API然后用自己的密钥为响应签名。服务器可同时查询多个公证服务器确保它们返回的公钥一致。

该方法借鉴了 Perspectives Project,但增加了 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..."
        }
    }
}

上例中的服务器名称是相关家服务器的服务器名,不受 服务器名称解析 中委托影响,始终用委托前的名称。此规则在后续签名流程中也适用。

步骤 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 示例代码:

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 节。简言之,头以授权机制 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 7235RFC 7230,已被 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。新事件应以 E4E6 作为 prev_events,因为两者尚无子事件:

E1
^
|
E2 <--- E5
^       ^
|       |
E3      E6
^
|
E4

完整的 PDU 模式见 房间版本规范

PDU 接收时的校验

服务器接收到远端事件时,必须确保该事件:

  1. 是一个有效事件,否则直接丢弃。有效事件必须含有 room_id,并符合该 房间版本 的事件格式。
  2. 签名校验通过,否则丢弃。
  3. 哈希校验通过否则事件被裁剪redacted后再继续处理。
  4. 基于认证事件auth events的授权规则校验通过否则拒绝。
  5. 基于事件前状态的授权规则校验通过,否则拒绝。
  6. 基于房间当前状态的授权规则校验通过否则“软失败”soft failed

各项校验及失败处理详述如下。

关于事件需要包含哪些哈希及签名及其计算,详见 事件签名

定义

所需权限等级Required Power Level

:每类事件类型对应一个 权限等级,由当前 m.room.power_levels 事件指定。若事件类型在 events 块未显式列出,则根据是否为状态事件,分别使用 state_defaultevents_default

邀请/踢出/封禁/撤回等级Invite Level, Kick Level, Ban Level, Redact Level

:由当前 m.room.power_levels 状态内的 invitekickbanredact 指定。邀请默认为 0踢出、封禁、撤回均默认为 50。

目标用户Target User

:对于 m.room.member 状态事件,由事件的 state_key 指定的用户。

{{% boxes/warning %}} 部分 房间版本 允许权限等级为字符串,仅为向后兼容。家服务器应合理防止用户发送带字符串值权限事件(如直接拒绝 API 请求),且默认权限值绝不可为字符串。

详情见 房间版本规范。 {{% /boxes/warning %}}

授权规则

授权与状态有关。单个事件需多次用不同状态集校验,具体规则和适用算法由房间版本决定。详细内容见 房间版本规范

认证事件选择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 事件(如有)
    • membershipjoininviteknock,则为当前 m.room.join_rules
    • membershipinvitecontentthird_party_invite,则加入当前 m.room.third_party_invite 事件,其 state_key 匹配 content.third_party_invite.signed.token
    • content.join_authorised_via_users_server 存在,且 房间版本支持受限房间,则加入 state_key 匹配 content.join_authorised_via_users_serverm.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

若后来有服务器发送同时引用 BC 的事件 D(如其先见到了 C 后到的 B

  A
 / \
B   C
 \ /
  D

D 可正常处理(前提授权通过)。D 处的状态可能包含 C,客户端应当收到包含 C 的新状态。(注意:实际取决于具体状态解析算法,对应 CB 优先。)

若所有服务器都先收到 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 处房间名称如何确定?

状态解析算法由房间版本决定,详见 房间版本规范

回溯填充与缺失事件获取

家服务器加入房间后,会收到所有在房间内其他家服务器产生的事件,因此近期历史不会丢失。用户可通过 /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 端点查询房间 ID 与加入候选服务器。若为被邀请后加入,则可直接选用邀请事件源服务器为候选,提高效率。但需考虑邀请服务器可能已不再是房间成员,因此失败时须回退到通常流程。

获得房间 ID 和候选服务器后,加入服务器选一常驻服务器,通过 GET /make_join 获取房间事件模板。常驻服务器返回填充事件各项所需信息。

加入服务器需补充完善 originorigin_server_tsevent_id,然后签名。

最后,加入服务器通过 PUT /send_join 将新事件送往常驻服务器。

常驻服务器为事件加签,接受并将其写入房间事件图,并将新事件及房间全状态(含刚签出的事件)发送给房间内其他服务器。

{{% http-api spec="server-server" api="joins-v1" %}}

{{% http-api spec="server-server" api="joins-v2" %}}

受限房间

受限房间详细描述见 客户端-服务器 API,仅在 支持受限加入的房间版本 下启用。

处理请求加入受限房间时,常驻服务器需确保加入服务器满足 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

敲门加入房间

房间可通过 join rules 允许敲门。允许时,用户可请求加入房间(即被邀请)。本地已在房间服务器可直接发送敲门事件,否则需如 加入房间 一样,通过握手流程让远端协助发送。

敲门握手同加入握手基本一致区别在于角色变为“敲门服务器”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 事件令用户离开房间、拒绝本地邀请或撤销敲门。针对其他家服务器发出的远程邀请或敲门,由于图谱中未参与,需采用特殊方式拒绝邀请。直接先加入再离开并不可取,因为客户端会认为用户先接受邀请再主动退出,这和拒绝邀请有本质区别。

加入房间 握手类似,发起离开的服务器需先向常驻服务器发 /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 的对应模块。 {{% /boxes/note %}}

用户欲邀请不知道 Matrix ID 的用户进房间时,可使用第三方标识(如邮箱或手机号)发起邀请。

此标识及其与 Matrix ID 的绑定由实现 身份服务 API 的身份服务器验证。

第三方标识已有绑定时

若标识已绑定 Matrix ID身份服务器查询会返回。邀请将作为普通 m.room.member 事件处理。

第三方标识尚无绑定时

若标识尚未绑定 Matrix ID则邀请服务器将请求身份服务器存储并待有人绑定该标识后推送。邀请服务器还需在房间发 m.room.third_party_invite 事件,写入显示名、令牌及身份服务器返回的公钥。

当某个 Matrix ID 绑定此标识后,身份服务器会按 邀请存储 的说明 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_keym.room.member 事件内容内 third_party_invite 里的 token 字段匹配,获得身份服务器提供的公钥。

用该公钥校验 m.room.member 事件 content.third_party_invite.signed 对象的签名,保证创建邀请事件的确为拥有此第三方标识的用户。

鉴于该签名对象仅能在绑定标识与 Matrix ID 时由身份服务器发送一次,且内含指明 Matrix ID 及令牌,因此能保证为真实所有者。

公共房间目录

为配合 客户端-服务器 API 的房间目录,家服务器需要可从远端查询目标服务器的公共房间。请求目标服务器的 /publicRooms 端点即可。

{{% http-api spec="server-server" api="public_rooms" %}}

空间Spaces

为配合 客户端-服务器 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 设备管理模块

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 端到端加密模块。详细加密流程可见该模块。

此处 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 的内容仓库 上传至家服务器。当服务器需获取远端服务器存储的媒体数据时,需从远端下载。

服务器必须基于 Matrix 内容 URI 的服务地址(格式 mxc://{ServerName}/{MediaID}),始终应从 {ServerName} 服务器下载,利用下述端点。

{{% changed-in v="1.11" %}} 之前推荐使用 /client-server-api/#content-repository 内描述的 /_matrix/media/* 端点,如今这些端点已废弃,新端点需认证。服务器(而非用户)无法提供所需访问令牌。因此服务器应优先尝试新端点,遇到 404 M_UNRECOGNIZED 时再尝试废弃端点,并确保设置 allow_remotefalse

{{% http-api spec="server-server" api="content_repository" %}}

服务器访问控制列表ACL

服务器 ACL 及其用途详见 客户端-服务器 API 的服务器 ACL 部分

远端服务器发起请求时,必须验证其是否有权限访问指定房间。被拒绝的服务器必须以 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} 端点必须按以下规则保护:

  • 对所有 PDU 逐个应用 ACL若发送服务器被拒绝访问 room_id 房间,则应忽略该 PDU并在各自事件 ID 对应响应项中注明错误。

  • 所有房间相关的 EDU 也须应用 ACL

事件签名

事件签名过程受事件被裁剪redact带来的复杂性影响。

为出站事件添加哈希和签名

签名前,需先计算事件的内容哈希content hash使用 Unpadded Base64 编码,放入事件 hashes.sha256 字段。

随后,执行裁剪redaction算法裁剪规则),再用 JSON 签名 算法及服务器签名密钥签名,生成的签名再拷贝回原事件对象。

签名事件范例见 房间版本规范

校验接收事件的哈希和签名

服务器收到联邦事件后,应立即校验哈希及签名。

首查签名,先裁剪事件,然后按 校验签名流程 检查签名(可接受完整或已裁剪事件)。

期望签名包括:

  • sender 服务器(如为第三方邀请则例外。否则 sender 要与第三方邀请吻合,而实际发事件或为不同服务器)。
  • 若为房间版本 1/2还包括事件 ID 创建方服务器。其他房间版本事件 ID 不在联邦传递,因此无需额外签名。

签名正确后,计算期望的内容哈希。hashes 字段的内容解码后与期望值比对。

如哈希校验失败,表明只收到裁剪后事件,故直接用裁剪结果。

计算事件引用哈希Reference Hash

引用哈希reference hash覆盖事件重要字段包括内容哈希。部分房间版本用于事件标识符具体见房间版本规范。计算过程如下:

  1. 事件经过裁剪算法处理。
  2. 移除 signaturesunsigned 字段。
  3. 转为 规范化 JSON
  4. 计算 sha256 哈希。

计算事件内容哈希Content Hash

内容哈希 覆盖原始未裁剪的完整事件。计算步骤:

  1. 移除已有的 unsignedsignatureshashes 字段。
  2. 规范化 JSON 编码后做 SHA-256 哈希。

示例代码

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