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/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.search !== "" || !WS_PATHS.has(target.decodedPathname)) throw new HttpError(404, "not_found");
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