@linxin666/dsh-remote-web-ui 0.2.8 → 0.2.9
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/LICENSE +202 -28
- package/README.i18n.yaml +2 -2
- package/README.md +10 -7
- package/README.zh.md +2 -2
- package/lib/index.js +22 -16
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +15 -4
- package/lib/types/remote-api.d.ts +13 -4
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +27 -17
- package/package.json +1 -1
- package/src/index.ts +15 -4
- package/src/remote-api.ts +34 -17
package/LICENSE
CHANGED
|
@@ -1,28 +1,202 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
1.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# node scripts/verify-docs.mjs --write <dir>
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: d3195d3b8a3dc58cf077538b0e18455b7d1706a6
|
|
6
|
+
README.zh.md: c0a06bf7ae0b49460e6f4aa250b42f2c227b4e0a
|
package/README.md
CHANGED
|
@@ -44,7 +44,8 @@ that probes and runs the update.
|
|
|
44
44
|
their next request. Pairing is this plugin's access control for its own
|
|
45
45
|
remote channels: `/m/api` for the phone and `/remote` for a desktop
|
|
46
46
|
browser opened at a non-loopback origin. Unpaired `/remote` callers
|
|
47
|
-
are refused before the request body is read
|
|
47
|
+
are refused before the request body is read while `requirePairingForLan` is
|
|
48
|
+
on (default). Loopback (127.0.0.1) keeps using
|
|
48
49
|
`/api` directly. The default remote-desktop path does not use
|
|
49
50
|
`--trusted-host`: the connection plugin's `/api` fence stays closed for
|
|
50
51
|
public and LAN hosts, and the paired PC rides `/remote` instead.
|
|
@@ -244,12 +245,14 @@ over Server-Sent Events on `/m/api/events.mux`. The canonical `/m/` page owns a
|
|
|
244
245
|
- The `/m/` worker uses network-first static-shell fallback and waits for current pages to close before an updated worker activates. It bypasses `/m/api`, `/api`, SSE, and every write request.
|
|
245
246
|
- Installing this plugin routes fenced non-loopback desktop traffic onto the
|
|
246
247
|
gated `/remote` channel (see `requirePairingForLan` in `src/index.ts`).
|
|
247
|
-
|
|
248
|
-
remote device — the unpaired state shows a
|
|
249
|
-
data; loopback (127.0.0.1) is unaffected
|
|
250
|
-
`requirePairingForLan: false` in the profile patch to
|
|
251
|
-
plain `/api`
|
|
252
|
-
|
|
248
|
+
While the flag is on (default), a desktop browser opened via the LAN URL or
|
|
249
|
+
the tunnel must pair like any remote device — the unpaired state shows a
|
|
250
|
+
persistent blocking page instead of data; loopback (127.0.0.1) is unaffected
|
|
251
|
+
and keeps `/api`. Set `requirePairingForLan: false` in the profile patch to
|
|
252
|
+
keep the desktop on plain `/api` and have a leftover `/remote` rewrite
|
|
253
|
+
proxy unpaired calls (only useful with the open-LAN stance; loopback-only
|
|
254
|
+
paths stay denied) while keeping tokens/status/revocation. Note the
|
|
255
|
+
underlying `/api` route itself is the
|
|
253
256
|
SDK's: on a `--host 0.0.0.0` bind the SDK auto-trusts LAN literals, so a
|
|
254
257
|
LAN client bypassing the UI can still reach `/api` directly — the posture
|
|
255
258
|
probe reports that stance on the panel.
|
package/README.zh.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- **面板**:「远程访问」标题、「通过手机或另一台电脑配对,远程访问当前工作区」副标题、「设备配对」卡片(含状态区「等待设备连接」+ 状态徽标)、大号二维码、各带复制按钮的手机链接(`/m/?pair=...`)与电脑链接(`/?pair=...`)、停止 / 刷新二维码操作,以及已授权设备列表(根据 User-Agent 推断的设备名称、在线/离线、最近活动时间、单台取消配对)。界面不渲染作为会话凭据的设备 id 或原始 User-Agent。两条链接共用一枚一次性令牌,任意设备配对成功后另一条立即失效。
|
|
13
13
|
- **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
|
|
14
14
|
- **PC 侧(远程桌面配对)**:同一份二维码链接和配对令牌也能配对 PC 浏览器——把手机侧配对流扩展到桌面 Web GUI。从面板复制链接,在另一台电脑的浏览器打开(局域网 URL 或隧道 URL 均可);接受往返后,完整 Web GUI 在那台设备上运行——工作区、会话、聊天、切模型——受围栏保护的同源流量走门控的 `/remote` 通道,而不是直接调用仅 loopback 的 host 路由。手机进 `/m/`,PC 进桌面 UI:一枚令牌、一套配对流、两种界面。未配对的 PC 只看到不渲染底层数据的完整阻断页,其中提供电脑配对步骤与「重新检测」操作。
|
|
15
|
-
- **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote
|
|
15
|
+
- **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote`。未配对调用方在 `requirePairingForLan` 开启(默认)时会在读取请求体之前就被 `/remote` 拒绝。loopback(127.0.0.1)继续直接使用 `/api`。默认的远程桌面路径不使用 `--trusted-host`:连接插件的 `/api` 围栏对公网和局域网主机保持关闭,已配对 PC 改走 `/remote`。`--trusted-host` 是 SDK 的另一种用法,会让该主机被信任访问 `/api` 本身——配对不门控 `/api`(任何插件都无法做到;围栏是 SDK 自己的接缝)。当 `/api` 无需配对即可到达时,下方姿态探测会报告该姿态。
|
|
16
16
|
- **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达,`/api/pair/*`、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。阻断页在任一门控调用成功后,或通道本身被拆除时退出——把 `requirePairingForLan` 关掉(或关闭插件)也会清掉通道短暂激活期间弹出的提示。
|
|
17
17
|
- **姿态探测**:插件用伪造 Host 头(公网 base 与每个 LAN base)探测 SDK 的 `/api` 围栏。403 是默认姿态(围栏关闭,远程访问走配对)。任何非 403 结果——`--trusted-host`,或 `--host 0.0.0.0` 下 SDK 的局域网自动信任——都会以 CRITICAL 日志与配对面板红色横幅呈现,因此 SDK 的 `/api` 信任姿态是被看见的,而不是被假设的。探测失败会丢掉进行中的目标缓存,相同 origin 会在下次触发时重试。
|
|
18
18
|
- **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开),并展示已授权设备名单。
|
|
@@ -77,7 +77,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
|
77
77
|
|
|
78
78
|
- 移动端输入框默认 Enter 发送(Shift+Enter 换行)。在插件设置卡片(或 profile patch)把 `mobileEnterToSend` 设为 false 后,普通 Enter 改为插入换行,只有「发送」按钮会发送;手机打开聊天时经自己的 `/m/api` 偏好方法读取该开关。在支持 `field-sizing: content` 的浏览器上,输入框随草稿自动增高,最高 120px 封顶(两种模式一致)。
|
|
79
79
|
- `/m/` Worker 对静态壳使用 network-first 回退,并等待当前页面关闭后才激活更新。它旁路 `/m/api`、`/api`、SSE 与所有写请求。
|
|
80
|
-
- 安装本插件后,非 loopback 桌面中受围栏保护的流量会改走门控的 `/remote` 通道(见 `src/index.ts` 的 `requirePairingForLan
|
|
80
|
+
- 安装本插件后,非 loopback 桌面中受围栏保护的流量会改走门控的 `/remote` 通道(见 `src/index.ts` 的 `requirePairingForLan`)。在该开关开启(默认)时,经局域网 URL 或隧道打开的桌面浏览器必须像任何远程设备一样配对——未配对状态显示完整阻断页而不是数据;loopback(127.0.0.1)不受影响,继续使用原始路径。把 profile patch 里 `requirePairingForLan` 设为 false 可让桌面继续走普通 `/api`,残留的 `/remote` 改写也会放行未配对调用(仅在开放局域网姿态下有意义;loopback-only 路径仍被拒绝),同时保留令牌/状态/撤销。注意 `/api` 路由本体属于 SDK:`--host 0.0.0.0` 绑定时 SDK 自动信任局域网字面量,绕过 UI 的局域网客户端仍能直接访问 `/api`——姿态探测会在面板上报告该姿态。
|
|
81
81
|
- `/api` 之外的兄弟 host 路由(`/pet/*`、`/git/*`、右侧面板的 `/aionui-panel/*`)可查询本插件的 `remoteWebUiPairing` 服务:有效的已配对设备 cookie 是放行路径,`stop()` 仍会切断它们;未安装本插件时该服务不存在。
|
|
82
82
|
- 二维码链接基于机器的非内部 IPv4 字面量构建;多宿主主机(Wi-Fi + 有线,或代理/VPN 虚拟适配器)会显示单选器供你发布手机实际可达的网络。第一个字面量是默认值。设 `publicBaseUrl` 后,单选器在顶部额外加一项 公网地址——默认二维码改用公网 base,选中局域网字面量会重新铸一枚网内链接。
|
|
83
83
|
- 配置的 `publicBaseUrl` 本身满足可达绑定需求:`dsh web` 绑定 `127.0.0.1`(不带 `--host 0.0.0.0`)仍能经隧道铸出可用的公网二维码链接。
|
package/lib/index.js
CHANGED
|
@@ -1989,17 +1989,19 @@ function loopbackOnlyDenial(innerPath) {
|
|
|
1989
1989
|
}
|
|
1990
1990
|
/**
|
|
1991
1991
|
* Build the remote desktop channel HTTP routes.
|
|
1992
|
-
* @param deps - pairing service + local port.
|
|
1992
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
1993
1993
|
* @returns the routes to register on webServer.
|
|
1994
1994
|
*/
|
|
1995
1995
|
function makeRemoteApiRoutes(deps) {
|
|
1996
|
-
const { service, port } = deps;
|
|
1996
|
+
const { service, port, requirePairingForLan = true } = deps;
|
|
1997
1997
|
const handler = (req, res) => {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1998
|
+
if (typeof requirePairingForLan === "function" ? requirePairingForLan() : requirePairingForLan) {
|
|
1999
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
2000
|
+
if (!(deviceId !== void 0 && service.touchDevice(deviceId))) {
|
|
2001
|
+
req.resume();
|
|
2002
|
+
envelopeError(res, 403, "invalid-request", "unpaired", "this device is not paired with the desktop");
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2003
2005
|
}
|
|
2004
2006
|
const method = req.method ?? "GET";
|
|
2005
2007
|
if (!ALLOWED_METHODS.has(method)) {
|
|
@@ -2046,17 +2048,19 @@ function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
|
2046
2048
|
/**
|
|
2047
2049
|
* Build the WebSocket upgrade routes for the event streams and known plugin
|
|
2048
2050
|
* sockets. webServer matches upgrades by exact path.
|
|
2049
|
-
* @param deps - pairing service + local port.
|
|
2051
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
2050
2052
|
* @returns the upgrade routes to register on webServer.
|
|
2051
2053
|
*/
|
|
2052
2054
|
function makeRemoteApiUpgradeRoutes(deps) {
|
|
2053
|
-
const { service, port } = deps;
|
|
2055
|
+
const { service, port, requirePairingForLan = true } = deps;
|
|
2054
2056
|
const handlerFor = (fallbackPath) => (req, socket, head) => {
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2057
|
+
if (typeof requirePairingForLan === "function" ? requirePairingForLan() : requirePairingForLan) {
|
|
2058
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
2059
|
+
if (deviceId === void 0 || !service.touchDevice(deviceId)) {
|
|
2060
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
2061
|
+
socket.destroy();
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2060
2064
|
}
|
|
2061
2065
|
const inner = upgradeInnerPath(req.url, fallbackPath);
|
|
2062
2066
|
if (loopbackOnlyDenial(inner.split("?")[0] ?? inner) !== void 0) {
|
|
@@ -3198,13 +3202,15 @@ function applyImpl(ctx, config) {
|
|
|
3198
3202
|
}) : [],
|
|
3199
3203
|
...makeRemoteApiRoutes({
|
|
3200
3204
|
service,
|
|
3201
|
-
port: ctx.webServer.port
|
|
3205
|
+
port: ctx.webServer.port,
|
|
3206
|
+
requirePairingForLan: () => resolve().requirePairingForLan
|
|
3202
3207
|
}),
|
|
3203
3208
|
...updateRoutes
|
|
3204
3209
|
];
|
|
3205
3210
|
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
3206
3211
|
service,
|
|
3207
|
-
port: ctx.webServer.port
|
|
3212
|
+
port: ctx.webServer.port,
|
|
3213
|
+
requirePairingForLan: () => resolve().requirePairingForLan
|
|
3208
3214
|
});
|
|
3209
3215
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
3210
3216
|
ctx.effect(() => ctx.on("api/gate", gate), "remote-web-ui: api gate");
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAE3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAsBzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA4B,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAY3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAC/F,GAAG,aAAa,CAShB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnE;AAiBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAE3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAsBzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA4B,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAY3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAC/F,GAAG,aAAa,CAShB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnE;AAiBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAkStD"}
|
package/lib/types/index.js
CHANGED
|
@@ -253,12 +253,23 @@ function applyImpl(ctx, config) {
|
|
|
253
253
|
...(apiProxy !== undefined
|
|
254
254
|
? makeMobileApiRoutes({ service, apiProxy, mobileEnterToSend: () => resolve().mobileEnterToSend })
|
|
255
255
|
: []),
|
|
256
|
-
// The remote desktop channel:
|
|
257
|
-
// re-issues fenced paths to loopback (see remote-api.ts).
|
|
258
|
-
|
|
256
|
+
// The remote desktop channel: policy-gated `/remote` prefix that
|
|
257
|
+
// re-issues fenced paths to loopback (see remote-api.ts). The live
|
|
258
|
+
// requirePairingForLan is re-read per request, same as the gate listener
|
|
259
|
+
// and routes above, so a stale client rewrite on an open-LAN deployment
|
|
260
|
+
// proxies instead of 403ing.
|
|
261
|
+
...makeRemoteApiRoutes({
|
|
262
|
+
service,
|
|
263
|
+
port: ctx.webServer.port,
|
|
264
|
+
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
265
|
+
}),
|
|
259
266
|
...updateRoutes,
|
|
260
267
|
];
|
|
261
|
-
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
268
|
+
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
269
|
+
service,
|
|
270
|
+
port: ctx.webServer.port,
|
|
271
|
+
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
272
|
+
});
|
|
262
273
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
263
274
|
ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate');
|
|
264
275
|
// ── posture probe ─────────────────────────────────────────────────────────
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* and no per-plugin pairing consult.
|
|
8
8
|
*
|
|
9
9
|
* Security model:
|
|
10
|
-
* -
|
|
11
|
-
* any bytes are forwarded and
|
|
10
|
+
* - While `requirePairingForLan` is on (default), every request must carry a
|
|
11
|
+
* live paired-device cookie, enforced before any bytes are forwarded and
|
|
12
|
+
* before any host call. With the policy off, the cookie gate is skipped
|
|
13
|
+
* (the loopback-only denials below still apply).
|
|
12
14
|
* - The SDK's loopback-only privileged methods (native dialogs, the settings
|
|
13
15
|
* plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
|
|
14
16
|
* are denied here. The set is pinned by tests/remote-contract.spec.ts.
|
|
@@ -29,6 +31,13 @@ export interface RemoteApiDeps {
|
|
|
29
31
|
service: PairingService;
|
|
30
32
|
/** The local webServer port the loopback proxy connects to. */
|
|
31
33
|
port: number;
|
|
34
|
+
/**
|
|
35
|
+
* Live policy: whether the paired-device cookie gates the /remote channel.
|
|
36
|
+
* When false, requests are proxied without a cookie (the loopback-only
|
|
37
|
+
* denials still apply). A function is re-read per request, so a settings
|
|
38
|
+
* edit takes effect without a restart. Defaults to true.
|
|
39
|
+
*/
|
|
40
|
+
requirePairingForLan?: boolean | (() => boolean);
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Map `/remote/...` to the inner path, or undefined when the outer path is
|
|
@@ -42,7 +51,7 @@ export declare function innerPathOf(pathname: string): string | undefined;
|
|
|
42
51
|
export declare function loopbackOnlyDenial(innerPath: string): string | undefined;
|
|
43
52
|
/**
|
|
44
53
|
* Build the remote desktop channel HTTP routes.
|
|
45
|
-
* @param deps - pairing service + local port.
|
|
54
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
46
55
|
* @returns the routes to register on webServer.
|
|
47
56
|
*/
|
|
48
57
|
export declare function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[];
|
|
@@ -53,7 +62,7 @@ export declare function upgradeInnerPath(reqUrl: string | undefined, fallbackPat
|
|
|
53
62
|
/**
|
|
54
63
|
* Build the WebSocket upgrade routes for the event streams and known plugin
|
|
55
64
|
* sockets. webServer matches upgrades by exact path.
|
|
56
|
-
* @param deps - pairing service + local port.
|
|
65
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
57
66
|
* @returns the upgrade routes to register on webServer.
|
|
58
67
|
*/
|
|
59
68
|
export declare function makeRemoteApiUpgradeRoutes(deps: RemoteApiDeps): WebUpgradeRoute[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAclD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CACjD;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsBxE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA2CnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CA2BjF"}
|
package/lib/types/remote-api.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* and no per-plugin pairing consult.
|
|
8
8
|
*
|
|
9
9
|
* Security model:
|
|
10
|
-
* -
|
|
11
|
-
* any bytes are forwarded and
|
|
10
|
+
* - While `requirePairingForLan` is on (default), every request must carry a
|
|
11
|
+
* live paired-device cookie, enforced before any bytes are forwarded and
|
|
12
|
+
* before any host call. With the policy off, the cookie gate is skipped
|
|
13
|
+
* (the loopback-only denials below still apply).
|
|
12
14
|
* - The SDK's loopback-only privileged methods (native dialogs, the settings
|
|
13
15
|
* plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
|
|
14
16
|
* are denied here. The set is pinned by tests/remote-contract.spec.ts.
|
|
@@ -95,19 +97,24 @@ export function loopbackOnlyDenial(innerPath) {
|
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
99
|
* Build the remote desktop channel HTTP routes.
|
|
98
|
-
* @param deps - pairing service + local port.
|
|
100
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
99
101
|
* @returns the routes to register on webServer.
|
|
100
102
|
*/
|
|
101
103
|
export function makeRemoteApiRoutes(deps) {
|
|
102
|
-
const { service, port } = deps;
|
|
104
|
+
const { service, port, requirePairingForLan = true } = deps;
|
|
103
105
|
const handler = (req, res) => {
|
|
104
106
|
// Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
// With the live policy off, untrusted-but-policy-open callers are proxied
|
|
108
|
+
// (a stale client rewrite must not 403); loopback-only denials stay below.
|
|
109
|
+
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
|
|
110
|
+
if (require) {
|
|
111
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
112
|
+
const paired = deviceId !== undefined && service.touchDevice(deviceId);
|
|
113
|
+
if (!paired) {
|
|
114
|
+
req.resume();
|
|
115
|
+
envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
111
118
|
}
|
|
112
119
|
const method = req.method ?? 'GET';
|
|
113
120
|
if (!ALLOWED_METHODS.has(method)) {
|
|
@@ -153,17 +160,20 @@ export function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
|
153
160
|
/**
|
|
154
161
|
* Build the WebSocket upgrade routes for the event streams and known plugin
|
|
155
162
|
* sockets. webServer matches upgrades by exact path.
|
|
156
|
-
* @param deps - pairing service + local port.
|
|
163
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
157
164
|
* @returns the upgrade routes to register on webServer.
|
|
158
165
|
*/
|
|
159
166
|
export function makeRemoteApiUpgradeRoutes(deps) {
|
|
160
|
-
const { service, port } = deps;
|
|
167
|
+
const { service, port, requirePairingForLan = true } = deps;
|
|
161
168
|
const handlerFor = (fallbackPath) => (req, socket, head) => {
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
|
|
170
|
+
if (require) {
|
|
171
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
172
|
+
if (deviceId === undefined || !service.touchDevice(deviceId)) {
|
|
173
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
174
|
+
socket.destroy();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
167
177
|
}
|
|
168
178
|
const inner = upgradeInnerPath(req.url, fallbackPath);
|
|
169
179
|
const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair mobile and PC remote control for the dsh web GUI: QR entry beside the settings button, one-time pairing tokens, a gated /remote/api desktop channel, live device status, and revocable sessions",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/index.ts
CHANGED
|
@@ -358,12 +358,23 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
358
358
|
...(apiProxy !== undefined
|
|
359
359
|
? makeMobileApiRoutes({ service, apiProxy, mobileEnterToSend: () => resolve().mobileEnterToSend })
|
|
360
360
|
: []),
|
|
361
|
-
// The remote desktop channel:
|
|
362
|
-
// re-issues fenced paths to loopback (see remote-api.ts).
|
|
363
|
-
|
|
361
|
+
// The remote desktop channel: policy-gated `/remote` prefix that
|
|
362
|
+
// re-issues fenced paths to loopback (see remote-api.ts). The live
|
|
363
|
+
// requirePairingForLan is re-read per request, same as the gate listener
|
|
364
|
+
// and routes above, so a stale client rewrite on an open-LAN deployment
|
|
365
|
+
// proxies instead of 403ing.
|
|
366
|
+
...makeRemoteApiRoutes({
|
|
367
|
+
service,
|
|
368
|
+
port: ctx.webServer.port,
|
|
369
|
+
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
370
|
+
}),
|
|
364
371
|
...updateRoutes,
|
|
365
372
|
]
|
|
366
|
-
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
373
|
+
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
374
|
+
service,
|
|
375
|
+
port: ctx.webServer.port,
|
|
376
|
+
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
377
|
+
})
|
|
367
378
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled)
|
|
368
379
|
ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate')
|
|
369
380
|
|
package/src/remote-api.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* and no per-plugin pairing consult.
|
|
8
8
|
*
|
|
9
9
|
* Security model:
|
|
10
|
-
* -
|
|
11
|
-
* any bytes are forwarded and
|
|
10
|
+
* - While `requirePairingForLan` is on (default), every request must carry a
|
|
11
|
+
* live paired-device cookie, enforced before any bytes are forwarded and
|
|
12
|
+
* before any host call. With the policy off, the cookie gate is skipped
|
|
13
|
+
* (the loopback-only denials below still apply).
|
|
12
14
|
* - The SDK's loopback-only privileged methods (native dialogs, the settings
|
|
13
15
|
* plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
|
|
14
16
|
* are denied here. The set is pinned by tests/remote-contract.spec.ts.
|
|
@@ -68,6 +70,13 @@ export interface RemoteApiDeps {
|
|
|
68
70
|
service: PairingService
|
|
69
71
|
/** The local webServer port the loopback proxy connects to. */
|
|
70
72
|
port: number
|
|
73
|
+
/**
|
|
74
|
+
* Live policy: whether the paired-device cookie gates the /remote channel.
|
|
75
|
+
* When false, requests are proxied without a cookie (the loopback-only
|
|
76
|
+
* denials still apply). A function is re-read per request, so a settings
|
|
77
|
+
* edit takes effect without a restart. Defaults to true.
|
|
78
|
+
*/
|
|
79
|
+
requirePairingForLan?: boolean | (() => boolean)
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
/** One SDK-shaped error envelope (keeps the desktop client's parse path intact). */
|
|
@@ -125,20 +134,25 @@ export function loopbackOnlyDenial(innerPath: string): string | undefined {
|
|
|
125
134
|
|
|
126
135
|
/**
|
|
127
136
|
* Build the remote desktop channel HTTP routes.
|
|
128
|
-
* @param deps - pairing service + local port.
|
|
137
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
129
138
|
* @returns the routes to register on webServer.
|
|
130
139
|
*/
|
|
131
140
|
export function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[] {
|
|
132
|
-
const { service, port } = deps
|
|
141
|
+
const { service, port, requirePairingForLan = true } = deps
|
|
133
142
|
|
|
134
143
|
const handler = (req: IncomingMessage, res: ServerResponse): void => {
|
|
135
144
|
// Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
// With the live policy off, untrusted-but-policy-open callers are proxied
|
|
146
|
+
// (a stale client rewrite must not 403); loopback-only denials stay below.
|
|
147
|
+
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan
|
|
148
|
+
if (require) {
|
|
149
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
|
|
150
|
+
const paired = deviceId !== undefined && service.touchDevice(deviceId)
|
|
151
|
+
if (!paired) {
|
|
152
|
+
req.resume()
|
|
153
|
+
envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
const method = req.method ?? 'GET'
|
|
@@ -187,18 +201,21 @@ export function upgradeInnerPath(reqUrl: string | undefined, fallbackPath: strin
|
|
|
187
201
|
/**
|
|
188
202
|
* Build the WebSocket upgrade routes for the event streams and known plugin
|
|
189
203
|
* sockets. webServer matches upgrades by exact path.
|
|
190
|
-
* @param deps - pairing service + local port.
|
|
204
|
+
* @param deps - pairing service + local port + live pairing policy.
|
|
191
205
|
* @returns the upgrade routes to register on webServer.
|
|
192
206
|
*/
|
|
193
207
|
export function makeRemoteApiUpgradeRoutes(deps: RemoteApiDeps): WebUpgradeRoute[] {
|
|
194
|
-
const { service, port } = deps
|
|
208
|
+
const { service, port, requirePairingForLan = true } = deps
|
|
195
209
|
|
|
196
210
|
const handlerFor = (fallbackPath: string) => (req: IncomingMessage, socket: Duplex, head: Buffer): void => {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan
|
|
212
|
+
if (require) {
|
|
213
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
|
|
214
|
+
if (deviceId === undefined || !service.touchDevice(deviceId)) {
|
|
215
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n')
|
|
216
|
+
socket.destroy()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
202
219
|
}
|
|
203
220
|
const inner = upgradeInnerPath(req.url, fallbackPath)
|
|
204
221
|
const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner)
|