@dbx-tools/tunnel 0.6.105 → 0.6.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -18
- package/index.ts +3 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +2 -1
- package/lib/src/frp.d.ts +44 -0
- package/lib/src/frp.js +118 -0
- package/lib/src/gate.d.ts +5 -5
- package/lib/src/gate.js +6 -5
- package/lib/src/interceptor.d.ts +22 -5
- package/lib/src/interceptor.js +58 -20
- package/lib/src/plugin.d.ts +5 -1
- package/lib/src/plugin.js +13 -4
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/frp.ts +183 -0
- package/src/gate.ts +10 -6
- package/src/interceptor.ts +71 -22
- package/src/plugin.ts +19 -4
package/src/gate.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface GateOptions {
|
|
|
47
47
|
* inert (everything passes through) - a tunnel with no public domain gates
|
|
48
48
|
* nothing.
|
|
49
49
|
*/
|
|
50
|
-
publicDomain?: string;
|
|
50
|
+
publicDomain?: string | readonly string[];
|
|
51
51
|
/**
|
|
52
52
|
* Extra `x-` request headers tunnel traffic may forward (unioned with the
|
|
53
53
|
* built-in allow-list). Every other `x-` header is stripped from tunnel traffic.
|
|
@@ -64,14 +64,18 @@ export interface GateOptions {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* True when the request's `Host` is the tunnel's public
|
|
68
|
-
* over
|
|
69
|
-
* domain is configured, nothing is tunnel traffic.
|
|
67
|
+
* True when the request's `Host` is one of the tunnel's public domains - i.e. it
|
|
68
|
+
* came in over Portr or FRP. Case-insensitive; the optional `:port` is ignored.
|
|
69
|
+
* When no public domain is configured, nothing is tunnel traffic.
|
|
70
70
|
*/
|
|
71
|
-
export function isTunnelHost(
|
|
71
|
+
export function isTunnelHost(
|
|
72
|
+
req: IncomingMessage,
|
|
73
|
+
publicDomain: string | readonly string[] | undefined,
|
|
74
|
+
): boolean {
|
|
72
75
|
if (!publicDomain) return false;
|
|
73
76
|
const host = (req.headers.host ?? "").toLowerCase().split(":")[0];
|
|
74
|
-
|
|
77
|
+
const domains = typeof publicDomain === "string" ? [publicDomain] : publicDomain;
|
|
78
|
+
return domains.some((domain) => host === domain.toLowerCase().split(":")[0]);
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
/**
|
package/src/interceptor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tunnelInterceptor()` - the {@link Interceptor} that fronts an app with a public
|
|
3
|
-
* portr tunnel, consuming the {@link InterceptorContext} `@dbx-tools/appkit`'s
|
|
3
|
+
* portr and/or frp tunnel, consuming the {@link InterceptorContext} `@dbx-tools/appkit`'s
|
|
4
4
|
* `createApp` hands it.
|
|
5
5
|
*
|
|
6
6
|
* The tunnel runs IN-PROCESS: the APP is the main process and hands this interceptor
|
|
@@ -17,15 +17,19 @@
|
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import type { ChildProcess } from "node:child_process";
|
|
20
21
|
import type { Interceptor, InterceptorContext } from "@dbx-tools/appkit";
|
|
21
22
|
import { log, object } from "@dbx-tools/shared-core";
|
|
23
|
+
import { installFrp, resolveFrpConfig, startFrp, writeFrpConfig } from "./frp.ts";
|
|
22
24
|
import { installPortr, resolvePortrConfig, startPortr, writePortrConfig } from "./portr.ts";
|
|
23
25
|
|
|
24
26
|
const logger = log.logger("tunnel:interceptor");
|
|
25
27
|
|
|
26
28
|
/** Options for {@link tunnelInterceptor} (each also resolvable from env). */
|
|
27
29
|
export interface TunnelInterceptorOptions {
|
|
28
|
-
/**
|
|
30
|
+
/** Tunnel clients to run. Env `DBX_TOOLS_TUNNEL_TRANSPORT`; defaults to `portr`. */
|
|
31
|
+
transport?: TunnelTransport;
|
|
32
|
+
/** Public host to serve on. Env `TUNNEL_PUBLIC_DOMAIN`. */
|
|
29
33
|
publicDomain?: string;
|
|
30
34
|
/** portr subdomain (else derived from {@link publicDomain}). */
|
|
31
35
|
subdomain?: string;
|
|
@@ -35,6 +39,32 @@ export interface TunnelInterceptorOptions {
|
|
|
35
39
|
* (then `8000`), which is the port the platform routes to.
|
|
36
40
|
*/
|
|
37
41
|
port?: number;
|
|
42
|
+
/** frps control host; defaults to the FRP public domain. Env `FRP_SERVER`. */
|
|
43
|
+
frpServer?: string;
|
|
44
|
+
/** FRP public HTTP host. Env `TUNNEL_FRP_PUBLIC_DOMAIN`. */
|
|
45
|
+
frpPublicDomain?: string;
|
|
46
|
+
/** frps control port. Env `FRP_SERVER_PORT`; defaults to `443`. */
|
|
47
|
+
frpServerPort?: number;
|
|
48
|
+
/** frpc transport protocol. Env `FRP_PROTOCOL`; defaults to `wss`. */
|
|
49
|
+
frpProtocol?: string;
|
|
50
|
+
/** frps auth token. Env `FRP_TOKEN` (or `TUNNEL_TOKEN`). */
|
|
51
|
+
frpToken?: string;
|
|
52
|
+
/** frp proxy registration name. Env `FRP_PROXY_NAME`. */
|
|
53
|
+
frpProxyName?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Public tunnel clients supported by the interceptor and CLI. */
|
|
57
|
+
export type TunnelTransport = "portr" | "frp" | "both";
|
|
58
|
+
|
|
59
|
+
/** Resolve and validate the tunnel transport selector. */
|
|
60
|
+
export function resolveTunnelTransport(transport?: string): TunnelTransport {
|
|
61
|
+
const resolved =
|
|
62
|
+
transport ??
|
|
63
|
+
process.env.DBX_TOOLS_TUNNEL_TRANSPORT ??
|
|
64
|
+
process.env.TUNNEL_TRANSPORT ??
|
|
65
|
+
"portr";
|
|
66
|
+
if (resolved === "portr" || resolved === "frp" || resolved === "both") return resolved;
|
|
67
|
+
throw new TypeError(`invalid tunnel transport: ${resolved} (expected portr, frp, or both)`);
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
/** Resolve the public port portr should target: explicit, else the Apps contract. */
|
|
@@ -45,9 +75,8 @@ function resolvePublicPort(port?: number): number {
|
|
|
45
75
|
/**
|
|
46
76
|
* Build the tunnel {@link Interceptor}. Pass it to `createApp({ interceptor })`.
|
|
47
77
|
*
|
|
48
|
-
* A no-op (logs and returns) when
|
|
49
|
-
*
|
|
50
|
-
* unconditionally and only actually tunnels where the deployment wired portr.
|
|
78
|
+
* A no-op (logs and returns) when none of the selected tunnel clients resolve
|
|
79
|
+
* usable configuration, so an app can register it unconditionally.
|
|
51
80
|
*
|
|
52
81
|
* @example
|
|
53
82
|
* import { appkit } from "@dbx-tools/appkit";
|
|
@@ -65,25 +94,45 @@ export function tunnelInterceptor(options: TunnelInterceptorOptions = {}): Inter
|
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
const port = resolvePublicPort(options.port);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
const transport = resolveTunnelTransport(options.transport);
|
|
98
|
+
const children: ChildProcess[] = [];
|
|
99
|
+
if (transport === "portr" || transport === "both") {
|
|
100
|
+
const portrConfig = resolvePortrConfig({
|
|
101
|
+
publicDomain: options.publicDomain,
|
|
102
|
+
subdomain: options.subdomain,
|
|
103
|
+
port,
|
|
104
|
+
});
|
|
105
|
+
if (portrConfig) {
|
|
106
|
+
const portrEnv = await installPortr();
|
|
107
|
+
await writePortrConfig(portrConfig, portrEnv);
|
|
108
|
+
children.push(await startPortr(portrConfig, portrEnv));
|
|
109
|
+
} else {
|
|
110
|
+
logger.info("portr not configured (requires PORTR_TOKEN and TUNNEL_PUBLIC_DOMAIN)");
|
|
111
|
+
}
|
|
76
112
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
if (transport === "frp" || transport === "both") {
|
|
114
|
+
const frpConfig = resolveFrpConfig({
|
|
115
|
+
publicDomain: options.frpPublicDomain,
|
|
116
|
+
server: options.frpServer,
|
|
117
|
+
serverPort: options.frpServerPort,
|
|
118
|
+
protocol: options.frpProtocol,
|
|
119
|
+
token: options.frpToken,
|
|
120
|
+
proxyName: options.frpProxyName,
|
|
121
|
+
port,
|
|
122
|
+
});
|
|
123
|
+
if (frpConfig) {
|
|
124
|
+
const frpEnv = await installFrp();
|
|
125
|
+
const configPath = await writeFrpConfig(frpConfig, frpEnv);
|
|
126
|
+
children.push(startFrp(frpConfig, frpEnv, configPath));
|
|
127
|
+
} else {
|
|
128
|
+
logger.info("frp not configured (requires TUNNEL_PUBLIC_DOMAIN)");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!children.length) return;
|
|
132
|
+
for (const child of children) ctx.bindProcess(child);
|
|
83
133
|
ctx.onLifecycle("shutdown", () => {
|
|
84
|
-
if (!
|
|
134
|
+
for (const child of children) if (!child.killed) child.kill("SIGTERM");
|
|
85
135
|
});
|
|
86
|
-
|
|
87
|
-
logger.info(`tunnel bound: portr -> :${port} (${portrConfig.subdomain}.${portrConfig.server})`);
|
|
136
|
+
logger.info(`tunnel bound: ${transport} -> :${port}`);
|
|
88
137
|
};
|
|
89
138
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -166,6 +166,8 @@ export interface AuthGateConfig extends BasePluginConfig, AuthStorageConfig {
|
|
|
166
166
|
* traffic).
|
|
167
167
|
*/
|
|
168
168
|
publicDomain?: string;
|
|
169
|
+
/** Additional public tunnel domains accepted by the gate. */
|
|
170
|
+
publicDomains?: string | string[];
|
|
169
171
|
/**
|
|
170
172
|
* Extra `x-` request headers tunnel traffic may forward (literal / glob /
|
|
171
173
|
* `/regex/`), unioned with the built-in allow-list. Env `TUNNEL_FORWARD_HEADERS`.
|
|
@@ -213,8 +215,10 @@ export interface ResolvedAuthGateConfig {
|
|
|
213
215
|
sessionCutoffMs: number;
|
|
214
216
|
/** Same-origin path returned after logout. */
|
|
215
217
|
logoutRedirectPath: string;
|
|
216
|
-
/**
|
|
218
|
+
/** Primary public domain; `undefined` when Portr is not configured. */
|
|
217
219
|
publicDomain?: string;
|
|
220
|
+
/** All public tunnel domains accepted by the gate. */
|
|
221
|
+
publicDomains: string[];
|
|
218
222
|
/** Extra `x-` headers tunnel traffic may forward (unioned with the defaults). */
|
|
219
223
|
forwardHeaders: string[];
|
|
220
224
|
/** Path prefixes to gate beyond `/api/` (e.g. `/ws`). */
|
|
@@ -249,6 +253,8 @@ export function resolveAuthGateConfig(config: AuthGateConfig): ResolvedAuthGateC
|
|
|
249
253
|
(coreConfig.text("AUTH_STORAGE", TUNNEL_CONFIG) as AuthStorageMode | undefined),
|
|
250
254
|
sqlitePath: config.sqlitePath ?? coreConfig.text("AUTH_SQLITE_PATH", TUNNEL_CONFIG),
|
|
251
255
|
});
|
|
256
|
+
const publicDomain = coreConfig.string(config.publicDomain, "PUBLIC_DOMAIN", TUNNEL_CONFIG);
|
|
257
|
+
const frpPublicDomain = coreConfig.text("FRP_PUBLIC_DOMAIN", TUNNEL_CONFIG);
|
|
252
258
|
return {
|
|
253
259
|
// Both sources are unioned rather than one overriding: a deployment-wide
|
|
254
260
|
// TUNNEL_AUTH_ALLOW and a per-invocation `--allow` should both grant access.
|
|
@@ -280,7 +286,16 @@ export function resolveAuthGateConfig(config: AuthGateConfig): ResolvedAuthGateC
|
|
|
280
286
|
coreConfig.string(config.logoutRedirectPath, "AUTH_LOGOUT_REDIRECT", TUNNEL_CONFIG) ??
|
|
281
287
|
DEFAULTS.logoutRedirectPath,
|
|
282
288
|
),
|
|
283
|
-
publicDomain
|
|
289
|
+
publicDomain,
|
|
290
|
+
publicDomains: [
|
|
291
|
+
...new Set(
|
|
292
|
+
[
|
|
293
|
+
publicDomain,
|
|
294
|
+
frpPublicDomain,
|
|
295
|
+
...string.parseList(config.publicDomains),
|
|
296
|
+
].filter((value): value is string => !!value),
|
|
297
|
+
),
|
|
298
|
+
],
|
|
284
299
|
forwardHeaders: [
|
|
285
300
|
...string.parseList(config.forwardHeaders),
|
|
286
301
|
...string.parseList(coreConfig.text("FORWARD_HEADERS", TUNNEL_CONFIG)),
|
|
@@ -404,7 +419,7 @@ export class AuthGatePlugin extends Plugin<AuthGateConfig> {
|
|
|
404
419
|
logger.info("ready", {
|
|
405
420
|
patterns: this.resolved.allow.length,
|
|
406
421
|
sessionTtlSeconds: this.resolved.sessionTtlSeconds,
|
|
407
|
-
|
|
422
|
+
publicDomains: this.resolved.publicDomains,
|
|
408
423
|
insecure: this.resolved.insecure,
|
|
409
424
|
storage: this.resolved.storage,
|
|
410
425
|
logoutRedirectPath: this.resolved.logoutRedirectPath,
|
|
@@ -434,7 +449,7 @@ export class AuthGatePlugin extends Plugin<AuthGateConfig> {
|
|
|
434
449
|
}
|
|
435
450
|
mountGateOnContext(context, {
|
|
436
451
|
gate: this.exports(),
|
|
437
|
-
publicDomain: this.resolved.
|
|
452
|
+
publicDomain: this.resolved.publicDomains,
|
|
438
453
|
forwardHeaders: this.resolved.forwardHeaders,
|
|
439
454
|
gatePaths: this.resolved.gatePaths,
|
|
440
455
|
});
|