dsh-zen-remote 2.2.8 → 2.3.1
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +1 -1
- package/docs/remote-access.md +20 -0
- package/lan-gate.mjs +11 -1
- package/lib/adapters/dsh-better-sidebar/index.js +6 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/adapters/dsh-better-sidebar/methods.js +3 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -1
- package/lib/admin-routes.js +33 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +52 -0
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +3390 -10
- package/lib/config.js +70 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +232 -2
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -3
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +29 -3
- package/lib/merge-streams.js +42 -12
- package/lib/merge-streams.js.map +1 -1
- package/lib/relay-client.js +71 -0
- package/lib/relay-client.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts +13 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +204 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts +29 -0
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -0
- package/lib/types/client/wizard/WizardCard.d.ts +30 -0
- package/lib/types/client/wizard/WizardCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/MigrateCard.d.ts +15 -0
- package/lib/types/client/wizard/cards/MigrateCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/PortForwardCard.d.ts +15 -0
- package/lib/types/client/wizard/cards/PortForwardCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/ProxyCard.d.ts +19 -0
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/TokenCard.d.ts +20 -0
- package/lib/types/client/wizard/cards/TokenCard.d.ts.map +1 -0
- package/lib/types/client/wizard/register-wizard.d.ts +51 -0
- package/lib/types/client/wizard/register-wizard.d.ts.map +1 -0
- package/lib/types/client/wizard/wizard-css.d.ts +10 -0
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -0
- package/lib/types/client-data/qr.d.ts +20 -0
- package/lib/types/client-data/qr.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +39 -2
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +7 -2
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +247 -0
- package/lib/types/client-data/wizard.d.ts.map +1 -0
- package/lib/types/client-routes.d.ts +19 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +27 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +10 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/wizard/binary-io.d.ts +111 -0
- package/lib/types/wizard/binary-io.d.ts.map +1 -0
- package/lib/types/wizard/binary.d.ts +118 -0
- package/lib/types/wizard/binary.d.ts.map +1 -0
- package/lib/types/wizard/caddy-process.d.ts +101 -0
- package/lib/types/wizard/caddy-process.d.ts.map +1 -0
- package/lib/types/wizard/caddy.d.ts +53 -0
- package/lib/types/wizard/caddy.d.ts.map +1 -0
- package/lib/types/wizard/cloudflare.d.ts +112 -0
- package/lib/types/wizard/cloudflare.d.ts.map +1 -0
- package/lib/types/wizard/cloudflared.d.ts +71 -0
- package/lib/types/wizard/cloudflared.d.ts.map +1 -0
- package/lib/types/wizard/probe.d.ts +66 -0
- package/lib/types/wizard/probe.d.ts.map +1 -0
- package/lib/types/wizard/proxy-snippets.d.ts +60 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -0
- package/lib/types/wizard/routes.d.ts +152 -0
- package/lib/types/wizard/routes.d.ts.map +1 -0
- package/lib/types/wizard/source-detect.d.ts +53 -0
- package/lib/types/wizard/source-detect.d.ts.map +1 -0
- package/lib/types/wizard/status.d.ts +115 -0
- package/lib/types/wizard/status.d.ts.map +1 -0
- package/lib/types/wizard/token-store.d.ts +27 -0
- package/lib/types/wizard/token-store.d.ts.map +1 -0
- package/lib/types/wizard/tunnel-orchestrator.d.ts +90 -0
- package/lib/types/wizard/tunnel-orchestrator.d.ts.map +1 -0
- package/lib/types/wizard/verify.d.ts +97 -0
- package/lib/types/wizard/verify.d.ts.map +1 -0
- package/lib/wizard/binary-io.js +386 -0
- package/lib/wizard/binary-io.js.map +1 -0
- package/lib/wizard/binary.js +138 -0
- package/lib/wizard/binary.js.map +1 -0
- package/lib/wizard/caddy-process.js +162 -0
- package/lib/wizard/caddy-process.js.map +1 -0
- package/lib/wizard/caddy.js +80 -0
- package/lib/wizard/caddy.js.map +1 -0
- package/lib/wizard/cloudflare.js +334 -0
- package/lib/wizard/cloudflare.js.map +1 -0
- package/lib/wizard/cloudflared.js +158 -0
- package/lib/wizard/cloudflared.js.map +1 -0
- package/lib/wizard/probe.js +118 -0
- package/lib/wizard/probe.js.map +1 -0
- package/lib/wizard/proxy-snippets.js +112 -0
- package/lib/wizard/proxy-snippets.js.map +1 -0
- package/lib/wizard/routes.js +462 -0
- package/lib/wizard/routes.js.map +1 -0
- package/lib/wizard/source-detect.js +77 -0
- package/lib/wizard/source-detect.js.map +1 -0
- package/lib/wizard/status.js +94 -0
- package/lib/wizard/status.js.map +1 -0
- package/lib/wizard/token-store.js +71 -0
- package/lib/wizard/token-store.js.map +1 -0
- package/lib/wizard/tunnel-orchestrator.js +155 -0
- package/lib/wizard/tunnel-orchestrator.js.map +1 -0
- package/lib/wizard/verify.js +171 -0
- package/lib/wizard/verify.js.map +1 -0
- package/package.json +37 -37
- package/scripts/check-settings-form.mjs +5 -4
- package/scripts/check-wizard-ui.mjs +690 -0
- package/src/adapters/dsh-better-sidebar/index.ts +6 -2
- package/src/adapters/dsh-better-sidebar/methods.ts +3 -0
- package/src/admin-routes.ts +44 -2
- package/src/client/index.tsx +9 -0
- package/src/client/locales.ts +410 -2
- package/src/client/wizard/GatewayWizard.tsx +2220 -0
- package/src/client/wizard/WizardCard.tsx +81 -0
- package/src/client/wizard/cards/MigrateCard.tsx +42 -0
- package/src/client/wizard/cards/PortForwardCard.tsx +37 -0
- package/src/client/wizard/cards/ProxyCard.tsx +56 -0
- package/src/client/wizard/cards/TokenCard.tsx +81 -0
- package/src/client/wizard/register-wizard.ts +99 -0
- package/src/client/wizard/wizard-css.ts +948 -0
- package/src/client-data/qr.ts +284 -0
- package/src/client-data/remote-status.ts +128 -2
- package/src/client-data/settings-form.ts +12 -3
- package/src/client-data/wizard.ts +432 -0
- package/src/client-routes.ts +53 -0
- package/src/config.ts +87 -0
- package/src/index.ts +227 -2
- package/src/intercept.ts +43 -3
- package/src/merge-streams.ts +41 -12
- package/src/relay-client.ts +76 -0
- package/src/wizard/binary-io.ts +449 -0
- package/src/wizard/binary.ts +177 -0
- package/src/wizard/caddy-process.ts +228 -0
- package/src/wizard/caddy.ts +97 -0
- package/src/wizard/cloudflare.ts +413 -0
- package/src/wizard/cloudflared.ts +196 -0
- package/src/wizard/probe.ts +161 -0
- package/src/wizard/proxy-snippets.ts +128 -0
- package/src/wizard/routes.ts +537 -0
- package/src/wizard/source-detect.ts +87 -0
- package/src/wizard/status.ts +180 -0
- package/src/wizard/token-store.ts +73 -0
- package/src/wizard/tunnel-orchestrator.ts +197 -0
- package/src/wizard/verify.ts +236 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed-Caddy process lifecycle (GW-3). The Caddyfile's content is
|
|
3
|
+
* caddy.ts's job; this module owns everything with a heartbeat:
|
|
4
|
+
*
|
|
5
|
+
* start: ensureBinary (GW-2, cached after the first download) →
|
|
6
|
+
* generateCaddyfile (typed refusals) → write the Caddyfile as a
|
|
7
|
+
* `.part` sibling → `caddy validate --config <part> --adapter
|
|
8
|
+
* caddyfile` → only on a clean validate rename it to the real
|
|
9
|
+
* path → `caddy run --config <Caddyfile>`. The ticket's "validate
|
|
10
|
+
* before write" is honored as validate-before-PUBLISH: the final
|
|
11
|
+
* Caddyfile on disk only ever holds bytes Caddy accepted, and a
|
|
12
|
+
* failed validate leaves neither a start nor a published file.
|
|
13
|
+
*
|
|
14
|
+
* The Cloudflare API token travels in the child's ENVIRONMENT
|
|
15
|
+
* (`CF_API_TOKEN`, what the generated `{env.CF_API_TOKEN}` placeholder
|
|
16
|
+
* reads) — never in argv, which other processes on the machine can see.
|
|
17
|
+
*
|
|
18
|
+
* Idempotency: start with a live process refuses (`already-running`) —
|
|
19
|
+
* switching configuration stops first (design: 切换路时先停旧进程再启新的).
|
|
20
|
+
* stop is idempotent; an exited child clears its own slot so a later
|
|
21
|
+
* start works. The host wiring hands stop to ctx.effect, so the row's
|
|
22
|
+
* disposal kills Caddy too.
|
|
23
|
+
*
|
|
24
|
+
* The subprocess service is structural (the tests feed a recorder), and
|
|
25
|
+
* `fetch` is injectable for the ensureBinary step.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
29
|
+
import { ensureBinary, BinaryDownloadError, type DownloadProgress } from './binary-io.js'
|
|
30
|
+
import { caddyDataDir, caddyfilePath, ingressDir, type BinaryPlatform } from './binary.js'
|
|
31
|
+
import { generateCaddyfile, type CaddyfileInput } from './caddy.js'
|
|
32
|
+
|
|
33
|
+
/** The slice of DSH's subprocess service the manager drives (structural so
|
|
34
|
+
* tests record instead of exec). */
|
|
35
|
+
export interface SubprocessHandleLike {
|
|
36
|
+
terminate(): unknown
|
|
37
|
+
done: Promise<{ exitCode?: number | null; signal?: string | null }>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SubprocessServiceLike {
|
|
41
|
+
spawn(options: {
|
|
42
|
+
argv: string[]
|
|
43
|
+
cwd?: string
|
|
44
|
+
env?: NodeJS.ProcessEnv
|
|
45
|
+
stdio?: unknown
|
|
46
|
+
graceMs?: number
|
|
47
|
+
}): SubprocessHandleLike
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CaddyManagerOptions {
|
|
51
|
+
/** The host's subprocess service (ctx.subprocess). */
|
|
52
|
+
subprocess: SubprocessServiceLike
|
|
53
|
+
/** `<DSH_HOME>` — binaries, Caddyfile and caddy-data all live under it. */
|
|
54
|
+
dshHome: string
|
|
55
|
+
/** Injected transport for the one-time binary download (tests point it
|
|
56
|
+
* at a mock server). */
|
|
57
|
+
fetchImpl?: typeof fetch
|
|
58
|
+
/** Forced platform mapping (tests). */
|
|
59
|
+
platform?: BinaryPlatform
|
|
60
|
+
/** Download progress relay (bytes, declared total or null). */
|
|
61
|
+
onProgress?: DownloadProgress
|
|
62
|
+
/** Stdio byte caps for the child, both streams. Default 64 KiB — enough
|
|
63
|
+
* for the startup banner and an error or two. */
|
|
64
|
+
maxLogBytes?: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type CaddyStartResult =
|
|
68
|
+
| { ok: true; binaryPath: string; caddyfile: string; alreadyRunning: false }
|
|
69
|
+
| {
|
|
70
|
+
ok: false
|
|
71
|
+
error: 'already-running' | 'missing-token' | 'bad-config' | 'download-failed' | 'validate-failed' | 'spawn-failed'
|
|
72
|
+
/** The generateCaddyfile error code for `bad-config`, or a short
|
|
73
|
+
* human-readable line for the transport/spawn failures. */
|
|
74
|
+
detail?: string
|
|
75
|
+
/** Official manual-install link, present for `download-failed`. */
|
|
76
|
+
installUrl?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CaddyManager {
|
|
80
|
+
/**
|
|
81
|
+
* Idempotent front door: a live Caddy refuses a second start (stop
|
|
82
|
+
* first to switch configuration). Throws nothing — every failure is a
|
|
83
|
+
* value. The storage directory is this manager's own (under dshHome) —
|
|
84
|
+
* callers supply only the user-facing fields.
|
|
85
|
+
*/
|
|
86
|
+
start(input: Omit<CaddyfileInput, 'storageDir'> & { cfToken: unknown }): Promise<CaddyStartResult>
|
|
87
|
+
/** Idempotent terminate; safe before any start. */
|
|
88
|
+
stop(): void
|
|
89
|
+
/** Whether a child is currently held (true between spawn and exit). */
|
|
90
|
+
isRunning(): boolean
|
|
91
|
+
/** The live child's Caddyfile path, or undefined. */
|
|
92
|
+
runningCaddyfile(): string | undefined
|
|
93
|
+
/** Whether any start has succeeded in this manager's lifetime. */
|
|
94
|
+
hasStarted(): boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createCaddyManager(options: CaddyManagerOptions): CaddyManager {
|
|
98
|
+
const { subprocess, dshHome } = options
|
|
99
|
+
const maxLogBytes = options.maxLogBytes ?? 64 * 1024
|
|
100
|
+
let handle: SubprocessHandleLike | null = null
|
|
101
|
+
let caddyfileArg: string | undefined
|
|
102
|
+
let startedOnce = false
|
|
103
|
+
|
|
104
|
+
// GW-fix-2/L3: a WHITELIST, not the host's full environment — the host
|
|
105
|
+
// process carries the relay secret and DSH's own credentials in env, and
|
|
106
|
+
// a child that only needs to reach the internet and the local disk has
|
|
107
|
+
// no business inheriting them. Include Windows system/profile variables so
|
|
108
|
+
// Go's TLS root-cert store and winsock initialize cleanly on Windows.
|
|
109
|
+
const childEnv = (cfToken: string): NodeJS.ProcessEnv => {
|
|
110
|
+
const env: NodeJS.ProcessEnv = {}
|
|
111
|
+
for (const key of [
|
|
112
|
+
'PATH', 'HOME', 'TMPDIR', 'LANG', 'TZ',
|
|
113
|
+
'SystemRoot', 'SYSTEMROOT', 'windir', 'WINDIR',
|
|
114
|
+
'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'TEMP', 'TMP',
|
|
115
|
+
]) {
|
|
116
|
+
if (process.env[key] !== undefined) env[key] = process.env[key]
|
|
117
|
+
}
|
|
118
|
+
// The one secret this child needs, by env only (the Caddyfile reads
|
|
119
|
+
// {env.CF_API_TOKEN}) — never argv.
|
|
120
|
+
env.CF_API_TOKEN = cfToken
|
|
121
|
+
return env
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const spawnChild = (argv: string[], env: NodeJS.ProcessEnv): SubprocessHandleLike =>
|
|
125
|
+
subprocess.spawn({
|
|
126
|
+
argv,
|
|
127
|
+
cwd: dshHome,
|
|
128
|
+
env,
|
|
129
|
+
stdio: {
|
|
130
|
+
stdin: 'ignore',
|
|
131
|
+
stdout: { maxBytes: maxLogBytes },
|
|
132
|
+
stderr: { maxBytes: maxLogBytes },
|
|
133
|
+
},
|
|
134
|
+
graceMs: 3000,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
async start(input) {
|
|
139
|
+
if (handle !== null) return { ok: false, error: 'already-running' }
|
|
140
|
+
const cfToken = typeof input.cfToken === 'string' && input.cfToken.trim() !== '' ? input.cfToken : undefined
|
|
141
|
+
if (cfToken === undefined) return { ok: false, error: 'missing-token' }
|
|
142
|
+
const generated = generateCaddyfile({ ...input, storageDir: caddyDataDir(dshHome) })
|
|
143
|
+
if (!generated.ok) return { ok: false, error: 'bad-config', detail: generated.error }
|
|
144
|
+
|
|
145
|
+
let binaryPath: string
|
|
146
|
+
try {
|
|
147
|
+
binaryPath = (await ensureBinary(dshHome, 'caddy', {
|
|
148
|
+
fetchImpl: options.fetchImpl,
|
|
149
|
+
platform: options.platform,
|
|
150
|
+
onProgress: options.onProgress,
|
|
151
|
+
})).path
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof BinaryDownloadError) {
|
|
154
|
+
return { ok: false, error: 'download-failed', detail: error.message, installUrl: error.installUrl }
|
|
155
|
+
}
|
|
156
|
+
return { ok: false, error: 'download-failed', detail: String((error as Error)?.message ?? error) }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Write → validate → publish: the `.part` file carries the exact
|
|
160
|
+
// bytes under validation, so what lands at the real path is exactly
|
|
161
|
+
// what Caddy accepted.
|
|
162
|
+
const finalPath = caddyfilePath(dshHome)
|
|
163
|
+
const partPath = `${finalPath}.part`
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(ingressDir(dshHome), { recursive: true })
|
|
166
|
+
writeFileSync(partPath, generated.caddyfile, { mode: 0o600 })
|
|
167
|
+
const validate = spawnChild([binaryPath, 'validate', '--config', partPath, '--adapter', 'caddyfile'], childEnv(cfToken))
|
|
168
|
+
const outcome = await validate.done
|
|
169
|
+
if ((outcome.exitCode ?? 1) !== 0) {
|
|
170
|
+
rmSync(partPath, { force: true })
|
|
171
|
+
return { ok: false, error: 'validate-failed' }
|
|
172
|
+
}
|
|
173
|
+
renameSync(partPath, finalPath)
|
|
174
|
+
} catch (error) {
|
|
175
|
+
rmSync(partPath, { force: true })
|
|
176
|
+
return { ok: false, error: 'spawn-failed', detail: String((error as Error)?.message ?? error) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const env = childEnv(cfToken)
|
|
180
|
+
try {
|
|
181
|
+
const child = spawnChild([binaryPath, 'run', '--config', finalPath], env)
|
|
182
|
+
handle = child
|
|
183
|
+
caddyfileArg = finalPath
|
|
184
|
+
startedOnce = true
|
|
185
|
+
// An exited child clears its own slot: isRunning() goes honest and
|
|
186
|
+
// a later start() works without a manual stop.
|
|
187
|
+
void child.done.then(() => {
|
|
188
|
+
if (handle === child) {
|
|
189
|
+
handle = null
|
|
190
|
+
caddyfileArg = undefined
|
|
191
|
+
}
|
|
192
|
+
}).catch(() => {
|
|
193
|
+
if (handle === child) {
|
|
194
|
+
handle = null
|
|
195
|
+
caddyfileArg = undefined
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
return { ok: true, binaryPath, caddyfile: finalPath, alreadyRunning: false }
|
|
199
|
+
} catch (error) {
|
|
200
|
+
handle = null
|
|
201
|
+
caddyfileArg = undefined
|
|
202
|
+
return { ok: false, error: 'spawn-failed', detail: String((error as Error)?.message ?? error) }
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
stop() {
|
|
207
|
+
if (handle === null) return
|
|
208
|
+
const child = handle
|
|
209
|
+
handle = null
|
|
210
|
+
caddyfileArg = undefined
|
|
211
|
+
try {
|
|
212
|
+
void child.terminate()
|
|
213
|
+
} catch {
|
|
214
|
+
// A dead child's terminate is a no-op or a throw — either way done.
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
isRunning: () => handle !== null,
|
|
219
|
+
runningCaddyfile: () => caddyfileArg,
|
|
220
|
+
/** Whether a start has EVER succeeded in this row's lifetime — the
|
|
221
|
+
* three-state status needs it to tell 已退出 (degraded) from 未启动. */
|
|
222
|
+
hasStarted: () => startedOnce,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Re-exported so the wiring (and later the routes) need one import for
|
|
227
|
+
* the whole ingress-directory layout. */
|
|
228
|
+
export { caddyDataDir, caddyfilePath, ingressDir }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caddyfile generation (GW-3), pure logic — every byte the managed Caddy
|
|
3
|
+
* will run comes out of {@link generateCaddyfile}, so the validation and
|
|
4
|
+
* the injection guards live HERE and the process half (caddy-process.ts)
|
|
5
|
+
* only writes, validates and spawns what this answered.
|
|
6
|
+
*
|
|
7
|
+
* Shape (design.md 路 A), with one deliberate correction to the design
|
|
8
|
+
* sketch: the global DNS-01 option in a real Caddyfile is `acme_dns
|
|
9
|
+
* <provider> <token-placeholder>` — the sketch's bare `dns cloudflare`
|
|
10
|
+
* line is site-block `tls` syntax and is not valid as a global option.
|
|
11
|
+
* The DNS-01 challenge itself is the point: the certificate is ordered
|
|
12
|
+
* through Cloudflare's API (the token rides the `CF_API_TOKEN`
|
|
13
|
+
* environment variable, never the file), so neither 80 nor 443 needs to
|
|
14
|
+
* be reachable from outside for issuance; only the HTTPS service port
|
|
15
|
+
* must be.
|
|
16
|
+
*
|
|
17
|
+
* Injection posture: every interpolated value passes a charset gate —
|
|
18
|
+
* the domain through config.ts's `hostOf` (the exact validator the row
|
|
19
|
+
* layer applies), the storage path free of control characters, quotes and
|
|
20
|
+
* braces (Caddyfile's block delimiters), the env-var name as a legal
|
|
21
|
+
* POSIX name. Nothing else from the outside world reaches the output.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { hostOf } from '../config.js'
|
|
25
|
+
|
|
26
|
+
/** The default environment variable the Cloudflare token rides in on. */
|
|
27
|
+
export const CF_TOKEN_ENV_DEFAULT = 'CF_API_TOKEN'
|
|
28
|
+
|
|
29
|
+
export interface CaddyfileInput {
|
|
30
|
+
/** Public ingress domain, bare hostname (validated like the row field). */
|
|
31
|
+
domain: unknown
|
|
32
|
+
/** HTTPS port the managed Caddy listens on. [1, 65535]. */
|
|
33
|
+
httpsPort: unknown
|
|
34
|
+
/** The gateway's ACTUAL listening port — the caller reads it from the
|
|
35
|
+
* running gateway, never the configured one (it may have retried). */
|
|
36
|
+
gatewayPort: unknown
|
|
37
|
+
/** Caddy storage directory (certificates + ACME account). */
|
|
38
|
+
storageDir: unknown
|
|
39
|
+
/** Environment variable name the Cloudflare token arrives in. */
|
|
40
|
+
cfTokenEnvName?: unknown
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CaddyfileResult =
|
|
44
|
+
| { ok: true; caddyfile: string; domain: string; httpsPort: number; gatewayPort: number }
|
|
45
|
+
| { ok: false; error: 'bad-domain' | 'bad-https-port' | 'bad-gateway-port' | 'bad-storage' | 'bad-token-env' }
|
|
46
|
+
|
|
47
|
+
/** Integers in [1, 65535] only. */
|
|
48
|
+
function portOf(value: unknown): number | undefined {
|
|
49
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535 ? value : undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Storage paths go into the Caddyfile quoted; anything that could break
|
|
53
|
+
* out of the quoting or forge a block delimiter is refused — control
|
|
54
|
+
* characters (newlines foremost), quotes, braces. */
|
|
55
|
+
function storageDirOf(value: unknown): string | undefined {
|
|
56
|
+
if (typeof value !== 'string' || value === '') return undefined
|
|
57
|
+
// eslint-disable-next-line no-control-regex
|
|
58
|
+
if (/[\u0000-\u001f\u007f"{}]/.test(value)) return undefined
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Env names: POSIX uppercase convention, strictly. */
|
|
63
|
+
const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build the complete Caddyfile for the managed Caddy. Refuses (typed)
|
|
67
|
+
* instead of throwing so the caller can map each refusal to a form error.
|
|
68
|
+
*/
|
|
69
|
+
export function generateCaddyfile(input: CaddyfileInput): CaddyfileResult {
|
|
70
|
+
const domain = hostOf(input.domain)
|
|
71
|
+
if (domain === undefined) return { ok: false, error: 'bad-domain' }
|
|
72
|
+
const httpsPort = portOf(input.httpsPort)
|
|
73
|
+
if (httpsPort === undefined) return { ok: false, error: 'bad-https-port' }
|
|
74
|
+
const gatewayPort = portOf(input.gatewayPort)
|
|
75
|
+
if (gatewayPort === undefined) return { ok: false, error: 'bad-gateway-port' }
|
|
76
|
+
const storageDir = storageDirOf(input.storageDir)
|
|
77
|
+
if (storageDir === undefined) return { ok: false, error: 'bad-storage' }
|
|
78
|
+
const tokenEnv = typeof input.cfTokenEnvName === 'string' && ENV_NAME_PATTERN.test(input.cfTokenEnvName)
|
|
79
|
+
? input.cfTokenEnvName
|
|
80
|
+
: CF_TOKEN_ENV_DEFAULT
|
|
81
|
+
if (input.cfTokenEnvName !== undefined && input.cfTokenEnvName !== '' && tokenEnv !== input.cfTokenEnvName) {
|
|
82
|
+
return { ok: false, error: 'bad-token-env' }
|
|
83
|
+
}
|
|
84
|
+
const caddyfile = [
|
|
85
|
+
'{',
|
|
86
|
+
`\tacme_dns cloudflare {env.${tokenEnv}}`,
|
|
87
|
+
`\tstorage file_system "${storageDir}"`,
|
|
88
|
+
'\tadmin off',
|
|
89
|
+
'}',
|
|
90
|
+
'',
|
|
91
|
+
`https://${domain}:${httpsPort} {`,
|
|
92
|
+
`\treverse_proxy 127.0.0.1:${gatewayPort}`,
|
|
93
|
+
'}',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n')
|
|
96
|
+
return { ok: true, caddyfile, domain, httpsPort, gatewayPort }
|
|
97
|
+
}
|