@linxin666/dsh-remote-web-ui 0.3.21 → 0.3.23
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 +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +47 -4
- package/lib/client.js.map +1 -1
- package/lib/index.js +138 -24
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/remote-channel.d.ts +35 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +55 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +26 -22
- package/lib/types/public-base.d.ts +67 -0
- package/lib/types/public-base.d.ts.map +1 -0
- package/lib/types/public-base.js +110 -0
- package/lib/types/remote-channel-boot.d.ts +5 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +28 -2
- package/lib/types/remote-channel-rules.d.ts +4 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +2 -0
- package/package.json +1 -1
- package/src/client/locales.ts +2 -2
- package/src/client/remote-channel.ts +64 -0
- package/src/index.ts +26 -22
- package/src/public-base.ts +122 -0
- package/src/remote-channel-boot.ts +28 -2
- package/src/remote-channel-rules.ts +6 -0
package/lib/index.js
CHANGED
|
@@ -3158,6 +3158,114 @@ var TunnelManager = class {
|
|
|
3158
3158
|
} catch {}
|
|
3159
3159
|
}
|
|
3160
3160
|
};
|
|
3161
|
+
//#endregion
|
|
3162
|
+
//#region src/public-base.ts
|
|
3163
|
+
/**
|
|
3164
|
+
* The public base the pairing fence trusts while a tunnel is not running
|
|
3165
|
+
* (issue #1547).
|
|
3166
|
+
*
|
|
3167
|
+
* The fence reads exactly one public host: `service.publicBaseUrl`. Dropping
|
|
3168
|
+
* it the instant the tunnel leaves `running` makes every public request from
|
|
3169
|
+
* the address still printed in the QR code answer 403 — including
|
|
3170
|
+
* `/api/pair/status`, which exists to be callable before pairing. Two of the
|
|
3171
|
+
* three tunnel modes never lose their host: a named tunnel keeps its fixed
|
|
3172
|
+
* dashboard hostname, and a registered relay keeps its stable subdomain. A
|
|
3173
|
+
* quick tunnel does mint a new hostname on restart, so its previous host is
|
|
3174
|
+
* kept only for a bounded grace window: Cloudflare's edge can still deliver a
|
|
3175
|
+
* connection the phone already opened, and a mobile network reconnects
|
|
3176
|
+
* constantly, so an instant drop turns the phone's retry into a silent 403.
|
|
3177
|
+
* @module @linxin666/dsh-remote-web-ui/public-base
|
|
3178
|
+
*/
|
|
3179
|
+
/** How long a quick-tunnel host stays trusted after the tunnel stops running. */
|
|
3180
|
+
const PUBLIC_BASE_GRACE_MS = 6e4;
|
|
3181
|
+
/**
|
|
3182
|
+
* Owns the trusted public base across tunnel and relay transitions.
|
|
3183
|
+
*
|
|
3184
|
+
* The keeper never decides on its own to widen trust: it only postpones
|
|
3185
|
+
* dropping an address the operator (named/relay) or the tunnel (quick) had
|
|
3186
|
+
* already published, and it forgets it once the tunnel comes back with the
|
|
3187
|
+
* address that actually serves the QR.
|
|
3188
|
+
*/
|
|
3189
|
+
var PublicBaseKeeper = class {
|
|
3190
|
+
onChange;
|
|
3191
|
+
graceMs;
|
|
3192
|
+
mode = "off";
|
|
3193
|
+
tunnelUrl;
|
|
3194
|
+
relayUrl;
|
|
3195
|
+
graceTimer;
|
|
3196
|
+
/**
|
|
3197
|
+
* @param onChange - receives the base the fence should trust from now on.
|
|
3198
|
+
* @param graceMs - how long a quick-tunnel host outlives its tunnel.
|
|
3199
|
+
*/
|
|
3200
|
+
constructor(onChange, graceMs = PUBLIC_BASE_GRACE_MS) {
|
|
3201
|
+
this.onChange = onChange;
|
|
3202
|
+
this.graceMs = graceMs;
|
|
3203
|
+
}
|
|
3204
|
+
/** The base a fresh fence read should trust: the stable relay origin wins. */
|
|
3205
|
+
current() {
|
|
3206
|
+
return this.relayUrl ?? this.tunnelUrl;
|
|
3207
|
+
}
|
|
3208
|
+
/** The mode that currently owns the public base. */
|
|
3209
|
+
setMode(mode) {
|
|
3210
|
+
this.mode = mode;
|
|
3211
|
+
}
|
|
3212
|
+
/** The last quick-tunnel URL, for callers that must announce it downstream. */
|
|
3213
|
+
quickUrl() {
|
|
3214
|
+
return this.tunnelUrl;
|
|
3215
|
+
}
|
|
3216
|
+
/** A relay registration result; undefined withdraws the stable origin. */
|
|
3217
|
+
setRelay(url) {
|
|
3218
|
+
this.relayUrl = url;
|
|
3219
|
+
this.emit();
|
|
3220
|
+
}
|
|
3221
|
+
/** The tunnel reported a live URL: it supersedes any pending grace. */
|
|
3222
|
+
markRunning(url) {
|
|
3223
|
+
this.cancelGrace();
|
|
3224
|
+
this.tunnelUrl = url;
|
|
3225
|
+
this.emit();
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* The tunnel left `running` (starting again, or failed). A named tunnel's
|
|
3229
|
+
* fixed hostname stays trusted; a quick tunnel's previous host stays
|
|
3230
|
+
* trusted for the grace window, then is dropped if nothing replaced it.
|
|
3231
|
+
*/
|
|
3232
|
+
markReconnecting() {
|
|
3233
|
+
if (this.mode === "named") return;
|
|
3234
|
+
this.startGrace();
|
|
3235
|
+
}
|
|
3236
|
+
/** Re-publish the current base after a branch change that did not move it. */
|
|
3237
|
+
refresh() {
|
|
3238
|
+
this.emit();
|
|
3239
|
+
}
|
|
3240
|
+
/** Forget everything (teardown, or a mode change that stops the tunnel). */
|
|
3241
|
+
reset() {
|
|
3242
|
+
this.cancelGrace();
|
|
3243
|
+
this.tunnelUrl = void 0;
|
|
3244
|
+
this.relayUrl = void 0;
|
|
3245
|
+
this.emit();
|
|
3246
|
+
}
|
|
3247
|
+
/** Cancel the pending grace timer; the keeper keeps its last base. */
|
|
3248
|
+
dispose() {
|
|
3249
|
+
this.cancelGrace();
|
|
3250
|
+
}
|
|
3251
|
+
startGrace() {
|
|
3252
|
+
if (this.tunnelUrl === void 0) return;
|
|
3253
|
+
if (this.graceTimer !== void 0) return;
|
|
3254
|
+
this.graceTimer = setTimeout(() => {
|
|
3255
|
+
this.graceTimer = void 0;
|
|
3256
|
+
this.tunnelUrl = void 0;
|
|
3257
|
+
this.emit();
|
|
3258
|
+
}, this.graceMs);
|
|
3259
|
+
}
|
|
3260
|
+
cancelGrace() {
|
|
3261
|
+
if (this.graceTimer === void 0) return;
|
|
3262
|
+
clearTimeout(this.graceTimer);
|
|
3263
|
+
this.graceTimer = void 0;
|
|
3264
|
+
}
|
|
3265
|
+
emit() {
|
|
3266
|
+
this.onChange(this.current());
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3161
3269
|
/** The aggregate package that is the canonical update entry point. */
|
|
3162
3270
|
const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
|
|
3163
3271
|
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
@@ -3859,7 +3967,9 @@ const REMOTE_CHANNEL_RULES = {
|
|
|
3859
3967
|
],
|
|
3860
3968
|
deviceHeader: REMOTE_DEVICE_HEADER,
|
|
3861
3969
|
deviceKey: "dsh-remote-device",
|
|
3862
|
-
deviceQuery: REMOTE_DEVICE_QUERY
|
|
3970
|
+
deviceQuery: REMOTE_DEVICE_QUERY,
|
|
3971
|
+
uploadPath: "/api/session/uploadFileBinary",
|
|
3972
|
+
uploadHookGlobal: "__DSH_FILE_UPLOAD__"
|
|
3863
3973
|
};
|
|
3864
3974
|
/** The window global the boot patch publishes its seat under. */
|
|
3865
3975
|
const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
@@ -3884,8 +3994,11 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
|
3884
3994
|
* transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
|
|
3885
3995
|
* connection plugin reads it: the paired remote desktop gets the full
|
|
3886
3996
|
* settings/credentials/presets surface, and every call still rides the
|
|
3887
|
-
* gated /remote channel.
|
|
3888
|
-
*
|
|
3997
|
+
* gated /remote channel. It finally publishes the official pre-Cordis
|
|
3998
|
+
* upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
|
|
3999
|
+
* transport otherwise runs inside a Web Worker whose own globals the
|
|
4000
|
+
* main-thread rewrite cannot reach (issue #1580). The script self-skips on
|
|
4001
|
+
* loopback origins and never throws.
|
|
3889
4002
|
* @module @linxin666/dsh-remote-web-ui/remote-channel-boot
|
|
3890
4003
|
*/
|
|
3891
4004
|
/**
|
|
@@ -3896,7 +4009,7 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
|
3896
4009
|
function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
3897
4010
|
const json = JSON.stringify(rules);
|
|
3898
4011
|
const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
|
|
3899
|
-
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
|
|
4012
|
+
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);function uf(u,init){var raw=typeof u===\"string\"?u:u.href;if(typeof raw!==\"string\")return of.call(w,u,init||{});var q=new URL(raw,loc.href);if(so(q)&&q.pathname===R.uploadPath)return w.fetch(raw,init||{});return of.call(w,u,init||{})}try{if(w[R.uploadHookGlobal]===undefined)w[R.uploadHookGlobal]={fetch:uf}}catch(e){}seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
|
|
3900
4013
|
}
|
|
3901
4014
|
/** Boot-watchdog latch: one self-reload per session while the boot is broken. */
|
|
3902
4015
|
const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
|
|
@@ -4079,17 +4192,20 @@ function applyImpl(ctx, config) {
|
|
|
4079
4192
|
const service = new PairingService(pairingConfigOf(resolved));
|
|
4080
4193
|
const tunnel = new TunnelManager();
|
|
4081
4194
|
let relayRegistrar;
|
|
4082
|
-
|
|
4083
|
-
|
|
4195
|
+
/**
|
|
4196
|
+
* The public base the pairing fence trusts. A tunnel reconnect must not
|
|
4197
|
+
* strip the host the QR still shows: named and relay hosts never change,
|
|
4198
|
+
* and a quick tunnel's host survives a bounded grace window (issue #1547).
|
|
4199
|
+
*/
|
|
4200
|
+
const publicBase = new PublicBaseKeeper((base) => {
|
|
4201
|
+
service.setPublicBaseUrl(base);
|
|
4202
|
+
});
|
|
4084
4203
|
/** The tunnel target the registrar last announced (dedupes sync re-runs). */
|
|
4085
4204
|
let relayAnnouncedFor;
|
|
4086
|
-
const setPublicBase = () => {
|
|
4087
|
-
service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
|
|
4088
|
-
};
|
|
4089
4205
|
const disposeRelayRegistrar = (unregister = false) => {
|
|
4090
4206
|
const registrar = relayRegistrar;
|
|
4091
4207
|
relayRegistrar = void 0;
|
|
4092
|
-
|
|
4208
|
+
publicBase.setRelay(void 0);
|
|
4093
4209
|
relayAnnouncedFor = void 0;
|
|
4094
4210
|
if (registrar === void 0) return;
|
|
4095
4211
|
if (unregister) registrar.unregister().catch(() => void 0);
|
|
@@ -4100,10 +4216,9 @@ function applyImpl(ctx, config) {
|
|
|
4100
4216
|
if (relayRegistrar === void 0) try {
|
|
4101
4217
|
relayRegistrar = new RelayRegistrar(loadRelayIdentity(resolve().profile), (state) => {
|
|
4102
4218
|
service.setRelayStatus(state.state === "off" ? void 0 : state);
|
|
4103
|
-
if (state.state === "running")
|
|
4104
|
-
else if (state.state === "off")
|
|
4219
|
+
if (state.state === "running") publicBase.setRelay(state.url);
|
|
4220
|
+
else if (state.state === "off") publicBase.setRelay(void 0);
|
|
4105
4221
|
else if (state.state === "failed") console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
|
|
4106
|
-
setPublicBase();
|
|
4107
4222
|
});
|
|
4108
4223
|
} catch (error) {
|
|
4109
4224
|
console.warn(`remote-web-ui: relay registry unavailable (${error instanceof Error ? error.message : String(error)}) — the quick URL is the QR base`);
|
|
@@ -4119,8 +4234,7 @@ function applyImpl(ctx, config) {
|
|
|
4119
4234
|
tunnel.onPhase((info) => {
|
|
4120
4235
|
if (tunnelMode === "off") return;
|
|
4121
4236
|
if (info.phase === "running" && info.url !== void 0) {
|
|
4122
|
-
|
|
4123
|
-
setPublicBase();
|
|
4237
|
+
publicBase.markRunning(info.url);
|
|
4124
4238
|
service.setTunnelStatus({
|
|
4125
4239
|
state: "running",
|
|
4126
4240
|
url: info.url
|
|
@@ -4129,13 +4243,10 @@ function applyImpl(ctx, config) {
|
|
|
4129
4243
|
if (registrar !== void 0) announceRelay(registrar, info.url);
|
|
4130
4244
|
runPostureProbe();
|
|
4131
4245
|
} else if (info.phase === "starting") {
|
|
4132
|
-
|
|
4133
|
-
relayUrl = void 0;
|
|
4134
|
-
setPublicBase();
|
|
4246
|
+
publicBase.markReconnecting();
|
|
4135
4247
|
service.setTunnelStatus({ state: "starting" });
|
|
4136
4248
|
} else if (info.phase === "failed") {
|
|
4137
|
-
|
|
4138
|
-
setPublicBase();
|
|
4249
|
+
publicBase.markReconnecting();
|
|
4139
4250
|
service.setTunnelStatus(info.error === void 0 ? { state: "failed" } : {
|
|
4140
4251
|
state: "failed",
|
|
4141
4252
|
error: info.error
|
|
@@ -4144,6 +4255,7 @@ function applyImpl(ctx, config) {
|
|
|
4144
4255
|
});
|
|
4145
4256
|
ctx.effect(() => () => {
|
|
4146
4257
|
disposeRelayRegistrar();
|
|
4258
|
+
publicBase.dispose();
|
|
4147
4259
|
tunnel.dispose();
|
|
4148
4260
|
}, "remote-web-ui: auto tunnel");
|
|
4149
4261
|
if ((ctx.webServer.host === "0.0.0.0" || ctx.webServer.host === "127.0.0.1") && Number.isFinite(ctx.webServer.port)) {
|
|
@@ -4391,15 +4503,17 @@ function applyImpl(ctx, config) {
|
|
|
4391
4503
|
}
|
|
4392
4504
|
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
4393
4505
|
tunnelMode = plan.mode;
|
|
4506
|
+
publicBase.setMode(plan.mode);
|
|
4507
|
+
const liveTunnelUrl = publicBase.quickUrl();
|
|
4394
4508
|
if (plan.mode !== "quick") {
|
|
4395
4509
|
disposeRelayRegistrar();
|
|
4396
|
-
if (plan.mode !== "named")
|
|
4510
|
+
if (plan.mode !== "named") publicBase.refresh();
|
|
4397
4511
|
} else if (value.relay === false) {
|
|
4398
4512
|
disposeRelayRegistrar(true);
|
|
4399
|
-
|
|
4400
|
-
} else if (
|
|
4513
|
+
publicBase.refresh();
|
|
4514
|
+
} else if (liveTunnelUrl !== void 0) {
|
|
4401
4515
|
const registrar = ensureRelayRegistrar();
|
|
4402
|
-
if (registrar !== void 0) announceRelay(registrar,
|
|
4516
|
+
if (registrar !== void 0) announceRelay(registrar, liveTunnelUrl);
|
|
4403
4517
|
}
|
|
4404
4518
|
if (plan.mode === "quick") {
|
|
4405
4519
|
for (const ignored of plan.ignored) console.warn(`remote-web-ui: autoTunnel is on — ignoring the configured ${ignored}`);
|
|
@@ -14,7 +14,7 @@ export const zh = {
|
|
|
14
14
|
'status.disconnected': '已配对设备离线',
|
|
15
15
|
'status.stopped': '已停止远程访问',
|
|
16
16
|
'status.lanRequired': '此功能需要局域网绑定或公网地址才能使用',
|
|
17
|
-
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1
|
|
17
|
+
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在「设置 → Web 插件 → 远程访问设置」卡片中打开“局域网访问”,或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
|
|
18
18
|
'status.loopbackRequired': '配对面板仅限本机使用',
|
|
19
19
|
'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
|
|
20
20
|
'status.unreachable': '无法连接配对服务',
|
|
@@ -181,7 +181,7 @@ export const en = {
|
|
|
181
181
|
'status.disconnected': 'Paired devices offline',
|
|
182
182
|
'status.stopped': 'Remote access stopped',
|
|
183
183
|
'status.lanRequired': 'This feature needs a LAN bind or a public address',
|
|
184
|
-
'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in the settings card
|
|
184
|
+
'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in the settings card under Settings → Web Plugins → Remote access, or restart with dsh web --host 0.0.0.0, or set the tunneled public address.',
|
|
185
185
|
'status.loopbackRequired': 'The pairing panel works on this machine only',
|
|
186
186
|
'status.loopbackRequiredHint': 'Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.',
|
|
187
187
|
'status.unreachable': 'Cannot reach the pairing service',
|
|
@@ -100,6 +100,41 @@ export declare function isUnpairedDenied(response: Response): Promise<boolean>;
|
|
|
100
100
|
* @returns a function restoring the originals.
|
|
101
101
|
*/
|
|
102
102
|
export declare function installRemoteChannel(window: ChannelWindow, options?: RemoteChannelOptions): () => void;
|
|
103
|
+
/** Page global the official pre-Cordis upload hook is published under. */
|
|
104
|
+
export declare const FILE_UPLOAD_HOOK_GLOBAL = "__DSH_FILE_UPLOAD__";
|
|
105
|
+
/** The subset of window the upload hook needs (injectable for tests). */
|
|
106
|
+
export interface UploadHookWindow {
|
|
107
|
+
fetch: typeof globalThis.fetch;
|
|
108
|
+
location: {
|
|
109
|
+
href: string;
|
|
110
|
+
origin: string;
|
|
111
|
+
};
|
|
112
|
+
/** Pre-existing hook owner; left untouched when present. */
|
|
113
|
+
__DSH_FILE_UPLOAD__?: {
|
|
114
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Publish the official pre-Cordis upload hook so background uploads keep
|
|
119
|
+
* riding the patched main-thread fetch (issue #1580).
|
|
120
|
+
*
|
|
121
|
+
* `@deepseek-ai/dsh-client-file-upload` reads `globalThis.__DSH_FILE_UPLOAD__`
|
|
122
|
+
* once when its runtime is constructed; without it the carrier is a Web
|
|
123
|
+
* Worker, whose own globals no main-thread patch reaches. That worker's XHR
|
|
124
|
+
* goes straight to `<origin>/api/session/uploadFileBinary` with neither the
|
|
125
|
+
* `/remote` rewrite nor the cookieless device credential, so the harness
|
|
126
|
+
* browser-auth fence answers 401 and every upload from a paired browser
|
|
127
|
+
* fails. Rewriting the worker URL cannot fix it: a worker context carries
|
|
128
|
+
* neither the pairing cookie nor the device header.
|
|
129
|
+
*
|
|
130
|
+
* The hook hands the runtime the same transport the rest of the page uses -
|
|
131
|
+
* the boot script publishes an identical one (remote-channel-boot.ts), and
|
|
132
|
+
* this is the fallback for pages served without it.
|
|
133
|
+
*
|
|
134
|
+
* @param window - the browser window (or a test double), BEFORE the channel patch.
|
|
135
|
+
* @returns a function retiring the hook (a pre-existing one is left alone).
|
|
136
|
+
*/
|
|
137
|
+
export declare function installFileUploadHook(window: UploadHookWindow): () => void;
|
|
103
138
|
/**
|
|
104
139
|
* The remote-channel lifecycle transition between two steady states:
|
|
105
140
|
* running (active + installed) and retired (inactive + not installed).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,iBAAiB,EAEjB,aAAa,EACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAA;AAC3C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAIvE,qEAAqE;AACrE,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAA;IACpD,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC9D;AAED,qFAAqF;AACrF,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,6BAA6B,EACvC,iBAAiB,EAAE,OAAO,GAAG,SAAS,GACrC,OAAO,CAQT;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAShE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAcnF;AAED,wEAAwE;AACxE,UAAU,cAAc;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC9B,SAAS,EAAE,OAAO,SAAS,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,WAAW,CAAA;IAChC,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,cAAc,CAAC,EAAE;QAAE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CACzD;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAoBD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAO3E;AA0BD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -172,6 +172,9 @@ export function installRemoteChannel(window, options = {}) {
|
|
|
172
172
|
const originalFetch = window.fetch;
|
|
173
173
|
const OriginalWebSocket = window.WebSocket;
|
|
174
174
|
const OriginalEventSource = window.EventSource;
|
|
175
|
+
// Publish the official upload hook before the file-upload service reads it
|
|
176
|
+
// (issue #1580); removed again by the restore below.
|
|
177
|
+
const restoreUploadHook = installFileUploadHook(window);
|
|
175
178
|
const sameOrigin = (url) => url.origin === window.location.origin;
|
|
176
179
|
const rewrite = (raw) => rewriteRawUrl(raw, window.location.href, window.location.origin);
|
|
177
180
|
// The cookieless device credential (set by the /pair-app capture script):
|
|
@@ -266,6 +269,58 @@ export function installRemoteChannel(window, options = {}) {
|
|
|
266
269
|
window.EventSource = OriginalEventSource;
|
|
267
270
|
for (const restore of restoreSrc)
|
|
268
271
|
restore();
|
|
272
|
+
restoreUploadHook();
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Page global the official pre-Cordis upload hook is published under. */
|
|
276
|
+
export const FILE_UPLOAD_HOOK_GLOBAL = '__DSH_FILE_UPLOAD__';
|
|
277
|
+
/**
|
|
278
|
+
* Publish the official pre-Cordis upload hook so background uploads keep
|
|
279
|
+
* riding the patched main-thread fetch (issue #1580).
|
|
280
|
+
*
|
|
281
|
+
* `@deepseek-ai/dsh-client-file-upload` reads `globalThis.__DSH_FILE_UPLOAD__`
|
|
282
|
+
* once when its runtime is constructed; without it the carrier is a Web
|
|
283
|
+
* Worker, whose own globals no main-thread patch reaches. That worker's XHR
|
|
284
|
+
* goes straight to `<origin>/api/session/uploadFileBinary` with neither the
|
|
285
|
+
* `/remote` rewrite nor the cookieless device credential, so the harness
|
|
286
|
+
* browser-auth fence answers 401 and every upload from a paired browser
|
|
287
|
+
* fails. Rewriting the worker URL cannot fix it: a worker context carries
|
|
288
|
+
* neither the pairing cookie nor the device header.
|
|
289
|
+
*
|
|
290
|
+
* The hook hands the runtime the same transport the rest of the page uses -
|
|
291
|
+
* the boot script publishes an identical one (remote-channel-boot.ts), and
|
|
292
|
+
* this is the fallback for pages served without it.
|
|
293
|
+
*
|
|
294
|
+
* @param window - the browser window (or a test double), BEFORE the channel patch.
|
|
295
|
+
* @returns a function retiring the hook (a pre-existing one is left alone).
|
|
296
|
+
*/
|
|
297
|
+
export function installFileUploadHook(window) {
|
|
298
|
+
if (window.__DSH_FILE_UPLOAD__ !== undefined)
|
|
299
|
+
return () => { };
|
|
300
|
+
const originalFetch = window.fetch;
|
|
301
|
+
const hook = {
|
|
302
|
+
fetch: (input, init) => {
|
|
303
|
+
const raw = typeof input === 'string' ? input : input.href;
|
|
304
|
+
let url;
|
|
305
|
+
try {
|
|
306
|
+
url = new URL(raw, window.location.href);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return originalFetch.call(window, input, init);
|
|
310
|
+
}
|
|
311
|
+
// Delegate the whole decision to the patched fetch: rewriting the path
|
|
312
|
+
// here first would make it skip its own rewrite branch and drop the
|
|
313
|
+
// device credential the fence requires.
|
|
314
|
+
if (url.origin === window.location.origin && url.pathname === RULES.uploadPath) {
|
|
315
|
+
return window.fetch.call(window, raw, init);
|
|
316
|
+
}
|
|
317
|
+
return originalFetch.call(window, input, init);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
window.__DSH_FILE_UPLOAD__ = hook;
|
|
321
|
+
return () => {
|
|
322
|
+
if (window.__DSH_FILE_UPLOAD__ === hook)
|
|
323
|
+
delete window.__DSH_FILE_UPLOAD__;
|
|
269
324
|
};
|
|
270
325
|
}
|
|
271
326
|
/**
|
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;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,CAAC,MAAM,aAAa,CAAA;AAO3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
|
|
|
27
27
|
import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
|
|
28
28
|
import { createInnerAuth } from "./inner-auth.js";
|
|
29
29
|
import { TunnelManager } from "./tunnel.js";
|
|
30
|
+
import { PublicBaseKeeper } from "./public-base.js";
|
|
30
31
|
import { checkUpdates, fetchGitHubReleaseNotes, fetchLatestVersion, RELEASE_NOTES_CACHE_TTL_MS, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
|
|
31
32
|
import { makeUpdateRoutes } from "./update-routes.js";
|
|
32
33
|
import { mountOnce } from "./mount-once.js";
|
|
@@ -162,17 +163,18 @@ function applyImpl(ctx, config) {
|
|
|
162
163
|
// raw quick URL is used, exactly as before. Named tunnels keep their fixed
|
|
163
164
|
// dashboard hostname and never touch the relay.
|
|
164
165
|
let relayRegistrar;
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The public base the pairing fence trusts. A tunnel reconnect must not
|
|
168
|
+
* strip the host the QR still shows: named and relay hosts never change,
|
|
169
|
+
* and a quick tunnel's host survives a bounded grace window (issue #1547).
|
|
170
|
+
*/
|
|
171
|
+
const publicBase = new PublicBaseKeeper((base) => { service.setPublicBaseUrl(base); });
|
|
167
172
|
/** The tunnel target the registrar last announced (dedupes sync re-runs). */
|
|
168
173
|
let relayAnnouncedFor;
|
|
169
|
-
const setPublicBase = () => {
|
|
170
|
-
service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
|
|
171
|
-
};
|
|
172
174
|
const disposeRelayRegistrar = (unregister = false) => {
|
|
173
175
|
const registrar = relayRegistrar;
|
|
174
176
|
relayRegistrar = undefined;
|
|
175
|
-
|
|
177
|
+
publicBase.setRelay(undefined);
|
|
176
178
|
relayAnnouncedFor = undefined;
|
|
177
179
|
if (registrar === undefined)
|
|
178
180
|
return;
|
|
@@ -193,10 +195,10 @@ function applyImpl(ctx, config) {
|
|
|
193
195
|
relayRegistrar = new RelayRegistrar(identity, (state) => {
|
|
194
196
|
service.setRelayStatus(state.state === 'off' ? undefined : state);
|
|
195
197
|
if (state.state === 'running') {
|
|
196
|
-
|
|
198
|
+
publicBase.setRelay(state.url);
|
|
197
199
|
}
|
|
198
200
|
else if (state.state === 'off') {
|
|
199
|
-
|
|
201
|
+
publicBase.setRelay(undefined);
|
|
200
202
|
}
|
|
201
203
|
else if (state.state === 'failed') {
|
|
202
204
|
// Keep the last relay URL on failures: the phone origin only
|
|
@@ -204,7 +206,6 @@ function applyImpl(ctx, config) {
|
|
|
204
206
|
// refresh call fails. The registrar retries with backoff.
|
|
205
207
|
console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
|
|
206
208
|
}
|
|
207
|
-
setPublicBase();
|
|
208
209
|
});
|
|
209
210
|
}
|
|
210
211
|
catch (error) {
|
|
@@ -226,8 +227,7 @@ function applyImpl(ctx, config) {
|
|
|
226
227
|
if (tunnelMode === 'off')
|
|
227
228
|
return;
|
|
228
229
|
if (info.phase === 'running' && info.url !== undefined) {
|
|
229
|
-
|
|
230
|
-
setPublicBase();
|
|
230
|
+
publicBase.markRunning(info.url);
|
|
231
231
|
service.setTunnelStatus({ state: 'running', url: info.url });
|
|
232
232
|
const registrar = tunnelMode === 'quick' ? ensureRelayRegistrar() : undefined;
|
|
233
233
|
if (registrar !== undefined)
|
|
@@ -235,21 +235,23 @@ function applyImpl(ctx, config) {
|
|
|
235
235
|
runPostureProbe();
|
|
236
236
|
}
|
|
237
237
|
else if (info.phase === 'starting') {
|
|
238
|
-
// A restart mints a
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
// A quick-tunnel restart mints a new hostname, but the old one is not
|
|
239
|
+
// dropped at once: the edge may still deliver a connection the phone
|
|
240
|
+
// already opened, and the reconnect usually lands inside the grace
|
|
241
|
+
// window (issue #1547). A named tunnel keeps its fixed hostname, and a
|
|
242
|
+
// registered relay its stable subdomain, so neither is ever dropped
|
|
243
|
+
// here — the previous code cleared the relay base on every restart.
|
|
244
|
+
publicBase.markReconnecting();
|
|
243
245
|
service.setTunnelStatus({ state: 'starting' });
|
|
244
246
|
}
|
|
245
247
|
else if (info.phase === 'failed') {
|
|
246
|
-
|
|
247
|
-
setPublicBase();
|
|
248
|
+
publicBase.markReconnecting();
|
|
248
249
|
service.setTunnelStatus(info.error === undefined ? { state: 'failed' } : { state: 'failed', error: info.error });
|
|
249
250
|
}
|
|
250
251
|
});
|
|
251
252
|
ctx.effect(() => () => {
|
|
252
253
|
disposeRelayRegistrar();
|
|
254
|
+
publicBase.dispose();
|
|
253
255
|
tunnel.dispose();
|
|
254
256
|
}, 'remote-web-ui: auto tunnel');
|
|
255
257
|
// The bind facts are known by now (webServer is an inject edge): the LAN
|
|
@@ -613,26 +615,28 @@ function applyImpl(ctx, config) {
|
|
|
613
615
|
// publicBaseUrl applies only when no tunnel runs.
|
|
614
616
|
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
615
617
|
tunnelMode = plan.mode;
|
|
618
|
+
publicBase.setMode(plan.mode);
|
|
619
|
+
const liveTunnelUrl = publicBase.quickUrl();
|
|
616
620
|
if (plan.mode !== 'quick') {
|
|
617
621
|
// The relay only fronts the quick tunnel; named mode owns its fixed
|
|
618
622
|
// dashboard hostname and the off mode has no public base at all.
|
|
619
623
|
disposeRelayRegistrar();
|
|
620
624
|
if (plan.mode !== 'named')
|
|
621
|
-
|
|
625
|
+
publicBase.refresh();
|
|
622
626
|
}
|
|
623
627
|
else if (value.relay === false) {
|
|
624
628
|
// The relay toggle is off: no stable origin, the raw quick URL is the
|
|
625
629
|
// QR base exactly as before the relay existed.
|
|
626
630
|
disposeRelayRegistrar(true);
|
|
627
|
-
|
|
631
|
+
publicBase.refresh();
|
|
628
632
|
}
|
|
629
|
-
else if (
|
|
633
|
+
else if (liveTunnelUrl !== undefined) {
|
|
630
634
|
// The relay just turned on (or the registrar is new) while the tunnel
|
|
631
635
|
// already runs: announce now — no phase event will fire for an
|
|
632
636
|
// unchanged target.
|
|
633
637
|
const registrar = ensureRelayRegistrar();
|
|
634
638
|
if (registrar !== undefined)
|
|
635
|
-
announceRelay(registrar,
|
|
639
|
+
announceRelay(registrar, liveTunnelUrl);
|
|
636
640
|
}
|
|
637
641
|
if (plan.mode === 'quick') {
|
|
638
642
|
for (const ignored of plan.ignored) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public base the pairing fence trusts while a tunnel is not running
|
|
3
|
+
* (issue #1547).
|
|
4
|
+
*
|
|
5
|
+
* The fence reads exactly one public host: `service.publicBaseUrl`. Dropping
|
|
6
|
+
* it the instant the tunnel leaves `running` makes every public request from
|
|
7
|
+
* the address still printed in the QR code answer 403 — including
|
|
8
|
+
* `/api/pair/status`, which exists to be callable before pairing. Two of the
|
|
9
|
+
* three tunnel modes never lose their host: a named tunnel keeps its fixed
|
|
10
|
+
* dashboard hostname, and a registered relay keeps its stable subdomain. A
|
|
11
|
+
* quick tunnel does mint a new hostname on restart, so its previous host is
|
|
12
|
+
* kept only for a bounded grace window: Cloudflare's edge can still deliver a
|
|
13
|
+
* connection the phone already opened, and a mobile network reconnects
|
|
14
|
+
* constantly, so an instant drop turns the phone's retry into a silent 403.
|
|
15
|
+
* @module @linxin666/dsh-remote-web-ui/public-base
|
|
16
|
+
*/
|
|
17
|
+
/** How long a quick-tunnel host stays trusted after the tunnel stops running. */
|
|
18
|
+
export declare const PUBLIC_BASE_GRACE_MS = 60000;
|
|
19
|
+
/** Tunnel ownership of the public base, as resolved from the settings document. */
|
|
20
|
+
export type TunnelBaseMode = 'off' | 'quick' | 'named';
|
|
21
|
+
/**
|
|
22
|
+
* Owns the trusted public base across tunnel and relay transitions.
|
|
23
|
+
*
|
|
24
|
+
* The keeper never decides on its own to widen trust: it only postpones
|
|
25
|
+
* dropping an address the operator (named/relay) or the tunnel (quick) had
|
|
26
|
+
* already published, and it forgets it once the tunnel comes back with the
|
|
27
|
+
* address that actually serves the QR.
|
|
28
|
+
*/
|
|
29
|
+
export declare class PublicBaseKeeper {
|
|
30
|
+
private readonly onChange;
|
|
31
|
+
private readonly graceMs;
|
|
32
|
+
private mode;
|
|
33
|
+
private tunnelUrl;
|
|
34
|
+
private relayUrl;
|
|
35
|
+
private graceTimer;
|
|
36
|
+
/**
|
|
37
|
+
* @param onChange - receives the base the fence should trust from now on.
|
|
38
|
+
* @param graceMs - how long a quick-tunnel host outlives its tunnel.
|
|
39
|
+
*/
|
|
40
|
+
constructor(onChange: (base: string | undefined) => void, graceMs?: number);
|
|
41
|
+
/** The base a fresh fence read should trust: the stable relay origin wins. */
|
|
42
|
+
current(): string | undefined;
|
|
43
|
+
/** The mode that currently owns the public base. */
|
|
44
|
+
setMode(mode: TunnelBaseMode): void;
|
|
45
|
+
/** The last quick-tunnel URL, for callers that must announce it downstream. */
|
|
46
|
+
quickUrl(): string | undefined;
|
|
47
|
+
/** A relay registration result; undefined withdraws the stable origin. */
|
|
48
|
+
setRelay(url: string | undefined): void;
|
|
49
|
+
/** The tunnel reported a live URL: it supersedes any pending grace. */
|
|
50
|
+
markRunning(url: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* The tunnel left `running` (starting again, or failed). A named tunnel's
|
|
53
|
+
* fixed hostname stays trusted; a quick tunnel's previous host stays
|
|
54
|
+
* trusted for the grace window, then is dropped if nothing replaced it.
|
|
55
|
+
*/
|
|
56
|
+
markReconnecting(): void;
|
|
57
|
+
/** Re-publish the current base after a branch change that did not move it. */
|
|
58
|
+
refresh(): void;
|
|
59
|
+
/** Forget everything (teardown, or a mode change that stops the tunnel). */
|
|
60
|
+
reset(): void;
|
|
61
|
+
/** Cancel the pending grace timer; the keeper keeps its last base. */
|
|
62
|
+
dispose(): void;
|
|
63
|
+
private startGrace;
|
|
64
|
+
private cancelGrace;
|
|
65
|
+
private emit;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=public-base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-base.d.ts","sourceRoot":"","sources":["../../src/public-base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAE1C,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,CAAA;AAEtD;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IAWzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX1B,OAAO,CAAC,IAAI,CAAwB;IACpC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,UAAU,CAA2C;IAE7D;;;OAGG;gBAEgB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,EAC5C,OAAO,GAAE,MAA6B;IAGzD,8EAA8E;IAC9E,OAAO,IAAI,MAAM,GAAG,SAAS;IAI7B,oDAAoD;IACpD,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAInC,+EAA+E;IAC/E,QAAQ,IAAI,MAAM,GAAG,SAAS;IAI9B,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKvC,uEAAuE;IACvE,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAM9B;;;;OAIG;IACH,gBAAgB,IAAI,IAAI;IAKxB,8EAA8E;IAC9E,OAAO,IAAI,IAAI;IAIf,4EAA4E;IAC5E,KAAK,IAAI,IAAI;IAOb,sEAAsE;IACtE,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,IAAI;CAGb"}
|