@eduwork/dsh-remote-access 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/README_EN.md +95 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/cordis.patch.yml +5 -0
- package/docs/DEPLOYMENT.md +168 -0
- package/docs/DEPLOYMENT_EN.md +168 -0
- package/docs/PROTOCOL.md +129 -0
- package/lib/THIRD_PARTY_LICENSES.txt +728 -0
- package/lib/client.js +105 -0
- package/lib/index.js +22275 -0
- package/lib/relay.js +4247 -0
- package/lib/typert.js +38 -0
- package/lib/web/icon.png +0 -0
- package/lib/web/icon.svg +1 -0
- package/lib/web/index.html +18 -0
- package/lib/web/loader.css +24 -0
- package/lib/web/loader.js +1 -0
- package/lib/web/manifest.webmanifest +21 -0
- package/lib/web/preamble.js +1 -0
- package/lib/web/sw.js +1 -0
- package/package.json +150 -4
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# 远程访问协议 `eduwork-remote/1`
|
|
2
|
+
|
|
3
|
+
本文描述当前版本的消息格式、状态转换和版本兼容规则。实现位于 [`src/protocol/`](../src/protocol/),浏览器和宿主共用同一份代码。
|
|
4
|
+
|
|
5
|
+
## 参与方
|
|
6
|
+
|
|
7
|
+
- **宿主**:运行 DSH 和本插件的电脑。长期持有两把密钥:Ed25519 签名密钥(向中继证明身份)和 X25519 Noise 静态密钥(向设备证明身份)。
|
|
8
|
+
- **设备**:手机或其他电脑上的浏览器。每个配对的宿主对应一把 WebCrypto 生成的不可导出 ECDSA P-256 密钥。
|
|
9
|
+
- **中继**:转发密文的公共服务,同时提供网页客户端。它只验证宿主的接入资格,看不到明文。
|
|
10
|
+
|
|
11
|
+
宿主 ID(hostId)是 Ed25519 公钥原文的 base64url 编码(43 个字符)。
|
|
12
|
+
|
|
13
|
+
## 中继
|
|
14
|
+
|
|
15
|
+
中继有一个公开的基础地址,可以带路径,例如 `https://example.edu/eduwork-remote/`;规范形式以 `/` 结尾,不含用户名、查询和片段。下文的路径都在这个地址之下,网页客户端的页面和 Service Worker 也一样。中继的 WebSocket 路径如下。文本帧是控制消息(JSON,带字段 `t`),二进制帧是端到端密文,单条消息不超过 65535 字节。
|
|
16
|
+
|
|
17
|
+
| 路径 | 连接方 | 过程 |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `v1/host` | 宿主 | 中继发 `challenge {nonce}`;宿主回 `register {protocol, hostId, sig, accessKey}` 或 `register {protocol, hostId, sig, idToken}`;中继回 `registered`。此后每有一台设备接入,中继发 `connect {id}`。宿主可随时发 `ping`,中继回 `pong`。 |
|
|
20
|
+
| `v1/host/accept` | 宿主 | 宿主第一条消息发 `accept {id}`,此后这条连接与对应设备的连接成对转发。 |
|
|
21
|
+
| `v1/connect/<hostId>` | 设备 | 宿主接受后中继发 `open`,此后成对转发。 |
|
|
22
|
+
|
|
23
|
+
注册签名覆盖 `` `eduwork-remote/1 relay-register\0<中继基础地址>\0<nonce>` ``,用 hostId 对应的 Ed25519 密钥签名。nonce 每次连接新生成,签名绑定中继的完整基础地址,不能在别处重放,包括同一域名下其他路径的中继。同一 hostId 的新注册会顶替旧的,旧连接以 4409 关闭。
|
|
24
|
+
|
|
25
|
+
注册凭据二选一:`accessKey` 是中继运营者发的接入密钥;`idToken` 是中继接纳的机构登录签发的 OpenID Connect ID Token(RS256,issuer 和 audience 由中继配置)。凭据只决定谁能使用这台中继,不决定谁能连接哪台电脑。Token 须由中继配置的 issuer 签发,签发对象(有 `azp` 时为 `azp`,否则为唯一的 `aud`)须是配置的 client ID 之一并在 `aud` 中;`aud` 有多个值时必须带 `azp`。以 ID Token 注册时,中继记下其中的 `sub` 作为账号;Token 过期时,中继以 4408 关闭这台宿主的控制连接,以及在它名下配对的全部设备连接。宿主在过期前 5 分钟用新的 Token 在另一条控制连接上重新注册,新注册顶替旧的,过期时间随之顺延,已连接的设备不受影响;重新注册失败时宿主保留原注册,稍后再试;机构登录已失效、只能重新登录时,宿主停止注册,等用户重新登录。控制连接因网络中断时,已配对的设备连接继续转发,直到 Token 过期。
|
|
26
|
+
|
|
27
|
+
中继的限制:同一来源地址一分钟内被拒的注册超过上限后,之后的注册直接以 4429 关闭;每个账号同时注册的电脑数有上限,超过以 4401 拒绝;每台电脑每分钟接受的设备连接数有上限,超过以 4429 关闭设备连接。机构登录服务暂时取不到公钥时以 4503 关闭,宿主稍后重试。
|
|
28
|
+
|
|
29
|
+
每台设备的每条连接都对应宿主新开的一条 `accept` 连接,因此各条连接分别做 TCP 背压,一台慢设备不会拖住其他设备。成对连接的一端关闭时,中继用同一关闭码关闭另一端(1000 和 4000–4999 原样转发,其余改为 4410)。
|
|
30
|
+
|
|
31
|
+
## 加密信道
|
|
32
|
+
|
|
33
|
+
设备在 `open` 之后发起 Noise 握手,模式为 `Noise_IK_25519_ChaChaPoly_BLAKE2b`:
|
|
34
|
+
|
|
35
|
+
- 宿主静态公钥来自配对链接,之后每次连接都固定使用它,所以中继无法冒充宿主。
|
|
36
|
+
- 设备的发起方静态密钥每条连接随机生成,不代表设备身份;设备身份由下一节的签名证明。
|
|
37
|
+
- prologue 为 `` `eduwork-remote/1\0relay\0<hostId>` ``。
|
|
38
|
+
|
|
39
|
+
握手后每条 WebSocket 二进制消息是一条 Noise 传输消息。明文第一个字节为 1 表示后面还有分片,为 0 表示帧结束;其余是帧的一段。一帧由 4 字节大端头部长度、JSON 头部和原始正文组成。一帧最大 16 MiB;任一方积压未发出的数据超过 64 MiB 时关闭连接。Noise 计数器为 32 位,单向达到 2³² 条消息后拒绝继续收发,实际使用中达不到。
|
|
40
|
+
|
|
41
|
+
## 认证与配对
|
|
42
|
+
|
|
43
|
+
握手完成后,设备的第一帧必须是 `auth`:
|
|
44
|
+
|
|
45
|
+
| 字段 | 说明 |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `key` | 设备 P-256 公钥(未压缩格式原文)的 base64url |
|
|
48
|
+
| `sig` | 对 `` `eduwork-remote/1 device-auth\0` `` 加握手哈希的 ECDSA P-256/SHA-256 签名(IEEE P1363 格式) |
|
|
49
|
+
| `invite` | 仅首次配对:配对链接中的一次性邀请 |
|
|
50
|
+
| `name` | 仅首次配对:设备名称,最多 64 个字符 |
|
|
51
|
+
|
|
52
|
+
签名覆盖本次连接的握手哈希,换一条连接就不能重放。宿主的回复:
|
|
53
|
+
|
|
54
|
+
| 回复 | 含义 |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `ready {host}` | 已认证,可以开始交换 |
|
|
57
|
+
| `pending {host}` | 新设备,等待宿主主人确认 |
|
|
58
|
+
| `rejected {reason}` | 拒绝后关闭连接,原因为 `unknown-device`、`invalid-invite`、`declined` 或 `timeout` |
|
|
59
|
+
| `revoked` | 已连接的设备被移除,随后以 4403 关闭 |
|
|
60
|
+
|
|
61
|
+
状态转换:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
握手 ──auth──▶ 已登记设备 ──────────────────────────▶ ready
|
|
65
|
+
未登记、无邀请或邀请无效 ─────────────▶ rejected
|
|
66
|
+
邀请有效 ──▶ pending ──主人允许──────▶ ready
|
|
67
|
+
──拒绝或 5 分钟未答─▶ rejected
|
|
68
|
+
ready ──主人移除设备──▶ revoked
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
设备在 10 秒内不发 `auth` 时连接关闭。收到 `ready` 前,设备除 `auth` 外不发其他帧;宿主在发出 `ready` 前不处理任何交换。
|
|
72
|
+
|
|
73
|
+
**配对码**:双方各自计算 `SHA-256("eduwork-remote/1 pairing-code\0" ‖ 握手哈希)` 前 4 字节(大端)对 1 000 000 取模,补足 6 位显示。两边的码一致,说明设备连到的正是这台宿主,中间没有第三方。
|
|
74
|
+
|
|
75
|
+
**配对链接**:`<中继基础地址>#<hostId>.<宿主 Noise 公钥>.<邀请>`,三段都是 base64url。浏览器不会把 `#` 之后的内容发给中继。邀请为 16 字节随机数,10 分钟内有效、只能用一次;新生成的邀请使旧邀请作废。
|
|
76
|
+
|
|
77
|
+
## 隧道
|
|
78
|
+
|
|
79
|
+
`ready` 之后,信道上承载两类交换:HTTP 请求和 DSH Gateway 流。帧名称沿用 DSH worker 隧道的约定。每个交换由设备分配一个正整数 `id`,同一连接内严格递增。
|
|
80
|
+
|
|
81
|
+
| 帧 | 方向 | 说明 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `req {id, method, url, headers, body}` | 设备 → 宿主 | `url` 为以 `/` 开头的路径和查询;`body` 为布尔值,表示随后是否有请求正文 |
|
|
84
|
+
| `req-chunk {id}` + 正文 | 设备 → 宿主 | 请求正文片段 |
|
|
85
|
+
| `req-end {id}` | 设备 → 宿主 | 请求正文结束 |
|
|
86
|
+
| `res-head {id, status, headers}` | 宿主 → 设备 | 响应状态与头部 |
|
|
87
|
+
| `res-chunk {id}` + 正文 | 宿主 → 设备 | 响应正文片段 |
|
|
88
|
+
| `res-end {id}` | 宿主 → 设备 | 响应结束 |
|
|
89
|
+
| `res-err {id, message}` | 宿主 → 设备 | 宿主无法给出响应(区别于返回错误状态码) |
|
|
90
|
+
| `stream-open {id, endpoint, payload}` | 设备 → 宿主 | 打开 Gateway 流,等同 DSH 的 `RpcStreamOpen` |
|
|
91
|
+
| `stream-uplink-item {id, value}` / `stream-uplink-end {id}` | 设备 → 宿主 | 流的上行数据 |
|
|
92
|
+
| `stream-item {id, value}` | 宿主 → 设备 | 流的一项 |
|
|
93
|
+
| `stream-end {id}` | 宿主 → 设备 | 流正常结束 |
|
|
94
|
+
| `stream-error {id, code, message, details}` | 宿主 → 设备 | 流以 DSH 的稳定错误结束 |
|
|
95
|
+
| `credit {id, bytes}` | 双向 | 正文接收方追加的额度 |
|
|
96
|
+
| `abort {id}` | 设备 → 宿主 | 设备不再需要该交换 |
|
|
97
|
+
| `ping` / `pong` | 设备 → 宿主 / 宿主 → 设备 | 不带 `id` 的存活探测 |
|
|
98
|
+
|
|
99
|
+
**压缩**:设备请求前端文件(首页、静态资源、插件包)时可在头部带 `accept-encoding: gzip`,宿主据此把其中的文本文件压缩后返回,并标上 `content-encoding: gzip`。RPC、文件下载和流一律不压缩:这些内容可能涉及隐私,压缩后的长度可能向中继透露内容特征。
|
|
100
|
+
|
|
101
|
+
**流控**:每个正文开始时有 1 MiB 额度,每片不超过 64000 字节;接收方读走四分之一窗口后补发 `credit`。超额发送是协议错误。
|
|
102
|
+
|
|
103
|
+
**存活**:设备每 20 秒发一次 `ping`,10 秒内没有 `pong` 就关闭并重连。宿主 90 秒没收到任何帧时以 4408 关闭连接。宿主到中继的控制连接也用 `ping`/`pong` 探测。
|
|
104
|
+
|
|
105
|
+
**失败语义**:交换只属于它所在的连接。连接关闭时,设备端未完成的交换立即失败,宿主端的处理被中止,之后产生的结果随连接丢弃。隧道不重发;重试由 DSH 客户端按自身规则处理(RPC 不重试,流按退避重开,prompt 由宿主按 `requestId` 去重)。
|
|
106
|
+
|
|
107
|
+
**宿主上的执行边界**:远程请求以宿主主人的身份进入 DSH,每条连接有独立的 Peer,连接关闭或设备被移除时 Peer 随即释放。可用的接口由 [`src/gate/gate.ts`](../src/gate/gate.ts) 列出,未列出的一律拒绝:RPC 拒绝时返回 DSH 的响应信封,错误码 `remote-access/forbidden`;流以同一错误码结束。个别接口只在参数符合时放行:`settings/mutate` 只能记录已读 DSH 的预览版说明。桌面端不显示这条说明,网页端不确认就无法继续,其他设置仍不能修改。
|
|
108
|
+
|
|
109
|
+
## 关闭码
|
|
110
|
+
|
|
111
|
+
| 码 | 含义 |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| 1000 | 正常关闭(例如宿主关闭了远程访问) |
|
|
114
|
+
| 4400 | 协议错误 |
|
|
115
|
+
| 4401 | 中继拒绝注册凭据或签名,或该账号注册的电脑已达上限 |
|
|
116
|
+
| 4403 | 设备被拒绝或被移除 |
|
|
117
|
+
| 4404 | 宿主不在线 |
|
|
118
|
+
| 4408 | 超时,或注册所用的 ID Token 过期 |
|
|
119
|
+
| 4409 | 同一宿主身份在别处注册,本连接被顶替 |
|
|
120
|
+
| 4410 | 对端连接断开 |
|
|
121
|
+
| 4426 | 协议版本不受支持 |
|
|
122
|
+
| 4429 | 超出频率、连接数或积压上限 |
|
|
123
|
+
| 4503 | 服务停止或内部错误 |
|
|
124
|
+
|
|
125
|
+
## 版本兼容
|
|
126
|
+
|
|
127
|
+
协议名 `eduwork-remote/1` 出现在中继注册、握手 prologue、设备签名和配对码的上下文中,所以不同版本的两端无法误连:中继以 4426 拒绝它不支持的宿主协议,宿主与设备版本不一致时握手或验签失败。
|
|
128
|
+
|
|
129
|
+
同一版本内只做兼容的扩展:新增可选字段,接收方忽略不认识的字段。改变已有帧的含义、增删必需字段或调整密码学构造时,协议名升级为 `eduwork-remote/2`。中继提供的网页客户端和宿主插件分别发布,二者只需协议名一致。网页客户端在远程设备的 DSH 页面根元素上标记 `data-eduwork-remote`,宿主插件据此按手机宽度调整 DSH 的布局,并且不显示远程访问设置;任一端较旧时只是少了这项调整。
|