jishushell 0.4.24 → 0.5.15
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/INSTALL-NOTICE +11 -0
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +90 -0
- package/apps/filebrowser-container.yaml +163 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +91 -90
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +91 -90
- package/apps/openclaw-binary.yaml +38 -1
- package/apps/openclaw-container.yaml +45 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +26 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +58 -7
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/app.js +79 -9
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/doctor.d.ts +12 -12
- package/dist/cli/doctor.js +242 -55
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/llm.d.ts +4 -3
- package/dist/cli/llm.js +4 -3
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/panel.d.ts +6 -5
- package/dist/cli/panel.js +10 -9
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.js.map +1 -1
- package/dist/install.js +3 -3
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +1 -1
- package/dist/routes/agent-apps.js +1 -1
- package/dist/routes/apps.js +44 -11
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +5 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +826 -17
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +10 -10
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +751 -20
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.js +4 -3
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/index.d.ts +1 -1
- package/dist/services/agent-apps/index.js +1 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
- package/dist/services/agent-apps/installers/adapter.js +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +3 -3
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +2 -2
- package/dist/services/agent-apps/types.js +1 -1
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +25 -1
- package/dist/services/app/app-manager.js +829 -150
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +7 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +20 -3
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app/provide-resolver.d.ts +29 -0
- package/dist/services/app/provide-resolver.js +112 -0
- package/dist/services/app/provide-resolver.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +177 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.d.ts +41 -0
- package/dist/services/capability-endpoint-validator.js +104 -0
- package/dist/services/capability-endpoint-validator.js.map +1 -0
- package/dist/services/capability-health.d.ts +16 -0
- package/dist/services/capability-health.js +121 -0
- package/dist/services/capability-health.js.map +1 -0
- package/dist/services/capability-registry.d.ts +106 -0
- package/dist/services/capability-registry.js +313 -0
- package/dist/services/capability-registry.js.map +1 -0
- package/dist/services/connection-apply.d.ts +91 -0
- package/dist/services/connection-apply.js +475 -0
- package/dist/services/connection-apply.js.map +1 -0
- package/dist/services/connection-resolver.d.ts +65 -0
- package/dist/services/connection-resolver.js +281 -0
- package/dist/services/connection-resolver.js.map +1 -0
- package/dist/services/connection-transactor.d.ts +39 -0
- package/dist/services/connection-transactor.js +351 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +138 -46
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +16 -2
- package/dist/services/llm-proxy/index.js +48 -44
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/probe.d.ts +6 -0
- package/dist/services/llm-proxy/probe.js +85 -0
- package/dist/services/llm-proxy/probe.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +1 -0
- package/dist/services/llm-proxy/ssrf.js +24 -9
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +428 -35
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +20 -1
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +30 -1
- package/dist/services/runtime/adapters/hermes.js +219 -6
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
- package/dist/services/runtime/adapters/openclaw.js +1171 -11
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
- package/dist/services/runtime/mcp-shims/firewall.js +129 -0
- package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +8 -0
- package/dist/services/runtime/migrations.js +100 -0
- package/dist/services/runtime/migrations.js.map +1 -1
- package/dist/services/runtime/types.d.ts +46 -0
- package/dist/services/setup-manager.js +99 -24
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.d.ts +27 -0
- package/dist/services/suggestions.js +133 -0
- package/dist/services/suggestions.js.map +1 -0
- package/dist/services/task-registry.js +4 -2
- package/dist/services/task-registry.js.map +1 -1
- package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
- package/dist/services/telemetry/device-fingerprint.js +1 -1
- package/dist/services/types-shim.d.ts +16 -0
- package/dist/services/types-shim.js +2 -0
- package/dist/services/types-shim.js.map +1 -0
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +231 -1
- package/dist/utils/instance-lock.d.ts +22 -0
- package/dist/utils/instance-lock.js +48 -0
- package/dist/utils/instance-lock.js.map +1 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +18 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
- package/public/assets/Settings-Cc-tYBil.js +1 -0
- package/public/assets/Setup-lGZEk5jq.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-BWnkJgZ1.js +2 -0
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +457 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/run.sh +4 -4
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
- package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
- package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
- package/public/assets/NewInstance-BCIrAd86.js +0 -1
- package/public/assets/Settings-xkDcduFz.js +0 -1
- package/public/assets/Setup-Cfuwj4gV.js +0 -1
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/index-DQsM6Joa.js +0 -19
- package/public/assets/providers-V-vwrExZ.js +0 -1
- package/public/assets/registry-B4UFJdpA.js +0 -2
package/dist/server.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import Fastify from "fastify";
|
|
2
2
|
import fastifyStatic from "@fastify/static";
|
|
3
3
|
import multipart from "@fastify/multipart";
|
|
4
|
-
import {
|
|
4
|
+
import { basename, dirname, join } from "path";
|
|
5
5
|
import { existsSync, realpathSync } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { request as httpRequest } from "http";
|
|
8
|
+
import { request as httpsRequest } from "https";
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
10
|
-
import { ensureDirs, ensurePanelConfig, getPanelConfig, migrateCatalogSchemaIfNeeded, migrateHermesShimOutIfNeeded, migrateOpenclawCatalogIfNeeded, migrateOpenclawImageTagIfNeeded, } from "./config.js";
|
|
11
|
+
import { ensureDirs, ensurePanelConfig, getPanelConfig, migrateCatalogSchemaIfNeeded, migrateHermesShimOutIfNeeded, migrateOpenclawCatalogIfNeeded, migrateOpenclawImageTagIfNeeded, getInternalMcpToken, } from "./config.js";
|
|
12
|
+
import { timingSafeEqual } from "node:crypto";
|
|
11
13
|
import { runStartupMigrations } from "./services/runtime/index.js";
|
|
12
14
|
import { verifyToken } from "./auth.js";
|
|
13
15
|
import { authRoutes } from "./routes/auth.js";
|
|
@@ -18,7 +20,15 @@ import { runtimeCatalogRoutes } from "./routes/runtime.js";
|
|
|
18
20
|
import { appRoutes } from "./routes/apps.js";
|
|
19
21
|
import { agentAppRoutes } from "./routes/agent-apps.js";
|
|
20
22
|
import { llmRoutes } from "./routes/llm.js";
|
|
23
|
+
import { filesRoutes } from "./routes/files.js";
|
|
24
|
+
import { internalRoutes } from "./routes/internal.js";
|
|
25
|
+
import { organizeRoutes } from "./routes/files-organize.js";
|
|
26
|
+
import { fileMountsRoutes } from "./routes/file-mounts.js";
|
|
27
|
+
import { webdavRoutes } from "./routes/webdav.js";
|
|
28
|
+
import { externalMountsRoutes } from "./routes/external-mounts.js";
|
|
29
|
+
import { FilesManager } from "./services/files-manager.js";
|
|
21
30
|
import * as appManager from "./services/app/app-manager.js";
|
|
31
|
+
import * as capabilityRegistry from "./services/capability-registry.js";
|
|
22
32
|
import backupRoutes from "./routes/backup.js";
|
|
23
33
|
import * as llmProxy from "./services/llm-proxy/index.js";
|
|
24
34
|
import * as nomadManager from "./services/nomad-manager.js";
|
|
@@ -29,6 +39,107 @@ import { checkUpdate } from "./services/panel-manager.js";
|
|
|
29
39
|
import { PROXY_IDENTITY_HEADERS } from "./constants.js";
|
|
30
40
|
import { supportsGatewayWebsocket } from "./services/runtime/instance.js";
|
|
31
41
|
const PUBLIC_PATHS = new Set(["/api/auth/status", "/api/auth/init", "/api/auth/login", "/api/setup/status", "/api/apps/validate-sudo-password"]);
|
|
42
|
+
/** Slug charset for /apps/:slug — same as instance ids. */
|
|
43
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
44
|
+
/** Regex for top-level app WebSocket upgrades (AI-FS v1 W2.5 PR-8). */
|
|
45
|
+
const APP_SLUG_WS_RE = /^\/apps\/([a-z0-9][a-z0-9-]{0,62})(\/.*)?$/;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the upstream host/port for a top-level /apps/:slug request using
|
|
48
|
+
* the capability registry. Resolution order:
|
|
49
|
+
* 1. `<slug>-ui` — canonical convention (e.g. filebrowser-ui)
|
|
50
|
+
* 2. `web-<slug>` — web-* convention (e.g. web-filebrowser)
|
|
51
|
+
* Returns the first entry with status === "running", or null if none found.
|
|
52
|
+
*/
|
|
53
|
+
function resolveAppSlugUpstream(slug) {
|
|
54
|
+
const candidates = [`${slug}-ui`, `web-${slug}`];
|
|
55
|
+
for (const cap of candidates) {
|
|
56
|
+
const providers = capabilityRegistry.listProviders(cap);
|
|
57
|
+
const running = providers.find((p) => p.status === "running");
|
|
58
|
+
if (running) {
|
|
59
|
+
return { host: running.host, port: running.hostPort };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function hasValidPanelUpgradeToken(req) {
|
|
65
|
+
const authHeader = req.headers.authorization || "";
|
|
66
|
+
const match = authHeader.match(/^Bearer\s+(\S+)$/);
|
|
67
|
+
let token = match?.[1] || "";
|
|
68
|
+
if (!token) {
|
|
69
|
+
const cookie = req.headers.cookie || "";
|
|
70
|
+
const cookieMatch = cookie.match(/(?:^|;\s*)jishushell_session=([^;]*)/);
|
|
71
|
+
token = cookieMatch?.[1] || "";
|
|
72
|
+
}
|
|
73
|
+
return Boolean(token && token.length <= 4096 && verifyToken(token));
|
|
74
|
+
}
|
|
75
|
+
function isLoopbackSocket(socket) {
|
|
76
|
+
const remoteIp = socket.remoteAddress || "";
|
|
77
|
+
return remoteIp === "127.0.0.1" || remoteIp === "::1" || remoteIp === "::ffff:127.0.0.1";
|
|
78
|
+
}
|
|
79
|
+
function joinCapabilityUpstreamPath(basePath, suffix) {
|
|
80
|
+
const normalizedBase = typeof basePath === "string" && basePath.trim()
|
|
81
|
+
? (basePath.startsWith("/") ? basePath : `/${basePath}`)
|
|
82
|
+
: "/";
|
|
83
|
+
const normalizedSuffix = suffix.replace(/^\/+/, "");
|
|
84
|
+
if (!normalizedSuffix)
|
|
85
|
+
return normalizedBase;
|
|
86
|
+
return `${normalizedBase.replace(/\/+$/, "")}/${normalizedSuffix}`;
|
|
87
|
+
}
|
|
88
|
+
export function stripPanelSessionCookie(value) {
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
const cookie = Array.isArray(value) ? value.join("; ") : value;
|
|
92
|
+
const preserved = cookie
|
|
93
|
+
.split(";")
|
|
94
|
+
.map((part) => part.trim())
|
|
95
|
+
.filter((part) => part && !/^jishushell_session=/i.test(part));
|
|
96
|
+
return preserved.length ? preserved.join("; ") : undefined;
|
|
97
|
+
}
|
|
98
|
+
export function buildCapabilityWebSocketHeaders(headers, upstreamOrigin) {
|
|
99
|
+
const upstreamHeaders = {};
|
|
100
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
const lower = key.toLowerCase();
|
|
104
|
+
if (lower === "host" || PROXY_IDENTITY_HEADERS.has(lower))
|
|
105
|
+
continue;
|
|
106
|
+
if (lower === "authorization")
|
|
107
|
+
continue;
|
|
108
|
+
if (lower === "origin") {
|
|
109
|
+
upstreamHeaders[key] = upstreamOrigin;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (lower === "cookie") {
|
|
113
|
+
const cookie = stripPanelSessionCookie(value);
|
|
114
|
+
if (cookie)
|
|
115
|
+
upstreamHeaders[key] = cookie;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
upstreamHeaders[key] = value;
|
|
119
|
+
}
|
|
120
|
+
return upstreamHeaders;
|
|
121
|
+
}
|
|
122
|
+
function forwardWebSocketRejection(socket, res) {
|
|
123
|
+
const statusCode = res.statusCode || 502;
|
|
124
|
+
const statusMessage = res.statusMessage || "Bad Gateway";
|
|
125
|
+
try {
|
|
126
|
+
socket.write(`HTTP/1.1 ${statusCode} ${statusMessage}\r\nConnection: close\r\n\r\n`);
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
res.resume();
|
|
130
|
+
socket.destroy();
|
|
131
|
+
}
|
|
132
|
+
export function selectRefererWebSocketCapability(capabilities, refererCapability, wsPath) {
|
|
133
|
+
const matches = capabilities.filter((capability) => {
|
|
134
|
+
if (capability.visibility === "internal")
|
|
135
|
+
return false;
|
|
136
|
+
const capabilityPath = (capability.path || "/").replace(/\/+$/, "") || "/";
|
|
137
|
+
return wsPath === capabilityPath || wsPath.startsWith(capabilityPath === "/" ? "/" : `${capabilityPath}/`);
|
|
138
|
+
});
|
|
139
|
+
return matches.find((capability) => capability.capability !== refererCapability)
|
|
140
|
+
?? matches.find((capability) => capability.capability === refererCapability)
|
|
141
|
+
?? null;
|
|
142
|
+
}
|
|
32
143
|
export async function createServer(options = {}) {
|
|
33
144
|
const port = options.port ?? 8090;
|
|
34
145
|
const host = options.host || "0.0.0.0";
|
|
@@ -42,7 +153,7 @@ export async function createServer(options = {}) {
|
|
|
42
153
|
bodyLimit: 1048576,
|
|
43
154
|
});
|
|
44
155
|
// Allow empty JSON body for POST requests
|
|
45
|
-
app.addContentTypeParser("application/json", { parseAs: "string" }, (
|
|
156
|
+
app.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
46
157
|
if (!body || body.length === 0) {
|
|
47
158
|
done(null, {});
|
|
48
159
|
}
|
|
@@ -56,9 +167,49 @@ export async function createServer(options = {}) {
|
|
|
56
167
|
}
|
|
57
168
|
});
|
|
58
169
|
// Parse text/plain and application/x-yaml as raw string (for app install endpoint)
|
|
59
|
-
app.addContentTypeParser(["text/plain", "application/x-yaml"], { parseAs: "string" }, (
|
|
170
|
+
app.addContentTypeParser(["text/plain", "application/x-yaml"], { parseAs: "string" }, (_req, body, done) => {
|
|
60
171
|
done(null, body);
|
|
61
172
|
});
|
|
173
|
+
// Fallback: pass any other content type through as the raw stream. Without
|
|
174
|
+
// this, /apps/:slug/* (W2.5 PR-8) would reject POST/PUT uploads with 415
|
|
175
|
+
// because Fastify's default behavior is to reject unparseable bodies.
|
|
176
|
+
// Specific routes that need typed bodies (octet-stream upload, etc.)
|
|
177
|
+
// register their own narrower parsers — those win over this catch-all.
|
|
178
|
+
app.addContentTypeParser("*", (_req, payload, done) => {
|
|
179
|
+
done(null, payload);
|
|
180
|
+
});
|
|
181
|
+
// Eager-init the internal MCP token so the file exists on disk before
|
|
182
|
+
// any agent container starts and tries to read it via env injection.
|
|
183
|
+
// Lazy-init would only fire on the first request that carries an
|
|
184
|
+
// X-Jishushell-Internal-Token header, which is too late for the
|
|
185
|
+
// adapter onBeforeStart that mounts the value into the container.
|
|
186
|
+
try {
|
|
187
|
+
getInternalMcpToken();
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
console.warn(`[server] internal MCP token init skipped: ${e?.message ?? e}`);
|
|
191
|
+
}
|
|
192
|
+
// ── Auth middleware for /apps/* (AI-FS v1 W2.5 PR-8) ──────────────────
|
|
193
|
+
// Top-level reverse proxy for installed apps. Added in AI-FS v1 W2.5 PR-8.
|
|
194
|
+
// Pairs with `/api/instances/:id/provides/:cap/*` which is instance-scoped.
|
|
195
|
+
// This hook is scoped to /apps/* only so it does not double-fire with the
|
|
196
|
+
// existing /api/* hook below.
|
|
197
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
198
|
+
const path = request.url.split("?")[0];
|
|
199
|
+
if (!path.startsWith("/apps/"))
|
|
200
|
+
return;
|
|
201
|
+
const authHeader = request.headers.authorization || "";
|
|
202
|
+
const match = authHeader.match(/^Bearer\s+(\S+)$/);
|
|
203
|
+
let token = match?.[1] || "";
|
|
204
|
+
if (!token) {
|
|
205
|
+
const cookie = request.headers.cookie || "";
|
|
206
|
+
const cookieMatch = cookie.match(/(?:^|;\s*)jishushell_session=([^;]*)/);
|
|
207
|
+
token = cookieMatch?.[1] || "";
|
|
208
|
+
}
|
|
209
|
+
if (!token || token.length > 4096 || !verifyToken(token)) {
|
|
210
|
+
return reply.status(401).send({ detail: "Unauthorized" });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
62
213
|
// Auth middleware
|
|
63
214
|
app.addHook("onRequest", async (request, reply) => {
|
|
64
215
|
const path = request.url.split("?")[0];
|
|
@@ -73,6 +224,35 @@ export async function createServer(options = {}) {
|
|
|
73
224
|
}
|
|
74
225
|
return;
|
|
75
226
|
}
|
|
227
|
+
// Internal token path — used by:
|
|
228
|
+
// (a) MCP shims spawned inside agent containers
|
|
229
|
+
// (need BOTH X-Jishushell-Internal-Token + X-Jishushell-Instance)
|
|
230
|
+
// (b) Panel app lifecycle scripts (post_start, etc.) calling back into
|
|
231
|
+
// /api/internal/* on the same host
|
|
232
|
+
// (need ONLY X-Jishushell-Internal-Token; no instance binding —
|
|
233
|
+
// apps aren't instances)
|
|
234
|
+
// Combined with the standard JWT path below; we never require both.
|
|
235
|
+
const internalToken = request.headers["x-jishushell-internal-token"];
|
|
236
|
+
if (typeof internalToken === "string" && internalToken.length >= 32) {
|
|
237
|
+
const expected = getInternalMcpToken();
|
|
238
|
+
if (internalToken.length === expected.length &&
|
|
239
|
+
timingSafeEqual(Buffer.from(internalToken), Buffer.from(expected))) {
|
|
240
|
+
const instanceId = request.headers["x-jishushell-instance"];
|
|
241
|
+
if (typeof instanceId === "string" && instanceManager.getInstance(instanceId)) {
|
|
242
|
+
request.internalCallerInstance = instanceId;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Path (b): app-lifecycle / internal-only routes. Allowed without
|
|
246
|
+
// an instance header. The route is responsible for asserting the
|
|
247
|
+
// (request as any).internalCallerScope === "panel" tag before
|
|
248
|
+
// returning sensitive data.
|
|
249
|
+
if (path.startsWith("/api/internal/")) {
|
|
250
|
+
request.internalCallerScope = "panel";
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return reply.status(401).send({ detail: "Unauthorized" });
|
|
255
|
+
}
|
|
76
256
|
// Check Authorization header first, then fall back to httpOnly cookie
|
|
77
257
|
const authHeader = request.headers.authorization || "";
|
|
78
258
|
const match = authHeader.match(/^Bearer\s+(\S+)$/);
|
|
@@ -120,6 +300,122 @@ export async function createServer(options = {}) {
|
|
|
120
300
|
await app.register(agentAppRoutes);
|
|
121
301
|
await app.register(llmRoutes);
|
|
122
302
|
await app.register(backupRoutes);
|
|
303
|
+
await app.register(internalRoutes);
|
|
304
|
+
// Single FilesManager instance shared across /api/files, /webdav, and
|
|
305
|
+
// /api/files/external-mounts so a mutation on the mount list (PUT
|
|
306
|
+
// /external-mounts) is visible to every layer immediately.
|
|
307
|
+
const panelCfgForFiles = getPanelConfig();
|
|
308
|
+
const initialExternalMounts = Array.isArray(panelCfgForFiles.external_mounts)
|
|
309
|
+
? panelCfgForFiles.external_mounts
|
|
310
|
+
: [];
|
|
311
|
+
const sharedFilesManager = new FilesManager({
|
|
312
|
+
externalMounts: initialExternalMounts,
|
|
313
|
+
});
|
|
314
|
+
await app.register(async (scope) => {
|
|
315
|
+
// Encapsulated registration so the route-scoped octet-stream parser
|
|
316
|
+
// does not leak to other routes (matches plan G6/D6 — Files is the
|
|
317
|
+
// only route that needs raw-stream uploads at W1).
|
|
318
|
+
await filesRoutes(scope, { filesManager: sharedFilesManager });
|
|
319
|
+
await organizeRoutes(scope, { filesManager: sharedFilesManager });
|
|
320
|
+
await fileMountsRoutes(scope);
|
|
321
|
+
await externalMountsRoutes(scope, { filesManager: sharedFilesManager });
|
|
322
|
+
});
|
|
323
|
+
// WebDAV runs in its own scope — it has independent auth (app password
|
|
324
|
+
// Basic Auth, NOT the panel JWT) and registers extra HTTP methods that
|
|
325
|
+
// we don't want leaking to other routes.
|
|
326
|
+
await app.register(async (scope) => {
|
|
327
|
+
await webdavRoutes(scope, { filesManager: sharedFilesManager });
|
|
328
|
+
});
|
|
329
|
+
// ── Top-level /apps/:slug/* reverse proxy (AI-FS v1 W2.5 PR-8) ───────────
|
|
330
|
+
// Reverse-proxies HTTP requests for installed apps to their capability
|
|
331
|
+
// upstream. Auth is handled by the onRequest hook above. The /apps/:slug
|
|
332
|
+
// prefix is NOT stripped — apps like Filebrowser are configured with
|
|
333
|
+
// --baseURL=/apps/filebrowser and expect the full path.
|
|
334
|
+
//
|
|
335
|
+
// Resolution order: `<slug>-ui` → `web-<slug>` (first running entry wins).
|
|
336
|
+
// 503 if no running provider found. 400 if slug is malformed.
|
|
337
|
+
//
|
|
338
|
+
// Registered BEFORE the SPA static fallback so explicit routes win.
|
|
339
|
+
const appsProxyHandler = async (req, reply) => {
|
|
340
|
+
const rawSlug = req.params.slug ?? "";
|
|
341
|
+
if (!SLUG_RE.test(rawSlug)) {
|
|
342
|
+
return reply.status(400).send({ detail: "Invalid app slug", slug: rawSlug });
|
|
343
|
+
}
|
|
344
|
+
const upstream = resolveAppSlugUpstream(rawSlug);
|
|
345
|
+
if (!upstream) {
|
|
346
|
+
return reply.status(503).send({ detail: "app not running", slug: rawSlug });
|
|
347
|
+
}
|
|
348
|
+
const { host: upstreamHost, port: upstreamPort } = upstream;
|
|
349
|
+
// Pass the full original path unchanged (including /apps/<slug> prefix).
|
|
350
|
+
const rawUrl = req.raw.url ?? "/";
|
|
351
|
+
const targetPath = rawUrl;
|
|
352
|
+
const hostHeader = `${upstreamHost}:${upstreamPort}`;
|
|
353
|
+
// Build forwarded headers: strip cookie and authorization to avoid
|
|
354
|
+
// leaking panel session credentials to third-party apps.
|
|
355
|
+
const forwardHeaders = {};
|
|
356
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
357
|
+
if (value === undefined)
|
|
358
|
+
continue;
|
|
359
|
+
const lower = key.toLowerCase();
|
|
360
|
+
if (lower === "host")
|
|
361
|
+
continue;
|
|
362
|
+
if (lower === "cookie") {
|
|
363
|
+
const stripped = stripPanelSessionCookie(value);
|
|
364
|
+
if (stripped)
|
|
365
|
+
forwardHeaders[key] = stripped;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (lower === "authorization")
|
|
369
|
+
continue;
|
|
370
|
+
forwardHeaders[key] = value;
|
|
371
|
+
}
|
|
372
|
+
forwardHeaders["host"] = hostHeader;
|
|
373
|
+
// Hijack before writing to reply.raw so Fastify does not attempt its own
|
|
374
|
+
// lifecycle send after the handler returns.
|
|
375
|
+
reply.hijack();
|
|
376
|
+
await new Promise((resolve) => {
|
|
377
|
+
const proxyReq = httpRequest({
|
|
378
|
+
hostname: upstreamHost,
|
|
379
|
+
port: upstreamPort,
|
|
380
|
+
path: targetPath,
|
|
381
|
+
method: req.method,
|
|
382
|
+
headers: forwardHeaders,
|
|
383
|
+
}, (proxyRes) => {
|
|
384
|
+
// Stream the response back without buffering.
|
|
385
|
+
reply.raw.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
386
|
+
proxyRes.pipe(reply.raw, { end: true });
|
|
387
|
+
proxyRes.on("end", resolve);
|
|
388
|
+
proxyRes.on("error", () => {
|
|
389
|
+
try {
|
|
390
|
+
reply.raw.end();
|
|
391
|
+
}
|
|
392
|
+
catch { }
|
|
393
|
+
resolve();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
proxyReq.on("error", (err) => {
|
|
397
|
+
console.error(`[apps-proxy] upstream error for slug=${rawSlug}:`, err.message);
|
|
398
|
+
if (!reply.raw.headersSent) {
|
|
399
|
+
reply.raw.writeHead(502, { "content-type": "application/json" });
|
|
400
|
+
reply.raw.end(JSON.stringify({ detail: "upstream error", slug: rawSlug }));
|
|
401
|
+
}
|
|
402
|
+
resolve();
|
|
403
|
+
});
|
|
404
|
+
// Forward request body if present (POST, PUT, PATCH, etc.)
|
|
405
|
+
if (req.rawBody) {
|
|
406
|
+
proxyReq.end(req.rawBody);
|
|
407
|
+
}
|
|
408
|
+
else if (req.raw.readable) {
|
|
409
|
+
req.raw.pipe(proxyReq, { end: true });
|
|
410
|
+
req.raw.on("end", () => { }); // ensure event fires
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
proxyReq.end();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
app.all("/apps/:slug", appsProxyHandler);
|
|
418
|
+
app.all("/apps/:slug/*", appsProxyHandler);
|
|
123
419
|
// Serve frontend static files
|
|
124
420
|
// Look for frontend dist in multiple locations
|
|
125
421
|
const resolvedDir = existsSync(__dirname) ? realpathSync(__dirname) : __dirname;
|
|
@@ -142,6 +438,10 @@ export async function createServer(options = {}) {
|
|
|
142
438
|
root: assetsDir,
|
|
143
439
|
prefix: "/assets/",
|
|
144
440
|
decorateReply: false,
|
|
441
|
+
cacheControl: false,
|
|
442
|
+
setHeaders: (res) => {
|
|
443
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
444
|
+
},
|
|
145
445
|
});
|
|
146
446
|
}
|
|
147
447
|
// Serve other static files and SPA fallback
|
|
@@ -150,6 +450,16 @@ export async function createServer(options = {}) {
|
|
|
150
450
|
prefix: "/",
|
|
151
451
|
decorateReply: true,
|
|
152
452
|
wildcard: false,
|
|
453
|
+
globIgnore: ["assets/**"],
|
|
454
|
+
cacheControl: false,
|
|
455
|
+
setHeaders: (res, filePath) => {
|
|
456
|
+
if (basename(filePath) === "index.html") {
|
|
457
|
+
res.setHeader("Cache-Control", "no-store");
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
res.setHeader("Cache-Control", "public, max-age=0");
|
|
461
|
+
}
|
|
462
|
+
},
|
|
153
463
|
});
|
|
154
464
|
// SPA fallback: serve index.html for non-API, non-asset routes
|
|
155
465
|
app.setNotFoundHandler(async (request, reply) => {
|
|
@@ -162,6 +472,10 @@ export async function createServer(options = {}) {
|
|
|
162
472
|
error: { message: "proxy route not found", type: "server_error" },
|
|
163
473
|
});
|
|
164
474
|
}
|
|
475
|
+
if (path.startsWith("/assets/")) {
|
|
476
|
+
return reply.status(404).type("text/plain").send("Asset not found");
|
|
477
|
+
}
|
|
478
|
+
reply.header("Cache-Control", "no-store");
|
|
165
479
|
return reply.sendFile("index.html");
|
|
166
480
|
});
|
|
167
481
|
}
|
|
@@ -242,20 +556,23 @@ export async function createServer(options = {}) {
|
|
|
242
556
|
// pinned digest tag rewrite can now succeed.
|
|
243
557
|
runStartupMigrations();
|
|
244
558
|
}
|
|
245
|
-
// Rebuild capability registry from
|
|
559
|
+
// Rebuild capability registry from app instances. PR 3 sub-step 3f
|
|
560
|
+
// of the app-interconnect design drops the legacy `portOverride`
|
|
561
|
+
// arg — each provide now resolves through `resolveProvideEndpoint`
|
|
562
|
+
// which reads the actual allocated port from runtime metadata, so
|
|
563
|
+
// multi-provide apps no longer collapse all entries onto the
|
|
564
|
+
// gateway port. For instances whose underlying job is currently
|
|
565
|
+
// not running, we still re-register the entries (so the
|
|
566
|
+
// Connections UI keeps them as greyed candidates) and immediately
|
|
567
|
+
// mark them stopped via `setProviderStatus` (§5.4 of the design).
|
|
246
568
|
try {
|
|
247
|
-
for (const
|
|
248
|
-
if (!
|
|
249
|
-
continue;
|
|
250
|
-
const appData = appManager.getApp(inst.app_id);
|
|
251
|
-
if (!appData || !appData.spec.provides?.length)
|
|
569
|
+
for (const appData of appManager.listApps()) {
|
|
570
|
+
if (!appData.spec.provides?.length)
|
|
252
571
|
continue;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (port > 0) {
|
|
258
|
-
appManager.registerCapabilities(inst.id, appData.spec, port);
|
|
572
|
+
appManager.registerCapabilities(appData.manifest.id, appData.spec);
|
|
573
|
+
const status = await nomadManager.getStatus(appData.manifest.id);
|
|
574
|
+
if (status.status === "stopped") {
|
|
575
|
+
capabilityRegistry.setProviderStatus(appData.manifest.id, "stopped");
|
|
259
576
|
}
|
|
260
577
|
}
|
|
261
578
|
}
|
|
@@ -296,11 +613,156 @@ export async function createServer(options = {}) {
|
|
|
296
613
|
// request lifecycle. Fastify's internal headersTimeout (73s) kills hijacked
|
|
297
614
|
// sockets, so we intercept upgrades before Fastify sees them.
|
|
298
615
|
const GATEWAY_WS_RE = /^\/api\/instances\/([a-z0-9][a-z0-9-]{0,62})\/gateway(?:\/(.*))?$/;
|
|
616
|
+
const CAPABILITY_WS_RE = /^\/api\/instances\/([a-z0-9][a-z0-9-]{0,62})\/provides\/([^/?]+)(?:\/(.*))?$/;
|
|
617
|
+
// Match Referer pointing at a capability proxy page (used to resolve
|
|
618
|
+
// root-level WS upgrades initiated by embedded pages like Browserless debugger
|
|
619
|
+
// whose Web Workers create WS connections to the panel origin directly).
|
|
620
|
+
const CAPABILITY_REF_RE = /\/api\/instances\/([a-z0-9][a-z0-9-]{0,62})\/provides\/([^/?]+)/;
|
|
299
621
|
app.server.on("upgrade", async (req, socket, head) => {
|
|
300
622
|
const url = req.url || "";
|
|
301
623
|
const path = url.split("?")[0];
|
|
302
|
-
const
|
|
303
|
-
|
|
624
|
+
const gatewayMatch = path.match(GATEWAY_WS_RE);
|
|
625
|
+
const capabilityMatch = path.match(CAPABILITY_WS_RE);
|
|
626
|
+
// ── /apps/:slug/* WebSocket upgrade (AI-FS v1 W2.5 PR-8) ──────────────
|
|
627
|
+
// Proxy WS upgrades for top-level installed apps. Same auth gate as HTTP.
|
|
628
|
+
const appSlugWsMatch = path.match(APP_SLUG_WS_RE);
|
|
629
|
+
if (appSlugWsMatch && !gatewayMatch && !capabilityMatch) {
|
|
630
|
+
const slug = appSlugWsMatch[1];
|
|
631
|
+
// Auth gate: same JWT cookie/bearer check as the HTTP handler above.
|
|
632
|
+
if (!hasValidPanelUpgradeToken(req)) {
|
|
633
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
634
|
+
socket.destroy();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const upstream = resolveAppSlugUpstream(slug);
|
|
638
|
+
if (!upstream) {
|
|
639
|
+
socket.write("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
|
|
640
|
+
socket.destroy();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const { host: upstreamHost, port: upstreamPort } = upstream;
|
|
644
|
+
const hostHeader = `${upstreamHost}:${upstreamPort}`;
|
|
645
|
+
const upstreamOrigin = `http://${hostHeader}`;
|
|
646
|
+
const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
647
|
+
const targetPath = `${path}${qs}`;
|
|
648
|
+
// Build upstream WS headers: strip cookie (panel session) and authorization.
|
|
649
|
+
const upstreamHeaders = buildCapabilityWebSocketHeaders(req.headers, upstreamOrigin);
|
|
650
|
+
socket.setTimeout?.(0);
|
|
651
|
+
socket.setNoDelay?.(true);
|
|
652
|
+
socket.setKeepAlive?.(true, 30000);
|
|
653
|
+
let proxyReq;
|
|
654
|
+
try {
|
|
655
|
+
proxyReq = httpRequest({
|
|
656
|
+
hostname: upstreamHost,
|
|
657
|
+
port: upstreamPort,
|
|
658
|
+
path: targetPath,
|
|
659
|
+
method: "GET",
|
|
660
|
+
timeout: 10_000,
|
|
661
|
+
headers: {
|
|
662
|
+
...upstreamHeaders,
|
|
663
|
+
host: hostHeader,
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
try {
|
|
669
|
+
socket.destroy();
|
|
670
|
+
}
|
|
671
|
+
catch { }
|
|
672
|
+
activeWsSockets.delete(socket);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
proxyReq.on("timeout", () => {
|
|
676
|
+
proxyReq.destroy();
|
|
677
|
+
socket.destroy();
|
|
678
|
+
});
|
|
679
|
+
proxyReq.on("upgrade", (_res, proxySocket, proxyHead) => {
|
|
680
|
+
proxySocket.setTimeout(0);
|
|
681
|
+
proxySocket.setNoDelay(true);
|
|
682
|
+
proxySocket.setKeepAlive(true, 30000);
|
|
683
|
+
proxySocket.pause();
|
|
684
|
+
let handshake = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
|
|
685
|
+
for (const [k, v] of Object.entries(_res.headers)) {
|
|
686
|
+
if (v !== undefined && k.toLowerCase().startsWith("sec-websocket-")) {
|
|
687
|
+
handshake += `${k}: ${v}\r\n`;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
handshake += "\r\n";
|
|
691
|
+
socket.write(handshake);
|
|
692
|
+
if (head.length > 0)
|
|
693
|
+
proxySocket.write(head);
|
|
694
|
+
if (proxyHead.length > 0)
|
|
695
|
+
socket.write(proxyHead);
|
|
696
|
+
proxySocket.pipe(socket);
|
|
697
|
+
socket.pipe(proxySocket);
|
|
698
|
+
proxySocket.resume();
|
|
699
|
+
activeWsSockets.add(socket);
|
|
700
|
+
activeWsSockets.add(proxySocket);
|
|
701
|
+
const release = (s) => { activeWsSockets.delete(s); };
|
|
702
|
+
const fail = () => {
|
|
703
|
+
try {
|
|
704
|
+
proxySocket.destroy();
|
|
705
|
+
}
|
|
706
|
+
catch { }
|
|
707
|
+
try {
|
|
708
|
+
socket.destroy();
|
|
709
|
+
}
|
|
710
|
+
catch { }
|
|
711
|
+
release(socket);
|
|
712
|
+
release(proxySocket);
|
|
713
|
+
};
|
|
714
|
+
proxySocket.on("close", () => {
|
|
715
|
+
release(proxySocket);
|
|
716
|
+
try {
|
|
717
|
+
if (!socket.destroyed && !socket.writableEnded)
|
|
718
|
+
socket.end();
|
|
719
|
+
}
|
|
720
|
+
catch { }
|
|
721
|
+
});
|
|
722
|
+
socket.on("close", () => {
|
|
723
|
+
release(socket);
|
|
724
|
+
try {
|
|
725
|
+
if (!proxySocket.destroyed && !proxySocket.writableEnded)
|
|
726
|
+
proxySocket.end();
|
|
727
|
+
}
|
|
728
|
+
catch { }
|
|
729
|
+
});
|
|
730
|
+
proxySocket.on("error", fail);
|
|
731
|
+
socket.on("error", fail);
|
|
732
|
+
});
|
|
733
|
+
proxyReq.on("response", (res) => {
|
|
734
|
+
forwardWebSocketRejection(socket, res);
|
|
735
|
+
activeWsSockets.delete(socket);
|
|
736
|
+
});
|
|
737
|
+
proxyReq.on("error", () => {
|
|
738
|
+
try {
|
|
739
|
+
socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
740
|
+
}
|
|
741
|
+
catch { }
|
|
742
|
+
socket.destroy();
|
|
743
|
+
activeWsSockets.delete(socket);
|
|
744
|
+
});
|
|
745
|
+
proxyReq.end();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
// ── end /apps/:slug/* WS upgrade ───────────────────────────────────────
|
|
749
|
+
// When an embedded page (e.g. Browserless debugger's puppeteer Worker)
|
|
750
|
+
// opens a WS to the panel origin at a non-API path (e.g. /?launch=...),
|
|
751
|
+
// resolve the target instance via the Referer header pointing at the
|
|
752
|
+
// capability proxy page that served the embedded UI.
|
|
753
|
+
let refererCapabilityMatch = null;
|
|
754
|
+
if (!gatewayMatch && !capabilityMatch) {
|
|
755
|
+
const referer = req.headers.referer || "";
|
|
756
|
+
try {
|
|
757
|
+
const refUrl = new URL(referer, "http://localhost");
|
|
758
|
+
const m = refUrl.pathname.match(CAPABILITY_REF_RE);
|
|
759
|
+
if (m) {
|
|
760
|
+
refererCapabilityMatch = { instanceId: m[1], capability: decodeURIComponent(m[2]) };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch { /* ignore malformed referer */ }
|
|
764
|
+
}
|
|
765
|
+
if (!gatewayMatch && !capabilityMatch && !refererCapabilityMatch) {
|
|
304
766
|
// Not a gateway WebSocket — let it drop (Fastify doesn't handle WS natively)
|
|
305
767
|
socket.destroy();
|
|
306
768
|
return;
|
|
@@ -308,8 +770,7 @@ export async function createServer(options = {}) {
|
|
|
308
770
|
// Defense-in-depth: validate Origin header for WebSocket upgrades.
|
|
309
771
|
// Require Origin for non-loopback clients; gateway has its own token auth as primary.
|
|
310
772
|
const origin = req.headers.origin || "";
|
|
311
|
-
const
|
|
312
|
-
const isLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1" || remoteIp === "::ffff:127.0.0.1";
|
|
773
|
+
const isLoopback = isLoopbackSocket(socket);
|
|
313
774
|
if (!origin && !isLoopback) {
|
|
314
775
|
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
315
776
|
socket.destroy();
|
|
@@ -331,6 +792,276 @@ export async function createServer(options = {}) {
|
|
|
331
792
|
return;
|
|
332
793
|
}
|
|
333
794
|
}
|
|
795
|
+
if ((capabilityMatch || refererCapabilityMatch) && !hasValidPanelUpgradeToken(req)) {
|
|
796
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
797
|
+
socket.destroy();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
// Referer-based WS proxy: embedded pages (like Browserless debugger's Web
|
|
801
|
+
// Worker) open WS connections to the panel origin (e.g. ws://panel:8090/?launch=...).
|
|
802
|
+
// We resolve the correct upstream by finding a capability on the same
|
|
803
|
+
// instance that serves the request path (e.g. path "/" with http protocol).
|
|
804
|
+
if (refererCapabilityMatch) {
|
|
805
|
+
const id = refererCapabilityMatch.instanceId;
|
|
806
|
+
const appData = instanceManager.getApp(id);
|
|
807
|
+
if (!appData) {
|
|
808
|
+
socket.destroy();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const capabilities = instanceManager.getProvidedCapabilitiesForApp(id);
|
|
812
|
+
// Find a capability whose path prefix matches the WS request path.
|
|
813
|
+
// Prefer capabilities with path "/" (root API) that aren't the debugger UI itself.
|
|
814
|
+
const wsPath = path || "/";
|
|
815
|
+
const capability = selectRefererWebSocketCapability(capabilities, refererCapabilityMatch.capability, wsPath);
|
|
816
|
+
if (!capability || typeof capability.port !== "number" || capability.port < 1) {
|
|
817
|
+
try {
|
|
818
|
+
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
819
|
+
}
|
|
820
|
+
catch { }
|
|
821
|
+
socket.destroy();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const protocol = String(capability.protocol || "http").toLowerCase();
|
|
825
|
+
const upstreamRequest = protocol === "https" || protocol === "wss" ? httpsRequest : httpRequest;
|
|
826
|
+
const targetHost = await instanceManager.getHostForAppPort(id, capability.port);
|
|
827
|
+
const hostHeader = `${instanceManager.urlHost(targetHost)}:${capability.port}`;
|
|
828
|
+
const upstreamOrigin = `${protocol === "https" || protocol === "wss" ? "https" : "http"}://${hostHeader}`;
|
|
829
|
+
const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
830
|
+
const targetPath = `${joinCapabilityUpstreamPath(capability.path, path.replace(/^\/+/, ""))}${qs}`;
|
|
831
|
+
const upstreamHeaders = buildCapabilityWebSocketHeaders(req.headers, upstreamOrigin);
|
|
832
|
+
socket.setTimeout?.(0);
|
|
833
|
+
socket.setNoDelay?.(true);
|
|
834
|
+
socket.setKeepAlive?.(true, 30000);
|
|
835
|
+
let proxyReq;
|
|
836
|
+
try {
|
|
837
|
+
proxyReq = upstreamRequest({
|
|
838
|
+
hostname: targetHost,
|
|
839
|
+
port: capability.port,
|
|
840
|
+
path: targetPath,
|
|
841
|
+
method: "GET",
|
|
842
|
+
timeout: 10_000,
|
|
843
|
+
headers: {
|
|
844
|
+
...upstreamHeaders,
|
|
845
|
+
host: hostHeader,
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
try {
|
|
851
|
+
socket.destroy();
|
|
852
|
+
}
|
|
853
|
+
catch { }
|
|
854
|
+
activeWsSockets.delete(socket);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
proxyReq.on("timeout", () => {
|
|
858
|
+
proxyReq.destroy();
|
|
859
|
+
socket.destroy();
|
|
860
|
+
});
|
|
861
|
+
proxyReq.on("upgrade", (_res, proxySocket, proxyHead) => {
|
|
862
|
+
proxySocket.setTimeout(0);
|
|
863
|
+
proxySocket.setNoDelay(true);
|
|
864
|
+
proxySocket.setKeepAlive(true, 30000);
|
|
865
|
+
proxySocket.pause();
|
|
866
|
+
let handshake = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
|
|
867
|
+
for (const [k, v] of Object.entries(_res.headers)) {
|
|
868
|
+
if (v !== undefined && k.toLowerCase().startsWith("sec-websocket-")) {
|
|
869
|
+
handshake += `${k}: ${v}\r\n`;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
handshake += "\r\n";
|
|
873
|
+
socket.write(handshake);
|
|
874
|
+
if (head.length > 0)
|
|
875
|
+
proxySocket.write(head);
|
|
876
|
+
if (proxyHead.length > 0)
|
|
877
|
+
socket.write(proxyHead);
|
|
878
|
+
proxySocket.pipe(socket);
|
|
879
|
+
socket.pipe(proxySocket);
|
|
880
|
+
proxySocket.resume();
|
|
881
|
+
activeWsSockets.add(socket);
|
|
882
|
+
activeWsSockets.add(proxySocket);
|
|
883
|
+
const release = (s) => { activeWsSockets.delete(s); };
|
|
884
|
+
const fail = () => {
|
|
885
|
+
try {
|
|
886
|
+
proxySocket.destroy();
|
|
887
|
+
}
|
|
888
|
+
catch { }
|
|
889
|
+
try {
|
|
890
|
+
socket.destroy();
|
|
891
|
+
}
|
|
892
|
+
catch { }
|
|
893
|
+
release(socket);
|
|
894
|
+
release(proxySocket);
|
|
895
|
+
};
|
|
896
|
+
proxySocket.on("close", () => {
|
|
897
|
+
release(proxySocket);
|
|
898
|
+
try {
|
|
899
|
+
if (!socket.destroyed && !socket.writableEnded)
|
|
900
|
+
socket.end();
|
|
901
|
+
}
|
|
902
|
+
catch { }
|
|
903
|
+
});
|
|
904
|
+
socket.on("close", () => {
|
|
905
|
+
release(socket);
|
|
906
|
+
try {
|
|
907
|
+
if (!proxySocket.destroyed && !proxySocket.writableEnded)
|
|
908
|
+
proxySocket.end();
|
|
909
|
+
}
|
|
910
|
+
catch { }
|
|
911
|
+
});
|
|
912
|
+
proxySocket.on("error", fail);
|
|
913
|
+
socket.on("error", fail);
|
|
914
|
+
});
|
|
915
|
+
proxyReq.on("response", (res) => {
|
|
916
|
+
forwardWebSocketRejection(socket, res);
|
|
917
|
+
activeWsSockets.delete(socket);
|
|
918
|
+
});
|
|
919
|
+
proxyReq.on("error", () => {
|
|
920
|
+
try {
|
|
921
|
+
socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
922
|
+
}
|
|
923
|
+
catch { }
|
|
924
|
+
socket.destroy();
|
|
925
|
+
activeWsSockets.delete(socket);
|
|
926
|
+
});
|
|
927
|
+
proxyReq.end();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (capabilityMatch) {
|
|
931
|
+
const id = capabilityMatch[1];
|
|
932
|
+
const capabilityName = decodeURIComponent(capabilityMatch[2] || "");
|
|
933
|
+
const appData = instanceManager.getApp(id);
|
|
934
|
+
if (!appData) {
|
|
935
|
+
socket.destroy();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const capability = instanceManager.getProvidedCapabilitiesForApp(id).find((entry) => entry.capability === capabilityName);
|
|
939
|
+
if (!capability || capability.visibility === "internal") {
|
|
940
|
+
try {
|
|
941
|
+
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
944
|
+
socket.destroy();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (typeof capability.port !== "number" || capability.port < 1) {
|
|
948
|
+
try {
|
|
949
|
+
socket.write("HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n");
|
|
950
|
+
}
|
|
951
|
+
catch { }
|
|
952
|
+
socket.destroy();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const protocol = String(capability.protocol || "http").toLowerCase();
|
|
956
|
+
const upstreamRequest = protocol === "https" || protocol === "wss" ? httpsRequest : httpRequest;
|
|
957
|
+
const targetHost = await instanceManager.getHostForAppPort(id, capability.port);
|
|
958
|
+
const hostHeader = `${instanceManager.urlHost(targetHost)}:${capability.port}`;
|
|
959
|
+
const upstreamOrigin = `${protocol === "https" || protocol === "wss" ? "https" : "http"}://${hostHeader}`;
|
|
960
|
+
const suffix = capabilityMatch[3] || "";
|
|
961
|
+
const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
962
|
+
const targetPath = `${joinCapabilityUpstreamPath(capability.path, suffix)}${qs}`;
|
|
963
|
+
const upstreamHeaders = buildCapabilityWebSocketHeaders(req.headers, upstreamOrigin);
|
|
964
|
+
socket.setTimeout?.(0);
|
|
965
|
+
socket.setNoDelay?.(true);
|
|
966
|
+
socket.setKeepAlive?.(true, 30000);
|
|
967
|
+
let proxyReq;
|
|
968
|
+
try {
|
|
969
|
+
proxyReq = upstreamRequest({
|
|
970
|
+
hostname: targetHost,
|
|
971
|
+
port: capability.port,
|
|
972
|
+
path: targetPath,
|
|
973
|
+
method: "GET",
|
|
974
|
+
timeout: 10_000,
|
|
975
|
+
headers: {
|
|
976
|
+
...upstreamHeaders,
|
|
977
|
+
host: hostHeader,
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
try {
|
|
983
|
+
socket.destroy();
|
|
984
|
+
}
|
|
985
|
+
catch { }
|
|
986
|
+
activeWsSockets.delete(socket);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
proxyReq.on("timeout", () => {
|
|
990
|
+
proxyReq.destroy();
|
|
991
|
+
socket.destroy();
|
|
992
|
+
});
|
|
993
|
+
proxyReq.on("upgrade", (_res, proxySocket, proxyHead) => {
|
|
994
|
+
proxySocket.setTimeout(0);
|
|
995
|
+
proxySocket.setNoDelay(true);
|
|
996
|
+
proxySocket.setKeepAlive(true, 30000);
|
|
997
|
+
proxySocket.pause();
|
|
998
|
+
let handshake = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
|
|
999
|
+
for (const [k, v] of Object.entries(_res.headers)) {
|
|
1000
|
+
if (v !== undefined && k.toLowerCase().startsWith("sec-websocket-")) {
|
|
1001
|
+
handshake += `${k}: ${v}\r\n`;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
handshake += "\r\n";
|
|
1005
|
+
socket.write(handshake);
|
|
1006
|
+
if (head.length > 0)
|
|
1007
|
+
proxySocket.write(head);
|
|
1008
|
+
if (proxyHead.length > 0)
|
|
1009
|
+
socket.write(proxyHead);
|
|
1010
|
+
proxySocket.pipe(socket);
|
|
1011
|
+
socket.pipe(proxySocket);
|
|
1012
|
+
proxySocket.resume();
|
|
1013
|
+
activeWsSockets.add(socket);
|
|
1014
|
+
activeWsSockets.add(proxySocket);
|
|
1015
|
+
const release = (s) => {
|
|
1016
|
+
activeWsSockets.delete(s);
|
|
1017
|
+
};
|
|
1018
|
+
const fail = () => {
|
|
1019
|
+
try {
|
|
1020
|
+
proxySocket.destroy();
|
|
1021
|
+
}
|
|
1022
|
+
catch { }
|
|
1023
|
+
try {
|
|
1024
|
+
socket.destroy();
|
|
1025
|
+
}
|
|
1026
|
+
catch { }
|
|
1027
|
+
release(socket);
|
|
1028
|
+
release(proxySocket);
|
|
1029
|
+
};
|
|
1030
|
+
proxySocket.on("close", () => {
|
|
1031
|
+
release(proxySocket);
|
|
1032
|
+
try {
|
|
1033
|
+
if (!socket.destroyed && !socket.writableEnded)
|
|
1034
|
+
socket.end();
|
|
1035
|
+
}
|
|
1036
|
+
catch { }
|
|
1037
|
+
});
|
|
1038
|
+
socket.on("close", () => {
|
|
1039
|
+
release(socket);
|
|
1040
|
+
try {
|
|
1041
|
+
if (!proxySocket.destroyed && !proxySocket.writableEnded)
|
|
1042
|
+
proxySocket.end();
|
|
1043
|
+
}
|
|
1044
|
+
catch { }
|
|
1045
|
+
});
|
|
1046
|
+
proxySocket.on("error", fail);
|
|
1047
|
+
socket.on("error", fail);
|
|
1048
|
+
});
|
|
1049
|
+
proxyReq.on("response", (res) => {
|
|
1050
|
+
forwardWebSocketRejection(socket, res);
|
|
1051
|
+
activeWsSockets.delete(socket);
|
|
1052
|
+
});
|
|
1053
|
+
proxyReq.on("error", () => {
|
|
1054
|
+
try {
|
|
1055
|
+
socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
1056
|
+
}
|
|
1057
|
+
catch { }
|
|
1058
|
+
socket.destroy();
|
|
1059
|
+
activeWsSockets.delete(socket);
|
|
1060
|
+
});
|
|
1061
|
+
proxyReq.end();
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const match = gatewayMatch;
|
|
334
1065
|
const id = match[1];
|
|
335
1066
|
const inst = instanceManager.getInstance(id);
|
|
336
1067
|
if (!inst) {
|