@linxin666/dsh-remote-web-ui 0.2.4 → 0.2.6
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/README.i18n.yaml +2 -2
- package/README.md +148 -77
- package/README.zh.md +32 -26
- package/lib/client.js +603 -68
- package/lib/client.js.map +1 -1
- package/lib/index.js +667 -32
- package/lib/mobile.js +3 -1
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FenceNotice.d.ts +21 -0
- package/lib/types/client/FenceNotice.d.ts.map +1 -0
- package/lib/types/client/FenceNotice.js +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +18 -11
- package/lib/types/client/RemotePanel.d.ts +10 -4
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +7 -6
- package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -1
- package/lib/types/client/device-name.d.ts +3 -0
- package/lib/types/client/device-name.d.ts.map +1 -0
- package/lib/types/client/device-name.js +30 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +48 -0
- package/lib/types/client/locales.d.ts +51 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +79 -29
- package/lib/types/client/pair-api.d.ts +35 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +39 -0
- package/lib/types/client/remote-channel.d.ts +93 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -0
- package/lib/types/client/remote-channel.js +246 -0
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -2
- package/lib/types/index.d.ts +19 -12
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +73 -5
- package/lib/types/loopback-proxy.d.ts +28 -0
- package/lib/types/loopback-proxy.d.ts.map +1 -0
- package/lib/types/loopback-proxy.js +112 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +6 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +6 -2
- package/lib/types/mobile-routes.js +1 -1
- package/lib/types/pairing.d.ts +63 -11
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +137 -20
- package/lib/types/pairing.persist.test.js +60 -5
- package/lib/types/posture.d.ts +75 -0
- package/lib/types/posture.d.ts.map +1 -0
- package/lib/types/posture.js +116 -0
- package/lib/types/remote-api.d.ts +60 -0
- package/lib/types/remote-api.d.ts.map +1 -0
- package/lib/types/remote-api.js +181 -0
- package/lib/types/remote-methods.d.ts +33 -0
- package/lib/types/remote-methods.d.ts.map +1 -0
- package/lib/types/remote-methods.js +54 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +33 -4
- package/package.json +2 -2
- package/src/client/FenceNotice.tsx +43 -0
- package/src/client/RemoteEntry.tsx +19 -9
- package/src/client/RemotePanel.tsx +83 -10
- package/src/client/RemoteSettingsCard.tsx +16 -0
- package/src/client/device-name.ts +31 -0
- package/src/client/index.ts +46 -0
- package/src/client/locales.ts +79 -29
- package/src/client/pair-api.ts +60 -0
- package/src/client/remote-channel.ts +269 -0
- package/src/client/remote.module.css +240 -16
- package/src/client/settings-card.module.css +2 -0
- package/src/dsh-home.ts +36 -0
- package/src/gate.ts +5 -2
- package/src/index.ts +89 -17
- package/src/loopback-proxy.ts +126 -0
- package/src/mobile/views/SessionListView.tsx +6 -1
- package/src/mobile-api.ts +6 -2
- package/src/mobile-routes.ts +1 -1
- package/src/pairing.persist.test.ts +64 -5
- package/src/pairing.ts +167 -20
- package/src/posture.ts +154 -0
- package/src/remote-api.ts +217 -0
- package/src/remote-methods.ts +62 -0
- package/src/routes.ts +33 -4
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote desktop channel — browser half. On a non-loopback origin (LAN
|
|
3
|
+
* address or public tunnel) fenced host routes refuse the request, and
|
|
4
|
+
* pairing is the real access control — so same-origin traffic the desktop
|
|
5
|
+
* issues is rewritten onto this plugin's gated `/remote` prefix (host half
|
|
6
|
+
* in src/remote-api.ts). The host then re-issues the call to 127.0.0.1 so
|
|
7
|
+
* plugin loopback fences pass.
|
|
8
|
+
*
|
|
9
|
+
* The rewrite is deliberately narrow:
|
|
10
|
+
* - loopback origins are untouched (the desktop at 127.0.0.1 keeps original paths);
|
|
11
|
+
* - the pairing routes (`/api/pair/*`) stay where they are — accept must
|
|
12
|
+
* work BEFORE a device is paired;
|
|
13
|
+
* - the update endpoints (`/api/update/*`) stay loopback-only;
|
|
14
|
+
* - desktop-launcher create/shutdown (`/api/dsh-desktop-launcher/*`) stay
|
|
15
|
+
* loopback-only (host-local files and process exit);
|
|
16
|
+
* - the family settings bridge (`/api/dsh-web-ui-settings/*`) stays
|
|
17
|
+
* loopback-only (same plane as SDK settings.*);
|
|
18
|
+
* - `/api/*` (SDK methods and `/api/<plugin>/...` plugin namespaces),
|
|
19
|
+
* `/sidebar/*`, `/git/*`, and `/pet/*` ride the channel;
|
|
20
|
+
* - fetch, EventSource, WebSocket, and img/script/iframe `src` are patched;
|
|
21
|
+
* everything else calls the original unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Pure helpers are exported for unit tests; `installRemoteChannel` patches
|
|
24
|
+
* the given window and returns their restore.
|
|
25
|
+
*/
|
|
26
|
+
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
27
|
+
export const REMOTE_PREFIX = '/remote';
|
|
28
|
+
/** Connection-plugin method prefix under the gated channel. */
|
|
29
|
+
export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
|
|
30
|
+
const API_PREFIX = '/api/';
|
|
31
|
+
const PAIR_PREFIX = '/api/pair/';
|
|
32
|
+
const UPDATE_PREFIX = '/api/update/';
|
|
33
|
+
const DESKTOP_LAUNCHER_PREFIX = '/api/dsh-desktop-launcher';
|
|
34
|
+
const SETTINGS_BRIDGE_PREFIX = '/api/dsh-web-ui-settings';
|
|
35
|
+
const SIDEBAR_PREFIX = '/sidebar/';
|
|
36
|
+
const GIT_PREFIX = '/git/';
|
|
37
|
+
const PET_PREFIX = '/pet/';
|
|
38
|
+
const WS_PATHS = new Set([
|
|
39
|
+
'/api/events.mux',
|
|
40
|
+
'/api/events.host',
|
|
41
|
+
'/sidebar/ws/terminal',
|
|
42
|
+
'/sidebar/ws/agent-terminals',
|
|
43
|
+
'/api/dsh-ssh/terminal',
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Browser-safe loopback classification for the page origin (the SDK client
|
|
47
|
+
* exports its own; this copy keeps the module dependency-free).
|
|
48
|
+
* @param hostname - a location hostname (IPv6 without brackets).
|
|
49
|
+
* @returns true for localhost, IPv6 loopback, or any 127/8 literal.
|
|
50
|
+
*/
|
|
51
|
+
export function isLoopbackHostname(hostname) {
|
|
52
|
+
if (hostname === 'localhost' || hostname === '::1')
|
|
53
|
+
return true;
|
|
54
|
+
const parts = hostname.split('.');
|
|
55
|
+
return parts.length === 4 && parts[0] === '127' && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether one same-origin path must ride the gated channel (fetch, EventSource,
|
|
59
|
+
* img/script/iframe src).
|
|
60
|
+
* @param pathname - the request URL pathname.
|
|
61
|
+
*/
|
|
62
|
+
export function shouldRewriteFetchPath(pathname) {
|
|
63
|
+
if (pathname.startsWith(PAIR_PREFIX))
|
|
64
|
+
return false;
|
|
65
|
+
if (pathname.startsWith(UPDATE_PREFIX))
|
|
66
|
+
return false;
|
|
67
|
+
if (pathname === DESKTOP_LAUNCHER_PREFIX || pathname.startsWith(`${DESKTOP_LAUNCHER_PREFIX}/`))
|
|
68
|
+
return false;
|
|
69
|
+
if (pathname === SETTINGS_BRIDGE_PREFIX || pathname.startsWith(`${SETTINGS_BRIDGE_PREFIX}/`))
|
|
70
|
+
return false;
|
|
71
|
+
if (pathname.startsWith(API_PREFIX))
|
|
72
|
+
return true;
|
|
73
|
+
if (pathname.startsWith(SIDEBAR_PREFIX) || pathname === '/sidebar')
|
|
74
|
+
return true;
|
|
75
|
+
if (pathname.startsWith(GIT_PREFIX) || pathname === '/git')
|
|
76
|
+
return true;
|
|
77
|
+
if (pathname.startsWith(PET_PREFIX) || pathname === '/pet')
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether one WebSocket path must ride the gated channel.
|
|
83
|
+
* @param pathname - the WebSocket URL pathname.
|
|
84
|
+
*/
|
|
85
|
+
export function shouldRewriteWsPath(pathname) {
|
|
86
|
+
return WS_PATHS.has(pathname);
|
|
87
|
+
}
|
|
88
|
+
/** The gated twin of one fenced path (`/remote` + original pathname). */
|
|
89
|
+
export function rewritePath(pathname) {
|
|
90
|
+
return `${REMOTE_PREFIX}${pathname}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Rewrite one raw URL string when it is same-origin and fenced. Relative
|
|
94
|
+
* inputs stay relative so resource loaders do not unexpectedly absolutize.
|
|
95
|
+
*/
|
|
96
|
+
export function rewriteRawUrl(raw, baseHref, origin) {
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(raw, baseHref);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
if (url.origin !== origin)
|
|
105
|
+
return raw;
|
|
106
|
+
if (!shouldRewriteFetchPath(url.pathname))
|
|
107
|
+
return raw;
|
|
108
|
+
url.pathname = rewritePath(url.pathname);
|
|
109
|
+
if (raw.startsWith('/') && !raw.startsWith('//')) {
|
|
110
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
111
|
+
}
|
|
112
|
+
return url.href;
|
|
113
|
+
}
|
|
114
|
+
/** Read an unpaired code from either the SDK envelope or a plugin JSON body. */
|
|
115
|
+
function unpairedCodeOf(value) {
|
|
116
|
+
if (typeof value !== 'object' || value === null)
|
|
117
|
+
return undefined;
|
|
118
|
+
const record = value;
|
|
119
|
+
const nested = record.result;
|
|
120
|
+
if (typeof nested === 'object' && nested !== null) {
|
|
121
|
+
const error = nested.error;
|
|
122
|
+
if (typeof error === 'object' && error !== null && typeof error.code === 'string') {
|
|
123
|
+
return error.code;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const error = record.error;
|
|
127
|
+
if (typeof error === 'object' && error !== null && typeof error.code === 'string') {
|
|
128
|
+
return error.code;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether a gated 403 is the unpaired-device fence (not a loopback-only
|
|
134
|
+
* method denial, which uses the same status with code `forbidden`).
|
|
135
|
+
*/
|
|
136
|
+
export async function isUnpairedDenied(response) {
|
|
137
|
+
if (response.status !== 403)
|
|
138
|
+
return false;
|
|
139
|
+
try {
|
|
140
|
+
return unpairedCodeOf(await response.json()) === 'unpaired';
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Wrap a prototype `src` setter so fenced same-origin URLs ride `/remote`.
|
|
148
|
+
* No-ops when the constructor is missing or `src` is not configurable.
|
|
149
|
+
*/
|
|
150
|
+
function patchSrcAccessor(ctor, rewrite) {
|
|
151
|
+
if (ctor === undefined)
|
|
152
|
+
return () => { };
|
|
153
|
+
const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, 'src');
|
|
154
|
+
if (descriptor === undefined || descriptor.configurable === false)
|
|
155
|
+
return () => { };
|
|
156
|
+
if (descriptor.set === undefined)
|
|
157
|
+
return () => { };
|
|
158
|
+
const originalSet = descriptor.set;
|
|
159
|
+
const originalGet = descriptor.get;
|
|
160
|
+
Object.defineProperty(ctor.prototype, 'src', {
|
|
161
|
+
configurable: true,
|
|
162
|
+
enumerable: descriptor.enumerable ?? true,
|
|
163
|
+
get: originalGet,
|
|
164
|
+
set(value) {
|
|
165
|
+
originalSet.call(this, rewrite(String(value)));
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
return () => {
|
|
169
|
+
Object.defineProperty(ctor.prototype, 'src', descriptor);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Patch `fetch`, `EventSource`, `WebSocket`, and resource `src` accessors on
|
|
174
|
+
* one window to route fenced traffic through the gated channel.
|
|
175
|
+
* @param window - the browser window (or a test double).
|
|
176
|
+
* @param options - the unpaired callback.
|
|
177
|
+
* @returns a function restoring the originals.
|
|
178
|
+
*/
|
|
179
|
+
export function installRemoteChannel(window, options = {}) {
|
|
180
|
+
const originalFetch = window.fetch;
|
|
181
|
+
const OriginalWebSocket = window.WebSocket;
|
|
182
|
+
const OriginalEventSource = window.EventSource;
|
|
183
|
+
const sameOrigin = (url) => url.origin === window.location.origin;
|
|
184
|
+
const rewrite = (raw) => rewriteRawUrl(raw, window.location.href, window.location.origin);
|
|
185
|
+
const patchedFetch = (input, init) => {
|
|
186
|
+
const url = new URL(typeof input === 'string' || input instanceof URL ? input.toString() : input.url, window.location.href);
|
|
187
|
+
if (sameOrigin(url) && shouldRewriteFetchPath(url.pathname)) {
|
|
188
|
+
const rewritten = new URL(url);
|
|
189
|
+
rewritten.pathname = rewritePath(url.pathname);
|
|
190
|
+
const next = typeof input === 'string' || input instanceof URL
|
|
191
|
+
? rewritten.toString()
|
|
192
|
+
: new Request(rewritten, input);
|
|
193
|
+
return Promise.resolve(originalFetch.call(window, next, init)).then(async (response) => {
|
|
194
|
+
if (await isUnpairedDenied(response.clone()))
|
|
195
|
+
options.onUnpaired?.();
|
|
196
|
+
else
|
|
197
|
+
options.onPaired?.();
|
|
198
|
+
return response;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return originalFetch.call(window, input, init);
|
|
202
|
+
};
|
|
203
|
+
class PatchedWebSocket extends OriginalWebSocket {
|
|
204
|
+
constructor(url, protocols) {
|
|
205
|
+
const parsed = new URL(url.toString(), window.location.href);
|
|
206
|
+
const wsOrigin = parsed.protocol === 'wss:' ? `https://${parsed.host}` : parsed.protocol === 'ws:' ? `http://${parsed.host}` : '';
|
|
207
|
+
if (wsOrigin !== '' && wsOrigin === window.location.origin && shouldRewriteWsPath(parsed.pathname)) {
|
|
208
|
+
const rewritten = new URL(parsed);
|
|
209
|
+
rewritten.pathname = rewritePath(parsed.pathname);
|
|
210
|
+
super(rewritten, protocols);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
super(url, protocols);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const restoreSrc = [
|
|
217
|
+
patchSrcAccessor(window.HTMLImageElement, rewrite),
|
|
218
|
+
patchSrcAccessor(window.HTMLScriptElement, rewrite),
|
|
219
|
+
patchSrcAccessor(window.HTMLIFrameElement, rewrite),
|
|
220
|
+
];
|
|
221
|
+
window.fetch = patchedFetch;
|
|
222
|
+
window.WebSocket = PatchedWebSocket;
|
|
223
|
+
if (OriginalEventSource !== undefined) {
|
|
224
|
+
class PatchedEventSource extends OriginalEventSource {
|
|
225
|
+
constructor(url, eventSourceInitDict) {
|
|
226
|
+
const parsed = new URL(url.toString(), window.location.href);
|
|
227
|
+
if (sameOrigin(parsed) && shouldRewriteFetchPath(parsed.pathname)) {
|
|
228
|
+
const rewritten = new URL(parsed);
|
|
229
|
+
rewritten.pathname = rewritePath(parsed.pathname);
|
|
230
|
+
super(rewritten, eventSourceInitDict);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
super(url, eventSourceInitDict);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
window.EventSource = PatchedEventSource;
|
|
237
|
+
}
|
|
238
|
+
return () => {
|
|
239
|
+
window.fetch = originalFetch;
|
|
240
|
+
window.WebSocket = OriginalWebSocket;
|
|
241
|
+
if (OriginalEventSource !== undefined)
|
|
242
|
+
window.EventSource = OriginalEventSource;
|
|
243
|
+
for (const restore of restoreSrc)
|
|
244
|
+
restore();
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
3
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
4
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
5
|
+
*/
|
|
6
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
7
|
+
export declare function expandHome(path: string, home?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the DSH home directory.
|
|
10
|
+
* @param env - process environment to read DSH_HOME from.
|
|
11
|
+
* @param home - platform home directory fallback (test seam).
|
|
12
|
+
* @returns the absolute DSH home path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveDshHome(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
15
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
16
|
+
export declare function dshHome(): string;
|
|
17
|
+
//# sourceMappingURL=dsh-home.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsh-home.d.ts","sourceRoot":"","sources":["../../src/dsh-home.ts"],"names":[],"mappings":"AACA;;;;GAIG;AAKH,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAIzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAOrG;AAED,gEAAgE;AAChE,wBAAgB,OAAO,IAAI,MAAM,CAEhC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/dsh-home.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
4
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
5
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { isAbsolute, join } from 'node:path';
|
|
9
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
10
|
+
export function expandHome(path, home = homedir()) {
|
|
11
|
+
if (path === '~')
|
|
12
|
+
return home;
|
|
13
|
+
if (path.startsWith('~/') || path.startsWith('~\\'))
|
|
14
|
+
return join(home, path.slice(2));
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the DSH home directory.
|
|
19
|
+
* @param env - process environment to read DSH_HOME from.
|
|
20
|
+
* @param home - platform home directory fallback (test seam).
|
|
21
|
+
* @returns the absolute DSH home path.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveDshHome(env = process.env, home = homedir()) {
|
|
24
|
+
const raw = env.DSH_HOME;
|
|
25
|
+
if (raw !== undefined && raw.trim() !== '') {
|
|
26
|
+
const expanded = expandHome(raw.trim(), home);
|
|
27
|
+
return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded);
|
|
28
|
+
}
|
|
29
|
+
return join(home, '.dsh');
|
|
30
|
+
}
|
|
31
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
32
|
+
export function dshHome() {
|
|
33
|
+
return resolveDshHome();
|
|
34
|
+
}
|
package/lib/types/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAc9H;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAIhG"}
|
package/lib/types/gate.js
CHANGED
|
@@ -78,16 +78,19 @@ export function isLoopbackClient(request) {
|
|
|
78
78
|
*/
|
|
79
79
|
export function makeGateListener(service, requirePairingForLan = true, enabled = true) {
|
|
80
80
|
return (request, _method, next) => {
|
|
81
|
+
// Loopback clients (desktop at 127.0.0.1) always pass without cookie check.
|
|
81
82
|
if (isLoopbackClient(request))
|
|
82
83
|
return next();
|
|
83
84
|
const active = typeof enabled === 'function' ? enabled() : enabled;
|
|
84
85
|
if (!active)
|
|
85
86
|
return false;
|
|
86
87
|
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
|
|
88
|
+
// When pairing is not required for LAN, non-loopback requests pass too.
|
|
87
89
|
if (!require)
|
|
88
90
|
return next();
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
+
// Non-loopback origin: verify paired-device cookie. Sibling plugins that
|
|
92
|
+
// own their own prefixes (/pet, /git, /sidebar, …) consult the same cookie
|
|
93
|
+
// through the remoteWebUiPairing service — this listener only covers /api.
|
|
91
94
|
return isPairedDeviceRequest(service, request) ? next() : false;
|
|
92
95
|
};
|
|
93
96
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -40,13 +40,19 @@ export interface Config {
|
|
|
40
40
|
offlineAfterMs?: number;
|
|
41
41
|
/** Hard cap on paired device sessions (oldest evicted when full). */
|
|
42
42
|
maxDevices?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Idle sessions older than this (ms) are deleted from memory and disk.
|
|
45
|
+
* Default is 7 days; a leftover cookie no longer authorizes after expiry.
|
|
46
|
+
*/
|
|
47
|
+
idleExpireMs?: number;
|
|
43
48
|
/** Cookie name carrying the paired device id. */
|
|
44
49
|
cookieName?: string;
|
|
45
50
|
/**
|
|
46
|
-
* When true (default),
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
51
|
+
* When true (default), a desktop Web GUI opened at a non-loopback origin
|
|
52
|
+
* rides the gated `/remote/api` channel and must carry a live paired-device
|
|
53
|
+
* cookie — the QR is the only way into remote desktop, and stop() cuts
|
|
54
|
+
* paired devices off. Set false to keep the desktop on plain `/api`
|
|
55
|
+
* (only useful when that origin is already trusted for `/api`).
|
|
50
56
|
*/
|
|
51
57
|
requirePairingForLan?: boolean;
|
|
52
58
|
/**
|
|
@@ -60,17 +66,16 @@ export interface Config {
|
|
|
60
66
|
publicBaseUrl?: string;
|
|
61
67
|
/**
|
|
62
68
|
* Absolute path to a JSON file where paired device sessions are persisted.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* (previous behavior).
|
|
69
|
+
* Defaults to `$DSH_HOME/remote-web-ui-devices.json` so a paired device
|
|
70
|
+
* keeps its session across `dsh web` restarts (the cookie already lives
|
|
71
|
+
* 365 days). Override to another absolute path when needed.
|
|
67
72
|
*/
|
|
68
73
|
devicesFile?: string;
|
|
69
74
|
/**
|
|
70
75
|
* When true, the plugin runs its own Cloudflare quick tunnel (the
|
|
71
76
|
* cloudflared binary ships with the package — no user-side install) and
|
|
72
|
-
* feeds the minted public URL into
|
|
73
|
-
* fence dynamically, so phones anywhere can pair without any manual
|
|
77
|
+
* feeds the minted public URL into the QR base and the phone-facing
|
|
78
|
+
* pairing fence dynamically, so phones anywhere can pair without any manual
|
|
74
79
|
* tunnel setup. The manual `publicBaseUrl` is ignored while this is on.
|
|
75
80
|
*/
|
|
76
81
|
autoTunnel?: boolean;
|
|
@@ -92,7 +97,7 @@ export declare const Config: z<Config>;
|
|
|
92
97
|
*/
|
|
93
98
|
type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'devicesFile'>> & {
|
|
94
99
|
publicBaseUrl: string | undefined;
|
|
95
|
-
devicesFile: string
|
|
100
|
+
devicesFile: string;
|
|
96
101
|
};
|
|
97
102
|
/**
|
|
98
103
|
* The single mapping from resolved plugin config to the pairing service
|
|
@@ -100,7 +105,9 @@ type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'devicesFile'>> &
|
|
|
100
105
|
* it, so no field can be silently dropped when the web settings surface
|
|
101
106
|
* pushes a new value into the running service.
|
|
102
107
|
*/
|
|
103
|
-
export declare function pairingConfigOf(resolved: Pick<ResolvedConfig, 'tokenTtlMs' | 'offlineAfterMs' | 'maxDevices' | 'cookieName' | 'devicesFile'>): PairingConfig;
|
|
108
|
+
export declare function pairingConfigOf(resolved: Pick<ResolvedConfig, 'tokenTtlMs' | 'offlineAfterMs' | 'maxDevices' | 'idleExpireMs' | 'cookieName' | 'devicesFile'>): PairingConfig;
|
|
109
|
+
/** Default paired-session store: `$DSH_HOME/remote-web-ui-devices.json`. */
|
|
110
|
+
export declare function defaultDevicesFile(home?: string): string;
|
|
104
111
|
/**
|
|
105
112
|
* Mount the pairing service, routes, gate listener, and presence sweep.
|
|
106
113
|
* @param ctx - host plugin context carrying webServer.
|
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;
|
|
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,CAuRtD"}
|
package/lib/types/index.js
CHANGED
|
@@ -8,15 +8,19 @@
|
|
|
8
8
|
* phone-side pair/accept + deep-link flow.
|
|
9
9
|
*/
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
-
import {
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { setInterval as nodeSetInterval, setTimeout as nodeSetTimeout } from 'node:timers';
|
|
12
13
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
13
14
|
import z from 'schemastery';
|
|
14
|
-
import { PairingService } from './pairing.js';
|
|
15
|
+
import { DEFAULT_IDLE_EXPIRE_MS, PairingService } from './pairing.js';
|
|
16
|
+
import { dshHome } from './dsh-home.js';
|
|
15
17
|
import { isPairedDeviceRequest, makeGateListener } from './gate.js';
|
|
16
18
|
import { RemoteWebUiPairing } from './pairing-access.js';
|
|
17
19
|
import { isTrustedApiRequest, makeRoutes } from './routes.js';
|
|
18
20
|
import { makeMobileRoutes } from './mobile-routes.js';
|
|
19
21
|
import { makeMobileApiRoutes } from './mobile-api.js';
|
|
22
|
+
import { makeRemoteApiRoutes, makeRemoteApiUpgradeRoutes } from './remote-api.js';
|
|
23
|
+
import { claimPostureKey, postureTargets, probePosture, releasePostureKey } from './posture.js';
|
|
20
24
|
import { lanIPv4Addresses } from './lan.js';
|
|
21
25
|
import { TunnelManager } from './tunnel.js';
|
|
22
26
|
import { checkUpdates, fetchLatestVersion, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from './update.js';
|
|
@@ -36,6 +40,7 @@ export const Config = z.object({
|
|
|
36
40
|
tokenTtlMs: z.number().step(1).min(60_000).default(10 * 60_000),
|
|
37
41
|
offlineAfterMs: z.number().step(1).min(5_000).default(25_000),
|
|
38
42
|
maxDevices: z.number().step(1).min(1).max(64).default(4),
|
|
43
|
+
idleExpireMs: z.number().step(1).min(60_000).default(DEFAULT_IDLE_EXPIRE_MS),
|
|
39
44
|
cookieName: z.string().min(1).default('dsh_pair'),
|
|
40
45
|
requirePairingForLan: z.boolean().default(true),
|
|
41
46
|
publicBaseUrl: z.string(),
|
|
@@ -57,19 +62,25 @@ export function pairingConfigOf(resolved) {
|
|
|
57
62
|
tokenTtlMs: resolved.tokenTtlMs,
|
|
58
63
|
offlineAfterMs: resolved.offlineAfterMs,
|
|
59
64
|
maxDevices: resolved.maxDevices,
|
|
65
|
+
idleExpireMs: resolved.idleExpireMs,
|
|
60
66
|
cookieName: resolved.cookieName,
|
|
61
67
|
devicesFile: resolved.devicesFile,
|
|
62
68
|
};
|
|
63
69
|
}
|
|
70
|
+
/** Default paired-session store: `$DSH_HOME/remote-web-ui-devices.json`. */
|
|
71
|
+
export function defaultDevicesFile(home = dshHome()) {
|
|
72
|
+
return join(home, 'remote-web-ui-devices.json');
|
|
73
|
+
}
|
|
64
74
|
/** Schema defaults, re-read for hand-built test contexts (the loader applies them normally). */
|
|
65
75
|
const DEFAULTS = {
|
|
66
76
|
tokenTtlMs: 10 * 60_000,
|
|
67
77
|
offlineAfterMs: 25_000,
|
|
68
78
|
maxDevices: 4,
|
|
79
|
+
idleExpireMs: DEFAULT_IDLE_EXPIRE_MS,
|
|
69
80
|
cookieName: 'dsh_pair',
|
|
70
81
|
requirePairingForLan: true,
|
|
71
82
|
publicBaseUrl: undefined,
|
|
72
|
-
devicesFile:
|
|
83
|
+
devicesFile: defaultDevicesFile(),
|
|
73
84
|
autoTunnel: false,
|
|
74
85
|
mobileEnterToSend: true,
|
|
75
86
|
enabled: true,
|
|
@@ -85,10 +96,11 @@ function applyImpl(ctx, config) {
|
|
|
85
96
|
tokenTtlMs: config?.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
86
97
|
offlineAfterMs: config?.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
|
87
98
|
maxDevices: config?.maxDevices ?? DEFAULTS.maxDevices,
|
|
99
|
+
idleExpireMs: config?.idleExpireMs ?? DEFAULTS.idleExpireMs,
|
|
88
100
|
cookieName: config?.cookieName ?? DEFAULTS.cookieName,
|
|
89
101
|
requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
90
102
|
publicBaseUrl: config?.publicBaseUrl,
|
|
91
|
-
devicesFile: config?.devicesFile,
|
|
103
|
+
devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
|
|
92
104
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
93
105
|
mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
94
106
|
enabled: config?.enabled ?? DEFAULTS.enabled,
|
|
@@ -103,6 +115,7 @@ function applyImpl(ctx, config) {
|
|
|
103
115
|
tokenTtlMs: value.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
104
116
|
offlineAfterMs: value.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
|
105
117
|
maxDevices: value.maxDevices ?? DEFAULTS.maxDevices,
|
|
118
|
+
idleExpireMs: value.idleExpireMs ?? DEFAULTS.idleExpireMs,
|
|
106
119
|
cookieName: value.cookieName ?? DEFAULTS.cookieName,
|
|
107
120
|
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
108
121
|
publicBaseUrl: value.publicBaseUrl,
|
|
@@ -127,6 +140,7 @@ function applyImpl(ctx, config) {
|
|
|
127
140
|
if (info.phase === 'running' && info.url !== undefined) {
|
|
128
141
|
service.setPublicBaseUrl(info.url);
|
|
129
142
|
service.setTunnelStatus({ state: 'running', url: info.url });
|
|
143
|
+
runPostureProbe();
|
|
130
144
|
}
|
|
131
145
|
else if (info.phase === 'starting') {
|
|
132
146
|
// A restart mints a NEW hostname: the previous URL dies with the old
|
|
@@ -239,10 +253,58 @@ function applyImpl(ctx, config) {
|
|
|
239
253
|
...(apiProxy !== undefined
|
|
240
254
|
? makeMobileApiRoutes({ service, apiProxy, mobileEnterToSend: () => resolve().mobileEnterToSend })
|
|
241
255
|
: []),
|
|
256
|
+
// The remote desktop channel: paired-cookie-gated `/remote` prefix that
|
|
257
|
+
// re-issues fenced paths to loopback (see remote-api.ts).
|
|
258
|
+
...makeRemoteApiRoutes({ service, port: ctx.webServer.port }),
|
|
242
259
|
...updateRoutes,
|
|
243
260
|
];
|
|
261
|
+
const upgrades = makeRemoteApiUpgradeRoutes({ service, port: ctx.webServer.port });
|
|
244
262
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
245
263
|
ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate');
|
|
264
|
+
// ── posture probe ─────────────────────────────────────────────────────────
|
|
265
|
+
// Guardrail for the one seam this plugin cannot mount a gate into: the
|
|
266
|
+
// connection plugin's /api Host fence. Forged-Host probes against every
|
|
267
|
+
// advertised origin (public base + LAN bases) make a re-opened /api (a
|
|
268
|
+
// re-added --trusted-host, or the SDK's LAN auto-trust under 0.0.0.0)
|
|
269
|
+
// visible on the panel and the log instead of silently trusted.
|
|
270
|
+
let postureKey;
|
|
271
|
+
let postureWasExposed = false;
|
|
272
|
+
const runPostureProbe = () => {
|
|
273
|
+
if (!resolve().enabled)
|
|
274
|
+
return;
|
|
275
|
+
const targets = postureTargets(service.publicBaseUrl, service.lanAddresses, ctx.webServer.port);
|
|
276
|
+
if (targets.length === 0) {
|
|
277
|
+
postureKey = undefined;
|
|
278
|
+
service.setPosture(undefined);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const key = targets.join('|');
|
|
282
|
+
const claim = claimPostureKey(postureKey, key);
|
|
283
|
+
if (!claim.run)
|
|
284
|
+
return;
|
|
285
|
+
postureKey = claim.next;
|
|
286
|
+
void probePosture({ port: ctx.webServer.port, targets }).then((snapshot) => {
|
|
287
|
+
service.setPosture(snapshot);
|
|
288
|
+
const exposedHosts = snapshot.hosts.filter(host => host.exposed).map(host => host.host);
|
|
289
|
+
const exposed = exposedHosts.length > 0;
|
|
290
|
+
if (exposed && !postureWasExposed) {
|
|
291
|
+
console.error(`remote-web-ui: CRITICAL — the /api fence is OPEN for [${exposedHosts.join(', ')}]: unpaired clients reach the full host API. Remove --trusted-host for these hosts (pairing covers them) or bind loopback.`);
|
|
292
|
+
}
|
|
293
|
+
else if (!exposed && postureWasExposed) {
|
|
294
|
+
console.log('remote-web-ui: the /api posture probe is clean again (every advertised origin refused with 403).');
|
|
295
|
+
}
|
|
296
|
+
postureWasExposed = exposed;
|
|
297
|
+
}).catch(() => {
|
|
298
|
+
// Keep the previous snapshot; drop the in-flight key so the same
|
|
299
|
+
// targets retry instead of sticking on a failed round.
|
|
300
|
+
postureKey = releasePostureKey(postureKey, key);
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
// The first round waits for the connection plugin's /api route: a probe
|
|
304
|
+
// before it mounts would read the SPA fallback and false-positive.
|
|
305
|
+
const initialPostureTimer = nodeSetTimeout(() => { runPostureProbe(); }, 5_000);
|
|
306
|
+
initialPostureTimer.unref();
|
|
307
|
+
ctx.effect(() => () => { clearTimeout(initialPostureTimer); }, 'remote-web-ui: posture probe boot');
|
|
246
308
|
// Sibling plugins (aionui-panel, …) look this up by name. Absent when this
|
|
247
309
|
// plugin is not installed; stop() / enabled=false still refuse cookies.
|
|
248
310
|
new RemoteWebUiPairing(ctx, (request) => {
|
|
@@ -280,7 +342,10 @@ function applyImpl(ctx, config) {
|
|
|
280
342
|
service.stop();
|
|
281
343
|
if (disposeRoutes === undefined && enabled) {
|
|
282
344
|
disposeRoutes = ctx.effect(() => {
|
|
283
|
-
const disposers =
|
|
345
|
+
const disposers = [
|
|
346
|
+
...routes.map(route => ctx.webServer.register(route)),
|
|
347
|
+
...upgrades.map(route => ctx.webServer.registerUpgrade(route)),
|
|
348
|
+
];
|
|
284
349
|
return () => { for (const dispose of disposers)
|
|
285
350
|
dispose(); };
|
|
286
351
|
}, 'remote-web-ui: pairing routes');
|
|
@@ -300,6 +365,9 @@ function applyImpl(ctx, config) {
|
|
|
300
365
|
disposeSweep();
|
|
301
366
|
disposeSweep = undefined;
|
|
302
367
|
}
|
|
368
|
+
// Settings changed the reachable posture (manual publicBaseUrl, bind):
|
|
369
|
+
// re-probe unless the target set is unchanged.
|
|
370
|
+
runPostureProbe();
|
|
303
371
|
};
|
|
304
372
|
installSettingsSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
305
373
|
setSource: (source) => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback-shaped reverse proxy used by the remote desktop channel: after
|
|
3
|
+
* the pairing cookie gate, traffic is re-issued to 127.0.0.1 so sibling
|
|
4
|
+
* plugin fences (socket + Host loopback) accept it. Origin, cookies, and
|
|
5
|
+
* caller-controlled Sec-Fetch markers are dropped. HTTP requests receive a
|
|
6
|
+
* synthetic same-origin marker after the pairing gate so sibling loopback
|
|
7
|
+
* routes that require a browser tripwire accept the authenticated proxy.
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
|
+
import type { Duplex } from 'node:stream';
|
|
11
|
+
/**
|
|
12
|
+
* Pipe one HTTP request to loopback and stream the response back.
|
|
13
|
+
* @param req - the already-gated outer request.
|
|
14
|
+
* @param res - the outer response.
|
|
15
|
+
* @param port - local webServer port.
|
|
16
|
+
* @param upstreamPath - path + query on 127.0.0.1 (must start with `/`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function proxyLoopbackHttp(req: IncomingMessage, res: ServerResponse, port: number, upstreamPath: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Rebuild a WebSocket handshake as loopback-shaped and pipe both directions.
|
|
21
|
+
* @param req - the already-gated upgrade request.
|
|
22
|
+
* @param socket - the client duplex.
|
|
23
|
+
* @param head - bytes already read past the handshake.
|
|
24
|
+
* @param port - local webServer port.
|
|
25
|
+
* @param upstreamPath - path + query on 127.0.0.1.
|
|
26
|
+
*/
|
|
27
|
+
export declare function proxyLoopbackUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, port: number, upstreamPath: string): void;
|
|
28
|
+
//# sourceMappingURL=loopback-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback-proxy.d.ts","sourceRoot":"","sources":["../../src/loopback-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAuB,cAAc,EAAE,MAAM,WAAW,CAAA;AAGrF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAqBzC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,IAAI,CAmCN;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,IAAI,CA6BN"}
|