@xopcai/xopc 0.0.66 → 0.0.67
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/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agents-JiJR38n1.js → agents-CmhOU0fB.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-JiJR38n1.js.map → agents-CmhOU0fB.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-aBDyr7Im.js → apps-page-CzS9A_6L.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-aBDyr7Im.js.map → apps-page-CzS9A_6L.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-settings-DXxfgG4N.js → channels-settings-Bn0YSztA.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-DXxfgG4N.js.map → channels-settings-Bn0YSztA.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-dreaming-jobs-3--QJSP5.js → cron-dreaming-jobs-bAYsiHdF.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-dreaming-jobs-3--QJSP5.js.map → cron-dreaming-jobs-bAYsiHdF.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-CCn-JwBy.js → cron-page-CKqDtScu.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-CCn-JwBy.js.map → cron-page-CKqDtScu.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-BS-cwfHQ.js → dist-CUMhnSuX.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-BS-cwfHQ.js.map → dist-CUMhnSuX.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-M8JS4Jid.js → extension-debug-page-BDls5xp3.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-M8JS4Jid.js.map → extension-debug-page-BDls5xp3.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-C8gjVE_t.js → extension-page-BSfv53OO.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-C8gjVE_t.js.map → extension-page-BSfv53OO.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-DeY2M2di.js → extension-settings-page-B0nkVsLi.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-DeY2M2di.js.map → extension-settings-page-B0nkVsLi.js.map} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-DWmeyjyR.js → heartbeat-config-api-P5zdbIBw.js} +2 -2
- package/dist/gateway/static/root/assets/{heartbeat-config-api-DWmeyjyR.js.map → heartbeat-config-api-P5zdbIBw.js.map} +1 -1
- package/dist/gateway/static/root/assets/{index-Cc57jhuG.js → index-DAglRwh4.js} +5 -5
- package/dist/gateway/static/root/assets/{index-Cc57jhuG.js.map → index-DAglRwh4.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-DHj3Cf9B.css +1 -0
- package/dist/gateway/static/root/assets/{logs-page-kIsSDMqb.js → logs-page-CfGDv4k7.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-kIsSDMqb.js.map → logs-page-CfGDv4k7.js.map} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-Da9gjMh7.js → sessions-page-DoGE8N-O.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-Da9gjMh7.js.map → sessions-page-DoGE8N-O.js.map} +1 -1
- package/dist/gateway/static/root/assets/settings-page-C5VrwDoO.js +3 -0
- package/dist/gateway/static/root/assets/settings-page-C5VrwDoO.js.map +1 -0
- package/dist/gateway/static/root/assets/{skills-page-B_msZcLx.js → skills-page-ADUi69Fa.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-B_msZcLx.js.map → skills-page-ADUi69Fa.js.map} +1 -1
- package/dist/gateway/static/root/assets/{use-image-provider-credentials-Bs8xl2E3.js → use-image-provider-credentials-C4x4dNv2.js} +2 -2
- package/dist/gateway/static/root/assets/{use-image-provider-credentials-Bs8xl2E3.js.map → use-image-provider-credentials-C4x4dNv2.js.map} +1 -1
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/cli/commands/tunnel.js +69 -8
- package/dist/src/cli/commands/tunnel.js.map +1 -1
- package/dist/src/config/schema.d.ts +2 -0
- package/dist/src/config/schema.js +2 -0
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +2 -5
- package/dist/src/gateway/hono/lib/config-payload.js +3 -5
- package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
- package/dist/src/gateway/hono/routes/tunnel.js +12 -10
- package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
- package/dist/src/tunnel/acme-client.d.ts +11 -0
- package/dist/src/tunnel/acme-client.js +54 -10
- package/dist/src/tunnel/acme-client.js.map +1 -1
- package/dist/src/tunnel/env.d.ts +14 -3
- package/dist/src/tunnel/env.js +34 -5
- package/dist/src/tunnel/env.js.map +1 -1
- package/dist/src/tunnel/frpc-binary.d.ts +6 -1
- package/dist/src/tunnel/frpc-binary.js +66 -40
- package/dist/src/tunnel/frpc-binary.js.map +1 -1
- package/dist/src/tunnel/frpc-extract.d.ts +10 -0
- package/dist/src/tunnel/frpc-extract.js +129 -0
- package/dist/src/tunnel/frpc-extract.js.map +1 -0
- package/dist/src/tunnel/gateway-lifecycle.d.ts +3 -1
- package/dist/src/tunnel/gateway-lifecycle.js +6 -4
- package/dist/src/tunnel/gateway-lifecycle.js.map +1 -1
- package/dist/src/tunnel/index.d.ts +3 -1
- package/dist/src/tunnel/index.js +2 -2
- package/dist/src/tunnel/tls-server.js +5 -0
- package/dist/src/tunnel/tls-server.js.map +1 -1
- package/dist/src/tunnel/tunnel-config.js +11 -1
- package/dist/src/tunnel/tunnel-config.js.map +1 -1
- package/dist/src/tunnel/tunnel-e2e-config.d.ts +4 -1
- package/dist/src/tunnel/tunnel-e2e-config.js +10 -3
- package/dist/src/tunnel/tunnel-e2e-config.js.map +1 -1
- package/dist/src/tunnel/tunnel-service.d.ts +4 -0
- package/dist/src/tunnel/tunnel-service.js +66 -5
- package/dist/src/tunnel/tunnel-service.js.map +1 -1
- package/dist/src/tunnel/tunnel-types.d.ts +16 -0
- package/package.json +3 -3
- package/dist/gateway/static/root/assets/index-B5gp15_q.css +0 -1
- package/dist/gateway/static/root/assets/settings-page-B0spIRVS.js +0 -3
- package/dist/gateway/static/root/assets/settings-page-B0spIRVS.js.map +0 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger/index.js";
|
|
2
|
+
import { init_logger } from "../utils/logger.js";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { gunzipSync } from "node:zlib";
|
|
7
|
+
//#region src/tunnel/frpc-extract.ts
|
|
8
|
+
init_logger();
|
|
9
|
+
const log = createLogger("TunnelFrpcExtract");
|
|
10
|
+
function buildFrpcArchiveMemberPath(folder, platform) {
|
|
11
|
+
return `${folder}/frpc${platform === "win32" ? ".exe" : ""}`.replace(/\\/g, "/");
|
|
12
|
+
}
|
|
13
|
+
/** Resolve on-disk path after system tar extracts a POSIX member path. */
|
|
14
|
+
function resolveExtractedMemberPath(extractDir, memberPath) {
|
|
15
|
+
return join(extractDir, ...memberPath.replace(/\\/g, "/").split("/"));
|
|
16
|
+
}
|
|
17
|
+
function readTarField(header, offset, length) {
|
|
18
|
+
return header.subarray(offset, offset + length).toString("utf8").replace(/\0.*$/, "").trim();
|
|
19
|
+
}
|
|
20
|
+
function parseTarEntryName(header) {
|
|
21
|
+
let name = readTarField(header, 0, 100);
|
|
22
|
+
const prefix = readTarField(header, 345, 155);
|
|
23
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
24
|
+
return name.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
25
|
+
}
|
|
26
|
+
function parseTarSize(header) {
|
|
27
|
+
const raw = readTarField(header, 124, 12);
|
|
28
|
+
if (!raw) return 0;
|
|
29
|
+
return parseInt(raw, 8) || 0;
|
|
30
|
+
}
|
|
31
|
+
/** Pure Node tar.gz member extract — no system `tar` (Windows / minimal Linux). */
|
|
32
|
+
function extractTarGzMemberNode(archivePath, memberPath, destPath) {
|
|
33
|
+
const target = memberPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
34
|
+
const tar = gunzipSync(readFileSync(archivePath));
|
|
35
|
+
let offset = 0;
|
|
36
|
+
while (offset + 512 <= tar.length) {
|
|
37
|
+
const header = tar.subarray(offset, offset + 512);
|
|
38
|
+
if (header.every((b) => b === 0)) break;
|
|
39
|
+
const name = parseTarEntryName(header);
|
|
40
|
+
const size = parseTarSize(header);
|
|
41
|
+
offset += 512;
|
|
42
|
+
if (size < 0 || offset + size > tar.length) throw new Error("Corrupt tar archive");
|
|
43
|
+
const content = tar.subarray(offset, offset + size);
|
|
44
|
+
offset += size;
|
|
45
|
+
offset += (512 - size % 512) % 512;
|
|
46
|
+
if (name === target) {
|
|
47
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
48
|
+
writeFileSync(destPath, content);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Archive missing expected path: ${target}`);
|
|
53
|
+
}
|
|
54
|
+
async function extractTarGzMemberSystemTar(archivePath, memberPath, destPath, extractDir) {
|
|
55
|
+
mkdirSync(extractDir, { recursive: true });
|
|
56
|
+
await new Promise((resolve, reject) => {
|
|
57
|
+
let stderr = "";
|
|
58
|
+
const child = spawn("tar", [
|
|
59
|
+
"xzf",
|
|
60
|
+
archivePath,
|
|
61
|
+
"-C",
|
|
62
|
+
extractDir,
|
|
63
|
+
memberPath
|
|
64
|
+
], { stdio: [
|
|
65
|
+
"ignore",
|
|
66
|
+
"ignore",
|
|
67
|
+
"pipe"
|
|
68
|
+
] });
|
|
69
|
+
child.stderr?.on("data", (chunk) => {
|
|
70
|
+
stderr += chunk.toString("utf8");
|
|
71
|
+
});
|
|
72
|
+
child.on("error", reject);
|
|
73
|
+
child.on("exit", (code) => {
|
|
74
|
+
if (code === 0) resolve();
|
|
75
|
+
else {
|
|
76
|
+
const detail = stderr.trim();
|
|
77
|
+
reject(/* @__PURE__ */ new Error(`tar exited with code ${code ?? "unknown"}${detail ? `: ${detail}` : ""}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
const extracted = resolveExtractedMemberPath(extractDir, memberPath);
|
|
82
|
+
if (!existsSync(extracted)) throw new Error(`Archive missing expected path: ${memberPath}`);
|
|
83
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
84
|
+
writeFileSync(destPath, readFileSync(extracted));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract frpc from a release tarball.
|
|
88
|
+
* Tries system `tar` first; falls back to built-in Node parser (macOS BSD tar quirks, Windows without tar, etc.).
|
|
89
|
+
*/
|
|
90
|
+
async function extractFrpcFromTarGzArchive(archivePath, destBin, folder, platform = process.platform) {
|
|
91
|
+
const memberPath = buildFrpcArchiveMemberPath(folder, platform);
|
|
92
|
+
const extractDir = join(dirname(destBin), `_frpc-extract-${process.pid}-${Date.now()}`);
|
|
93
|
+
let systemErr;
|
|
94
|
+
try {
|
|
95
|
+
await extractTarGzMemberSystemTar(archivePath, memberPath, destBin, extractDir);
|
|
96
|
+
log.debug({
|
|
97
|
+
memberPath,
|
|
98
|
+
method: "system-tar"
|
|
99
|
+
}, "Extracted frpc from archive");
|
|
100
|
+
return;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
systemErr = err;
|
|
103
|
+
log.debug({
|
|
104
|
+
err,
|
|
105
|
+
memberPath,
|
|
106
|
+
phase: "frpc_extract_system_tar"
|
|
107
|
+
}, "System tar extract failed — trying Node fallback");
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(extractDir, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
force: true
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
extractTarGzMemberNode(archivePath, memberPath, destBin);
|
|
116
|
+
log.debug({
|
|
117
|
+
memberPath,
|
|
118
|
+
method: "node-tar"
|
|
119
|
+
}, "Extracted frpc from archive");
|
|
120
|
+
} catch (nodeErr) {
|
|
121
|
+
const systemEm = systemErr instanceof Error ? systemErr.message : String(systemErr);
|
|
122
|
+
const nodeEm = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
123
|
+
throw new Error(`Failed to extract ${memberPath}: ${nodeEm} (system tar: ${systemEm})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
export { buildFrpcArchiveMemberPath, extractFrpcFromTarGzArchive, extractTarGzMemberNode, resolveExtractedMemberPath };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=frpc-extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frpc-extract.js","names":[],"sources":["../../../src/tunnel/frpc-extract.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { gunzipSync } from 'node:zlib';\n\nimport { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('TunnelFrpcExtract');\n\nexport function buildFrpcArchiveMemberPath(folder: string, platform: NodeJS.Platform): string {\n const ext = platform === 'win32' ? '.exe' : '';\n return `${folder}/frpc${ext}`.replace(/\\\\/g, '/');\n}\n\n/** Resolve on-disk path after system tar extracts a POSIX member path. */\nexport function resolveExtractedMemberPath(extractDir: string, memberPath: string): string {\n return join(extractDir, ...memberPath.replace(/\\\\/g, '/').split('/'));\n}\n\nfunction readTarField(header: Buffer, offset: number, length: number): string {\n return header.subarray(offset, offset + length).toString('utf8').replace(/\\0.*$/, '').trim();\n}\n\nfunction parseTarEntryName(header: Buffer): string {\n let name = readTarField(header, 0, 100);\n const prefix = readTarField(header, 345, 155);\n if (prefix) name = `${prefix}/${name}`;\n return name.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction parseTarSize(header: Buffer): number {\n const raw = readTarField(header, 124, 12);\n if (!raw) return 0;\n return parseInt(raw, 8) || 0;\n}\n\n/** Pure Node tar.gz member extract — no system `tar` (Windows / minimal Linux). */\nexport function extractTarGzMemberNode(\n archivePath: string,\n memberPath: string,\n destPath: string,\n): void {\n const target = memberPath.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n const tar = gunzipSync(readFileSync(archivePath));\n\n let offset = 0;\n while (offset + 512 <= tar.length) {\n const header = tar.subarray(offset, offset + 512);\n if (header.every((b) => b === 0)) break;\n\n const name = parseTarEntryName(header);\n const size = parseTarSize(header);\n offset += 512;\n\n if (size < 0 || offset + size > tar.length) {\n throw new Error('Corrupt tar archive');\n }\n\n const content = tar.subarray(offset, offset + size);\n offset += size;\n offset += (512 - (size % 512)) % 512;\n\n if (name === target) {\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, content);\n return;\n }\n }\n\n throw new Error(`Archive missing expected path: ${target}`);\n}\n\nasync function extractTarGzMemberSystemTar(\n archivePath: string,\n memberPath: string,\n destPath: string,\n extractDir: string,\n): Promise<void> {\n mkdirSync(extractDir, { recursive: true });\n await new Promise<void>((resolve, reject) => {\n let stderr = '';\n const child = spawn('tar', ['xzf', archivePath, '-C', extractDir, memberPath], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf8');\n });\n child.on('error', reject);\n child.on('exit', (code) => {\n if (code === 0) resolve();\n else {\n const detail = stderr.trim();\n reject(\n new Error(`tar exited with code ${code ?? 'unknown'}${detail ? `: ${detail}` : ''}`),\n );\n }\n });\n });\n\n const extracted = resolveExtractedMemberPath(extractDir, memberPath);\n if (!existsSync(extracted)) {\n throw new Error(`Archive missing expected path: ${memberPath}`);\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, readFileSync(extracted));\n}\n\n/**\n * Extract frpc from a release tarball.\n * Tries system `tar` first; falls back to built-in Node parser (macOS BSD tar quirks, Windows without tar, etc.).\n */\nexport async function extractFrpcFromTarGzArchive(\n archivePath: string,\n destBin: string,\n folder: string,\n platform: NodeJS.Platform = process.platform,\n): Promise<void> {\n const memberPath = buildFrpcArchiveMemberPath(folder, platform);\n const extractDir = join(dirname(destBin), `_frpc-extract-${process.pid}-${Date.now()}`);\n let systemErr: unknown;\n\n try {\n await extractTarGzMemberSystemTar(archivePath, memberPath, destBin, extractDir);\n log.debug({ memberPath, method: 'system-tar' }, 'Extracted frpc from archive');\n return;\n } catch (err) {\n systemErr = err;\n log.debug(\n { err, memberPath, phase: 'frpc_extract_system_tar' },\n 'System tar extract failed — trying Node fallback',\n );\n } finally {\n rmSync(extractDir, { recursive: true, force: true });\n }\n\n try {\n extractTarGzMemberNode(archivePath, memberPath, destBin);\n log.debug({ memberPath, method: 'node-tar' }, 'Extracted frpc from archive');\n } catch (nodeErr) {\n const systemEm = systemErr instanceof Error ? systemErr.message : String(systemErr);\n const nodeEm = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);\n throw new Error(`Failed to extract ${memberPath}: ${nodeEm} (system tar: ${systemEm})`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,9 @@ type TunnelEventSink = {
|
|
|
4
4
|
};
|
|
5
5
|
/** Publish `tunnel.status` on gateway SSE when tunnel lifecycle changes. */
|
|
6
6
|
export declare function wireTunnelEventsToGateway(service: TunnelEventSink): void;
|
|
7
|
-
export declare function configureTunnelFromGatewayConfig(config: Config
|
|
7
|
+
export declare function configureTunnelFromGatewayConfig(config: Config, opts?: {
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
8
10
|
/**
|
|
9
11
|
* Start FRP tunnel when `tunnel.autoStart` is set (CLI gateway / GatewayServer after HTTP listen).
|
|
10
12
|
*/
|
|
@@ -22,14 +22,16 @@ function wireTunnelEventsToGateway(service) {
|
|
|
22
22
|
tunnel.on("tunnel:connected", publish);
|
|
23
23
|
tunnel.on("tunnel:disconnected", publish);
|
|
24
24
|
tunnel.on("tunnel:error", publish);
|
|
25
|
+
tunnel.on("tunnel:progress", publish);
|
|
25
26
|
subscribeCertStatus((cert) => {
|
|
26
27
|
service.emit("tunnel.cert.status", cert);
|
|
27
28
|
publish();
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
|
-
async function configureTunnelFromGatewayConfig(config) {
|
|
31
|
-
if (!config.tunnel?.enabled) return;
|
|
31
|
+
async function configureTunnelFromGatewayConfig(config, opts) {
|
|
32
|
+
if (!opts?.force && !config.tunnel?.enabled) return;
|
|
32
33
|
const gateway = config.gateway ?? {};
|
|
34
|
+
const gatewayPort = gateway.port ?? 18790;
|
|
33
35
|
let brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);
|
|
34
36
|
try {
|
|
35
37
|
const wellKnown = await fetchTunnelWellKnown(brokerUrl);
|
|
@@ -43,7 +45,7 @@ async function configureTunnelFromGatewayConfig(config) {
|
|
|
43
45
|
}
|
|
44
46
|
let registrationSecret;
|
|
45
47
|
try {
|
|
46
|
-
registrationSecret = resolveTunnelRegistrationSecret(process.env, brokerUrl);
|
|
48
|
+
registrationSecret = resolveTunnelRegistrationSecret(process.env, brokerUrl, config.tunnel?.registrationSecret);
|
|
47
49
|
} catch (err) {
|
|
48
50
|
const em = err instanceof Error ? err.message : String(err);
|
|
49
51
|
log.warn({
|
|
@@ -57,7 +59,7 @@ async function configureTunnelFromGatewayConfig(config) {
|
|
|
57
59
|
registrationSecret,
|
|
58
60
|
autoStart: config.tunnel?.autoStart ?? false,
|
|
59
61
|
gatewayHost: gateway.host ?? "127.0.0.1",
|
|
60
|
-
e2e: resolveTunnelE2eConfig(config.tunnel),
|
|
62
|
+
e2e: resolveTunnelE2eConfig(config.tunnel, gatewayPort),
|
|
61
63
|
frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl)
|
|
62
64
|
});
|
|
63
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-lifecycle.js","names":[],"sources":["../../../src/tunnel/gateway-lifecycle.ts"],"sourcesContent":["import type { Config } from '../config/schema.js';\nimport { createLogger } from '../utils/logger.js';\nimport { hasValidTunnelConsent } from './consent.js';\nimport { subscribeCertStatus } from './acme-cert-store.js';\nimport { resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from './env.js';\nimport { getTunnelService } from './tunnel-service.js';\nimport { resolveFrpSubdomainHost, resolveTunnelE2eConfig } from './tunnel-e2e-config.js';\nimport { fetchTunnelWellKnown } from './well-known.js';\n\nconst log = createLogger('Tunnel');\n\ntype TunnelEventSink = { emit(type: string, payload: unknown): void };\n\nlet tunnelSseWired = false;\n\n/** Publish `tunnel.status` on gateway SSE when tunnel lifecycle changes. */\nexport function wireTunnelEventsToGateway(service: TunnelEventSink): void {\n if (tunnelSseWired) return;\n tunnelSseWired = true;\n\n const tunnel = getTunnelService();\n const publish = () => {\n service.emit('tunnel.status', tunnel.getStatus());\n };\n\n tunnel.on('tunnel:connecting', publish);\n tunnel.on('tunnel:connected', publish);\n tunnel.on('tunnel:disconnected', publish);\n tunnel.on('tunnel:error', publish);\n\n subscribeCertStatus((cert) => {\n service.emit('tunnel.cert.status', cert);\n publish();\n });\n}\n\nexport async function configureTunnelFromGatewayConfig(config: Config): Promise<void> {\n if (!config.tunnel?.enabled) return;\n\n const gateway = config.gateway ?? {};\n let brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n\n try {\n const wellKnown = await fetchTunnelWellKnown(brokerUrl);\n if (wellKnown.brokerUrl?.trim()) {\n brokerUrl = wellKnown.brokerUrl.trim();\n }\n } catch (err) {\n log.debug(\n { err, brokerUrl, phase: 'tunnel_well_known' },\n 'Tunnel well-known fetch skipped (using config/env broker URL)',\n );\n }\n\n let registrationSecret: string;\n try {\n registrationSecret = resolveTunnelRegistrationSecret(process.env
|
|
1
|
+
{"version":3,"file":"gateway-lifecycle.js","names":[],"sources":["../../../src/tunnel/gateway-lifecycle.ts"],"sourcesContent":["import type { Config } from '../config/schema.js';\nimport { createLogger } from '../utils/logger.js';\nimport { hasValidTunnelConsent } from './consent.js';\nimport { subscribeCertStatus } from './acme-cert-store.js';\nimport { resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from './env.js';\nimport { getTunnelService } from './tunnel-service.js';\nimport { resolveFrpSubdomainHost, resolveTunnelE2eConfig } from './tunnel-e2e-config.js';\nimport { fetchTunnelWellKnown } from './well-known.js';\n\nconst log = createLogger('Tunnel');\n\ntype TunnelEventSink = { emit(type: string, payload: unknown): void };\n\nlet tunnelSseWired = false;\n\n/** Publish `tunnel.status` on gateway SSE when tunnel lifecycle changes. */\nexport function wireTunnelEventsToGateway(service: TunnelEventSink): void {\n if (tunnelSseWired) return;\n tunnelSseWired = true;\n\n const tunnel = getTunnelService();\n const publish = () => {\n service.emit('tunnel.status', tunnel.getStatus());\n };\n\n tunnel.on('tunnel:connecting', publish);\n tunnel.on('tunnel:connected', publish);\n tunnel.on('tunnel:disconnected', publish);\n tunnel.on('tunnel:error', publish);\n tunnel.on('tunnel:progress', publish);\n\n subscribeCertStatus((cert) => {\n service.emit('tunnel.cert.status', cert);\n publish();\n });\n}\n\nexport async function configureTunnelFromGatewayConfig(\n config: Config,\n opts?: { force?: boolean },\n): Promise<void> {\n if (!opts?.force && !config.tunnel?.enabled) return;\n\n const gateway = config.gateway ?? {};\n const gatewayPort = gateway.port ?? 18790;\n let brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n\n try {\n const wellKnown = await fetchTunnelWellKnown(brokerUrl);\n if (wellKnown.brokerUrl?.trim()) {\n brokerUrl = wellKnown.brokerUrl.trim();\n }\n } catch (err) {\n log.debug(\n { err, brokerUrl, phase: 'tunnel_well_known' },\n 'Tunnel well-known fetch skipped (using config/env broker URL)',\n );\n }\n\n let registrationSecret: string;\n try {\n registrationSecret = resolveTunnelRegistrationSecret(\n process.env,\n brokerUrl,\n config.tunnel?.registrationSecret,\n );\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.warn({ phase: 'tunnel_configure', errorMessage: em }, em);\n throw err;\n }\n\n getTunnelService().configure({\n brokerUrl,\n registrationSecret,\n autoStart: config.tunnel?.autoStart ?? false,\n gatewayHost: gateway.host ?? '127.0.0.1',\n e2e: resolveTunnelE2eConfig(config.tunnel, gatewayPort),\n frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl),\n });\n}\n\n/**\n * Start FRP tunnel when `tunnel.autoStart` is set (CLI gateway / GatewayServer after HTTP listen).\n */\nexport async function maybeAutoStartTunnelFromConfig(\n config: Config,\n gatewayToken: string | undefined,\n): Promise<void> {\n if (!config.tunnel?.autoStart) return;\n\n if (!hasValidTunnelConsent(config)) {\n log.warn(\n { phase: 'tunnel_autostart', consentVersion: config.tunnel?.consent?.version ?? null },\n 'tunnel.autoStart skipped: security consent required or outdated',\n );\n return;\n }\n\n if (config.tunnel.enabled !== true) {\n log.debug(\n { phase: 'tunnel_autostart' },\n 'tunnel.autoStart skipped: tunnel.enabled is false (start remote access once)',\n );\n return;\n }\n\n const gateway = config.gateway ?? {};\n const port = gateway.port ?? 18790;\n const host = gateway.host ?? '127.0.0.1';\n\n await configureTunnelFromGatewayConfig(config);\n\n if (!gatewayToken) {\n log.warn(\n { phase: 'tunnel_autostart' },\n 'tunnel.autoStart is enabled but gateway auth token is unavailable (auth mode may be none)',\n );\n return;\n }\n\n const tunnel = getTunnelService();\n const { state } = tunnel.getStatus();\n if (state === 'connected' || state === 'connecting' || state === 'reconnecting') {\n log.debug({ phase: 'tunnel_autostart', state }, 'Tunnel already active — skip autostart');\n return;\n }\n\n try {\n await tunnel.start(port, gatewayToken);\n log.info(\n { phase: 'tunnel_autostart', host, port },\n 'Tunnel auto-started after gateway HTTP listen',\n );\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.error({ err, phase: 'tunnel_autostart', errorMessage: em }, `Tunnel autostart failed: ${em}`);\n }\n}\n"],"mappings":";;;;;;;;;aACkD;AAQlD,MAAM,MAAM,aAAa,SAAS;AAIlC,IAAI,iBAAiB;;AAGrB,SAAgB,0BAA0B,SAAgC;AACxE,KAAI,eAAgB;AACpB,kBAAiB;CAEjB,MAAM,SAAS,kBAAkB;CACjC,MAAM,gBAAgB;AACpB,UAAQ,KAAK,iBAAiB,OAAO,WAAW,CAAC;;AAGnD,QAAO,GAAG,qBAAqB,QAAQ;AACvC,QAAO,GAAG,oBAAoB,QAAQ;AACtC,QAAO,GAAG,uBAAuB,QAAQ;AACzC,QAAO,GAAG,gBAAgB,QAAQ;AAClC,QAAO,GAAG,mBAAmB,QAAQ;AAErC,sBAAqB,SAAS;AAC5B,UAAQ,KAAK,sBAAsB,KAAK;AACxC,WAAS;GACT;;AAGJ,eAAsB,iCACpB,QACA,MACe;AACf,KAAI,CAAC,MAAM,SAAS,CAAC,OAAO,QAAQ,QAAS;CAE7C,MAAM,UAAU,OAAO,WAAW,EAAE;CACpC,MAAM,cAAc,QAAQ,QAAQ;CACpC,IAAI,YAAY,uBAAuB,OAAO,QAAQ,UAAU;AAEhE,KAAI;EACF,MAAM,YAAY,MAAM,qBAAqB,UAAU;AACvD,MAAI,UAAU,WAAW,MAAM,CAC7B,aAAY,UAAU,UAAU,MAAM;UAEjC,KAAK;AACZ,MAAI,MACF;GAAE;GAAK;GAAW,OAAO;GAAqB,EAC9C,gEACD;;CAGH,IAAI;AACJ,KAAI;AACF,uBAAqB,gCACnB,QAAQ,KACR,WACA,OAAO,QAAQ,mBAChB;UACM,KAAK;EACZ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC3D,MAAI,KAAK;GAAE,OAAO;GAAoB,cAAc;GAAI,EAAE,GAAG;AAC7D,QAAM;;AAGR,mBAAkB,CAAC,UAAU;EAC3B;EACA;EACA,WAAW,OAAO,QAAQ,aAAa;EACvC,aAAa,QAAQ,QAAQ;EAC7B,KAAK,uBAAuB,OAAO,QAAQ,YAAY;EACvD,kBAAkB,wBAAwB,UAAU;EACrD,CAAC;;;;;AAMJ,eAAsB,+BACpB,QACA,cACe;AACf,KAAI,CAAC,OAAO,QAAQ,UAAW;AAE/B,KAAI,CAAC,sBAAsB,OAAO,EAAE;AAClC,MAAI,KACF;GAAE,OAAO;GAAoB,gBAAgB,OAAO,QAAQ,SAAS,WAAW;GAAM,EACtF,kEACD;AACD;;AAGF,KAAI,OAAO,OAAO,YAAY,MAAM;AAClC,MAAI,MACF,EAAE,OAAO,oBAAoB,EAC7B,+EACD;AACD;;CAGF,MAAM,UAAU,OAAO,WAAW,EAAE;CACpC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;AAE7B,OAAM,iCAAiC,OAAO;AAE9C,KAAI,CAAC,cAAc;AACjB,MAAI,KACF,EAAE,OAAO,oBAAoB,EAC7B,4FACD;AACD;;CAGF,MAAM,SAAS,kBAAkB;CACjC,MAAM,EAAE,UAAU,OAAO,WAAW;AACpC,KAAI,UAAU,eAAe,UAAU,gBAAgB,UAAU,gBAAgB;AAC/E,MAAI,MAAM;GAAE,OAAO;GAAoB;GAAO,EAAE,yCAAyC;AACzF;;AAGF,KAAI;AACF,QAAM,OAAO,MAAM,MAAM,aAAa;AACtC,MAAI,KACF;GAAE,OAAO;GAAoB;GAAM;GAAM,EACzC,gDACD;UACM,KAAK;EACZ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC3D,MAAI,MAAM;GAAE;GAAK,OAAO;GAAoB,cAAc;GAAI,EAAE,4BAA4B,KAAK"}
|
|
@@ -2,9 +2,11 @@ export { assertTunnelMayStart, CURRENT_TUNNEL_CONSENT_VERSION, getTunnelConsentS
|
|
|
2
2
|
export { TunnelBrokerClient, resolveBrokerApiBase } from './broker-client.js';
|
|
3
3
|
export { applyTunnelConsentToConfig, mergeTunnelConfigPatch, sanitizeTunnelConfig, setTunnelEnabledInConfig, } from './tunnel-config.js';
|
|
4
4
|
export { clearFrpcPathForProcess, ensureFrpcBinary, FRPC_VERSION, publishFrpcPathForProcess, } from './frpc-binary.js';
|
|
5
|
+
export type { EnsureFrpcBinaryOptions, FrpcDownloadProgress } from './frpc-binary.js';
|
|
5
6
|
export { configureTunnelFromGatewayConfig, maybeAutoStartTunnelFromConfig, wireTunnelEventsToGateway, } from './gateway-lifecycle.js';
|
|
6
7
|
export { fetchTunnelWellKnown, clearTunnelWellKnownCache } from './well-known.js';
|
|
7
|
-
export { isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret, } from './env.js';
|
|
8
|
+
export { getTunnelRegistrationSecretMeta, isProductionTunnelBroker, isMaskedTunnelSecretPatchValue, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret, } from './env.js';
|
|
9
|
+
export type { TunnelRegistrationSecretMeta, TunnelRegistrationSecretSource } from './env.js';
|
|
8
10
|
export { logTunnelAudit } from './tunnel-audit.js';
|
|
9
11
|
export type { TunnelAuditEvent } from './tunnel-audit.js';
|
|
10
12
|
export { consumeTunnelMutationLimit, resetTunnelMutationLimitsForTests } from './tunnel-rate-limit.js';
|
package/dist/src/tunnel/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CURRENT_TUNNEL_CONSENT_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelConsentError, assertTunnelMayStart, getTunnelConsentState, hasValidTunnelConsent } from "./consent.js";
|
|
2
2
|
import { getCertStatusSummary, recordRenewalFailure, subscribeCertStatus } from "./acme-cert-store.js";
|
|
3
|
-
import { isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from "./env.js";
|
|
3
|
+
import { getTunnelRegistrationSecretMeta, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from "./env.js";
|
|
4
4
|
import { TunnelBrokerClient, resolveBrokerApiBase } from "./broker-client.js";
|
|
5
5
|
import { FRPC_VERSION, clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from "./frpc-binary.js";
|
|
6
6
|
import { buildMobileConnectQrPayload, resolveLanGatewayUrl } from "./tunnel-qr.js";
|
|
@@ -14,4 +14,4 @@ import { clearTunnelWellKnownCache, fetchTunnelWellKnown } from "./well-known.js
|
|
|
14
14
|
import { configureTunnelFromGatewayConfig, maybeAutoStartTunnelFromConfig, wireTunnelEventsToGateway } from "./gateway-lifecycle.js";
|
|
15
15
|
import { applyTunnelConsentToConfig, mergeTunnelConfigPatch, sanitizeTunnelConfig, setTunnelEnabledInConfig } from "./tunnel-config.js";
|
|
16
16
|
import { consumeTunnelMutationLimit, resetTunnelMutationLimitsForTests } from "./tunnel-rate-limit.js";
|
|
17
|
-
export { CURRENT_TUNNEL_CONSENT_VERSION, FRPC_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelBrokerClient, TunnelConsentError, TunnelService, applyTunnelConsentToConfig, assertTunnelMayStart, buildMobileConnectQrPayload, clearFrpcPathForProcess, clearTunnelWellKnownCache, configureTunnelFromGatewayConfig, consumePairingSecret, consumeTunnelMutationLimit, createPairingSecret, ensureFrpcBinary, fetchTunnelWellKnown, getActiveTlsCert, getCertStatusSummary, getTunnelConsentState, getTunnelService, hasValidTunnelConsent, hashGatewayToken, isProductionTunnelBroker, loadTunnelState, logTunnelAudit, maybeAutoStartTunnelFromConfig, mergeTunnelConfigPatch, publishFrpcPathForProcess, recordRenewalFailure, resetPairingSessionsForTests, resetTunnelMutationLimitsForTests, resolveBrokerApiBase, resolveFrpSubdomainHost, resolveLanGatewayUrl, resolveTunnelBrokerUrl, resolveTunnelE2eConfig, resolveTunnelRegistrationSecret, resolveTunnelStatePath, sanitizeTunnelConfig, saveTunnelState, setTunnelEnabledInConfig, stopTunnelTlsServer, subscribeCertStatus, wireTunnelEventsToGateway };
|
|
17
|
+
export { CURRENT_TUNNEL_CONSENT_VERSION, FRPC_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelBrokerClient, TunnelConsentError, TunnelService, applyTunnelConsentToConfig, assertTunnelMayStart, buildMobileConnectQrPayload, clearFrpcPathForProcess, clearTunnelWellKnownCache, configureTunnelFromGatewayConfig, consumePairingSecret, consumeTunnelMutationLimit, createPairingSecret, ensureFrpcBinary, fetchTunnelWellKnown, getActiveTlsCert, getCertStatusSummary, getTunnelConsentState, getTunnelRegistrationSecretMeta, getTunnelService, hasValidTunnelConsent, hashGatewayToken, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, loadTunnelState, logTunnelAudit, maybeAutoStartTunnelFromConfig, mergeTunnelConfigPatch, publishFrpcPathForProcess, recordRenewalFailure, resetPairingSessionsForTests, resetTunnelMutationLimitsForTests, resolveBrokerApiBase, resolveFrpSubdomainHost, resolveLanGatewayUrl, resolveTunnelBrokerUrl, resolveTunnelE2eConfig, resolveTunnelRegistrationSecret, resolveTunnelStatePath, sanitizeTunnelConfig, saveTunnelState, setTunnelEnabledInConfig, stopTunnelTlsServer, subscribeCertStatus, wireTunnelEventsToGateway };
|
|
@@ -79,6 +79,11 @@ async function startTunnelTlsServer(config) {
|
|
|
79
79
|
await new Promise((resolve, reject) => {
|
|
80
80
|
httpsNodeServer.once("error", reject);
|
|
81
81
|
httpsNodeServer.listen(config.tlsPort, "127.0.0.1", () => resolve());
|
|
82
|
+
}).catch((err) => {
|
|
83
|
+
httpsNodeServer?.close();
|
|
84
|
+
httpsNodeServer = null;
|
|
85
|
+
if ((err instanceof Error ? err.message : String(err)).includes("EADDRINUSE")) throw new Error(`Tunnel TLS port ${config.tlsPort} is already in use. Stop other xopc tunnel instances or set tunnel.e2e.tlsPort (try ${config.gatewayPort + 1} for gateway port ${config.gatewayPort}).`);
|
|
86
|
+
throw err;
|
|
82
87
|
});
|
|
83
88
|
log.info({
|
|
84
89
|
port: config.tlsPort,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tls-server.js","names":["createHttpsServer"],"sources":["../../../src/tunnel/tls-server.ts"],"sourcesContent":["import { createServer as createHttpsServer, type Server } from 'node:https';\nimport { Readable } from 'node:stream';\n\nimport { createLogger } from '../utils/logger.js';\nimport {\n ensureValidCert,\n loadStoredCert,\n startRenewalScheduler,\n stopRenewalScheduler,\n type StoredCert,\n} from './acme-cert-store.js';\nimport type { AcmeConfig } from './acme-client.js';\n\nconst log = createLogger('TunnelTLS');\n\nlet httpsNodeServer: Server | null = null;\n\nexport type TunnelTlsServerConfig = {\n tlsPort: number;\n gatewayPort: number;\n acmeConfig: AcmeConfig;\n fetch?: typeof fetch;\n};\n\nfunction resolveTlsFetch(\n gatewayPort: number,\n customFetch?: typeof fetch,\n): (req: Request) => Response | Promise<Response> {\n if (customFetch) {\n return (req: Request) => customFetch(req);\n }\n return async (req: Request) => {\n const url = new URL(req.url);\n url.protocol = 'http:';\n url.hostname = '127.0.0.1';\n url.port = String(gatewayPort);\n return fetch(url, {\n method: req.method,\n headers: req.headers,\n body: req.body,\n redirect: 'manual',\n duplex: 'half',\n } as RequestInit);\n };\n}\n\nasync function nodeRequestToFetch(req: import('node:http').IncomingMessage, tlsPort: number): Promise<Request> {\n const url = new URL(req.url ?? '/', `https://127.0.0.1:${tlsPort}`);\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const part of value) headers.append(key, part);\n } else {\n headers.set(key, value);\n }\n }\n\n const hasBody = req.method !== 'GET' && req.method !== 'HEAD';\n const init: RequestInit = {\n method: req.method,\n headers,\n };\n if (hasBody) {\n init.body = Readable.toWeb(req) as RequestInit['body'];\n (init as RequestInit & { duplex?: 'half' }).duplex = 'half';\n }\n return new Request(url, init);\n}\n\nasync function sendFetchResponse(res: import('node:http').ServerResponse, response: Response): Promise<void> {\n res.writeHead(response.status, Object.fromEntries(response.headers.entries()));\n if (response.body) {\n const reader = response.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n }\n res.end();\n}\n\nexport async function startTunnelTlsServer(config: TunnelTlsServerConfig): Promise<Server> {\n if (httpsNodeServer) return httpsNodeServer;\n\n const cert = await ensureValidCert(config.acmeConfig);\n const handler = resolveTlsFetch(config.gatewayPort, config.fetch);\n\n httpsNodeServer = createHttpsServer(\n {\n key: cert.keyPem,\n cert: cert.certPem,\n minVersion: 'TLSv1.2',\n },\n (req, res) => {\n void (async () => {\n try {\n const request = await nodeRequestToFetch(req, config.tlsPort);\n const response = await handler(request);\n await sendFetchResponse(res, response);\n } catch (err) {\n log.warn({ err, phase: 'tls_proxy' }, 'TLS proxy request failed');\n if (!res.headersSent) res.writeHead(502);\n res.end('Bad Gateway');\n }\n })();\n },\n );\n\n await new Promise<void>((resolve, reject) => {\n httpsNodeServer!.once('error', reject);\n httpsNodeServer!.listen(config.tlsPort, '127.0.0.1', () => resolve());\n });\n\n log.info(\n { port: config.tlsPort, domain: cert.domain, expiresAt: cert.expiresAt },\n \"Tunnel TLS server listening (Let's Encrypt cert)\",\n );\n\n startRenewalScheduler(config.acmeConfig, () => reloadTlsCert());\n\n return httpsNodeServer;\n}\n\nfunction reloadTlsCert(): void {\n const cert = loadStoredCert();\n if (!cert || !httpsNodeServer) return;\n httpsNodeServer.setSecureContext({ key: cert.keyPem, cert: cert.certPem });\n log.info({ domain: cert.domain, expiresAt: cert.expiresAt }, 'TLS cert hot-reloaded');\n}\n\nexport function stopTunnelTlsServer(): void {\n stopRenewalScheduler();\n if (httpsNodeServer) {\n httpsNodeServer.close();\n httpsNodeServer = null;\n log.info('TLS server stopped');\n }\n}\n\nexport function getActiveTlsCert(): StoredCert | null {\n return loadStoredCert();\n}\n\n/** @internal */\nexport function resetTunnelTlsServerForTests(): void {\n stopTunnelTlsServer();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tls-server.js","names":["createHttpsServer"],"sources":["../../../src/tunnel/tls-server.ts"],"sourcesContent":["import { createServer as createHttpsServer, type Server } from 'node:https';\nimport { Readable } from 'node:stream';\n\nimport { createLogger } from '../utils/logger.js';\nimport {\n ensureValidCert,\n loadStoredCert,\n startRenewalScheduler,\n stopRenewalScheduler,\n type StoredCert,\n} from './acme-cert-store.js';\nimport type { AcmeConfig } from './acme-client.js';\n\nconst log = createLogger('TunnelTLS');\n\nlet httpsNodeServer: Server | null = null;\n\nexport type TunnelTlsServerConfig = {\n tlsPort: number;\n gatewayPort: number;\n acmeConfig: AcmeConfig;\n fetch?: typeof fetch;\n};\n\nfunction resolveTlsFetch(\n gatewayPort: number,\n customFetch?: typeof fetch,\n): (req: Request) => Response | Promise<Response> {\n if (customFetch) {\n return (req: Request) => customFetch(req);\n }\n return async (req: Request) => {\n const url = new URL(req.url);\n url.protocol = 'http:';\n url.hostname = '127.0.0.1';\n url.port = String(gatewayPort);\n return fetch(url, {\n method: req.method,\n headers: req.headers,\n body: req.body,\n redirect: 'manual',\n duplex: 'half',\n } as RequestInit);\n };\n}\n\nasync function nodeRequestToFetch(req: import('node:http').IncomingMessage, tlsPort: number): Promise<Request> {\n const url = new URL(req.url ?? '/', `https://127.0.0.1:${tlsPort}`);\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const part of value) headers.append(key, part);\n } else {\n headers.set(key, value);\n }\n }\n\n const hasBody = req.method !== 'GET' && req.method !== 'HEAD';\n const init: RequestInit = {\n method: req.method,\n headers,\n };\n if (hasBody) {\n init.body = Readable.toWeb(req) as RequestInit['body'];\n (init as RequestInit & { duplex?: 'half' }).duplex = 'half';\n }\n return new Request(url, init);\n}\n\nasync function sendFetchResponse(res: import('node:http').ServerResponse, response: Response): Promise<void> {\n res.writeHead(response.status, Object.fromEntries(response.headers.entries()));\n if (response.body) {\n const reader = response.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n }\n res.end();\n}\n\nexport async function startTunnelTlsServer(config: TunnelTlsServerConfig): Promise<Server> {\n if (httpsNodeServer) return httpsNodeServer;\n\n const cert = await ensureValidCert(config.acmeConfig);\n const handler = resolveTlsFetch(config.gatewayPort, config.fetch);\n\n httpsNodeServer = createHttpsServer(\n {\n key: cert.keyPem,\n cert: cert.certPem,\n minVersion: 'TLSv1.2',\n },\n (req, res) => {\n void (async () => {\n try {\n const request = await nodeRequestToFetch(req, config.tlsPort);\n const response = await handler(request);\n await sendFetchResponse(res, response);\n } catch (err) {\n log.warn({ err, phase: 'tls_proxy' }, 'TLS proxy request failed');\n if (!res.headersSent) res.writeHead(502);\n res.end('Bad Gateway');\n }\n })();\n },\n );\n\n await new Promise<void>((resolve, reject) => {\n httpsNodeServer!.once('error', reject);\n httpsNodeServer!.listen(config.tlsPort, '127.0.0.1', () => resolve());\n }).catch((err: unknown) => {\n httpsNodeServer?.close();\n httpsNodeServer = null;\n const em = err instanceof Error ? err.message : String(err);\n if (em.includes('EADDRINUSE')) {\n throw new Error(\n `Tunnel TLS port ${config.tlsPort} is already in use. ` +\n `Stop other xopc tunnel instances or set tunnel.e2e.tlsPort (try ${config.gatewayPort + 1} for gateway port ${config.gatewayPort}).`,\n );\n }\n throw err;\n });\n\n log.info(\n { port: config.tlsPort, domain: cert.domain, expiresAt: cert.expiresAt },\n \"Tunnel TLS server listening (Let's Encrypt cert)\",\n );\n\n startRenewalScheduler(config.acmeConfig, () => reloadTlsCert());\n\n return httpsNodeServer;\n}\n\nfunction reloadTlsCert(): void {\n const cert = loadStoredCert();\n if (!cert || !httpsNodeServer) return;\n httpsNodeServer.setSecureContext({ key: cert.keyPem, cert: cert.certPem });\n log.info({ domain: cert.domain, expiresAt: cert.expiresAt }, 'TLS cert hot-reloaded');\n}\n\nexport function stopTunnelTlsServer(): void {\n stopRenewalScheduler();\n if (httpsNodeServer) {\n httpsNodeServer.close();\n httpsNodeServer = null;\n log.info('TLS server stopped');\n }\n}\n\nexport function getActiveTlsCert(): StoredCert | null {\n return loadStoredCert();\n}\n\n/** @internal */\nexport function resetTunnelTlsServerForTests(): void {\n stopTunnelTlsServer();\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { TunnelConfigSchema, TunnelConsentSchema, init_schema } from "../config/schema.js";
|
|
2
2
|
import { assertTunnelAutoStartAllowed, buildTunnelConsentRecord, hasValidTunnelConsent } from "./consent.js";
|
|
3
|
+
import { isMaskedTunnelSecretPatchValue } from "./env.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/tunnel/tunnel-config.ts
|
|
5
6
|
init_schema();
|
|
6
7
|
const TunnelConfigPatchSchema = z.object({
|
|
7
8
|
enabled: z.boolean().optional(),
|
|
8
9
|
brokerUrl: z.string().url().optional(),
|
|
10
|
+
registrationSecret: z.union([z.string(), z.null()]).optional(),
|
|
9
11
|
autoStart: z.boolean().optional(),
|
|
10
12
|
subdomain: z.string().optional(),
|
|
11
13
|
consent: TunnelConsentSchema.optional()
|
|
@@ -16,10 +18,18 @@ function mergeTunnelConfigPatch(config, patch) {
|
|
|
16
18
|
ok: false,
|
|
17
19
|
message: parsed.error.issues.map((i) => i.message).join("; ")
|
|
18
20
|
};
|
|
21
|
+
const patchFields = { ...parsed.data };
|
|
22
|
+
if (parsed.data.registrationSecret !== void 0) if (parsed.data.registrationSecret === null) delete patchFields.registrationSecret;
|
|
23
|
+
else {
|
|
24
|
+
const trimmed = parsed.data.registrationSecret.trim();
|
|
25
|
+
if (!trimmed || isMaskedTunnelSecretPatchValue(trimmed)) delete patchFields.registrationSecret;
|
|
26
|
+
else patchFields.registrationSecret = trimmed;
|
|
27
|
+
}
|
|
19
28
|
const next = {
|
|
20
29
|
...config.tunnel ?? TunnelConfigSchema.parse({}),
|
|
21
|
-
...
|
|
30
|
+
...patchFields
|
|
22
31
|
};
|
|
32
|
+
if (parsed.data.registrationSecret === null) delete next.registrationSecret;
|
|
23
33
|
if (parsed.data.autoStart === true) {
|
|
24
34
|
const probe = {
|
|
25
35
|
...config,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-config.js","names":[],"sources":["../../../src/tunnel/tunnel-config.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport type { Config } from '../config/schema.js';\nimport { TunnelConfigSchema, TunnelConsentSchema } from '../config/schema.js';\n\nimport {\n assertTunnelAutoStartAllowed,\n buildTunnelConsentRecord,\n hasValidTunnelConsent,\n} from './consent.js';\n\nconst TunnelConfigPatchSchema = z.object({\n enabled: z.boolean().optional(),\n brokerUrl: z.string().url().optional(),\n autoStart: z.boolean().optional(),\n subdomain: z.string().optional(),\n consent: TunnelConsentSchema.optional(),\n});\n\nexport function mergeTunnelConfigPatch(\n config: Config,\n patch: Record<string, unknown>,\n): { ok: true } | { ok: false; message: string } {\n const parsed = TunnelConfigPatchSchema.safeParse(patch);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.issues.map((i) => i.message).join('; ') };\n }\n\n const next = {\n ...(config.tunnel ?? TunnelConfigSchema.parse({})),\n ...parsed.data
|
|
1
|
+
{"version":3,"file":"tunnel-config.js","names":[],"sources":["../../../src/tunnel/tunnel-config.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport type { Config } from '../config/schema.js';\nimport { TunnelConfigSchema, TunnelConsentSchema } from '../config/schema.js';\n\nimport {\n assertTunnelAutoStartAllowed,\n buildTunnelConsentRecord,\n hasValidTunnelConsent,\n} from './consent.js';\nimport { isMaskedTunnelSecretPatchValue } from './env.js';\n\nconst TunnelConfigPatchSchema = z.object({\n enabled: z.boolean().optional(),\n brokerUrl: z.string().url().optional(),\n registrationSecret: z.union([z.string(), z.null()]).optional(),\n autoStart: z.boolean().optional(),\n subdomain: z.string().optional(),\n consent: TunnelConsentSchema.optional(),\n});\n\nexport function mergeTunnelConfigPatch(\n config: Config,\n patch: Record<string, unknown>,\n): { ok: true } | { ok: false; message: string } {\n const parsed = TunnelConfigPatchSchema.safeParse(patch);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.issues.map((i) => i.message).join('; ') };\n }\n\n const patchFields = { ...parsed.data };\n if (parsed.data.registrationSecret !== undefined) {\n if (parsed.data.registrationSecret === null) {\n delete patchFields.registrationSecret;\n } else {\n const trimmed = parsed.data.registrationSecret.trim();\n if (!trimmed || isMaskedTunnelSecretPatchValue(trimmed)) {\n delete patchFields.registrationSecret;\n } else {\n patchFields.registrationSecret = trimmed;\n }\n }\n }\n\n const next = {\n ...(config.tunnel ?? TunnelConfigSchema.parse({})),\n ...patchFields,\n };\n\n if (parsed.data.registrationSecret === null) {\n delete next.registrationSecret;\n }\n\n if (parsed.data.autoStart === true) {\n const probe: Config = { ...config, tunnel: { ...next, autoStart: true } };\n try {\n assertTunnelAutoStartAllowed(probe);\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n return { ok: false, message: em };\n }\n }\n\n if (parsed.data.enabled === true && !hasValidTunnelConsent({ ...config, tunnel: next })) {\n return {\n ok: false,\n message:\n 'Cannot enable tunnel without accepting the security notice. Start remote access from settings or record consent first.',\n };\n }\n\n config.tunnel = next;\n return { ok: true };\n}\n\nexport function applyTunnelConsentToConfig(config: Config): void {\n if (!config.tunnel) {\n config.tunnel = TunnelConfigSchema.parse({});\n }\n config.tunnel.consent = buildTunnelConsentRecord();\n}\n\nexport function setTunnelEnabledInConfig(config: Config, enabled: boolean): void {\n if (!config.tunnel) {\n config.tunnel = TunnelConfigSchema.parse({});\n }\n config.tunnel.enabled = enabled;\n}\n\n/**\n * Clear stale tunnel flags when consent is missing or outdated (Phase 2: config/runtime alignment).\n * Returns true when `config.tunnel` was modified.\n */\nexport function sanitizeTunnelConfig(config: Config): boolean {\n const tunnel = config.tunnel;\n if (!tunnel) return false;\n if (hasValidTunnelConsent(config)) return false;\n\n let changed = false;\n if (tunnel.enabled) {\n tunnel.enabled = false;\n changed = true;\n }\n if (tunnel.autoStart) {\n tunnel.autoStart = false;\n changed = true;\n }\n return changed;\n}\n"],"mappings":";;;;;aAG8E;AAS9E,MAAM,0BAA0B,EAAE,OAAO;CACvC,SAAS,EAAE,SAAS,CAAC,UAAU;CAC/B,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CACtC,oBAAoB,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU;CAC9D,WAAW,EAAE,SAAS,CAAC,UAAU;CACjC,WAAW,EAAE,QAAQ,CAAC,UAAU;CAChC,SAAS,oBAAoB,UAAU;CACxC,CAAC;AAEF,SAAgB,uBACd,QACA,OAC+C;CAC/C,MAAM,SAAS,wBAAwB,UAAU,MAAM;AACvD,KAAI,CAAC,OAAO,QACV,QAAO;EAAE,IAAI;EAAO,SAAS,OAAO,MAAM,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;EAAE;CAGrF,MAAM,cAAc,EAAE,GAAG,OAAO,MAAM;AACtC,KAAI,OAAO,KAAK,uBAAuB,KAAA,EACrC,KAAI,OAAO,KAAK,uBAAuB,KACrC,QAAO,YAAY;MACd;EACL,MAAM,UAAU,OAAO,KAAK,mBAAmB,MAAM;AACrD,MAAI,CAAC,WAAW,+BAA+B,QAAQ,CACrD,QAAO,YAAY;MAEnB,aAAY,qBAAqB;;CAKvC,MAAM,OAAO;EACX,GAAI,OAAO,UAAU,mBAAmB,MAAM,EAAE,CAAC;EACjD,GAAG;EACJ;AAED,KAAI,OAAO,KAAK,uBAAuB,KACrC,QAAO,KAAK;AAGd,KAAI,OAAO,KAAK,cAAc,MAAM;EAClC,MAAM,QAAgB;GAAE,GAAG;GAAQ,QAAQ;IAAE,GAAG;IAAM,WAAW;IAAM;GAAE;AACzE,MAAI;AACF,gCAA6B,MAAM;WAC5B,KAAK;AAEZ,UAAO;IAAE,IAAI;IAAO,SADT,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAC1B;;;AAIrC,KAAI,OAAO,KAAK,YAAY,QAAQ,CAAC,sBAAsB;EAAE,GAAG;EAAQ,QAAQ;EAAM,CAAC,CACrF,QAAO;EACL,IAAI;EACJ,SACE;EACH;AAGH,QAAO,SAAS;AAChB,QAAO,EAAE,IAAI,MAAM;;AAGrB,SAAgB,2BAA2B,QAAsB;AAC/D,KAAI,CAAC,OAAO,OACV,QAAO,SAAS,mBAAmB,MAAM,EAAE,CAAC;AAE9C,QAAO,OAAO,UAAU,0BAA0B;;AAGpD,SAAgB,yBAAyB,QAAgB,SAAwB;AAC/E,KAAI,CAAC,OAAO,OACV,QAAO,SAAS,mBAAmB,MAAM,EAAE,CAAC;AAE9C,QAAO,OAAO,UAAU;;;;;;AAO1B,SAAgB,qBAAqB,QAAyB;CAC5D,MAAM,SAAS,OAAO;AACtB,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,sBAAsB,OAAO,CAAE,QAAO;CAE1C,IAAI,UAAU;AACd,KAAI,OAAO,SAAS;AAClB,SAAO,UAAU;AACjB,YAAU;;AAEZ,KAAI,OAAO,WAAW;AACpB,SAAO,YAAY;AACnB,YAAU;;AAEZ,QAAO"}
|
|
@@ -4,5 +4,8 @@ export type ResolvedTunnelE2eConfig = {
|
|
|
4
4
|
tlsPort: number;
|
|
5
5
|
staging: boolean;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
/** Historical schema default — not suitable when gateway.port ≠ 18790 (e.g. Electron 28790). */
|
|
8
|
+
export declare const LEGACY_DEFAULT_TUNNEL_TLS_PORT = 18791;
|
|
9
|
+
export declare function resolveTunnelTlsPort(e2eTlsPort: number | undefined, gatewayPort: number): number;
|
|
10
|
+
export declare function resolveTunnelE2eConfig(tunnel?: TunnelConfig, gatewayPort?: number): ResolvedTunnelE2eConfig;
|
|
8
11
|
export declare function resolveFrpSubdomainHost(brokerUrl: string, override?: string): string;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
//#region src/tunnel/tunnel-e2e-config.ts
|
|
2
|
-
|
|
2
|
+
/** Historical schema default — not suitable when gateway.port ≠ 18790 (e.g. Electron 28790). */
|
|
3
|
+
const LEGACY_DEFAULT_TUNNEL_TLS_PORT = 18791;
|
|
4
|
+
function resolveTunnelTlsPort(e2eTlsPort, gatewayPort) {
|
|
5
|
+
if (e2eTlsPort === void 0) return gatewayPort + 1;
|
|
6
|
+
if (e2eTlsPort === 18791 && gatewayPort !== 18790) return gatewayPort + 1;
|
|
7
|
+
return e2eTlsPort;
|
|
8
|
+
}
|
|
9
|
+
function resolveTunnelE2eConfig(tunnel, gatewayPort = 18790) {
|
|
3
10
|
const e2e = tunnel?.e2e;
|
|
4
11
|
return {
|
|
5
12
|
enabled: e2e?.enabled ?? true,
|
|
6
|
-
tlsPort: e2e?.tlsPort
|
|
13
|
+
tlsPort: resolveTunnelTlsPort(e2e?.tlsPort, gatewayPort),
|
|
7
14
|
staging: e2e?.staging ?? false
|
|
8
15
|
};
|
|
9
16
|
}
|
|
@@ -17,6 +24,6 @@ function resolveFrpSubdomainHost(brokerUrl, override) {
|
|
|
17
24
|
return "frp.xopc.ai";
|
|
18
25
|
}
|
|
19
26
|
//#endregion
|
|
20
|
-
export { resolveFrpSubdomainHost, resolveTunnelE2eConfig };
|
|
27
|
+
export { LEGACY_DEFAULT_TUNNEL_TLS_PORT, resolveFrpSubdomainHost, resolveTunnelE2eConfig, resolveTunnelTlsPort };
|
|
21
28
|
|
|
22
29
|
//# sourceMappingURL=tunnel-e2e-config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-e2e-config.js","names":[],"sources":["../../../src/tunnel/tunnel-e2e-config.ts"],"sourcesContent":["import type { TunnelConfig } from '../config/schema.js';\n\nexport type ResolvedTunnelE2eConfig = {\n enabled: boolean;\n tlsPort: number;\n staging: boolean;\n};\n\nexport function resolveTunnelE2eConfig(tunnel?: TunnelConfig): ResolvedTunnelE2eConfig {\n const e2e = tunnel?.e2e;\n return {\n enabled: e2e?.enabled ?? true,\n tlsPort: e2e?.tlsPort
|
|
1
|
+
{"version":3,"file":"tunnel-e2e-config.js","names":[],"sources":["../../../src/tunnel/tunnel-e2e-config.ts"],"sourcesContent":["import type { TunnelConfig } from '../config/schema.js';\n\nexport type ResolvedTunnelE2eConfig = {\n enabled: boolean;\n tlsPort: number;\n staging: boolean;\n};\n\n/** Historical schema default — not suitable when gateway.port ≠ 18790 (e.g. Electron 28790). */\nexport const LEGACY_DEFAULT_TUNNEL_TLS_PORT = 18791;\n\nexport function resolveTunnelTlsPort(e2eTlsPort: number | undefined, gatewayPort: number): number {\n if (e2eTlsPort === undefined) {\n return gatewayPort + 1;\n }\n if (e2eTlsPort === LEGACY_DEFAULT_TUNNEL_TLS_PORT && gatewayPort !== 18790) {\n return gatewayPort + 1;\n }\n return e2eTlsPort;\n}\n\nexport function resolveTunnelE2eConfig(\n tunnel?: TunnelConfig,\n gatewayPort = 18790,\n): ResolvedTunnelE2eConfig {\n const e2e = tunnel?.e2e;\n return {\n enabled: e2e?.enabled ?? true,\n tlsPort: resolveTunnelTlsPort(e2e?.tlsPort, gatewayPort),\n staging: e2e?.staging ?? false,\n };\n}\n\nexport function resolveFrpSubdomainHost(brokerUrl: string, override?: string): string {\n if (override?.trim()) return override.trim();\n try {\n const host = new URL(brokerUrl.replace(/\\/api\\/?$/, '')).hostname;\n if (host === 'frp.xopc.ai' || host.endsWith('.frp.xopc.ai')) return 'frp.xopc.ai';\n if (host.includes('.')) return host;\n } catch {\n /* fall through */\n }\n return 'frp.xopc.ai';\n}\n"],"mappings":";;AASA,MAAa,iCAAiC;AAE9C,SAAgB,qBAAqB,YAAgC,aAA6B;AAChG,KAAI,eAAe,KAAA,EACjB,QAAO,cAAc;AAEvB,KAAI,eAAA,SAAiD,gBAAgB,MACnE,QAAO,cAAc;AAEvB,QAAO;;AAGT,SAAgB,uBACd,QACA,cAAc,OACW;CACzB,MAAM,MAAM,QAAQ;AACpB,QAAO;EACL,SAAS,KAAK,WAAW;EACzB,SAAS,qBAAqB,KAAK,SAAS,YAAY;EACxD,SAAS,KAAK,WAAW;EAC1B;;AAGH,SAAgB,wBAAwB,WAAmB,UAA2B;AACpF,KAAI,UAAU,MAAM,CAAE,QAAO,SAAS,MAAM;AAC5C,KAAI;EACF,MAAM,OAAO,IAAI,IAAI,UAAU,QAAQ,aAAa,GAAG,CAAC,CAAC;AACzD,MAAI,SAAS,iBAAiB,KAAK,SAAS,eAAe,CAAE,QAAO;AACpE,MAAI,KAAK,SAAS,IAAI,CAAE,QAAO;SACzB;AAGR,QAAO"}
|
|
@@ -24,9 +24,13 @@ export declare class TunnelService extends EventEmitter {
|
|
|
24
24
|
private reconnectAttempt;
|
|
25
25
|
private stopping;
|
|
26
26
|
private startContext;
|
|
27
|
+
private frpcDownload;
|
|
28
|
+
private startProgress;
|
|
27
29
|
configure(cfg: TunnelServiceConfig): void;
|
|
28
30
|
setGatewayFetch(fetchFn: typeof fetch): void;
|
|
29
31
|
getStatus(): TunnelStatus;
|
|
32
|
+
private setStartPhase;
|
|
33
|
+
private clearStartProgress;
|
|
30
34
|
buildQr(gatewayPort: number, gatewayHost: string): TunnelQrPayload;
|
|
31
35
|
start(gatewayPort: number, gatewayToken: string): Promise<TunnelQrPayload>;
|
|
32
36
|
stop(opts?: {
|
|
@@ -41,6 +41,8 @@ var TunnelService = class extends EventEmitter {
|
|
|
41
41
|
reconnectAttempt = 0;
|
|
42
42
|
stopping = false;
|
|
43
43
|
startContext = null;
|
|
44
|
+
frpcDownload = null;
|
|
45
|
+
startProgress = null;
|
|
44
46
|
configure(cfg) {
|
|
45
47
|
this.serviceConfig = {
|
|
46
48
|
...cfg,
|
|
@@ -63,6 +65,8 @@ var TunnelService = class extends EventEmitter {
|
|
|
63
65
|
frpcPid: this.frpcHandle?.pid ?? null,
|
|
64
66
|
lastHeartbeatAt: this.lastHeartbeatAt,
|
|
65
67
|
lastError: this.lastError,
|
|
68
|
+
frpcDownload: this.frpcDownload,
|
|
69
|
+
startProgress: this.startProgress,
|
|
66
70
|
config: {
|
|
67
71
|
autoStart: cfg?.autoStart ?? false,
|
|
68
72
|
brokerUrl: cfg?.brokerUrl ?? "https://frp.xopc.ai/api",
|
|
@@ -75,6 +79,22 @@ var TunnelService = class extends EventEmitter {
|
|
|
75
79
|
cert: getCertStatusSummary()
|
|
76
80
|
};
|
|
77
81
|
}
|
|
82
|
+
setStartPhase(phase, patch) {
|
|
83
|
+
const prev = this.startProgress;
|
|
84
|
+
const samePhase = prev?.phase === phase;
|
|
85
|
+
this.startProgress = {
|
|
86
|
+
phase,
|
|
87
|
+
startedAt: samePhase && prev ? prev.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
+
acmeStep: patch?.acmeStep !== void 0 ? patch.acmeStep : samePhase ? prev?.acmeStep ?? null : null,
|
|
89
|
+
publicUrl: patch?.publicUrl !== void 0 ? patch.publicUrl : samePhase ? prev?.publicUrl ?? null : prev?.publicUrl ?? null
|
|
90
|
+
};
|
|
91
|
+
this.emit("tunnel:progress");
|
|
92
|
+
}
|
|
93
|
+
clearStartProgress() {
|
|
94
|
+
if (!this.startProgress) return;
|
|
95
|
+
this.startProgress = null;
|
|
96
|
+
this.emit("tunnel:progress");
|
|
97
|
+
}
|
|
78
98
|
buildQr(gatewayPort, gatewayHost) {
|
|
79
99
|
const publicUrl = loadTunnelState()?.publicUrl ?? null;
|
|
80
100
|
if (!publicUrl) return {
|
|
@@ -100,15 +120,35 @@ var TunnelService = class extends EventEmitter {
|
|
|
100
120
|
};
|
|
101
121
|
this.state = "connecting";
|
|
102
122
|
this.lastError = null;
|
|
123
|
+
this.frpcDownload = null;
|
|
124
|
+
this.startProgress = null;
|
|
125
|
+
this.setStartPhase("preparing_frpc");
|
|
103
126
|
this.emit("tunnel:connecting");
|
|
104
|
-
|
|
127
|
+
let frpcBin;
|
|
128
|
+
try {
|
|
129
|
+
frpcBin = await ensureFrpcBinary({ onProgress: (progress) => {
|
|
130
|
+
this.frpcDownload = progress;
|
|
131
|
+
this.setStartPhase("preparing_frpc");
|
|
132
|
+
} });
|
|
133
|
+
this.frpcDownload = null;
|
|
134
|
+
this.emit("tunnel:progress");
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.frpcDownload = null;
|
|
137
|
+
this.clearStartProgress();
|
|
138
|
+
this.state = "error";
|
|
139
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
140
|
+
this.emit("tunnel:error", this.lastError);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
105
143
|
publishFrpcPathForProcess(frpcBin);
|
|
106
144
|
const persisted = loadTunnelState();
|
|
107
145
|
const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));
|
|
108
146
|
let registration;
|
|
109
147
|
try {
|
|
148
|
+
this.setStartPhase("registering");
|
|
110
149
|
registration = await this.resolveRegistration(broker, cfg, gatewayToken, persisted);
|
|
111
150
|
} catch (err) {
|
|
151
|
+
this.clearStartProgress();
|
|
112
152
|
this.state = "error";
|
|
113
153
|
this.lastError = err instanceof Error ? err.message : String(err);
|
|
114
154
|
this.emit("tunnel:error", this.lastError);
|
|
@@ -116,9 +156,20 @@ var TunnelService = class extends EventEmitter {
|
|
|
116
156
|
}
|
|
117
157
|
const state = persistedFromRegistration(registration);
|
|
118
158
|
saveTunnelState(state);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
159
|
+
this.setStartPhase("registering", { publicUrl: registration.publicUrl });
|
|
160
|
+
try {
|
|
161
|
+
const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(broker, registration, cfg, gatewayPort);
|
|
162
|
+
const configPath = writeFrpcConfig(registration, frpcLocalPort, "127.0.0.1", frpcMode);
|
|
163
|
+
this.setStartPhase("starting_frpc", { publicUrl: registration.publicUrl });
|
|
164
|
+
await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.clearStartProgress();
|
|
167
|
+
this.state = "error";
|
|
168
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
169
|
+
this.emit("tunnel:error", this.lastError);
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
this.clearStartProgress();
|
|
122
173
|
this.state = "connected";
|
|
123
174
|
this.connectedSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
124
175
|
this.reconnectAttempt = 0;
|
|
@@ -173,6 +224,7 @@ var TunnelService = class extends EventEmitter {
|
|
|
173
224
|
this.state = "disconnected";
|
|
174
225
|
this.connectedSince = null;
|
|
175
226
|
this.startContext = null;
|
|
227
|
+
this.clearStartProgress();
|
|
176
228
|
this.emit("tunnel:disconnected");
|
|
177
229
|
return { released };
|
|
178
230
|
}
|
|
@@ -240,6 +292,7 @@ var TunnelService = class extends EventEmitter {
|
|
|
240
292
|
return;
|
|
241
293
|
}
|
|
242
294
|
this.state = "reconnecting";
|
|
295
|
+
this.setStartPhase("reconnecting_frpc", { publicUrl: state.publicUrl ?? null });
|
|
243
296
|
const delayMs = Math.min(16e3, 1e3 * 2 ** (this.reconnectAttempt - 1));
|
|
244
297
|
log.info({
|
|
245
298
|
attempt: this.reconnectAttempt,
|
|
@@ -249,6 +302,7 @@ var TunnelService = class extends EventEmitter {
|
|
|
249
302
|
if (this.stopping) return;
|
|
250
303
|
try {
|
|
251
304
|
await this.spawnAndWait(frpcBin, configPath, broker, state, heartbeatIntervalMs);
|
|
305
|
+
this.clearStartProgress();
|
|
252
306
|
this.state = "connected";
|
|
253
307
|
this.reconnectAttempt = 0;
|
|
254
308
|
this.emit("tunnel:connected");
|
|
@@ -309,13 +363,20 @@ var TunnelService = class extends EventEmitter {
|
|
|
309
363
|
frpcLocalPort: gatewayPort,
|
|
310
364
|
frpcMode: "http"
|
|
311
365
|
};
|
|
366
|
+
this.setStartPhase("provisioning_tls", { publicUrl: registration.publicUrl });
|
|
312
367
|
const acmeConfig = {
|
|
313
368
|
broker,
|
|
314
369
|
tunnelId: registration.tunnelId,
|
|
315
370
|
tunnelToken: registration.tunnelToken,
|
|
316
371
|
subdomain: registration.subdomain,
|
|
317
372
|
frpSubdomainHost: cfg.frpSubdomainHost,
|
|
318
|
-
staging: cfg.e2e.staging
|
|
373
|
+
staging: cfg.e2e.staging,
|
|
374
|
+
onProgress: (step) => {
|
|
375
|
+
this.setStartPhase("provisioning_tls", {
|
|
376
|
+
publicUrl: registration.publicUrl,
|
|
377
|
+
acmeStep: step
|
|
378
|
+
});
|
|
379
|
+
}
|
|
319
380
|
};
|
|
320
381
|
await startTunnelTlsServer({
|
|
321
382
|
tlsPort: cfg.e2e.tlsPort,
|