@jingyi0605/codingns4dsh 0.1.0
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.md +1114 -0
- package/assets/provider-icons/claude-code.png +0 -0
- package/assets/provider-icons/codex.png +0 -0
- package/assets/provider-icons/command-code.svg +5 -0
- package/assets/provider-icons/deepseek-harness.svg +9 -0
- package/assets/provider-icons/gemini.png +0 -0
- package/assets/provider-icons/grok.png +0 -0
- package/assets/provider-icons/kimi.png +0 -0
- package/assets/provider-icons/opencode.png +0 -0
- package/assets/provider-icons/pi.svg +7 -0
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +97 -0
- package/data/build/dist/bootstrap/dsh-connection-adapter.js +169 -0
- package/data/build/dist/bootstrap/index.d.ts +38 -0
- package/data/build/dist/bootstrap/index.js +44 -0
- package/data/build/dist/client/account-bar.d.ts +9 -0
- package/data/build/dist/client/account-bar.js +398 -0
- package/data/build/dist/client/bundle.js +26167 -0
- package/data/build/dist/client/cli-catalog.d.ts +24 -0
- package/data/build/dist/client/cli-catalog.js +108 -0
- package/data/build/dist/client/cli-slots.d.ts +38 -0
- package/data/build/dist/client/cli-slots.js +297 -0
- package/data/build/dist/client/debug/ui.d.ts +12 -0
- package/data/build/dist/client/debug/ui.js +451 -0
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +64 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +293 -0
- package/data/build/dist/client/dsh-runtime-version.d.ts +3 -0
- package/data/build/dist/client/dsh-runtime-version.js +11 -0
- package/data/build/dist/client/external-tool-stream.d.ts +24 -0
- package/data/build/dist/client/external-tool-stream.js +114 -0
- package/data/build/dist/client/features/cli-adapters.d.ts +7 -0
- package/data/build/dist/client/features/cli-adapters.js +215 -0
- package/data/build/dist/client/features/debug.d.ts +4 -0
- package/data/build/dist/client/features/debug.js +24 -0
- package/data/build/dist/client/features/index.d.ts +33 -0
- package/data/build/dist/client/features/index.js +44 -0
- package/data/build/dist/client/features/lan-access-panel.d.ts +5 -0
- package/data/build/dist/client/features/lan-access-panel.js +146 -0
- package/data/build/dist/client/features/lan-access.d.ts +9 -0
- package/data/build/dist/client/features/lan-access.js +30 -0
- package/data/build/dist/client/features/login-protection-panel.d.ts +5 -0
- package/data/build/dist/client/features/login-protection-panel.js +91 -0
- package/data/build/dist/client/features/login-protection-session.d.ts +6 -0
- package/data/build/dist/client/features/login-protection-session.js +25 -0
- package/data/build/dist/client/features/login-protection.d.ts +4 -0
- package/data/build/dist/client/features/login-protection.js +23 -0
- package/data/build/dist/client/features/reverse-proxy-panel.d.ts +9 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +159 -0
- package/data/build/dist/client/features/reverse-proxy.d.ts +12 -0
- package/data/build/dist/client/features/reverse-proxy.js +112 -0
- package/data/build/dist/client/features/terminal-enhancement-panel.d.ts +5 -0
- package/data/build/dist/client/features/terminal-enhancement-panel.js +160 -0
- package/data/build/dist/client/features/terminal-enhancement.d.ts +4 -0
- package/data/build/dist/client/features/terminal-enhancement.js +24 -0
- package/data/build/dist/client/features/types.d.ts +58 -0
- package/data/build/dist/client/features/types.js +2 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.d.ts +5 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +66 -0
- package/data/build/dist/client/features/workspace-session-enhancement.d.ts +4 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +101 -0
- package/data/build/dist/client/h5-bootstrap-entry.d.ts +2 -0
- package/data/build/dist/client/h5-bootstrap-entry.js +17 -0
- package/data/build/dist/client/index.d.ts +31 -0
- package/data/build/dist/client/index.js +98 -0
- package/data/build/dist/client/lan-access.d.ts +28 -0
- package/data/build/dist/client/lan-access.js +68 -0
- package/data/build/dist/client/locale.d.ts +20 -0
- package/data/build/dist/client/locale.js +393 -0
- package/data/build/dist/client/provider-icon-assets.d.ts +2 -0
- package/data/build/dist/client/provider-icon-assets.js +23 -0
- package/data/build/dist/client/provider-icons.d.ts +19 -0
- package/data/build/dist/client/provider-icons.js +39 -0
- package/data/build/dist/client/remote-web-context.d.ts +57 -0
- package/data/build/dist/client/remote-web-context.js +939 -0
- package/data/build/dist/client/session-adapter-cache.d.ts +18 -0
- package/data/build/dist/client/session-adapter-cache.js +84 -0
- package/data/build/dist/client/settings-bridge.d.ts +32 -0
- package/data/build/dist/client/settings-bridge.js +126 -0
- package/data/build/dist/client/settings-section.d.ts +30 -0
- package/data/build/dist/client/settings-section.js +93 -0
- package/data/build/dist/client/subscription-slot.d.ts +18 -0
- package/data/build/dist/client/subscription-slot.js +223 -0
- package/data/build/dist/client/terminal/index.d.ts +4 -0
- package/data/build/dist/client/terminal/index.js +3 -0
- package/data/build/dist/client/terminal/model.d.ts +116 -0
- package/data/build/dist/client/terminal/model.js +435 -0
- package/data/build/dist/client/terminal/styles.d.ts +21 -0
- package/data/build/dist/client/terminal/styles.js +51 -0
- package/data/build/dist/client/terminal/ui.d.ts +19 -0
- package/data/build/dist/client/terminal/ui.js +257 -0
- package/data/build/dist/client/terminal/xterm-view.d.ts +13 -0
- package/data/build/dist/client/terminal/xterm-view.js +213 -0
- package/data/build/dist/client/theme.d.ts +56 -0
- package/data/build/dist/client/theme.js +186 -0
- package/data/build/dist/client/workspace-session-archive-dom.d.ts +29 -0
- package/data/build/dist/client/workspace-session-archive-dom.js +525 -0
- package/data/build/dist/client/workspace-session-fiber.d.ts +3 -0
- package/data/build/dist/client/workspace-session-fiber.js +33 -0
- package/data/build/dist/client/workspace-session-logo-dom.d.ts +26 -0
- package/data/build/dist/client/workspace-session-logo-dom.js +128 -0
- package/data/build/dist/features/index.d.ts +3 -0
- package/data/build/dist/features/index.js +2 -0
- package/data/build/dist/features/registry.d.ts +74 -0
- package/data/build/dist/features/registry.js +382 -0
- package/data/build/dist/features/resource-scope/index.d.ts +39 -0
- package/data/build/dist/features/resource-scope/index.js +161 -0
- package/data/build/dist/host/auth-session.d.ts +42 -0
- package/data/build/dist/host/auth-session.js +186 -0
- package/data/build/dist/host/cli-adapters/claude-driver.d.ts +14 -0
- package/data/build/dist/host/cli-adapters/claude-driver.js +103 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +45 -0
- package/data/build/dist/host/cli-adapters/codex-driver.js +534 -0
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +38 -0
- package/data/build/dist/host/cli-adapters/command-code-driver.js +394 -0
- package/data/build/dist/host/cli-adapters/command-code-subscription.d.ts +19 -0
- package/data/build/dist/host/cli-adapters/command-code-subscription.js +126 -0
- package/data/build/dist/host/cli-adapters/driver.d.ts +41 -0
- package/data/build/dist/host/cli-adapters/driver.js +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +37 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +180 -0
- package/data/build/dist/host/cli-adapters/dsh-tool-history.d.ts +48 -0
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +373 -0
- package/data/build/dist/host/cli-adapters/feature.d.ts +7 -0
- package/data/build/dist/host/cli-adapters/feature.js +335 -0
- package/data/build/dist/host/cli-adapters/gemini-driver.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/gemini-driver.js +427 -0
- package/data/build/dist/host/cli-adapters/grok-driver.d.ts +38 -0
- package/data/build/dist/host/cli-adapters/grok-driver.js +372 -0
- package/data/build/dist/host/cli-adapters/http-sse-client.d.ts +22 -0
- package/data/build/dist/host/cli-adapters/http-sse-client.js +99 -0
- package/data/build/dist/host/cli-adapters/interaction-events.d.ts +11 -0
- package/data/build/dist/host/cli-adapters/interaction-events.js +80 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +65 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +256 -0
- package/data/build/dist/host/cli-adapters/kimi-driver.d.ts +21 -0
- package/data/build/dist/host/cli-adapters/kimi-driver.js +382 -0
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +22 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +89 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.d.ts +55 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +728 -0
- package/data/build/dist/host/cli-adapters/pi-driver.d.ts +43 -0
- package/data/build/dist/host/cli-adapters/pi-driver.js +312 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +16 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +27 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +82 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.js +610 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +95 -0
- package/data/build/dist/host/cli-adapters/registry.js +659 -0
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +119 -0
- package/data/build/dist/host/cli-adapters/session-probe.d.ts +20 -0
- package/data/build/dist/host/cli-adapters/session-probe.js +139 -0
- package/data/build/dist/host/cli-adapters/session-store.d.ts +68 -0
- package/data/build/dist/host/cli-adapters/session-store.js +180 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +45 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +205 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts +25 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.js +83 -0
- package/data/build/dist/host/cli-adapters/tool-observation.d.ts +9 -0
- package/data/build/dist/host/cli-adapters/tool-observation.js +39 -0
- package/data/build/dist/host/control-api-client.d.ts +84 -0
- package/data/build/dist/host/control-api-client.js +130 -0
- package/data/build/dist/host/credential-store.d.ts +54 -0
- package/data/build/dist/host/credential-store.js +114 -0
- package/data/build/dist/host/debug.d.ts +79 -0
- package/data/build/dist/host/debug.js +295 -0
- package/data/build/dist/host/dsh-device-runtime.d.ts +39 -0
- package/data/build/dist/host/dsh-device-runtime.js +118 -0
- package/data/build/dist/host/dsh-gateway-feature.d.ts +5 -0
- package/data/build/dist/host/dsh-gateway-feature.js +55 -0
- package/data/build/dist/host/dsh-runtime-version.d.ts +12 -0
- package/data/build/dist/host/dsh-runtime-version.js +69 -0
- package/data/build/dist/host/features/auth.d.ts +11 -0
- package/data/build/dist/host/features/auth.js +273 -0
- package/data/build/dist/host/features/debug.d.ts +5 -0
- package/data/build/dist/host/features/debug.js +90 -0
- package/data/build/dist/host/features/host-status.d.ts +12 -0
- package/data/build/dist/host/features/host-status.js +54 -0
- package/data/build/dist/host/features/index.d.ts +19 -0
- package/data/build/dist/host/features/index.js +29 -0
- package/data/build/dist/host/features/lan-access-dsh.d.ts +8 -0
- package/data/build/dist/host/features/lan-access-dsh.js +45 -0
- package/data/build/dist/host/features/terminal-process.d.ts +5 -0
- package/data/build/dist/host/features/terminal-process.js +110 -0
- package/data/build/dist/host/features/terminal-status.d.ts +18 -0
- package/data/build/dist/host/features/terminal-status.js +56 -0
- package/data/build/dist/host/features/types.d.ts +41 -0
- package/data/build/dist/host/features/types.js +2 -0
- package/data/build/dist/host/index.d.ts +43 -0
- package/data/build/dist/host/index.js +135 -0
- package/data/build/dist/host/lan-access-dsh.d.ts +83 -0
- package/data/build/dist/host/lan-access-dsh.js +831 -0
- package/data/build/dist/host/native-session-bridge.d.ts +130 -0
- package/data/build/dist/host/native-session-bridge.js +317 -0
- package/data/build/dist/host/relay-tunnel-runtime.d.ts +97 -0
- package/data/build/dist/host/relay-tunnel-runtime.js +361 -0
- package/data/build/dist/host/remote-web-runtime.d.ts +78 -0
- package/data/build/dist/host/remote-web-runtime.js +559 -0
- package/data/build/dist/host/rpc-table.d.ts +32 -0
- package/data/build/dist/host/rpc-table.js +47 -0
- package/data/build/dist/host/rpc.d.ts +18 -0
- package/data/build/dist/host/rpc.js +248 -0
- package/data/build/dist/host/settings.d.ts +19 -0
- package/data/build/dist/host/settings.js +80 -0
- package/data/build/dist/host/terminal/attachment-registry.d.ts +17 -0
- package/data/build/dist/host/terminal/attachment-registry.js +49 -0
- package/data/build/dist/host/terminal/backends/conpty-backend.d.ts +39 -0
- package/data/build/dist/host/terminal/backends/conpty-backend.js +160 -0
- package/data/build/dist/host/terminal/backends/local-pty-backend.d.ts +36 -0
- package/data/build/dist/host/terminal/backends/local-pty-backend.js +178 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.d.ts +40 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.js +191 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-client.d.ts +27 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-client.js +154 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-process.d.ts +10 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-process.js +147 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-protocol.d.ts +92 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-protocol.js +134 -0
- package/data/build/dist/host/terminal/controller-factory.d.ts +37 -0
- package/data/build/dist/host/terminal/controller-factory.js +82 -0
- package/data/build/dist/host/terminal/index.d.ts +17 -0
- package/data/build/dist/host/terminal/index.js +17 -0
- package/data/build/dist/host/terminal/runtime-adapter.d.ts +77 -0
- package/data/build/dist/host/terminal/runtime-adapter.js +18 -0
- package/data/build/dist/host/terminal/runtime-manager.d.ts +41 -0
- package/data/build/dist/host/terminal/runtime-manager.js +179 -0
- package/data/build/dist/host/terminal/shell-detection.d.ts +23 -0
- package/data/build/dist/host/terminal/shell-detection.js +105 -0
- package/data/build/dist/host/terminal/startup.d.ts +33 -0
- package/data/build/dist/host/terminal/startup.js +100 -0
- package/data/build/dist/host/terminal/terminal-controller.d.ts +78 -0
- package/data/build/dist/host/terminal/terminal-controller.js +324 -0
- package/data/build/dist/host/terminal/terminal-process-service.d.ts +30 -0
- package/data/build/dist/host/terminal/terminal-process-service.js +194 -0
- package/data/build/dist/host/terminal/terminal-process-store.d.ts +45 -0
- package/data/build/dist/host/terminal/terminal-process-store.js +183 -0
- package/data/build/dist/host/terminal/terminal-service.d.ts +87 -0
- package/data/build/dist/host/terminal/terminal-service.js +528 -0
- package/data/build/dist/host/terminal/terminal-store.d.ts +49 -0
- package/data/build/dist/host/terminal/terminal-store.js +242 -0
- package/data/build/dist/index.d.ts +37 -0
- package/data/build/dist/index.js +26 -0
- package/data/build/dist/shared/contracts/auth.d.ts +122 -0
- package/data/build/dist/shared/contracts/auth.js +8 -0
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +192 -0
- package/data/build/dist/shared/contracts/cli-adapter.js +2 -0
- package/data/build/dist/shared/contracts/command-code.d.ts +5 -0
- package/data/build/dist/shared/contracts/command-code.js +2 -0
- package/data/build/dist/shared/contracts/config.d.ts +113 -0
- package/data/build/dist/shared/contracts/config.js +86 -0
- package/data/build/dist/shared/contracts/debug-config.d.ts +28 -0
- package/data/build/dist/shared/contracts/debug-config.js +78 -0
- package/data/build/dist/shared/contracts/debug.d.ts +75 -0
- package/data/build/dist/shared/contracts/debug.js +121 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +72 -0
- package/data/build/dist/shared/contracts/dsh-device.js +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +33 -0
- package/data/build/dist/shared/contracts/errors.js +41 -0
- package/data/build/dist/shared/contracts/feature.d.ts +71 -0
- package/data/build/dist/shared/contracts/feature.js +2 -0
- package/data/build/dist/shared/contracts/host-status.d.ts +14 -0
- package/data/build/dist/shared/contracts/host-status.js +2 -0
- package/data/build/dist/shared/contracts/lan-access-dsh.d.ts +30 -0
- package/data/build/dist/shared/contracts/lan-access-dsh.js +2 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +37 -0
- package/data/build/dist/shared/contracts/peer-host.js +9 -0
- package/data/build/dist/shared/contracts/signaling.d.ts +67 -0
- package/data/build/dist/shared/contracts/signaling.js +2 -0
- package/data/build/dist/shared/contracts/subscription.d.ts +69 -0
- package/data/build/dist/shared/contracts/subscription.js +2 -0
- package/data/build/dist/shared/contracts/terminal-process.d.ts +65 -0
- package/data/build/dist/shared/contracts/terminal-process.js +2 -0
- package/data/build/dist/shared/contracts/terminal.d.ts +127 -0
- package/data/build/dist/shared/contracts/terminal.js +2 -0
- package/data/build/dist/shared/contracts/transport.d.ts +45 -0
- package/data/build/dist/shared/contracts/transport.js +3 -0
- package/data/build/dist/shared/contracts/version.d.ts +19 -0
- package/data/build/dist/shared/contracts/version.js +74 -0
- package/data/build/dist/shared/index.d.ts +22 -0
- package/data/build/dist/shared/index.js +9 -0
- package/data/build/dist/transport/carrier.d.ts +38 -0
- package/data/build/dist/transport/carrier.js +268 -0
- package/data/build/dist/transport/debug.d.ts +22 -0
- package/data/build/dist/transport/debug.js +57 -0
- package/data/build/dist/transport/dsh-envelope.d.ts +41 -0
- package/data/build/dist/transport/dsh-envelope.js +107 -0
- package/data/build/dist/transport/dsh-gateway.d.ts +64 -0
- package/data/build/dist/transport/dsh-gateway.js +254 -0
- package/data/build/dist/transport/dsh-session.d.ts +55 -0
- package/data/build/dist/transport/dsh-session.js +256 -0
- package/data/build/dist/transport/dsh-transport.d.ts +57 -0
- package/data/build/dist/transport/dsh-transport.js +125 -0
- package/data/build/dist/transport/frame.d.ts +132 -0
- package/data/build/dist/transport/frame.js +168 -0
- package/data/build/dist/transport/index.d.ts +11 -0
- package/data/build/dist/transport/index.js +11 -0
- package/data/build/dist/transport/multiplexer.d.ts +63 -0
- package/data/build/dist/transport/multiplexer.js +323 -0
- package/data/build/dist/transport/webrtc-client.d.ts +65 -0
- package/data/build/dist/transport/webrtc-client.js +255 -0
- package/data/build/dist/transport/webrtc-host.d.ts +58 -0
- package/data/build/dist/transport/webrtc-host.js +252 -0
- package/data/build/dist/typert.host.d.ts +25 -0
- package/data/build/dist/typert.host.js +150 -0
- package/dsh.bundle.patch +12 -0
- package/package.json +135 -0
- package/profile/README.md +27 -0
- package/profile/cordis.patch.yml +2 -0
- package/profile/package.json +20 -0
- package/profile/scripts/check-dsh-install.mjs +71 -0
- package/profile/version.json +5 -0
- package/scripts/check-dsh-install.mjs +88 -0
- package/version.json +6 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
import { connect, createServer } from 'node:net';
|
|
2
|
+
import { networkInterfaces } from 'node:os';
|
|
3
|
+
import { Transform } from 'node:stream';
|
|
4
|
+
import { createHmac, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { CodingNsRpcError } from './rpc-table.js';
|
|
9
|
+
/** 浏览器中继只携带短期签名票据,密码哈希和签名密钥始终留在 Host。 */
|
|
10
|
+
export async function openLoginProtectionSession(store, username, password, scope) {
|
|
11
|
+
const config = await store.read();
|
|
12
|
+
if (config === null || !config.enabled || !config.scopes[scope])
|
|
13
|
+
return { token: '', expiresAt: new Date().toISOString() };
|
|
14
|
+
if (username !== config.username || !verifyPassword(password, config))
|
|
15
|
+
throw new CodingNsRpcError('CODINGNS_RPC_UNAUTHENTICATED', '本地用户名或密码错误');
|
|
16
|
+
const expiresAt = Date.now() + config.timeoutSeconds * 1000;
|
|
17
|
+
const payload = encodeSessionPayload({ username: config.username, scope, expiresAt, nonce: randomBytes(16).toString('base64url') });
|
|
18
|
+
return { token: `${payload}.${signSessionPayload(payload, config)}`, expiresAt: new Date(expiresAt).toISOString() };
|
|
19
|
+
}
|
|
20
|
+
/** 校验中继票据;配置关闭或未覆盖该范围时保持向后兼容,直接放行。 */
|
|
21
|
+
export async function verifyLoginProtectionSession(store, token, scope) {
|
|
22
|
+
const config = await store.read();
|
|
23
|
+
if (config === null || !config.enabled || !config.scopes[scope])
|
|
24
|
+
return true;
|
|
25
|
+
return typeof token === 'string' && verifySignedSessionToken(token, config, scope);
|
|
26
|
+
}
|
|
27
|
+
/** 测试和嵌入式宿主使用的内存凭据存储。 */
|
|
28
|
+
export class InMemoryLanAccessDshLoginStore {
|
|
29
|
+
value = null;
|
|
30
|
+
async read() { return this.value === null ? null : { ...this.value }; }
|
|
31
|
+
async write(record) { this.value = { ...record }; }
|
|
32
|
+
async clear() { this.value = null; }
|
|
33
|
+
}
|
|
34
|
+
/** 登录保护凭据与 Codingns4DSH refresh token 分离保存,文件权限限制为当前用户。 */
|
|
35
|
+
export class FileLanAccessDshLoginStore {
|
|
36
|
+
filePath;
|
|
37
|
+
constructor(filePath = join(defaultStateDir(), 'lan-access-login.json')) {
|
|
38
|
+
this.filePath = filePath;
|
|
39
|
+
}
|
|
40
|
+
async read() {
|
|
41
|
+
try {
|
|
42
|
+
return parseLoginRecord(JSON.parse(await readFile(this.filePath, 'utf8')));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (isNodeError(error, 'ENOENT'))
|
|
46
|
+
return null;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async write(record) {
|
|
51
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
52
|
+
const temp = `${this.filePath}.${process.pid}.tmp`;
|
|
53
|
+
await writeFile(temp, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
54
|
+
await rename(temp, this.filePath);
|
|
55
|
+
}
|
|
56
|
+
async clear() {
|
|
57
|
+
try {
|
|
58
|
+
await (await import('node:fs/promises')).unlink(this.filePath);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!isNodeError(error, 'ENOENT'))
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function defaultStateDir() {
|
|
67
|
+
const configured = process.env.CODINGNS4DSH_STATE_DIR?.trim();
|
|
68
|
+
return configured === undefined || configured === '' ? join(homedir(), '.config', 'codingns4dsh') : configured;
|
|
69
|
+
}
|
|
70
|
+
const LISTEN_HOSTS = new Set(['127.0.0.1', '0.0.0.0', '::1', '::']);
|
|
71
|
+
/** Host 侧默认运行时:枚举网卡、读取 DSH 启动参数并建立 TCP 转发。 */
|
|
72
|
+
export function createNodeLanAccessDshRuntime(dshWebPort) {
|
|
73
|
+
return {
|
|
74
|
+
listListenHosts: () => {
|
|
75
|
+
const addresses = new Set(['0.0.0.0', '127.0.0.1']);
|
|
76
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
77
|
+
for (const entry of entries ?? []) {
|
|
78
|
+
if (entry.address !== '127.0.0.1' && entry.address !== '::1')
|
|
79
|
+
addresses.add(entry.address);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...addresses];
|
|
83
|
+
},
|
|
84
|
+
detectDshPorts: async () => detectDshPortsFromRuntime(dshWebPort),
|
|
85
|
+
listen: (config, onConnection) => new Promise((resolve, reject) => {
|
|
86
|
+
const server = createServer((socket) => onConnection(socket));
|
|
87
|
+
const onError = (error) => {
|
|
88
|
+
server.removeListener('error', onError);
|
|
89
|
+
reject(error);
|
|
90
|
+
};
|
|
91
|
+
server.once('error', onError);
|
|
92
|
+
server.listen({ port: config.listenPort, host: config.listenHost }, () => {
|
|
93
|
+
server.removeListener('error', onError);
|
|
94
|
+
const address = server.address();
|
|
95
|
+
if (address === null || typeof address === 'string') {
|
|
96
|
+
reject(new Error('局域网访问 DSH 监听器未返回有效端口'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
resolve({
|
|
100
|
+
actualPort: address.port,
|
|
101
|
+
close: () => new Promise((resolveClose, rejectClose) => {
|
|
102
|
+
server.close((error) => error ? rejectClose(error) : resolveClose());
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}),
|
|
107
|
+
connect: (dshPort, onConnect, onError) => {
|
|
108
|
+
const socket = connect({ port: dshPort, host: '127.0.0.1' });
|
|
109
|
+
socket.once('connect', () => onConnect(socket));
|
|
110
|
+
socket.once('error', (error) => onError(error instanceof Error ? error : new Error(String(error))));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** 管理唯一一条“监听地址/端口 -> 当前 DSH Web 端口”的映射。 */
|
|
115
|
+
export class LanAccessDshProxy {
|
|
116
|
+
runtime;
|
|
117
|
+
authenticatedUrl;
|
|
118
|
+
active = null;
|
|
119
|
+
detectedDshPorts = [];
|
|
120
|
+
loginConfig = null;
|
|
121
|
+
/** 当前进程内主动退出的会话;正常会话由签名令牌承载,可跨代理重建复用。 */
|
|
122
|
+
revokedSessions = new Set();
|
|
123
|
+
upstreamCookie = null;
|
|
124
|
+
constructor(runtime = createNodeLanAccessDshRuntime(), authenticatedUrl) {
|
|
125
|
+
this.runtime = runtime;
|
|
126
|
+
this.authenticatedUrl = authenticatedUrl;
|
|
127
|
+
}
|
|
128
|
+
listenHosts() {
|
|
129
|
+
return this.runtime.listListenHosts();
|
|
130
|
+
}
|
|
131
|
+
async detect() {
|
|
132
|
+
this.detectedDshPorts = uniquePorts(await this.runtime.detectDshPorts());
|
|
133
|
+
return this.detectedDshPorts;
|
|
134
|
+
}
|
|
135
|
+
async start(input) {
|
|
136
|
+
const dshPort = input.dshPort ?? (await this.resolveDetectedPort());
|
|
137
|
+
const config = normalizeLanAccessDshConfig({
|
|
138
|
+
listenHost: input.listenHost ?? '0.0.0.0',
|
|
139
|
+
listenPort: input.listenPort ?? 13080,
|
|
140
|
+
dshPort,
|
|
141
|
+
...(input.login === undefined
|
|
142
|
+
? (this.loginConfig === null ? {} : { login: this.loginConfig })
|
|
143
|
+
: { login: normalizeLoginConfig(input.login) }),
|
|
144
|
+
}, this.listenHosts());
|
|
145
|
+
this.setLoginConfig(config.login ?? null);
|
|
146
|
+
if (this.active)
|
|
147
|
+
await this.stop();
|
|
148
|
+
const active = {
|
|
149
|
+
config,
|
|
150
|
+
actualListenPort: 0,
|
|
151
|
+
close: async () => undefined,
|
|
152
|
+
sockets: new Set(),
|
|
153
|
+
state: 'starting',
|
|
154
|
+
error: null,
|
|
155
|
+
};
|
|
156
|
+
this.active = active;
|
|
157
|
+
try {
|
|
158
|
+
const listener = await this.runtime.listen(config, (socket) => this.accept(active, socket));
|
|
159
|
+
active.actualListenPort = listener.actualPort;
|
|
160
|
+
active.close = listener.close;
|
|
161
|
+
active.state = 'listening';
|
|
162
|
+
try {
|
|
163
|
+
await this.takeOverDshWebToken();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
// Token 交换失败不能撤销已经绑定的局域网端口;保留监听并记录错误,后续请求会收到上游认证状态。
|
|
167
|
+
active.error = error instanceof Error ? error.message : String(error);
|
|
168
|
+
console.error('codingns4dsh: DSH Web Token 接管失败,局域网监听仍保持可用', error);
|
|
169
|
+
}
|
|
170
|
+
return this.snapshot(active);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.active = null;
|
|
174
|
+
active.state = 'error';
|
|
175
|
+
active.error = error instanceof Error ? error.message : String(error);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async stop() {
|
|
180
|
+
const active = this.active;
|
|
181
|
+
if (!active)
|
|
182
|
+
return;
|
|
183
|
+
this.active = null;
|
|
184
|
+
active.state = 'stopped';
|
|
185
|
+
for (const socket of active.sockets)
|
|
186
|
+
socket.destroy();
|
|
187
|
+
active.sockets.clear();
|
|
188
|
+
this.revokedSessions.clear();
|
|
189
|
+
await active.close();
|
|
190
|
+
}
|
|
191
|
+
async close() {
|
|
192
|
+
await this.stop();
|
|
193
|
+
}
|
|
194
|
+
get() {
|
|
195
|
+
return this.active ? this.snapshot(this.active) : null;
|
|
196
|
+
}
|
|
197
|
+
setLoginConfig(config) {
|
|
198
|
+
this.loginConfig = config === null ? null : normalizeLoginConfig(config);
|
|
199
|
+
if (config === null || !config.enabled)
|
|
200
|
+
this.revokedSessions.clear();
|
|
201
|
+
}
|
|
202
|
+
loginSettings() {
|
|
203
|
+
const config = this.loginConfig;
|
|
204
|
+
return {
|
|
205
|
+
enabled: config?.enabled === true,
|
|
206
|
+
username: config?.username ?? '',
|
|
207
|
+
passwordConfigured: config !== null && config.passwordHash.length > 0,
|
|
208
|
+
timeoutSeconds: config?.timeoutSeconds ?? 1800,
|
|
209
|
+
scopes: config?.scopes ?? defaultLoginScopes(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async resolveDetectedPort() {
|
|
213
|
+
const ports = this.detectedDshPorts.length > 0 ? this.detectedDshPorts : await this.detect();
|
|
214
|
+
if (ports.length === 0)
|
|
215
|
+
throw new LanAccessDshError('DSH_PORT_NOT_FOUND', '未能自动探测 DSH Web 端口,请手动填写');
|
|
216
|
+
if (ports.length > 1)
|
|
217
|
+
throw new LanAccessDshError('DSH_PORT_AMBIGUOUS', `自动探测到多个 DSH Web 端口,请手动选择: ${ports.join(', ')}`);
|
|
218
|
+
return ports[0];
|
|
219
|
+
}
|
|
220
|
+
accept(active, localSocket) {
|
|
221
|
+
if (active.state !== 'listening') {
|
|
222
|
+
localSocket.destroy();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
active.sockets.add(localSocket);
|
|
226
|
+
const removeLocal = () => { active.sockets.delete(localSocket); };
|
|
227
|
+
localSocket.once('close', removeLocal);
|
|
228
|
+
localSocket.once('error', removeLocal);
|
|
229
|
+
this.runtime.connect(active.config.dshPort, (dshSocket) => {
|
|
230
|
+
if (active.state !== 'listening') {
|
|
231
|
+
dshSocket.destroy();
|
|
232
|
+
localSocket.destroy();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
active.sockets.add(dshSocket);
|
|
236
|
+
const removeDsh = () => { active.sockets.delete(dshSocket); };
|
|
237
|
+
dshSocket.once('close', removeDsh);
|
|
238
|
+
dshSocket.once('error', removeDsh);
|
|
239
|
+
// DSH 的 API 信任校验依据上游看到的 Host/Origin。局域网入口的地址
|
|
240
|
+
// 与真实 DSH 地址不同,直接透传会导致静态页面能打开但所有 /api 返回 403。
|
|
241
|
+
// 只改写请求头,响应和 WebSocket 帧仍然原样双向转发。测试运行时的最小
|
|
242
|
+
// FakeStream 没有 Node Writable 接口,保留原始 pipe 以便验证连接关系。
|
|
243
|
+
if (typeof dshSocket.on === 'function') {
|
|
244
|
+
const localAddress = isLoopbackSocket(localSocket);
|
|
245
|
+
const authTransform = new LanAccessDshAuthTransform(localSocket, (request) => this.authorize(request, localAddress));
|
|
246
|
+
const requestTransform = new LanAccessDshRequestTransform(`127.0.0.1:${active.config.dshPort}`, this.upstreamCookie);
|
|
247
|
+
localSocket.pipe(authTransform);
|
|
248
|
+
authTransform.pipe(requestTransform);
|
|
249
|
+
requestTransform.pipe(dshSocket);
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
localSocket.pipe(dshSocket);
|
|
253
|
+
dshSocket.pipe(localSocket);
|
|
254
|
+
}, (error) => localSocket.destroy(error));
|
|
255
|
+
}
|
|
256
|
+
snapshot(active) {
|
|
257
|
+
const { login: _login, ...publicConfig } = active.config;
|
|
258
|
+
return {
|
|
259
|
+
...publicConfig,
|
|
260
|
+
state: active.state,
|
|
261
|
+
actualListenPort: active.actualListenPort || null,
|
|
262
|
+
detectedDshPorts: this.detectedDshPorts,
|
|
263
|
+
error: active.error,
|
|
264
|
+
loginEnabled: this.loginConfig?.enabled === true,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
authorize(request, localAddress) {
|
|
268
|
+
const config = this.loginConfig;
|
|
269
|
+
if (localAddress || config === null || !config.enabled || !config.scopes.lan)
|
|
270
|
+
return 'pass';
|
|
271
|
+
if (request.path === '/__codingns/login' && request.method === 'POST') {
|
|
272
|
+
const form = new URLSearchParams(new TextDecoder().decode(request.body));
|
|
273
|
+
if (form.get('username') !== config.username || !verifyPassword(form.get('password') ?? '', config)) {
|
|
274
|
+
return loginResponse(401, loginPageWithError('用户名或密码错误', form.get('username') ?? ''));
|
|
275
|
+
}
|
|
276
|
+
const expiresAt = Date.now() + config.timeoutSeconds * 1000;
|
|
277
|
+
const payload = encodeSessionPayload({ username: config.username, scope: 'lan', expiresAt, nonce: randomBytes(16).toString('base64url') });
|
|
278
|
+
const token = `${payload}.${signSessionPayload(payload, config)}`;
|
|
279
|
+
this.revokedSessions.delete(token);
|
|
280
|
+
return loginResponse(303, '', { 'Set-Cookie': sessionCookie(token, config.timeoutSeconds), Location: '/' });
|
|
281
|
+
}
|
|
282
|
+
if (request.path === '/__codingns/logout') {
|
|
283
|
+
const token = readCookie(request.headers.cookie, 'codingns4dsh_session');
|
|
284
|
+
if (token !== undefined && verifySignedSessionToken(token, config, 'lan'))
|
|
285
|
+
this.revokedSessions.add(token);
|
|
286
|
+
return loginResponse(303, '', { 'Set-Cookie': 'codingns4dsh_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0', Location: '/' });
|
|
287
|
+
}
|
|
288
|
+
// 浏览器会在页面刷新时独立请求 Web App manifest;它只包含静态元数据,
|
|
289
|
+
// 不应因为登录保护缺少 Cookie 而产生 401 控制台噪声。
|
|
290
|
+
if (request.path === '/manifest.webmanifest' && request.method === 'GET')
|
|
291
|
+
return 'pass';
|
|
292
|
+
const token = readCookie(request.headers.cookie, 'codingns4dsh_session');
|
|
293
|
+
if (token !== undefined && !this.revokedSessions.has(token) && verifySignedSessionToken(token, config, 'lan')) {
|
|
294
|
+
return 'pass';
|
|
295
|
+
}
|
|
296
|
+
return request.path === '/' || request.path.endsWith('.html')
|
|
297
|
+
? loginResponse(200, loginPage())
|
|
298
|
+
: loginResponse(401, '需要登录');
|
|
299
|
+
}
|
|
300
|
+
/** DSH 启动时的认证 URL 只在 Host 内交换一次 Cookie,绝不下发到浏览器。 */
|
|
301
|
+
async takeOverDshWebToken() {
|
|
302
|
+
if (this.authenticatedUrl === undefined)
|
|
303
|
+
return;
|
|
304
|
+
const response = await fetch(this.authenticatedUrl, { redirect: 'manual', signal: AbortSignal.timeout(5000) });
|
|
305
|
+
const cookie = getSetCookie(response.headers);
|
|
306
|
+
if (response.status !== 303 || cookie === undefined)
|
|
307
|
+
throw new Error(`DSH Web Token 接管失败 (${response.status})`);
|
|
308
|
+
this.upstreamCookie = cookie;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function isLoopbackSocket(socket) {
|
|
312
|
+
const remoteAddress = socket.remoteAddress;
|
|
313
|
+
return remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1';
|
|
314
|
+
}
|
|
315
|
+
/** 在 TCP 转发前完成登录校验;未通过时直接向局域网客户端返回页面/错误。 */
|
|
316
|
+
class LanAccessDshAuthTransform extends Transform {
|
|
317
|
+
client;
|
|
318
|
+
authorize;
|
|
319
|
+
pending = new Uint8Array(0);
|
|
320
|
+
handled = false;
|
|
321
|
+
constructor(client, authorize) {
|
|
322
|
+
super();
|
|
323
|
+
this.client = client;
|
|
324
|
+
this.authorize = authorize;
|
|
325
|
+
}
|
|
326
|
+
_transform(chunk, _encoding, callback) {
|
|
327
|
+
if (this.handled) {
|
|
328
|
+
callback(null, chunk);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.pending = concatBytes(this.pending, chunk);
|
|
332
|
+
const end = findHeaderEnd(this.pending);
|
|
333
|
+
if (end < 0) {
|
|
334
|
+
if (this.pending.length > 64 * 1024)
|
|
335
|
+
this.finish(loginResponse(431, '请求头过大'));
|
|
336
|
+
callback();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const head = this.pending.subarray(0, end + 4);
|
|
340
|
+
const headers = parseHeaders(head);
|
|
341
|
+
const bodyLength = contentLengthOf(head);
|
|
342
|
+
if (this.pending.length < end + 4 + bodyLength) {
|
|
343
|
+
callback();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const requestLine = decodeLatin1(head).split('\r\n', 1)[0] ?? '';
|
|
347
|
+
const parts = requestLine.split(' ');
|
|
348
|
+
const request = {
|
|
349
|
+
method: parts[0] ?? 'GET',
|
|
350
|
+
path: (parts[1] ?? '/').split('?', 1)[0] ?? '/',
|
|
351
|
+
headers,
|
|
352
|
+
body: this.pending.subarray(end + 4, end + 4 + bodyLength),
|
|
353
|
+
};
|
|
354
|
+
const decision = this.authorize(request);
|
|
355
|
+
if (decision === 'pass') {
|
|
356
|
+
this.handled = true;
|
|
357
|
+
this.push(this.pending);
|
|
358
|
+
this.pending = new Uint8Array(0);
|
|
359
|
+
}
|
|
360
|
+
else
|
|
361
|
+
this.finish(decision);
|
|
362
|
+
callback();
|
|
363
|
+
}
|
|
364
|
+
_flush(callback) { callback(); }
|
|
365
|
+
finish(response) {
|
|
366
|
+
if (this.handled)
|
|
367
|
+
return;
|
|
368
|
+
this.handled = true;
|
|
369
|
+
const writable = this.client;
|
|
370
|
+
// 让 Node Socket 自己 flush 完响应后关闭,不能 end 后立即 destroy,否则登录页可能被截断。
|
|
371
|
+
writable.end?.(response);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function parseHeaders(input) {
|
|
375
|
+
const result = {};
|
|
376
|
+
for (const line of decodeLatin1(input).split('\r\n').slice(1)) {
|
|
377
|
+
const separator = line.indexOf(':');
|
|
378
|
+
if (separator > 0)
|
|
379
|
+
result[line.slice(0, separator).trim().toLowerCase()] = line.slice(separator + 1).trim();
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function readCookie(value, name) {
|
|
384
|
+
for (const item of (value ?? '').split(';')) {
|
|
385
|
+
const separator = item.indexOf('=');
|
|
386
|
+
if (separator > 0 && item.slice(0, separator).trim() === name)
|
|
387
|
+
return item.slice(separator + 1).trim();
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
function sessionCookie(token, timeoutSeconds) {
|
|
392
|
+
return `codingns4dsh_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${timeoutSeconds}`;
|
|
393
|
+
}
|
|
394
|
+
function getSetCookie(headers) {
|
|
395
|
+
const extended = headers;
|
|
396
|
+
const values = extended.getSetCookie?.() ?? (headers.get('set-cookie') === null ? [] : [headers.get('set-cookie')]);
|
|
397
|
+
return values.map((value) => value.split(';', 1)[0]).find((value) => value !== '');
|
|
398
|
+
}
|
|
399
|
+
function loginResponse(status, body, extra = {}) {
|
|
400
|
+
const isHtml = body.startsWith('<!doctype html>');
|
|
401
|
+
const content = new TextEncoder().encode(isHtml ? body : escapeHtml(body));
|
|
402
|
+
const headers = {
|
|
403
|
+
'Content-Type': isHtml ? 'text/html; charset=utf-8' : 'text/plain; charset=utf-8',
|
|
404
|
+
'Content-Length': String(content.length),
|
|
405
|
+
'Cache-Control': 'no-store',
|
|
406
|
+
'X-Content-Type-Options': 'nosniff',
|
|
407
|
+
'X-Frame-Options': 'DENY',
|
|
408
|
+
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'",
|
|
409
|
+
'Referrer-Policy': 'no-referrer',
|
|
410
|
+
Connection: 'close',
|
|
411
|
+
...extra,
|
|
412
|
+
};
|
|
413
|
+
const head = encodeLatin1(`HTTP/1.1 ${status} ${statusText(status)}\r\n${Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n')}\r\n\r\n`);
|
|
414
|
+
return concatBytes(head, content);
|
|
415
|
+
}
|
|
416
|
+
function loginPage() {
|
|
417
|
+
// 视觉规则直接复用 codingns4dsh-h5/styles.css 的 Cyber 登录区,只把 Connect
|
|
418
|
+
// 账号字段替换成本地用户名/密码;不能依赖外部 CSS,避免局域网入口离线时失效。
|
|
419
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>DSH Web | 本地登录</title><style>
|
|
420
|
+
:root{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;color:#f1f5f9;background:#0a0f1d}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0}body{overflow:hidden}.cyber-login-page{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:radial-gradient(ellipse at center,#0f172a 0%,#0a0f1d 100%);color:#f1f5f9}.cyber-login-page:before{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(59,130,246,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(59,130,246,.08) 1px,transparent 1px);background-size:50px 50px;transform:perspective(500px) rotateX(60deg);transform-origin:center top}.cyber-login-page:after{content:"";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.15) 2px,rgba(0,0,0,.15) 4px)}.cyber-login-container{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:30px;width:min(440px,calc(100% - 32px));padding:24px}.cyber-login-content{display:flex;flex-direction:column;align-items:center;gap:28px;width:100%}.cyber-brand{display:flex;flex-direction:column;align-items:center;gap:13px;text-align:center}.cyber-logo{display:grid;place-items:center;width:80px;height:80px;border:1px solid rgba(59,130,246,.48);border-radius:22px;color:#60a5fa;font-size:19px;font-weight:700;letter-spacing:2px;box-shadow:0 0 25px rgba(0,212,255,.35),inset 0 0 22px rgba(59,130,246,.18);transform:rotate(30deg)}.cyber-logo span{transform:rotate(-30deg)}.cyber-brand-title{margin:0;color:#f1f5f9;font-size:28px;font-weight:700;letter-spacing:4px}.cyber-brand-subtitle{margin:0;color:#94a3b8;font-size:12px;letter-spacing:1.5px}.cyber-card{width:100%;padding:30px;border:1px solid rgba(59,130,246,.28);border-radius:12px;background:rgba(30,41,59,.78);backdrop-filter:blur(10px);box-shadow:0 8px 32px rgba(0,0,0,.4),0 0 0 1px rgba(59,130,246,.28)}.cyber-card-header{display:flex;align-items:center;gap:12px;margin-bottom:22px}.cyber-line{flex:1;height:1px;background:linear-gradient(90deg,transparent,rgba(59,130,246,.48),transparent)}.cyber-card-label{color:#94a3b8;font-size:11px;font-weight:600;letter-spacing:3px;white-space:nowrap}.cyber-form{display:flex;flex-direction:column;gap:18px}.cyber-connect-hint{margin:0;color:#94a3b8;font-size:12px;line-height:1.7}.cyber-field{position:relative;padding:12px 16px;border:1px solid rgba(59,130,246,.28);border-radius:8px;background:rgba(15,23,42,.68)}.cyber-field-label{display:flex;align-items:center;gap:8px;margin-bottom:8px;color:#94a3b8;font-size:11px;letter-spacing:1px}.cyber-field-icon{color:#60a5fa}.cyber-input{width:100%;padding:0;border:0;outline:0;background:transparent;color:#f1f5f9;font:14px var(--font-mono)}.cyber-input::placeholder{color:#94a3b8;opacity:.55}.cyber-submit{position:relative;width:100%;min-height:48px;margin-top:4px;padding:14px 24px;overflow:hidden;border:0;border-radius:8px;background:linear-gradient(135deg,#3b82f6,#06b6d4);color:white;font:600 13px var(--font-mono);letter-spacing:2px;cursor:pointer;box-shadow:0 0 18px rgba(59,130,246,.32)}.cyber-submit:hover{filter:brightness(1.12)}.cyber-submit:active{transform:translateY(1px)}.cyber-submit:disabled{cursor:wait;opacity:.6}.cyber-submit-text{position:relative;display:flex;align-items:center;justify-content:center;gap:8px}.cyber-status{display:flex;align-items:center;gap:8px;margin:0;padding:10px 12px;border:1px solid rgba(239,68,68,.25);border-radius:6px;background:rgba(239,68,68,.1);color:#fca5a5;font-size:12px;line-height:1.5}.cyber-footer{margin-top:2px}.cyber-divider{display:flex;align-items:center;gap:12px;margin-bottom:14px}.cyber-divider-line{flex:1;height:1px;background:linear-gradient(90deg,transparent,rgba(59,130,246,.48),transparent)}.cyber-divider-text{color:#64748b;font-size:10px;letter-spacing:2px}.cyber-version{display:flex;align-items:center;gap:12px;color:#64748b;font-size:10px;letter-spacing:2px;opacity:.65}@media(prefers-color-scheme:light){.cyber-login-page{color:#0f172a;background:radial-gradient(ellipse at center,#f8fafc 0%,#eef4fb 100%)}.cyber-card{background:rgba(255,255,255,.86);box-shadow:0 8px 28px rgba(15,23,42,.12),0 0 0 1px rgba(37,99,235,.13)}.cyber-brand-title{color:#0f172a}.cyber-input{color:#0f172a}.cyber-connect-hint,.cyber-field-label,.cyber-card-label{color:#64748b}.cyber-field{background:rgba(241,245,249,.86)}}@media(max-width:520px){.cyber-login-container{width:100%;padding:20px 16px}.cyber-card{padding:24px 20px}.cyber-brand-subtitle{font-size:10px;letter-spacing:1px}}
|
|
421
|
+
</style></head><body><main class="cyber-login-page"><div class="cyber-login-container"><div class="cyber-login-content"><div class="cyber-brand"><div class="cyber-logo"><span>DSH</span></div><h1 class="cyber-brand-title">DSH Web</h1><p class="cyber-brand-subtitle">LOCAL SECURE DSH ENVIRONMENT</p></div><div class="cyber-card"><form class="cyber-form" method="post" action="/__codingns/login"><div class="cyber-card-header"><div class="cyber-line"></div><span class="cyber-card-label">LOCAL ACCESS</span><div class="cyber-line"></div></div><p class="cyber-connect-hint">使用本机设置的本地账号进入 DSH Web。</p><div class="cyber-field"><label class="cyber-field-label" for="login-username"><span class="cyber-field-icon" aria-hidden="true">⌁</span>用户名</label><input class="cyber-input" id="login-username" name="username" autocomplete="username" placeholder="输入本地用户名" required></div><div class="cyber-field"><label class="cyber-field-label" for="login-password"><span class="cyber-field-icon" aria-hidden="true">⚷</span>密码</label><input class="cyber-input" id="login-password" name="password" type="password" autocomplete="current-password" placeholder="输入本地密码" required></div><button class="cyber-submit" type="submit"><span class="cyber-submit-text"><span aria-hidden="true">➤</span>登录 DSH Web</span></button><div class="cyber-footer"><div class="cyber-divider"><span class="cyber-divider-line"></span><span class="cyber-divider-text">CODINGNS</span><span class="cyber-divider-line"></span></div></div></form></div></div><div class="cyber-version"><span>CODINGNS4DSH</span><span>|</span><span>LOCAL AUTH READY</span></div></div></main></body></html>`;
|
|
422
|
+
}
|
|
423
|
+
/** 登录失败时继续渲染完整登录页,避免浏览器落到无样式的纯文本错误页。 */
|
|
424
|
+
function loginPageWithError(message, username) {
|
|
425
|
+
const page = loginPage();
|
|
426
|
+
const status = `<p class="cyber-status" role="alert" aria-live="polite"><span aria-hidden="true">!</span>${escapeHtml(message)}</p>`;
|
|
427
|
+
return page
|
|
428
|
+
.replace('<p class="cyber-connect-hint">', `${status}<p class="cyber-connect-hint">`)
|
|
429
|
+
.replace('name="username" autocomplete="username"', `name="username" autocomplete="username" value="${escapeHtml(username)}"`);
|
|
430
|
+
}
|
|
431
|
+
function statusText(status) { return status === 200 ? 'OK' : status === 303 ? 'See Other' : status === 401 ? 'Unauthorized' : 'Request Error'; }
|
|
432
|
+
function escapeHtml(value) { return value.replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char] ?? char); }
|
|
433
|
+
/**
|
|
434
|
+
* 将局域网入口发往上游 DSH 的 HTTP 请求头改成上游自身的 authority。
|
|
435
|
+
*
|
|
436
|
+
* 代理是单用途的:每个普通 HTTP 请求都要求上游关闭连接,避免在不知道
|
|
437
|
+
* Content-Length/分块边界时误把 keep-alive 上的第二个请求当成正文。WebSocket
|
|
438
|
+
* 升级请求则保持 Upgrade,升级后的帧不再经过头部解析。
|
|
439
|
+
*/
|
|
440
|
+
class LanAccessDshRequestTransform extends Transform {
|
|
441
|
+
targetAuthority;
|
|
442
|
+
upstreamCookie;
|
|
443
|
+
pending = new Uint8Array(0);
|
|
444
|
+
bodyRemaining = 0;
|
|
445
|
+
finished = false;
|
|
446
|
+
constructor(targetAuthority, upstreamCookie = null) {
|
|
447
|
+
super();
|
|
448
|
+
this.targetAuthority = targetAuthority;
|
|
449
|
+
this.upstreamCookie = upstreamCookie;
|
|
450
|
+
}
|
|
451
|
+
_transform(chunk, _encoding, callback) {
|
|
452
|
+
if (this.finished) {
|
|
453
|
+
callback(null, chunk);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.pending = concatBytes(this.pending, chunk);
|
|
457
|
+
this.drainPending();
|
|
458
|
+
callback();
|
|
459
|
+
}
|
|
460
|
+
_flush(callback) {
|
|
461
|
+
if (this.pending.length > 0)
|
|
462
|
+
callback(null, this.pending);
|
|
463
|
+
else
|
|
464
|
+
callback();
|
|
465
|
+
}
|
|
466
|
+
drainPending() {
|
|
467
|
+
while (!this.finished) {
|
|
468
|
+
if (this.bodyRemaining > 0) {
|
|
469
|
+
const size = Math.min(this.bodyRemaining, this.pending.length);
|
|
470
|
+
if (size === 0)
|
|
471
|
+
return;
|
|
472
|
+
this.push(this.pending.subarray(0, size));
|
|
473
|
+
this.pending = this.pending.subarray(size);
|
|
474
|
+
this.bodyRemaining -= size;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const end = findHeaderEnd(this.pending);
|
|
478
|
+
if (end < 0) {
|
|
479
|
+
// 请求头不应超过这个上限;超过时直接透传,避免代理因异常请求无限缓存。
|
|
480
|
+
if (this.pending.length > 64 * 1024) {
|
|
481
|
+
this.finished = true;
|
|
482
|
+
this.push(this.pending);
|
|
483
|
+
this.pending = new Uint8Array(0);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const head = this.pending.subarray(0, end + 4);
|
|
488
|
+
this.pending = this.pending.subarray(end + 4);
|
|
489
|
+
const upgrade = isUpgradeRequest(head);
|
|
490
|
+
this.push(rewriteLanAccessDshRequestHeaders(head, this.targetAuthority, this.upstreamCookie ?? undefined));
|
|
491
|
+
if (upgrade) {
|
|
492
|
+
// WebSocket 头部之后全部是帧数据,不能再次进入 HTTP 头缓存。
|
|
493
|
+
this.finished = true;
|
|
494
|
+
if (this.pending.length > 0) {
|
|
495
|
+
this.push(this.pending);
|
|
496
|
+
this.pending = new Uint8Array(0);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// DSH 的普通请求均为无体 GET 或带 Content-Length 的 JSON POST。
|
|
501
|
+
// 分块上传无法安全识别下一条请求,遇到它时透传本连接剩余字节。
|
|
502
|
+
if (/\r\ntransfer-encoding:\s*[^\r\n]*\bchunked\b/iu.test(decodeLatin1(head))) {
|
|
503
|
+
this.finished = true;
|
|
504
|
+
if (this.pending.length > 0) {
|
|
505
|
+
this.push(this.pending);
|
|
506
|
+
this.pending = new Uint8Array(0);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.bodyRemaining = contentLengthOf(head);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** 改写请求中的 Host、Origin,并关闭普通 HTTP 上游连接。 */
|
|
515
|
+
export function rewriteLanAccessDshRequestHeaders(input, targetAuthority, upstreamCookie) {
|
|
516
|
+
const text = decodeLatin1(input);
|
|
517
|
+
const lines = text.split('\r\n');
|
|
518
|
+
const upgrade = isUpgradeRequest(input);
|
|
519
|
+
let hasConnection = false;
|
|
520
|
+
let hasCookie = false;
|
|
521
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
522
|
+
const line = lines[index];
|
|
523
|
+
if (line === undefined || line === '')
|
|
524
|
+
continue;
|
|
525
|
+
const separator = line.indexOf(':');
|
|
526
|
+
if (separator <= 0)
|
|
527
|
+
continue;
|
|
528
|
+
const name = line.slice(0, separator).toLowerCase();
|
|
529
|
+
if (name === 'host')
|
|
530
|
+
lines[index] = `Host: ${targetAuthority}`;
|
|
531
|
+
else if (name === 'origin')
|
|
532
|
+
lines[index] = `Origin: http://${targetAuthority}`;
|
|
533
|
+
else if (name === 'connection') {
|
|
534
|
+
hasConnection = true;
|
|
535
|
+
if (!upgrade)
|
|
536
|
+
lines[index] = 'Connection: close';
|
|
537
|
+
}
|
|
538
|
+
else if (name === 'cookie')
|
|
539
|
+
hasCookie = true;
|
|
540
|
+
}
|
|
541
|
+
if (upstreamCookie !== undefined) {
|
|
542
|
+
if (hasCookie) {
|
|
543
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
544
|
+
if (lines[index]?.toLowerCase().startsWith('cookie:'))
|
|
545
|
+
lines[index] = `Cookie: ${lines[index].slice(lines[index].indexOf(':') + 1).trim()}; ${upstreamCookie}`;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else
|
|
549
|
+
lines.splice(-2, 0, `Cookie: ${upstreamCookie}`);
|
|
550
|
+
}
|
|
551
|
+
if (!upgrade && !hasConnection)
|
|
552
|
+
lines.splice(-2, 0, 'Connection: close');
|
|
553
|
+
return encodeLatin1(lines.join('\r\n'));
|
|
554
|
+
}
|
|
555
|
+
function contentLengthOf(input) {
|
|
556
|
+
const match = /\r\ncontent-length:\s*(\d+)/iu.exec(decodeLatin1(input));
|
|
557
|
+
return match?.[1] === undefined ? 0 : Number(match[1]);
|
|
558
|
+
}
|
|
559
|
+
function isUpgradeRequest(input) {
|
|
560
|
+
const text = decodeLatin1(input);
|
|
561
|
+
return /\r\nconnection:\s*[^\r\n]*\bupgrade\b/iu.test(text)
|
|
562
|
+
&& /\r\nupgrade:\s*websocket\b/iu.test(text);
|
|
563
|
+
}
|
|
564
|
+
function concatBytes(first, second) {
|
|
565
|
+
const result = new Uint8Array(first.length + second.length);
|
|
566
|
+
result.set(first);
|
|
567
|
+
result.set(second, first.length);
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
function findHeaderEnd(input) {
|
|
571
|
+
for (let index = 0; index <= input.length - 4; index += 1) {
|
|
572
|
+
if (input[index] === 13 && input[index + 1] === 10 && input[index + 2] === 13 && input[index + 3] === 10)
|
|
573
|
+
return index;
|
|
574
|
+
}
|
|
575
|
+
return -1;
|
|
576
|
+
}
|
|
577
|
+
function decodeLatin1(input) {
|
|
578
|
+
return Array.from(input, (byte) => String.fromCharCode(byte)).join('');
|
|
579
|
+
}
|
|
580
|
+
function encodeLatin1(input) {
|
|
581
|
+
const result = new Uint8Array(input.length);
|
|
582
|
+
for (let index = 0; index < input.length; index += 1)
|
|
583
|
+
result[index] = input.charCodeAt(index) & 0xff;
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
export class LanAccessDshError extends Error {
|
|
587
|
+
code;
|
|
588
|
+
constructor(code, message) {
|
|
589
|
+
super(message);
|
|
590
|
+
this.code = code;
|
|
591
|
+
this.name = code;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
export function normalizeLanAccessDshConfig(value, allowedListenHosts) {
|
|
595
|
+
const listenHost = requireText(value.listenHost, 'listenHost');
|
|
596
|
+
const listenPort = requirePort(value.listenPort ?? 13080, 'listenPort', true);
|
|
597
|
+
const dshPort = requirePort(value.dshPort, 'dshPort', false);
|
|
598
|
+
const allowedHosts = allowedListenHosts === undefined ? LISTEN_HOSTS : new Set(allowedListenHosts);
|
|
599
|
+
if (!allowedHosts.has(listenHost))
|
|
600
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '监听地址必须来自本机网卡或 0.0.0.0');
|
|
601
|
+
return {
|
|
602
|
+
listenHost,
|
|
603
|
+
listenPort,
|
|
604
|
+
dshPort,
|
|
605
|
+
...(value.login === undefined ? {} : { login: normalizeLoginConfig(value.login) }),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
export function createLanAccessDshRpcHandler(proxy, settings, loginStore = new FileLanAccessDshLoginStore()) {
|
|
609
|
+
return async (action, payload) => {
|
|
610
|
+
switch (action) {
|
|
611
|
+
case 'addresses':
|
|
612
|
+
return proxy.listenHosts();
|
|
613
|
+
case 'detect':
|
|
614
|
+
return { ports: await proxy.detect() };
|
|
615
|
+
case 'get':
|
|
616
|
+
return proxy.get();
|
|
617
|
+
case 'settings/get':
|
|
618
|
+
return settings?.get().lanAccessDsh ?? defaultLanAccessDshSettings();
|
|
619
|
+
case 'settings/set': {
|
|
620
|
+
if (settings === undefined)
|
|
621
|
+
throw new CodingNsRpcError('CODINGNS_SETTINGS_UNAVAILABLE', 'Codingns4DSH 设置服务不可用');
|
|
622
|
+
const next = parseLanAccessDshSettings(payload, proxy.listenHosts());
|
|
623
|
+
await settings.update({ lanAccessDsh: next });
|
|
624
|
+
return settings.get().lanAccessDsh;
|
|
625
|
+
}
|
|
626
|
+
case 'login/get':
|
|
627
|
+
return proxy.loginSettings();
|
|
628
|
+
case 'login/set': {
|
|
629
|
+
const current = await loginStore.read();
|
|
630
|
+
const next = parseLoginSettings(payload, current);
|
|
631
|
+
if (next === null) {
|
|
632
|
+
await loginStore.clear();
|
|
633
|
+
proxy.setLoginConfig(null);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
await loginStore.write(next);
|
|
637
|
+
proxy.setLoginConfig(next);
|
|
638
|
+
}
|
|
639
|
+
const settings = proxy.loginSettings();
|
|
640
|
+
if (next !== null && next.scopes.relay && isRecord(payload) && typeof payload.password === 'string' && payload.password !== '') {
|
|
641
|
+
const sessionInput = parseLoginSessionPayload({ username: next.username, password: payload.password, scope: 'relay' });
|
|
642
|
+
return { ...settings, relaySession: await openLoginProtectionSession(loginStore, sessionInput.username, sessionInput.password, 'relay') };
|
|
643
|
+
}
|
|
644
|
+
return settings;
|
|
645
|
+
}
|
|
646
|
+
case 'login/session/open': {
|
|
647
|
+
const input = parseLoginSessionPayload(payload);
|
|
648
|
+
return openLoginProtectionSession(loginStore, input.username, input.password, input.scope);
|
|
649
|
+
}
|
|
650
|
+
case 'start':
|
|
651
|
+
return proxy.start(parseStartPayload(payload));
|
|
652
|
+
case 'stop':
|
|
653
|
+
await proxy.stop();
|
|
654
|
+
return { stopped: true };
|
|
655
|
+
default:
|
|
656
|
+
throw new CodingNsRpcError('CODINGNS_RPC_NOT_FOUND', `未知局域网访问 DSH RPC: lanAccessDsh/${action}`);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function parseLoginSessionPayload(value) {
|
|
661
|
+
if (!value || typeof value !== 'object')
|
|
662
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录会话参数必须是对象');
|
|
663
|
+
const input = value;
|
|
664
|
+
const username = requireLoginText(input.username, '用户名', 1, 128);
|
|
665
|
+
if (typeof input.password !== 'string' || input.password.length < 1 || input.password.length > 256)
|
|
666
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '密码格式无效');
|
|
667
|
+
const scope = input.scope;
|
|
668
|
+
if (scope !== 'lan' && scope !== 'relay')
|
|
669
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录会话应用范围无效');
|
|
670
|
+
return { username, password: input.password, scope };
|
|
671
|
+
}
|
|
672
|
+
function defaultLanAccessDshSettings() {
|
|
673
|
+
return { autoStart: false, listenHost: '0.0.0.0', listenPort: 13080, dshPort: 0 };
|
|
674
|
+
}
|
|
675
|
+
function parseLoginSettings(value, current) {
|
|
676
|
+
if (!value || typeof value !== 'object')
|
|
677
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录保护设置必须是对象');
|
|
678
|
+
const input = value;
|
|
679
|
+
if (input.enabled === false)
|
|
680
|
+
return null;
|
|
681
|
+
if (input.enabled !== true)
|
|
682
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录保护 enabled 必须是布尔值');
|
|
683
|
+
const username = requireLoginText(input.username, '用户名', 1, 128);
|
|
684
|
+
const timeoutSeconds = requireInteger(input.timeoutSeconds, '超时时间', 60, 604800);
|
|
685
|
+
const scopes = parseLoginScopes(input.scopes ?? current?.scopes);
|
|
686
|
+
const password = typeof input.password === 'string' ? input.password : '';
|
|
687
|
+
if (password === '' && current === null)
|
|
688
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '启用登录保护时必须设置密码');
|
|
689
|
+
if (password !== '' && (password.length < 8 || password.length > 256))
|
|
690
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '密码长度必须是 8 到 256 个字符');
|
|
691
|
+
const passwordSalt = password === '' ? current.passwordSalt : randomBytes(16).toString('hex');
|
|
692
|
+
const passwordHash = password === '' ? current.passwordHash : hashPassword(password, passwordSalt);
|
|
693
|
+
return { enabled: true, username, passwordHash, passwordSalt, timeoutSeconds, scopes };
|
|
694
|
+
}
|
|
695
|
+
function normalizeLoginConfig(value) {
|
|
696
|
+
if (typeof value !== 'object' || value === null)
|
|
697
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录保护配置无效');
|
|
698
|
+
return {
|
|
699
|
+
enabled: value.enabled === true,
|
|
700
|
+
username: requireLoginText(value.username, '用户名', 1, 128),
|
|
701
|
+
passwordHash: requireLoginText(value.passwordHash, '密码哈希', 1, 512),
|
|
702
|
+
passwordSalt: requireLoginText(value.passwordSalt, '密码盐', 1, 128),
|
|
703
|
+
timeoutSeconds: requireInteger(value.timeoutSeconds, '超时时间', 60, 604800),
|
|
704
|
+
scopes: parseLoginScopes(value.scopes),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function parseLoginRecord(value) {
|
|
708
|
+
return normalizeLoginConfig(value);
|
|
709
|
+
}
|
|
710
|
+
function hashPassword(password, salt) { return scryptSync(password, salt, 64).toString('hex'); }
|
|
711
|
+
function verifyPassword(password, config) {
|
|
712
|
+
try {
|
|
713
|
+
const expected = Buffer.from(config.passwordHash, 'hex');
|
|
714
|
+
const actual = Buffer.from(hashPassword(password, config.passwordSalt), 'hex');
|
|
715
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function encodeSessionPayload(value) {
|
|
722
|
+
return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
|
|
723
|
+
}
|
|
724
|
+
function signSessionPayload(payload, config) {
|
|
725
|
+
return createHmac('sha256', `${config.passwordSalt}:${config.passwordHash}`).update(payload).digest('base64url');
|
|
726
|
+
}
|
|
727
|
+
function verifySignedSessionToken(token, config, scope) {
|
|
728
|
+
if (token.length < 32 || token.length > 4096)
|
|
729
|
+
return false;
|
|
730
|
+
const separator = token.lastIndexOf('.');
|
|
731
|
+
if (separator <= 0)
|
|
732
|
+
return false;
|
|
733
|
+
const payload = token.slice(0, separator);
|
|
734
|
+
const signature = token.slice(separator + 1);
|
|
735
|
+
const expectedBytes = Buffer.from(signSessionPayload(payload, config));
|
|
736
|
+
const actualBytes = Buffer.from(signature);
|
|
737
|
+
if (expectedBytes.length !== actualBytes.length || !timingSafeEqual(expectedBytes, actualBytes))
|
|
738
|
+
return false;
|
|
739
|
+
try {
|
|
740
|
+
const value = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
741
|
+
return value.username === config.username && value.scope === scope && typeof value.nonce === 'string' && typeof value.expiresAt === 'number' && value.expiresAt > Date.now();
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function requireLoginText(value, field, min, max) {
|
|
748
|
+
if (typeof value !== 'string' || value.trim().length < min || value.trim().length > max || /[\u0000-\u001F\u007F]/u.test(value))
|
|
749
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', `${field} 格式无效`);
|
|
750
|
+
return value.trim();
|
|
751
|
+
}
|
|
752
|
+
function requireInteger(value, field, min, max) {
|
|
753
|
+
if (!Number.isInteger(value) || value < min || value > max)
|
|
754
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', `${field} 必须是 ${min} 到 ${max} 的整数`);
|
|
755
|
+
return value;
|
|
756
|
+
}
|
|
757
|
+
function defaultLoginScopes() { return { lan: true, relay: true }; }
|
|
758
|
+
function parseLoginScopes(value) {
|
|
759
|
+
if (value === undefined)
|
|
760
|
+
return defaultLoginScopes();
|
|
761
|
+
if (!value || typeof value !== 'object')
|
|
762
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录保护应用范围无效');
|
|
763
|
+
const input = value;
|
|
764
|
+
if (typeof input.lan !== 'boolean' || typeof input.relay !== 'boolean')
|
|
765
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录保护应用范围必须是布尔值');
|
|
766
|
+
if (!input.lan && !input.relay)
|
|
767
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '至少选择一个登录保护应用范围');
|
|
768
|
+
return { lan: input.lan, relay: input.relay };
|
|
769
|
+
}
|
|
770
|
+
function isNodeError(error, code) { return typeof error === 'object' && error !== null && 'code' in error && error.code === code; }
|
|
771
|
+
function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
772
|
+
function parseLanAccessDshSettings(value, allowedListenHosts) {
|
|
773
|
+
if (!value || typeof value !== 'object')
|
|
774
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '局域网访问 DSH 设置必须是对象');
|
|
775
|
+
const input = value;
|
|
776
|
+
if (typeof input.autoStart !== 'boolean')
|
|
777
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', 'autoStart 必须是布尔值');
|
|
778
|
+
const listenHost = requireText(input.listenHost, 'listenHost');
|
|
779
|
+
const listenPort = requirePort(input.listenPort, 'listenPort', true);
|
|
780
|
+
const dshPort = requirePort(input.dshPort, 'dshPort', true);
|
|
781
|
+
if (!new Set(allowedListenHosts).has(listenHost))
|
|
782
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '监听地址必须来自本机网卡或 0.0.0.0');
|
|
783
|
+
return { autoStart: input.autoStart, listenHost, listenPort, dshPort };
|
|
784
|
+
}
|
|
785
|
+
function parseStartPayload(value) {
|
|
786
|
+
if (!value || typeof value !== 'object')
|
|
787
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '局域网访问 DSH 参数必须是对象');
|
|
788
|
+
const input = value;
|
|
789
|
+
return {
|
|
790
|
+
...(typeof input.listenHost === 'string' ? { listenHost: input.listenHost } : {}),
|
|
791
|
+
...(typeof input.listenPort === 'number' ? { listenPort: input.listenPort } : {}),
|
|
792
|
+
...(typeof input.dshPort === 'number' ? { dshPort: input.dshPort } : {}),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function requireText(value, field) {
|
|
796
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
797
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', `${field} 不能为空`);
|
|
798
|
+
return value.trim();
|
|
799
|
+
}
|
|
800
|
+
function requirePort(value, field, allowZero) {
|
|
801
|
+
const minimum = allowZero ? 0 : 1;
|
|
802
|
+
if (!Number.isInteger(value) || value < minimum || value > 65535) {
|
|
803
|
+
throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', `${field} 必须是 ${minimum} 到 65535 的整数`);
|
|
804
|
+
}
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
807
|
+
function uniquePorts(ports) {
|
|
808
|
+
return [...new Set(ports.filter((port) => Number.isInteger(port) && port >= 1 && port <= 65535))];
|
|
809
|
+
}
|
|
810
|
+
function detectDshPortsFromRuntime(dshWebPort) {
|
|
811
|
+
const runtime = globalThis;
|
|
812
|
+
const values = [
|
|
813
|
+
dshWebPort,
|
|
814
|
+
runtime.__DSH_WEB_PORT__,
|
|
815
|
+
runtime.process?.env?.DSH_WEB_PORT,
|
|
816
|
+
runtime.process?.env?.DSH_PORT,
|
|
817
|
+
runtime.process?.env?.PORT,
|
|
818
|
+
];
|
|
819
|
+
const argv = Array.isArray(runtime.process?.argv) ? runtime.process.argv : [];
|
|
820
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
821
|
+
const value = argv[index];
|
|
822
|
+
if (value === '--port' || value === '--dsh-port' || value === '--web-port')
|
|
823
|
+
values.push(argv[index + 1]);
|
|
824
|
+
if (typeof value === 'string' && (value.startsWith('--port=') || value.startsWith('--dsh-port=') || value.startsWith('--web-port=')))
|
|
825
|
+
values.push(value.slice(value.indexOf('=') + 1));
|
|
826
|
+
}
|
|
827
|
+
return uniquePorts(values
|
|
828
|
+
.map((value) => typeof value === 'string' ? Number(value) : value)
|
|
829
|
+
.filter((value) => typeof value === 'number'));
|
|
830
|
+
}
|
|
831
|
+
//# sourceMappingURL=lan-access-dsh.js.map
|