dsh-mobile 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.en.md +7 -7
- package/README.md +7 -7
- package/lib/client.js +104 -5
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +31 -2
- package/lib/index.mjs +91 -5
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +8 -0
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +1 -1
package/lib/index.d.mts
CHANGED
|
@@ -535,6 +535,7 @@ declare class MobileAccessGateway {
|
|
|
535
535
|
readonly config: ResolvedGatewayConfig;
|
|
536
536
|
private readonly extensions?;
|
|
537
537
|
private readonly upstreamAuthenticatedUrl?;
|
|
538
|
+
private readonly extraWebSocketPaths?;
|
|
538
539
|
readonly access: AccessController;
|
|
539
540
|
private readonly listenerTlsEnabled;
|
|
540
541
|
private readonly tlsEnabled;
|
|
@@ -565,7 +566,9 @@ declare class MobileAccessGateway {
|
|
|
565
566
|
private readonly removeSessionListener;
|
|
566
567
|
private readonly removeExtensionContentListener;
|
|
567
568
|
private readonly renewLimiter;
|
|
568
|
-
constructor(config: ResolvedGatewayConfig, store: DeviceStore, extensions?: MobileAccessService | undefined, upstreamAuthenticatedUrl?: string | undefined
|
|
569
|
+
constructor(config: ResolvedGatewayConfig, store: DeviceStore, extensions?: MobileAccessService | undefined, upstreamAuthenticatedUrl?: string | undefined, extraWebSocketPaths?: {
|
|
570
|
+
has(pathname: string): boolean;
|
|
571
|
+
} | undefined);
|
|
569
572
|
/** Initialize durable state, validate TLS, and bind the externally reachable listener. */
|
|
570
573
|
start(): Promise<void>;
|
|
571
574
|
private startDiscovery;
|
|
@@ -688,6 +691,32 @@ declare class FrpComponentManager {
|
|
|
688
691
|
private enqueue;
|
|
689
692
|
}
|
|
690
693
|
//#endregion
|
|
694
|
+
//#region src/websocket-paths.d.ts
|
|
695
|
+
/** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
|
|
696
|
+
declare const MAX_EXTRA_WEBSOCKET_PATHS = 16;
|
|
697
|
+
declare const MAX_WEBSOCKET_PATH_LENGTH = 256;
|
|
698
|
+
/**
|
|
699
|
+
* Validate one exact pathname for proxying. Query strings are matched at
|
|
700
|
+
* upgrade time, so only the pathname is stored. Rejects anything the
|
|
701
|
+
* gateway cannot match exactly.
|
|
702
|
+
*/
|
|
703
|
+
declare function validateWebSocketPath(value: unknown): string;
|
|
704
|
+
/** Validate a whole replacement list all-or-nothing; duplicates collapse. */
|
|
705
|
+
declare function normalizeWebSocketPaths(value: unknown): string[];
|
|
706
|
+
/** File-backed store shared by every gateway instance (LAN and remote). */
|
|
707
|
+
declare class WebSocketPathStore {
|
|
708
|
+
private readonly file;
|
|
709
|
+
private paths;
|
|
710
|
+
private loaded;
|
|
711
|
+
constructor(file: string);
|
|
712
|
+
/** Snapshot for the upgrade check. */
|
|
713
|
+
has(pathname: string): boolean;
|
|
714
|
+
list(): string[];
|
|
715
|
+
load(): Promise<string[]>;
|
|
716
|
+
/** Replace the whole list after validating; persists atomically. */
|
|
717
|
+
replace(paths: readonly string[]): Promise<string[]>;
|
|
718
|
+
}
|
|
719
|
+
//#endregion
|
|
691
720
|
//#region src/frp-template.d.ts
|
|
692
721
|
/** Loopback-only HTTP vhost port used between Caddy and frps. */
|
|
693
722
|
declare const FRP_VHOST_HTTP_PORT = 7080;
|
|
@@ -878,5 +907,5 @@ declare const inject: string[];
|
|
|
878
907
|
/** Mount the resident control route and its optional authenticated LAN gateway. */
|
|
879
908
|
declare function apply(ctx: Context, config: PluginConfig): Promise<void>;
|
|
880
909
|
//#endregion
|
|
881
|
-
export { AUTH_PREFIX, AccessController, type AccessControllerOptions, AccessError, type AuthoritySpec, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, type DeviceSnapshot, type DeviceStore, type DeviceSummary, type DisabledTlsConfig, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, type FrpComponentStatus, FrpConfigStore, type FrpConfigurationStatus, FrpController, type FrpControllerOptions, type FrpSettings, type FrpState, type FrpStatus, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, type LocalExtensionManifest, MemoryDeviceStore, type MobileAccessControlState, type MobileAccessControlStore, MobileAccessGateway, MobileAccessGatewayController, type MobileAccessService as MobileAccessRegistry, MobileAccessService, type MobileAccessRuntime, type MobileActionContext, type MobileExtensionClientEntry, type MobileExtensionDefinition, MobileExtensionError, type MobileExtensionManifest, type MobileExtensionStatus, type MobileHostAction, type MobileHostRoute, type MobileRouteRequest, type MobileRouteResponse, type PairingResult, type ParsedCidr, type PluginConfig, type ProvidedTlsConfig, type RemoteProvider, type RemoteProviderController, type RemoteProviderState, type RemoteProviderStatus, type RenewalResult, RequestTrustPolicy, type ResolvedGatewayConfig, SESSION_COOKIE, type SessionAuthorization, type StoredDevice, type TlsConfig, WS_PATHS, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken };
|
|
910
|
+
export { AUTH_PREFIX, AccessController, type AccessControllerOptions, AccessError, type AuthoritySpec, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, type DeviceSnapshot, type DeviceStore, type DeviceSummary, type DisabledTlsConfig, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, type FrpComponentStatus, FrpConfigStore, type FrpConfigurationStatus, FrpController, type FrpControllerOptions, type FrpSettings, type FrpState, type FrpStatus, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, type LocalExtensionManifest, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, type MobileAccessControlState, type MobileAccessControlStore, MobileAccessGateway, MobileAccessGatewayController, type MobileAccessService as MobileAccessRegistry, MobileAccessService, type MobileAccessRuntime, type MobileActionContext, type MobileExtensionClientEntry, type MobileExtensionDefinition, MobileExtensionError, type MobileExtensionManifest, type MobileExtensionStatus, type MobileHostAction, type MobileHostRoute, type MobileRouteRequest, type MobileRouteResponse, type PairingResult, type ParsedCidr, type PluginConfig, type ProvidedTlsConfig, type RemoteProvider, type RemoteProviderController, type RemoteProviderState, type RemoteProviderStatus, type RenewalResult, RequestTrustPolicy, type ResolvedGatewayConfig, SESSION_COOKIE, type SessionAuthorization, type StoredDevice, type TlsConfig, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateWebSocketPath };
|
|
882
911
|
//# sourceMappingURL=index.d.mts.map
|
package/lib/index.mjs
CHANGED
|
@@ -2693,6 +2693,7 @@ var MobileAccessGateway = class {
|
|
|
2693
2693
|
config;
|
|
2694
2694
|
extensions;
|
|
2695
2695
|
upstreamAuthenticatedUrl;
|
|
2696
|
+
extraWebSocketPaths;
|
|
2696
2697
|
access;
|
|
2697
2698
|
listenerTlsEnabled;
|
|
2698
2699
|
tlsEnabled;
|
|
@@ -2723,10 +2724,11 @@ var MobileAccessGateway = class {
|
|
|
2723
2724
|
removeSessionListener;
|
|
2724
2725
|
removeExtensionContentListener;
|
|
2725
2726
|
renewLimiter;
|
|
2726
|
-
constructor(config, store, extensions, upstreamAuthenticatedUrl) {
|
|
2727
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths) {
|
|
2727
2728
|
this.config = config;
|
|
2728
2729
|
this.extensions = extensions;
|
|
2729
2730
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
2731
|
+
this.extraWebSocketPaths = extraWebSocketPaths;
|
|
2730
2732
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
2731
2733
|
this.tlsEnabled = config.publicTls;
|
|
2732
2734
|
this.access = new AccessController(store, {
|
|
@@ -3915,7 +3917,7 @@ var MobileAccessGateway = class {
|
|
|
3915
3917
|
const policy = this.requirePolicy();
|
|
3916
3918
|
assertExternalTrust(request, policy, false);
|
|
3917
3919
|
if (!policy.acceptsOrigin(request.headers.origin)) throw new HttpError(403, "forbidden");
|
|
3918
|
-
if (target.
|
|
3920
|
+
if (!(WS_PATHS.has(target.decodedPathname) || (this.extraWebSocketPaths?.has(target.decodedPathname) ?? false))) throw new HttpError(404, "not_found");
|
|
3919
3921
|
if (request.method !== "GET" || headerValue(request.headers, "upgrade")?.toLowerCase() !== "websocket" || !hasToken(headerValue(request.headers, "connection"), "upgrade")) throw new HttpError(400, "bad_request");
|
|
3920
3922
|
const key = headerValue(request.headers, "sec-websocket-key");
|
|
3921
3923
|
if (key === void 0 || headerValue(request.headers, "sec-websocket-version") !== "13") throw new HttpError(400, "bad_request");
|
|
@@ -4598,6 +4600,76 @@ var FrpComponentManager = class {
|
|
|
4598
4600
|
}
|
|
4599
4601
|
};
|
|
4600
4602
|
//#endregion
|
|
4603
|
+
//#region src/websocket-paths.ts
|
|
4604
|
+
/** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
|
|
4605
|
+
const MAX_EXTRA_WEBSOCKET_PATHS = 16;
|
|
4606
|
+
const MAX_WEBSOCKET_PATH_LENGTH = 256;
|
|
4607
|
+
function fail(code) {
|
|
4608
|
+
throw new Error(code);
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
4611
|
+
* Validate one exact pathname for proxying. Query strings are matched at
|
|
4612
|
+
* upgrade time, so only the pathname is stored. Rejects anything the
|
|
4613
|
+
* gateway cannot match exactly.
|
|
4614
|
+
*/
|
|
4615
|
+
function validateWebSocketPath(value) {
|
|
4616
|
+
if (typeof value !== "string") fail("websocket_path_invalid");
|
|
4617
|
+
const path = value;
|
|
4618
|
+
if (path.length === 0 || path.length > 256) fail("websocket_path_invalid");
|
|
4619
|
+
if (!path.startsWith("/")) fail("websocket_path_invalid");
|
|
4620
|
+
if (/[\s\u0000-\u001f\u007f#?]/u.test(path)) fail("websocket_path_invalid");
|
|
4621
|
+
if (path.includes("..")) fail("websocket_path_invalid");
|
|
4622
|
+
return path;
|
|
4623
|
+
}
|
|
4624
|
+
/** Validate a whole replacement list all-or-nothing; duplicates collapse. */
|
|
4625
|
+
function normalizeWebSocketPaths(value) {
|
|
4626
|
+
if (!Array.isArray(value)) fail("websocket_paths_invalid");
|
|
4627
|
+
if (value.length > 16) fail("websocket_paths_invalid");
|
|
4628
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4629
|
+
for (const entry of value) {
|
|
4630
|
+
const path = validateWebSocketPath(entry);
|
|
4631
|
+
seen.add(path);
|
|
4632
|
+
}
|
|
4633
|
+
return [...seen];
|
|
4634
|
+
}
|
|
4635
|
+
/** File-backed store shared by every gateway instance (LAN and remote). */
|
|
4636
|
+
var WebSocketPathStore = class {
|
|
4637
|
+
file;
|
|
4638
|
+
paths = /* @__PURE__ */ new Set();
|
|
4639
|
+
loaded = false;
|
|
4640
|
+
constructor(file) {
|
|
4641
|
+
this.file = file;
|
|
4642
|
+
}
|
|
4643
|
+
/** Snapshot for the upgrade check. */
|
|
4644
|
+
has(pathname) {
|
|
4645
|
+
return this.paths.has(pathname);
|
|
4646
|
+
}
|
|
4647
|
+
list() {
|
|
4648
|
+
return [...this.paths];
|
|
4649
|
+
}
|
|
4650
|
+
async load() {
|
|
4651
|
+
if (!this.loaded) {
|
|
4652
|
+
try {
|
|
4653
|
+
const raw = JSON.parse(await readFile(this.file, "utf8"));
|
|
4654
|
+
this.paths = new Set(normalizeWebSocketPaths(raw.paths ?? []));
|
|
4655
|
+
} catch {
|
|
4656
|
+
this.paths = /* @__PURE__ */ new Set();
|
|
4657
|
+
}
|
|
4658
|
+
this.loaded = true;
|
|
4659
|
+
}
|
|
4660
|
+
return this.list();
|
|
4661
|
+
}
|
|
4662
|
+
/** Replace the whole list after validating; persists atomically. */
|
|
4663
|
+
async replace(paths) {
|
|
4664
|
+
const next = normalizeWebSocketPaths([...paths]);
|
|
4665
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
4666
|
+
await writeFile(this.file, `${JSON.stringify({ paths: next })}\n`, "utf8");
|
|
4667
|
+
this.paths = new Set(next);
|
|
4668
|
+
this.loaded = true;
|
|
4669
|
+
return this.list();
|
|
4670
|
+
}
|
|
4671
|
+
};
|
|
4672
|
+
//#endregion
|
|
4601
4673
|
//#region src/frp-template.ts
|
|
4602
4674
|
/** Loopback-only HTTP vhost port used between Caddy and frps. */
|
|
4603
4675
|
const FRP_VHOST_HTTP_PORT = 7080;
|
|
@@ -8661,10 +8733,12 @@ async function apply(ctx, config) {
|
|
|
8661
8733
|
}
|
|
8662
8734
|
}]
|
|
8663
8735
|
});
|
|
8736
|
+
const webSocketPaths = new WebSocketPathStore(join(stateDirectory, "websocket-paths.json"));
|
|
8737
|
+
await webSocketPaths.load();
|
|
8664
8738
|
let lanGateway;
|
|
8665
8739
|
const startGateway = async (candidateConfig) => {
|
|
8666
8740
|
const resolved = parseGatewayConfig(candidateConfig);
|
|
8667
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl);
|
|
8741
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths);
|
|
8668
8742
|
await candidate.start();
|
|
8669
8743
|
lanGateway = candidate;
|
|
8670
8744
|
return { close: async () => {
|
|
@@ -8704,7 +8778,7 @@ async function apply(ctx, config) {
|
|
|
8704
8778
|
}
|
|
8705
8779
|
const createRemoteGateway = async (publicOrigin, listenPort = 0) => {
|
|
8706
8780
|
const resolved = remoteGatewayConfig(template, publicOrigin, remoteDeviceFile, instanceId, listenPort);
|
|
8707
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl);
|
|
8781
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths);
|
|
8708
8782
|
await candidate.start();
|
|
8709
8783
|
return candidate;
|
|
8710
8784
|
};
|
|
@@ -8866,6 +8940,18 @@ async function apply(ctx, config) {
|
|
|
8866
8940
|
sendJson(response, 200, remotePayload(), false);
|
|
8867
8941
|
return;
|
|
8868
8942
|
}
|
|
8943
|
+
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/remote/websocket-paths`) {
|
|
8944
|
+
sendJson(response, 200, { paths: webSocketPaths.list() }, false);
|
|
8945
|
+
return;
|
|
8946
|
+
}
|
|
8947
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/websocket-paths`) {
|
|
8948
|
+
const body = await readJsonObject(request, 4096);
|
|
8949
|
+
if (!Array.isArray(body.paths)) throw new HttpError(400, "bad_request");
|
|
8950
|
+
const paths = await webSocketPaths.replace(body.paths);
|
|
8951
|
+
logger.info("websocket upgrade paths updated count=%d paths=%o", paths.length, paths);
|
|
8952
|
+
sendJson(response, 200, { paths }, false);
|
|
8953
|
+
return;
|
|
8954
|
+
}
|
|
8869
8955
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/vps/host-keys`) {
|
|
8870
8956
|
const body = await readJsonObject(request, 8192);
|
|
8871
8957
|
const serverAddress = typeof body.serverAddress === "string" ? body.serverAddress : "";
|
|
@@ -9091,6 +9177,6 @@ async function apply(ctx, config) {
|
|
|
9091
9177
|
}, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
|
|
9092
9178
|
}
|
|
9093
9179
|
//#endregion
|
|
9094
|
-
export { AUTH_PREFIX, AccessController, AccessError, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, RequestTrustPolicy, SESSION_COOKIE, WS_PATHS, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken };
|
|
9180
|
+
export { AUTH_PREFIX, AccessController, AccessError, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, RequestTrustPolicy, SESSION_COOKIE, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateWebSocketPath };
|
|
9095
9181
|
|
|
9096
9182
|
//# sourceMappingURL=index.mjs.map
|