dsh-zen-remote 2.2.9 → 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.js +3289 -6
- 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/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/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/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/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/settings-form.ts +12 -3
- package/src/client-data/wizard.ts +432 -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/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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hostPath probe (GW-8): the server machine walks its OWN public URL and
|
|
3
|
+
* classifies what answers, so the wizard can say something sharper than
|
|
4
|
+
* "it didn't work" — the pairing page showing up means TLS + proxying to
|
|
5
|
+
* the gateway both work; the admin surface answering 200 means the proxy
|
|
6
|
+
* is DROPPING the forwarded headers the gateway trusts; a redirect smells
|
|
7
|
+
* like Cloudflare Access or a proxy rewriting to a login page.
|
|
8
|
+
*
|
|
9
|
+
* Split exactly as the ticket asks: the two `classify*Response` functions
|
|
10
|
+
* are pure (the check scripts can drive them), `probeHostPath` owns the
|
|
11
|
+
* fetch I/O — `redirect: 'manual'` so a proxy's 3xx is SEEN, never
|
|
12
|
+
* followed — with the URL admission reusing client-pairing's
|
|
13
|
+
* `normalizeServerUrl` (https anywhere; plain http only for the explicit
|
|
14
|
+
* private-network allowances) and a 10 s ceiling per request.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { normalizeServerUrl } from '../client-pairing.js'
|
|
18
|
+
|
|
19
|
+
/** What answered on `GET <publicUrl>/`. */
|
|
20
|
+
export type RootClassification =
|
|
21
|
+
| 'gateway-pairing-page'
|
|
22
|
+
| 'unexpected-401'
|
|
23
|
+
| 'unexpected-200'
|
|
24
|
+
| 'redirect'
|
|
25
|
+
| 'unexpected'
|
|
26
|
+
|
|
27
|
+
/** What answered on `GET <publicUrl>/lan-gate/admin`. */
|
|
28
|
+
export type AdminClassification =
|
|
29
|
+
| 'gateway-admin-403'
|
|
30
|
+
| 'admin-exposed'
|
|
31
|
+
| 'proxy-403'
|
|
32
|
+
| 'unexpected'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `GET /`: the ONE right answer is the gateway's pairing page — 401,
|
|
36
|
+
* text/html, carrying the claim-endpoint marker the page's own script
|
|
37
|
+
* posts to. A 200 means the URL belongs to something else entirely.
|
|
38
|
+
*/
|
|
39
|
+
export function classifyRootResponse(status: number, contentType: string, body: string): RootClassification {
|
|
40
|
+
if (status === 401) {
|
|
41
|
+
if (contentType.includes('text/html') && body.includes('/lan-gate/pair/claim')) return 'gateway-pairing-page'
|
|
42
|
+
return 'unexpected-401'
|
|
43
|
+
}
|
|
44
|
+
if (status >= 200 && status < 300) return 'unexpected-200'
|
|
45
|
+
if (status >= 300 && status < 400) return 'redirect'
|
|
46
|
+
return 'unexpected'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `GET /lan-gate/admin`: right is the gateway's own 403 JSON — a public
|
|
51
|
+
* visitor must not get in. 200 + HTML is the DANGEROUS shape: something
|
|
52
|
+
* answered with a page, meaning the proxy forwarded without the
|
|
53
|
+
* forwarded headers and the gateway's local-only wall never saw a
|
|
54
|
+
* proxied request (the admin page leaked). 403 + HTML smells like
|
|
55
|
+
* Cloudflare Access interposing its own login.
|
|
56
|
+
*/
|
|
57
|
+
export function classifyAdminResponse(status: number, contentType: string, body: string): AdminClassification {
|
|
58
|
+
const isHtml = contentType.includes('text/html')
|
|
59
|
+
if (status === 403) {
|
|
60
|
+
if (isHtml) return 'proxy-403'
|
|
61
|
+
if (contentType.includes('application/json')) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed: unknown = JSON.parse(body)
|
|
64
|
+
if (parsed !== null && typeof parsed === 'object' && (parsed as { ok?: unknown }).ok === false) {
|
|
65
|
+
return 'gateway-admin-403'
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// JSON-shaped header with a non-JSON body: not our gateway's answer.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return 'unexpected'
|
|
72
|
+
}
|
|
73
|
+
if (status >= 200 && status < 300 && isHtml) return 'admin-exposed'
|
|
74
|
+
return 'unexpected'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface HostPathReached {
|
|
78
|
+
kind: 'reached'
|
|
79
|
+
root: RootClassification
|
|
80
|
+
admin: AdminClassification
|
|
81
|
+
/** Wall-clock cost of both requests, milliseconds. */
|
|
82
|
+
ms: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface HostPathUnreachable {
|
|
86
|
+
kind: 'unreachable'
|
|
87
|
+
/** `invalid-url` / `insecure-http` — the URL never stood a chance;
|
|
88
|
+
* `timeout` — the 10 s ceiling fired; `network` — DNS, refused, reset. */
|
|
89
|
+
reason: 'invalid-url' | 'insecure-http' | 'timeout' | 'network'
|
|
90
|
+
detail?: string
|
|
91
|
+
ms: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type HostPathOutcome = HostPathReached | HostPathUnreachable
|
|
95
|
+
|
|
96
|
+
export interface ProbeOptions {
|
|
97
|
+
fetchImpl?: typeof fetch
|
|
98
|
+
/** Per-request ceiling. Default 10 000 ms (the ticket's value). */
|
|
99
|
+
timeoutMs?: number
|
|
100
|
+
/** Clock for the `ms` measurement (tests). */
|
|
101
|
+
now?: () => number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One manual-redirect GET, classified. Sends a browser-shaped `Accept:
|
|
105
|
+
* text/html` (never `application/json`, which tells `lan-gate-server.cjs`
|
|
106
|
+
* the caller is an XHR/API client and makes `GET /` answer 401 JSON
|
|
107
|
+
* `{"ok":false,"reason":"unpaired"}` instead of the HTML pairing page;
|
|
108
|
+
* `/lan-gate/admin` answers 403 JSON regardless of `Accept`). */
|
|
109
|
+
async function probeOnce(
|
|
110
|
+
fetchImpl: typeof fetch,
|
|
111
|
+
url: string,
|
|
112
|
+
timeoutMs: number,
|
|
113
|
+
): Promise<{ status: number; contentType: string; body: string }> {
|
|
114
|
+
const response = await fetchImpl(url, {
|
|
115
|
+
redirect: 'manual',
|
|
116
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
117
|
+
headers: { Accept: 'text/html' },
|
|
118
|
+
})
|
|
119
|
+
const body = await response.text()
|
|
120
|
+
return { status: response.status, contentType: String(response.headers.get('content-type') ?? ''), body }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Probe the machine's public URL from here: both the root path and the
|
|
125
|
+
* admin surface, never following a redirect. The two requests run
|
|
126
|
+
* concurrently; the answer carries both classifications and the elapsed
|
|
127
|
+
* time. Every failure mode is a VALUE (`kind: 'unreachable'`), never a
|
|
128
|
+
* throw — the wizard routes render whichever verdict arrived.
|
|
129
|
+
*/
|
|
130
|
+
export async function probeHostPath(publicUrl: unknown, options: ProbeOptions = {}): Promise<HostPathOutcome> {
|
|
131
|
+
const started = (options.now ?? Date.now)()
|
|
132
|
+
const elapsed = () => (options.now ?? Date.now)() - started
|
|
133
|
+
// Same admission the client pairing uses: https anywhere, plain http
|
|
134
|
+
// only into the explicit private-network allowances.
|
|
135
|
+
const normalized = normalizeServerUrl(typeof publicUrl === 'string' ? publicUrl : '')
|
|
136
|
+
if (!normalized.ok) {
|
|
137
|
+
return { kind: 'unreachable', reason: normalized.reason === 'insecure-http' ? 'insecure-http' : 'invalid-url', ms: 0 }
|
|
138
|
+
}
|
|
139
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
140
|
+
const timeoutMs = options.timeoutMs ?? 10_000
|
|
141
|
+
try {
|
|
142
|
+
const [root, admin] = await Promise.all([
|
|
143
|
+
probeOnce(fetchImpl, `${normalized.url}/`, timeoutMs),
|
|
144
|
+
probeOnce(fetchImpl, `${normalized.url}/lan-gate/admin`, timeoutMs),
|
|
145
|
+
])
|
|
146
|
+
return {
|
|
147
|
+
kind: 'reached',
|
|
148
|
+
root: classifyRootResponse(root.status, root.contentType, root.body),
|
|
149
|
+
admin: classifyAdminResponse(admin.status, admin.contentType, admin.body),
|
|
150
|
+
ms: elapsed(),
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const name = (error as Error)?.name
|
|
154
|
+
return {
|
|
155
|
+
kind: 'unreachable',
|
|
156
|
+
reason: name === 'TimeoutError' || name === 'AbortError' ? 'timeout' : 'network',
|
|
157
|
+
detail: String((error as Error)?.message ?? error),
|
|
158
|
+
ms: elapsed(),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse-proxy configuration snippets (GW-5, 路 B) — for the user who
|
|
3
|
+
* ALREADY runs their own proxy, the wizard hands out copy-paste material
|
|
4
|
+
* instead of managing a process. Everything here is pure text generation:
|
|
5
|
+
* every interpolated value passes a charset gate (the domain through
|
|
6
|
+
* config.ts's `hostOf`, the upstream host likewise, the port in [1,
|
|
7
|
+
* 65535]), so a hostile form value cannot smuggle a directive into
|
|
8
|
+
* anyone's nginx.
|
|
9
|
+
*
|
|
10
|
+
* The upstream port is the gateway's ACTUAL port — the caller reads it
|
|
11
|
+
* from the running gateway (it may have retried past the configured one),
|
|
12
|
+
* never from config.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { hostOf } from '../config.js'
|
|
16
|
+
|
|
17
|
+
export type SnippetResult =
|
|
18
|
+
| { ok: true; text: string }
|
|
19
|
+
| { ok: false; error: 'bad-domain' | 'bad-upstream-host' | 'bad-upstream-port' }
|
|
20
|
+
|
|
21
|
+
export interface SnippetInput {
|
|
22
|
+
/** Public domain the proxy terminates TLS for (bare hostname). */
|
|
23
|
+
domain: unknown
|
|
24
|
+
/** Where the gateway listens — a hostname or IP literal (e.g. the
|
|
25
|
+
* loopback default). */
|
|
26
|
+
upstreamHost: unknown
|
|
27
|
+
/** The gateway's ACTUAL port. */
|
|
28
|
+
upstreamPort: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Integers in [1, 65535] only. */
|
|
32
|
+
function portOf(value: unknown): number | undefined {
|
|
33
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535 ? value : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validatedParts(input: SnippetInput): { domain: string; upstream: string; port: number } | { error: SnippetResult & { ok: false } } {
|
|
37
|
+
const domain = hostOf(input.domain)
|
|
38
|
+
if (domain === undefined) return { error: { ok: false, error: 'bad-domain' } }
|
|
39
|
+
const upstream = hostOf(input.upstreamHost)
|
|
40
|
+
if (upstream === undefined) return { error: { ok: false, error: 'bad-upstream-host' } }
|
|
41
|
+
const port = portOf(input.upstreamPort)
|
|
42
|
+
if (port === undefined) return { error: { ok: false, error: 'bad-upstream-port' } }
|
|
43
|
+
return { domain, upstream, port }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* nginx: a complete server block. The WebSocket upgrade map sits outside
|
|
48
|
+
* the server (http-level context) — the snippet says so in a comment, the
|
|
49
|
+
* user pastes it into the http/server scope their setup offers. Long
|
|
50
|
+
* idle tunnels need the 3600s read/send timeouts, uploads the 128m
|
|
51
|
+
* client body cap, and streaming the buffering switch — all three are
|
|
52
|
+
* the shapes DSH's relay traffic actually needs.
|
|
53
|
+
*/
|
|
54
|
+
export function nginxSnippet(input: SnippetInput): SnippetResult {
|
|
55
|
+
const parts = validatedParts(input)
|
|
56
|
+
if ('error' in parts) return parts.error
|
|
57
|
+
const { domain, upstream, port } = parts
|
|
58
|
+
const text = [
|
|
59
|
+
'# 这段放在 http {} 层(若已有同名 map 则跳过)',
|
|
60
|
+
'map $http_upgrade $connection_upgrade {',
|
|
61
|
+
' default upgrade;',
|
|
62
|
+
" '' close;",
|
|
63
|
+
'}',
|
|
64
|
+
'',
|
|
65
|
+
'server {',
|
|
66
|
+
' listen 443 ssl;',
|
|
67
|
+
` server_name ${domain};`,
|
|
68
|
+
'',
|
|
69
|
+
' location / {',
|
|
70
|
+
` proxy_pass http://${upstream}:${port};`,
|
|
71
|
+
' proxy_http_version 1.1;',
|
|
72
|
+
' proxy_set_header Upgrade $http_upgrade;',
|
|
73
|
+
' proxy_set_header Connection $connection_upgrade;',
|
|
74
|
+
' proxy_set_header Host $host;',
|
|
75
|
+
' proxy_set_header X-Real-IP $remote_addr;',
|
|
76
|
+
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
|
|
77
|
+
' proxy_set_header X-Forwarded-Proto $scheme;',
|
|
78
|
+
' proxy_buffering off;',
|
|
79
|
+
' client_max_body_size 128m;',
|
|
80
|
+
' proxy_read_timeout 3600s;',
|
|
81
|
+
' proxy_send_timeout 3600s;',
|
|
82
|
+
' }',
|
|
83
|
+
'}',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n')
|
|
86
|
+
return { ok: true, text }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Caddy (user-operated, not the wizard-managed one): the site block is
|
|
91
|
+
* one `reverse_proxy` line — Caddy routes the rest itself.
|
|
92
|
+
*/
|
|
93
|
+
export function caddySnippet(input: SnippetInput): SnippetResult {
|
|
94
|
+
const parts = validatedParts(input)
|
|
95
|
+
if ('error' in parts) return parts.error
|
|
96
|
+
const { domain, upstream, port } = parts
|
|
97
|
+
const text = [
|
|
98
|
+
`${domain} {`,
|
|
99
|
+
` reverse_proxy ${upstream}:${port}`,
|
|
100
|
+
'}',
|
|
101
|
+
'',
|
|
102
|
+
].join('\n')
|
|
103
|
+
return { ok: true, text }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type StepsResult =
|
|
107
|
+
| { ok: true; steps: string[] }
|
|
108
|
+
| { ok: false; error: 'bad-upstream-host' | 'bad-upstream-port' }
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Lucky (Web 服务反代) three steps: listen 443, proxy to the upstream,
|
|
112
|
+
* flip the catch-all switch. Text only — Lucky has no config file format
|
|
113
|
+
* to generate.
|
|
114
|
+
*/
|
|
115
|
+
export function luckySteps(input: { upstreamHost: unknown; upstreamPort: unknown }): StepsResult {
|
|
116
|
+
const upstream = hostOf(input.upstreamHost)
|
|
117
|
+
if (upstream === undefined) return { ok: false, error: 'bad-upstream-host' }
|
|
118
|
+
const port = portOf(input.upstreamPort)
|
|
119
|
+
if (port === undefined) return { ok: false, error: 'bad-upstream-port' }
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
steps: [
|
|
123
|
+
'在 Lucky 的「Web 服务」里新建一条规则,监听 443 端口,绑定你的域名。',
|
|
124
|
+
`后端(反代目标)填 http://${upstream}:${port},开启 WebSocket 支持。`,
|
|
125
|
+
'打开「万事大吉」开关(放行全部路径),保存即可生效。',
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
}
|