42 KiB
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 对象。未来可能会指定更高效的传输方式作为可选扩展。
所有的 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,不应发送)。
建议服务器利用 证书透明计划。
不支持的端点
若收到对不支持(或未知)端点的请求,服务器必须返回 404 M_UNRECOGNIZED
错误。
同样,405 M_UNRECOGNIZED
错误用于指示对已知端点的不支持 HTTP 方法。
服务器发现
解析服务器名称
每个 Matrix 家服务器通过一个包含主机名和可选端口的服务器名称唯一标识,详见 语法说明。如适用,委托服务器名采用相同语法。
服务器名需解析为可连接的 IP 地址与端口,解析过程中涉及不同证书和 Host
头的设置。整体流程如下:
- 如果主机名是 IP 字面量,则应直接使用该 IP 与指定端口(未指定则为 8448)。目标服务器必须呈现对应 IP 地址的有效证书。请求中的
Host
头应设为服务器名称(若含端口也需带端口)。 - 若主机名不是 IP 字面量,且服务器名称中包含明确端口,需通过 CNAME、AAAA 或 A 记录解析主机名为 IP 地址;请求将发至解析得到的 IP 和端口,
Host
头为原始服务器名称(含端口)。目标服务器必须呈现该主机名的有效证书。 - 若主机名非 IP 字面量,无明确端口,则向
https://<hostname>/.well-known/matrix/server
发起常规 HTTPS 请求,期望返回本节后续定义的模式。须跟随 30x 跳转,但需避免重定向循环。/.well-known
端点的响应(无论成功与否)应由请求服务器进行缓存。服务器应遵守响应内的缓存控制头,如无则使用合理默认值(建议 24 小时)。另外应限制响应最大缓存时间,建议为 48 小时。错误建议缓存最多一小时,并对重复失败采用指数退避。/.well-known
返回的响应模式详见本节后续。若响应无效(JSON 无效、字段缺失、返回非 200 等)则跳转到步骤 4。若响应有效,解析m.server
字段(格式<delegated_hostname>[:<delegated_port>]
)并按如下处理:- 若
<delegated_hostname>
为 IP 字面量,则用该 IP 和<delegated_port>
(未提供则 8448)。目标服务器必须有对应 IP 的有效 TLS 证书。请求Host
头为该 IP(若含端口亦包含端口)。 - 若
<delegated_hostname>
非 IP 字面量,且<delegated_port>
存在,查找其 CNAME、AAAA 或 A 记录,得出 IP,连同<delegated_port>
使用。请求Host
头为<delegated_hostname>:<delegated_port>
。目标服务器需有<delegated_hostname>
的有效证书。 - {{% added-in v="1.8" %}} 若
<delegated_hostname>
不是 IP 字面量,且未指定<delegated_port>
,则查找_matrix-fed._tcp.<delegated_hostname>
的 SRV 记录,可能带来新的主机名(需 AAAA 或 A 记录解析)及端口。请求应发往解析出的 IP 与端口,Host
头为<delegated_hostname>
。目标服务器需有<delegated_hostname>
的有效证书。 - [已废弃] 若
<delegated_hostname>
不是 IP 字面量,未指定<delegated_port>
,且找不到_matrix-fed._tcp.<delegated_hostname>
SRV 记录,则查_matrix._tcp.<delegated_hostname>
,同样可能得到主机名和端口。请求应发往解析到的 IP 和端口,Host
头为<delegated_hostname>
。目标服务器需有<delegated_hostname>
的有效证书。 - 若未找到 SRV 记录,通过 CNAME、AAAA 或 A 记录解析 IP,之后用 8448 端口发请求,
Host
头为<delegated_hostname>
。目标服务器须有<delegated_hostname>
的有效证书。
- 若
- {{% added-in v="1.8" %}} 若
/.well-known
请求返回错误,则尝试解析_matrix-fed._tcp.<hostname>
的 SRV 记录,或得主机名和端口。请求发往解析到的 IP 与端口,Host
头为<hostname>
,目标服务器需有<hostname>
的有效证书。 - [已废弃] 若
/.well-known
请求错误且找不到_matrix-fed._tcp.<hostname>
SRV 记录,则解析_matrix._tcp.<hostname>
SRV 记录,同样可能获主机名和端口。请求发往解析到的 IP 和端口,Host
头为<hostname>
,目标服务器需有<hostname>
的有效证书。 - 若
/.well-known
返回错误,且未找到 SRV 记录,则用 CNAME、AAAA、A 记录解析 IP,发往 8448 端口,Host
头为<hostname>
,目标服务器需有<hostname>
有效证书。
{{% boxes/note %}}
我们强制要求 SRV 委托使用 <hostname>
而非 <delegated_hostname>
的原因:
- DNS 并不安全(并非所有域名都部署 DNSSEC),因此委托目标必须通过 TLS 证明自己是
<hostname>
的合法代理。 - 与 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 7235 和 RFC 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。新事件应以 E4
和 E6
作为 prev_events
,因为两者尚无子事件:
E1
^
|
E2 <--- E5
^ ^
| |
E3 E6
^
|
E4
完整的 PDU 模式见 房间版本规范。
PDU 接收时的校验
服务器接收到远端事件时,必须确保该事件:
- 是一个有效事件,否则直接丢弃。有效事件必须含有
room_id
,并符合该 房间版本 的事件格式。 - 签名校验通过,否则丢弃。
- 哈希校验通过,否则事件被裁剪(redacted)后再继续处理。
- 基于认证事件(auth events)的授权规则校验通过,否则拒绝。
- 基于事件前状态的授权规则校验通过,否则拒绝。
- 基于房间当前状态的授权规则校验通过,否则“软失败”(soft failed)。
各项校验及失败处理详述如下。
关于事件需要包含哪些哈希及签名及其计算,详见 事件签名。
定义
所需权限等级(Required Power Level)
:每类事件类型对应一个 权限等级,由当前 m.room.power_levels
事件指定。若事件类型在 events
块未显式列出,则根据是否为状态事件,分别使用 state_default
或 events_default
。
邀请/踢出/封禁/撤回等级(Invite Level, Kick Level, Ban Level, Redact Level)
:由当前 m.room.power_levels
状态内的 invite
、kick
、ban
、redact
指定。邀请默认为 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
事件(如有) - 若
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
存在,且 房间版本支持受限房间,则加入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 处房间名称如何确定?
状态解析算法由房间版本决定,详见 房间版本规范。
回溯填充与缺失事件获取
家服务器加入房间后,会收到所有在房间内其他家服务器产生的事件,因此近期历史不会丢失。用户可通过 /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
获取房间事件模板。常驻服务器返回填充事件各项所需信息。
加入服务器需补充完善 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,仅在 支持受限加入的房间版本 下启用。
处理请求加入受限房间时,常驻服务器需确保加入服务器满足 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_key
与 m.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_remote
为 false
。
{{% 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:
- 对 输入状态通知 (
m.typing
),发送服务器被拒绝访问相应room_id
时忽略其 EDU。 - 对 已读回执 (
m.receipt
),若发送服务器被拒绝访问某房间,则对应该房间的所有回执均应忽略。
- 对 输入状态通知 (
事件签名
事件签名过程受事件被裁剪(redact)带来的复杂性影响。
为出站事件添加哈希和签名
签名前,需先计算事件的内容哈希(content hash),使用 Unpadded Base64 编码,放入事件 hashes.sha256
字段。
随后,执行裁剪(redaction)算法(见 裁剪规则),再用 JSON 签名 算法及服务器签名密钥签名,生成的签名再拷贝回原事件对象。
签名事件范例见 房间版本规范。
校验接收事件的哈希和签名
服务器收到联邦事件后,应立即校验哈希及签名。
首查签名,先裁剪事件,然后按 校验签名流程 检查签名(可接受完整或已裁剪事件)。
期望签名包括:
sender
服务器(如为第三方邀请则例外。否则 sender 要与第三方邀请吻合,而实际发事件或为不同服务器)。- 若为房间版本 1/2,还包括事件 ID 创建方服务器。其他房间版本事件 ID 不在联邦传递,因此无需额外签名。
签名正确后,计算期望的内容哈希。hashes
字段的内容解码后与期望值比对。
如哈希校验失败,表明只收到裁剪后事件,故直接用裁剪结果。
计算事件引用哈希(Reference Hash)
引用哈希(reference hash)覆盖事件重要字段,包括内容哈希。部分房间版本用于事件标识符,具体见房间版本规范。计算过程如下:
- 事件经过裁剪算法处理。
- 移除
signatures
和unsigned
字段。 - 转为 规范化 JSON。
- 计算 sha256 哈希。
计算事件内容哈希(Content Hash)
内容哈希 覆盖原始未裁剪的完整事件。计算步骤:
- 移除已有的
unsigned
、signatures
和hashes
字段。 - 用 规范化 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 方案将解决此类问题。