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,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingress binary I/O (GW-2, revised by GW-fix-1/H1): download, unpack,
|
|
3
|
+
* install and cache-probe for the two wizard-managed binaries. The
|
|
4
|
+
* arithmetic lives in binary.ts; this file is the only one allowed to touch
|
|
5
|
+
* the filesystem or the network for it.
|
|
6
|
+
*
|
|
7
|
+
* House rules the tasks pin down:
|
|
8
|
+
*
|
|
9
|
+
* - No shell, no `child_process` at all. Downloads ride `fetch`, the
|
|
10
|
+
* body streams straight to a `.part` file, and the install is a POSIX
|
|
11
|
+
* rename — a crash mid-download can never leave a half binary at the
|
|
12
|
+
* install path.
|
|
13
|
+
* - The archive wrapper comes from binary.ts's METADATA (`options.archive`,
|
|
14
|
+
* what binaryUrls() models per platform), never from sniffing the URL:
|
|
15
|
+
* the Caddy build API's URL is a query string whose ending says nothing
|
|
16
|
+
* about the tar.gz / zip payload it answers with (GW-fix-1/H1).
|
|
17
|
+
* - Windows' Caddy asset is a zip and there is no zip reader here yet:
|
|
18
|
+
* refused up front with the typed `windows-zip-unsupported` code, the
|
|
19
|
+
* manual-install link stays the fallback (GW-fix-1/H1 item 3).
|
|
20
|
+
* - After unpacking (or for a bare download), a unix image must carry an
|
|
21
|
+
* ELF or Mach-O magic; anything else is refused as `bad-binary-image`
|
|
22
|
+
* and leaves nothing on disk (GW-fix-1/H1 item 4). Windows keeps no
|
|
23
|
+
* magic check — PE inspection is not this module's job.
|
|
24
|
+
* - The darwin cloudflared asset is a `.tgz`; it is unpacked with
|
|
25
|
+
* `zlib.gunzipSync` plus a minimal ustar reader (the archive carries a
|
|
26
|
+
* single entry — no PAX/GNU long-name handling needed, but those
|
|
27
|
+
* header types are skipped correctly if an upstream rebuild adds one).
|
|
28
|
+
* - Installed images get 0755; `ensureBinary` is idempotent (an existing
|
|
29
|
+
* executable short-circuits before any network traffic).
|
|
30
|
+
*
|
|
31
|
+
* `fetch` is injectable so the tests drive real downloads against a local
|
|
32
|
+
* mock server without touching the network.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
accessSync,
|
|
37
|
+
chmodSync,
|
|
38
|
+
closeSync,
|
|
39
|
+
constants as fsConstants,
|
|
40
|
+
createWriteStream,
|
|
41
|
+
mkdirSync,
|
|
42
|
+
openSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
readSync,
|
|
45
|
+
renameSync,
|
|
46
|
+
rmSync,
|
|
47
|
+
statSync,
|
|
48
|
+
writeFileSync,
|
|
49
|
+
} from 'node:fs'
|
|
50
|
+
import { Readable, Transform } from 'node:stream'
|
|
51
|
+
import { pipeline } from 'node:stream/promises'
|
|
52
|
+
import type { ReadableStream as NodeWebReadableStream } from 'node:stream/web'
|
|
53
|
+
import { gunzipSync } from 'node:zlib'
|
|
54
|
+
import {
|
|
55
|
+
MANUAL_INSTALL_LINKS,
|
|
56
|
+
binaryDownloadCandidates,
|
|
57
|
+
binaryInstallPath,
|
|
58
|
+
binDir,
|
|
59
|
+
binaryUrls,
|
|
60
|
+
currentPlatform,
|
|
61
|
+
type BinaryArchive,
|
|
62
|
+
type BinaryPlatform,
|
|
63
|
+
type IngressBinary,
|
|
64
|
+
} from './binary.js'
|
|
65
|
+
|
|
66
|
+
/** Generous ceiling on one download; a mirror gone wrong must not be able
|
|
67
|
+
* to fill the disk. Both real binaries sit far below 200 MB. */
|
|
68
|
+
const MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024
|
|
69
|
+
/** Connection / response-headers timeout before a mirror is skipped. */
|
|
70
|
+
const CONNECT_TIMEOUT_MS = 10_000
|
|
71
|
+
/** Maximum idle gap between body chunks before a stalled mirror is aborted. */
|
|
72
|
+
const STALL_TIMEOUT_MS = 15_000
|
|
73
|
+
|
|
74
|
+
/** ELF: `\x7fELF`. */
|
|
75
|
+
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46])
|
|
76
|
+
/** Mach-O 32 big-endian, Mach-O 64 big-endian, Mach-O 64 little-endian —
|
|
77
|
+
* the three shapes Apple toolchains emit for this codebase's targets. */
|
|
78
|
+
const MACH_O_MAGICS = [
|
|
79
|
+
Buffer.from([0xfe, 0xed, 0xfa, 0xce]),
|
|
80
|
+
Buffer.from([0xfe, 0xed, 0xfa, 0xcf]),
|
|
81
|
+
Buffer.from([0xcf, 0xfa, 0xed, 0xfe]),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
/** Whether the first bytes of an image look like an executable this module
|
|
85
|
+
* is willing to install on a unix host (GW-fix-1/H1 item 4). */
|
|
86
|
+
function looksLikeUnixImage(head: Buffer): boolean {
|
|
87
|
+
return ELF_MAGIC.equals(head) || MACH_O_MAGICS.some((magic) => magic.equals(head))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The first four bytes of a file (fewer when the file is shorter). */
|
|
91
|
+
function headBytes(path: string): Buffer {
|
|
92
|
+
const head = Buffer.alloc(4)
|
|
93
|
+
const fd = openSync(path, 'r')
|
|
94
|
+
try {
|
|
95
|
+
return head.subarray(0, readSync(fd, head, 0, 4, 0))
|
|
96
|
+
} finally {
|
|
97
|
+
closeSync(fd)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One failed or refused download/install. `code` is the wire-stable marker
|
|
103
|
+
* the UI classifies on — `'download-failed'` for transport/shaping
|
|
104
|
+
* failures, `'windows-zip-unsupported'` for the zip refusal, and
|
|
105
|
+
* `'bad-binary-image'` when the payload did not carry an executable
|
|
106
|
+
* image's magic. `installUrl` is the official manual-install page the
|
|
107
|
+
* failure card links to. Fields are assigned in the constructor body rather
|
|
108
|
+
* than declared as parameter properties (the check scripts import modules
|
|
109
|
+
* through Node's strip-only type stripping, which rejects that syntax).
|
|
110
|
+
*/
|
|
111
|
+
export class BinaryDownloadError extends Error {
|
|
112
|
+
readonly code: string
|
|
113
|
+
readonly tool: IngressBinary
|
|
114
|
+
readonly url: string
|
|
115
|
+
readonly status: number | null
|
|
116
|
+
readonly installUrl: string
|
|
117
|
+
|
|
118
|
+
constructor(tool: IngressBinary, url: string, status: number | null, message: string, options?: { cause?: unknown; code?: string }) {
|
|
119
|
+
super(message, options)
|
|
120
|
+
this.name = 'BinaryDownloadError'
|
|
121
|
+
this.code = options?.code ?? 'download-failed'
|
|
122
|
+
this.tool = tool
|
|
123
|
+
this.url = url
|
|
124
|
+
this.status = status
|
|
125
|
+
this.installUrl = MANUAL_INSTALL_LINKS[tool]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether `path` exists and is executable (the "already installed" test). */
|
|
130
|
+
function isExecutableFile(path: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
if (!statSync(path).isFile()) return false
|
|
133
|
+
accessSync(path, fsConstants.X_OK)
|
|
134
|
+
return true
|
|
135
|
+
} catch {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The installed binary's absolute path, or undefined when it is not (yet)
|
|
142
|
+
* on disk in executable shape — the read-only probe; see binary.ts's
|
|
143
|
+
* `binaryInstallPath` for the pure arithmetic.
|
|
144
|
+
*/
|
|
145
|
+
export function binaryPath(dshHome: string, tool: IngressBinary, platform: BinaryPlatform = currentPlatform()): string | undefined {
|
|
146
|
+
const path = binaryInstallPath(dshHome, tool, platform)
|
|
147
|
+
return isExecutableFile(path) ? path : undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Progress callback: bytes on disk so far, and the declared total when the
|
|
151
|
+
* server sent one. */
|
|
152
|
+
export type DownloadProgress = (received: number, total: number | null) => void
|
|
153
|
+
|
|
154
|
+
export interface DownloadBinaryOptions {
|
|
155
|
+
fetchImpl?: typeof fetch
|
|
156
|
+
platform?: BinaryPlatform
|
|
157
|
+
onProgress?: DownloadProgress
|
|
158
|
+
/** The archive wrapper the URL delivers — metadata from binary.ts
|
|
159
|
+
* (`binaryUrls` / `caddyDownload` / `cloudflaredDownload`), NOT guessed
|
|
160
|
+
* from the URL shape. Default `'none'`: a bare executable. */
|
|
161
|
+
archive?: BinaryArchive
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Write one fetched response body to `partPath` under the size cap and
|
|
165
|
+
* per-chunk stall timeout so a throttled or hung mirror fails over quickly. */
|
|
166
|
+
async function streamToFile(
|
|
167
|
+
tool: IngressBinary,
|
|
168
|
+
url: string,
|
|
169
|
+
body: NodeWebReadableStream<Uint8Array>,
|
|
170
|
+
partPath: string,
|
|
171
|
+
onProgress: DownloadProgress | undefined,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
let received = 0
|
|
174
|
+
let stallTimer: ReturnType<typeof setTimeout> | undefined
|
|
175
|
+
const counter = new Transform({
|
|
176
|
+
transform(chunk: Buffer, _encoding, done) {
|
|
177
|
+
received += chunk.length
|
|
178
|
+
if (received > MAX_DOWNLOAD_BYTES) {
|
|
179
|
+
done(new BinaryDownloadError(tool, url, null, `the download of ${tool} exceeds the ${MAX_DOWNLOAD_BYTES}-byte cap`))
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (stallTimer !== undefined) clearTimeout(stallTimer)
|
|
183
|
+
stallTimer = setTimeout(() => {
|
|
184
|
+
counter.destroy(new BinaryDownloadError(tool, url, null, `the download of ${tool} stalled after ${received} bytes`))
|
|
185
|
+
}, STALL_TIMEOUT_MS)
|
|
186
|
+
if (typeof stallTimer.unref === 'function') stallTimer.unref()
|
|
187
|
+
onProgress?.(received, null)
|
|
188
|
+
done(null, chunk)
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
stallTimer = setTimeout(() => {
|
|
192
|
+
counter.destroy(new BinaryDownloadError(tool, url, null, `the download of ${tool} stalled before first byte`))
|
|
193
|
+
}, STALL_TIMEOUT_MS)
|
|
194
|
+
if (typeof stallTimer.unref === 'function') stallTimer.unref()
|
|
195
|
+
try {
|
|
196
|
+
await pipeline(Readable.fromWeb(body), counter, createWriteStream(partPath, { mode: 0o644 }))
|
|
197
|
+
} catch (error) {
|
|
198
|
+
rmSync(partPath, { force: true })
|
|
199
|
+
throw error
|
|
200
|
+
} finally {
|
|
201
|
+
if (stallTimer !== undefined) clearTimeout(stallTimer)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Size bytes of a ustar header's octal size field, 0 when unparsable. */
|
|
206
|
+
function tarEntrySize(header: Buffer): number {
|
|
207
|
+
const raw = header.subarray(124, 136).toString('ascii').replace(/[\0 ]+$/g, '').trim()
|
|
208
|
+
const size = parseInt(raw === '' ? '0' : raw, 8)
|
|
209
|
+
return Number.isFinite(size) && size > 0 ? size : 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The first regular-file payload of a ustar archive as one buffer slice.
|
|
214
|
+
* The cloudflared `.tgz` carries exactly one (`cloudflared`); anything else
|
|
215
|
+
* (PAX/GNU metadata, directories) is skipped by advancing over its data
|
|
216
|
+
* blocks, so a future archive layout change degrades to "not found" instead
|
|
217
|
+
* of corrupt bytes.
|
|
218
|
+
*/
|
|
219
|
+
function firstTarFileEntry(archive: Buffer): Buffer | undefined {
|
|
220
|
+
let offset = 0
|
|
221
|
+
while (offset + 512 <= archive.length) {
|
|
222
|
+
const header = archive.subarray(offset, offset + 512)
|
|
223
|
+
if (header.every((byte) => byte === 0)) return undefined
|
|
224
|
+
const size = tarEntrySize(header)
|
|
225
|
+
const typeflag = String.fromCharCode(header[156] ?? 0x20)
|
|
226
|
+
const dataStart = offset + 512
|
|
227
|
+
if ((typeflag === '0' || typeflag === '\0') && size > 0 && dataStart + size <= archive.length) {
|
|
228
|
+
return archive.subarray(dataStart, dataStart + size)
|
|
229
|
+
}
|
|
230
|
+
offset = dataStart + Math.ceil(size / 512) * 512
|
|
231
|
+
}
|
|
232
|
+
return undefined
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function chmodExec(path: string): void {
|
|
236
|
+
try {
|
|
237
|
+
chmodSync(path, 0o755)
|
|
238
|
+
} catch {
|
|
239
|
+
// Windows: no executable bit — the extension does that job.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Install `bytes` at `installPath`: `.part` sibling, chmod, atomic rename. */
|
|
244
|
+
function installBytes(bytes: Uint8Array, installPath: string): void {
|
|
245
|
+
const partPath = `${installPath}.part`
|
|
246
|
+
try {
|
|
247
|
+
writeFileSync(partPath, bytes, { mode: 0o644 })
|
|
248
|
+
chmodExec(partPath)
|
|
249
|
+
renameSync(partPath, installPath)
|
|
250
|
+
} catch (error) {
|
|
251
|
+
rmSync(partPath, { force: true })
|
|
252
|
+
throw error
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The unix image-magic gate (GW-fix-1/H1 item 4): a payload that does not
|
|
257
|
+
* open with an ELF or Mach-O magic is refused, nothing lands on disk. */
|
|
258
|
+
function assertUnixImageMagic(tool: IngressBinary, url: string, status: number | null, head: Buffer): void {
|
|
259
|
+
if (looksLikeUnixImage(head)) return
|
|
260
|
+
throw new BinaryDownloadError(
|
|
261
|
+
tool,
|
|
262
|
+
url,
|
|
263
|
+
status,
|
|
264
|
+
`the downloaded ${tool} does not carry an ELF or Mach-O image magic`,
|
|
265
|
+
{ code: 'bad-binary-image' },
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Download one binary from an explicit URL and install it. The archive
|
|
271
|
+
* wrapper is the CALLER'S metadata (`options.archive`) — `ensureBinary`
|
|
272
|
+
* feeds it from binaryUrls(); tests pin it per fixture. `platform` only
|
|
273
|
+
* matters for the install name and the magic-check gate; it defaults to
|
|
274
|
+
* this process's.
|
|
275
|
+
* @returns the absolute install path.
|
|
276
|
+
*/
|
|
277
|
+
export async function downloadBinary(
|
|
278
|
+
dshHome: string,
|
|
279
|
+
tool: IngressBinary,
|
|
280
|
+
url: string,
|
|
281
|
+
options: DownloadBinaryOptions = {},
|
|
282
|
+
): Promise<string> {
|
|
283
|
+
const platform = options.platform ?? currentPlatform()
|
|
284
|
+
const archive: BinaryArchive = options.archive ?? 'none'
|
|
285
|
+
const installPath = binaryInstallPath(dshHome, tool, platform)
|
|
286
|
+
if (archive === 'zip') {
|
|
287
|
+
// Refused BEFORE any network traffic: there is no zip reader in this
|
|
288
|
+
// codebase yet (GW-fix-1/H1 item 3), and half-supporting the format
|
|
289
|
+
// would only install a zip's bytes as an image.
|
|
290
|
+
throw new BinaryDownloadError(
|
|
291
|
+
tool,
|
|
292
|
+
url,
|
|
293
|
+
null,
|
|
294
|
+
`the ${tool} asset for Windows ships as a zip, which cannot be unpacked here yet`,
|
|
295
|
+
{ code: 'windows-zip-unsupported' },
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
299
|
+
let response: Response
|
|
300
|
+
try {
|
|
301
|
+
response = await fetchImpl(url, {
|
|
302
|
+
redirect: 'follow',
|
|
303
|
+
signal: AbortSignal.timeout(CONNECT_TIMEOUT_MS),
|
|
304
|
+
})
|
|
305
|
+
} catch (cause) {
|
|
306
|
+
throw new BinaryDownloadError(tool, url, null, `the download of ${tool} failed before an answer arrived`, { cause })
|
|
307
|
+
}
|
|
308
|
+
if (!response.ok || response.body === null) {
|
|
309
|
+
throw new BinaryDownloadError(tool, url, response.status, `the download of ${tool} answered ${response.status}`)
|
|
310
|
+
}
|
|
311
|
+
mkdirSync(binDir(dshHome), { recursive: true })
|
|
312
|
+
const declared = Number(response.headers.get('content-length'))
|
|
313
|
+
const total = Number.isFinite(declared) && declared > 0 ? declared : null
|
|
314
|
+
const onProgress: DownloadProgress | undefined = options.onProgress
|
|
315
|
+
? (received) => options.onProgress?.(received, total)
|
|
316
|
+
: undefined
|
|
317
|
+
if (archive === 'tgz') {
|
|
318
|
+
// Archive (or auto-detected bare binary if an upstream endpoint serves a
|
|
319
|
+
// raw ELF/Mach-O image despite tgz metadata): land the bytes in a sibling,
|
|
320
|
+
// then either install directly when the payload already starts with a
|
|
321
|
+
// unix image magic or gunzip + extract the tar entry.
|
|
322
|
+
const tgzPath = `${installPath}.download.tgz`
|
|
323
|
+
try {
|
|
324
|
+
await streamToFile(tool, url, response.body as unknown as NodeWebReadableStream<Uint8Array>, tgzPath, onProgress)
|
|
325
|
+
const rawBytes = readFileSync(tgzPath)
|
|
326
|
+
const gzipMagic = rawBytes.length >= 2 && rawBytes[0] === 0x1f && rawBytes[1] === 0x8b
|
|
327
|
+
const entry = (!gzipMagic && platform.os !== 'windows' && looksLikeUnixImage(rawBytes.subarray(0, 4)))
|
|
328
|
+
? rawBytes
|
|
329
|
+
: firstTarFileEntry(gunzipSync(rawBytes))
|
|
330
|
+
if (entry === undefined) {
|
|
331
|
+
throw new BinaryDownloadError(tool, url, response.status, `the ${tool} archive carried no binary entry`)
|
|
332
|
+
}
|
|
333
|
+
if (platform.os !== 'windows') assertUnixImageMagic(tool, url, response.status, entry.subarray(0, 4))
|
|
334
|
+
installBytes(entry, installPath)
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (error instanceof BinaryDownloadError) throw error
|
|
337
|
+
throw new BinaryDownloadError(tool, url, response.status, `the ${tool} archive could not be unpacked`, { cause: error })
|
|
338
|
+
} finally {
|
|
339
|
+
rmSync(tgzPath, { force: true })
|
|
340
|
+
}
|
|
341
|
+
return installPath
|
|
342
|
+
}
|
|
343
|
+
// Bare executable: stream to the .part, gate the magic, then rename.
|
|
344
|
+
const partPath = `${installPath}.part`
|
|
345
|
+
try {
|
|
346
|
+
await streamToFile(tool, url, response.body as unknown as NodeWebReadableStream<Uint8Array>, partPath, onProgress)
|
|
347
|
+
if (platform.os !== 'windows') assertUnixImageMagic(tool, url, response.status, headBytes(partPath))
|
|
348
|
+
chmodExec(partPath)
|
|
349
|
+
renameSync(partPath, installPath)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
rmSync(partPath, { force: true })
|
|
352
|
+
if (error instanceof BinaryDownloadError) throw error
|
|
353
|
+
throw new BinaryDownloadError(tool, url, response.status, `the download of ${tool} could not be written to disk`, { cause: error })
|
|
354
|
+
}
|
|
355
|
+
return installPath
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export interface EnsureBinaryOptions extends DownloadBinaryOptions {
|
|
359
|
+
/** Force the platform mapping (tests); defaults to this process's. */
|
|
360
|
+
platform?: BinaryPlatform
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Cached mainland-China network detection so repeated downloads do not
|
|
364
|
+
* re-probe within the same process. */
|
|
365
|
+
let cachedCnNetwork: boolean | undefined
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Detect whether the current host is in mainland China:
|
|
369
|
+
* 1. Fast local check: system timezone is `Asia/Shanghai`, `Asia/Chongqing`,
|
|
370
|
+
* `Asia/Harbin`, or `Asia/Urumqi`, or `LANG`/`LC_ALL` carries `zh_CN`.
|
|
371
|
+
* 2. Quick 1.5s probe to Cloudflare's `/cdn-cgi/trace` (`loc=CN`), or if
|
|
372
|
+
* direct `github.com` HEAD fails/times out within 2s.
|
|
373
|
+
*/
|
|
374
|
+
export async function detectCnNetwork(fetchImpl: typeof fetch = fetch): Promise<boolean> {
|
|
375
|
+
if (cachedCnNetwork !== undefined) return cachedCnNetwork
|
|
376
|
+
try {
|
|
377
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? ''
|
|
378
|
+
if (/^Asia\/(Shanghai|Chongqing|Harbin|Urumqi)$/i.test(tz)) {
|
|
379
|
+
cachedCnNetwork = true
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
} catch { /* ignore */ }
|
|
383
|
+
const envLang = `${process.env.LANG ?? ''} ${process.env.LC_ALL ?? ''}`
|
|
384
|
+
if (/zh_CN/i.test(envLang)) {
|
|
385
|
+
cachedCnNetwork = true
|
|
386
|
+
return true
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const trace = await fetchImpl('https://www.cloudflare.com/cdn-cgi/trace', {
|
|
390
|
+
signal: AbortSignal.timeout(1500),
|
|
391
|
+
})
|
|
392
|
+
if (trace.ok) {
|
|
393
|
+
const text = await trace.text()
|
|
394
|
+
if (/\bloc=CN\b/i.test(text)) {
|
|
395
|
+
cachedCnNetwork = true
|
|
396
|
+
return true
|
|
397
|
+
}
|
|
398
|
+
cachedCnNetwork = false
|
|
399
|
+
return false
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
// ignore trace timeout
|
|
403
|
+
}
|
|
404
|
+
cachedCnNetwork = false
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The idempotent front door: an already-installed executable short-circuits
|
|
410
|
+
* (no network), anything else downloads from the official distribution
|
|
411
|
+
* point for the tool, archive shape included.
|
|
412
|
+
*
|
|
413
|
+
* When using the default `fetch` transport, automatically detects mainland
|
|
414
|
+
* China network environments and tries prebuilt GitHub Release mirrors
|
|
415
|
+
* (`gh-proxy.com` / `ghfast.top`) + direct GitHub Release before falling back
|
|
416
|
+
* to `caddyserver.com/api/download`. When a custom `fetchImpl` is injected
|
|
417
|
+
* (unit tests), calls the canonical `binaryUrls(platform)` endpoint directly.
|
|
418
|
+
* @returns the absolute install path and whether THIS call downloaded.
|
|
419
|
+
*/
|
|
420
|
+
export async function ensureBinary(
|
|
421
|
+
dshHome: string,
|
|
422
|
+
tool: IngressBinary,
|
|
423
|
+
options: EnsureBinaryOptions = {},
|
|
424
|
+
): Promise<{ path: string; downloaded: boolean }> {
|
|
425
|
+
const platform = options.platform ?? currentPlatform()
|
|
426
|
+
const existing = binaryPath(dshHome, tool, platform)
|
|
427
|
+
if (existing !== undefined) return { path: existing, downloaded: false }
|
|
428
|
+
if (options.fetchImpl !== undefined) {
|
|
429
|
+
const urls = binaryUrls(platform)
|
|
430
|
+
const [url, archive] = tool === 'caddy' ? [urls.caddy, urls.caddyArchive] : [urls.cloudflared, urls.cloudflaredArchive]
|
|
431
|
+
const path = await downloadBinary(dshHome, tool, url, { ...options, archive })
|
|
432
|
+
return { path, downloaded: true }
|
|
433
|
+
}
|
|
434
|
+
const preferCnMirror = await detectCnNetwork()
|
|
435
|
+
const candidates = binaryDownloadCandidates(tool, platform, preferCnMirror)
|
|
436
|
+
let lastError: unknown
|
|
437
|
+
for (const candidate of candidates) {
|
|
438
|
+
try {
|
|
439
|
+
const path = await downloadBinary(dshHome, tool, candidate.url, {
|
|
440
|
+
...options,
|
|
441
|
+
archive: candidate.archive,
|
|
442
|
+
})
|
|
443
|
+
return { path, downloaded: true }
|
|
444
|
+
} catch (error) {
|
|
445
|
+
lastError = error
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
throw lastError
|
|
449
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingress binary arithmetic (GW-2): platform mapping, download-URL
|
|
3
|
+
* construction and install-path computation for the two binaries the
|
|
4
|
+
* gateway wizard manages — Caddy (custom build with the Cloudflare DNS
|
|
5
|
+
* module, for DNS-01 certificates) and cloudflared (the tunnel client).
|
|
6
|
+
*
|
|
7
|
+
* Pure by contract: URL strings, path strings and platform facts only —
|
|
8
|
+
* every touch of the filesystem or the network lives in binary-io.ts, and
|
|
9
|
+
* the routes layer reads both. `node:path`/`node:os` here are arithmetic,
|
|
10
|
+
* not I/O: the same discipline as virtual-id.ts's zero-dependency rule,
|
|
11
|
+
* relaxed one notch because path building is this module's whole job.
|
|
12
|
+
*
|
|
13
|
+
* Platform coverage mirrors what both upstream distribution points actually
|
|
14
|
+
* ship: darwin/linux/windows × amd64/arm64. macOS arm64's cloudflared asset
|
|
15
|
+
* is a `.tgz` (the only archive shape in the set) — binary-io unpacks it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
|
|
21
|
+
/** The two binaries the wizard downloads and runs. */
|
|
22
|
+
export type IngressBinary = 'caddy' | 'cloudflared'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The platform shapes both distribution points understand. Windows is
|
|
26
|
+
* deliberately amd64-only: that is all Caddy's build API and cloudflared's
|
|
27
|
+
* Releases publish a gateway-wizard-usable image for (a windows-arm64 host
|
|
28
|
+
* has no supported download either way, so pretending otherwise would only
|
|
29
|
+
* move the failure somewhere less explicable).
|
|
30
|
+
*/
|
|
31
|
+
export type BinaryPlatform =
|
|
32
|
+
| { os: 'darwin'; arch: 'amd64' | 'arm64' }
|
|
33
|
+
| { os: 'linux'; arch: 'amd64' | 'arm64' }
|
|
34
|
+
| { os: 'windows'; arch: 'amd64' }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The wrapper an asset arrives in: `tgz` gzipped tar (needs unpacking),
|
|
38
|
+
* `zip` (no reader in this codebase yet — refused before download), `exe`
|
|
39
|
+
* Windows' own format, `none` a bare executable.
|
|
40
|
+
*/
|
|
41
|
+
export type BinaryArchive = 'tgz' | 'zip' | 'exe' | 'none'
|
|
42
|
+
|
|
43
|
+
/** This process's platform, mapped into the download points' naming. */
|
|
44
|
+
export function currentPlatform(): BinaryPlatform {
|
|
45
|
+
if (process.platform === 'win32') return { os: 'windows', arch: 'amd64' }
|
|
46
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64'
|
|
47
|
+
if (process.platform === 'darwin') return { os: 'darwin', arch }
|
|
48
|
+
return { os: 'linux', arch }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Caddy's custom-build API, pinned to the Cloudflare DNS module the
|
|
52
|
+
* DNS-01 challenge needs (plain Caddy cannot write the TXT record). */
|
|
53
|
+
export function caddyDownloadUrl(platform: BinaryPlatform): string {
|
|
54
|
+
return `https://caddyserver.com/api/download?os=${platform.os}&arch=${platform.arch}&p=github.com/caddy-dns/cloudflare`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Caddy asset the build API answers with, PLUS its archive shape —
|
|
59
|
+
* `caddyserver.com/api/download` returns a bare Mach-O/ELF binary on unix
|
|
60
|
+
* (`Content-Disposition: attachment; filename="caddy_<os>_<arch>_custom"`,
|
|
61
|
+
* `Content-Type: application/octet-stream`) and a bare `.exe` on Windows.
|
|
62
|
+
*/
|
|
63
|
+
export function caddyDownload(platform: BinaryPlatform): { url: string; archive: BinaryArchive } {
|
|
64
|
+
return { url: caddyDownloadUrl(platform), archive: platform.os === 'windows' ? 'exe' : 'none' }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** cloudflared's GitHub Releases asset for one platform, plus the archive
|
|
68
|
+
* shape the I/O half must unwrap (darwin assets are `.tgz`, windows ships
|
|
69
|
+
* a bare `.exe`, linux a bare binary). */
|
|
70
|
+
export function cloudflaredDownload(platform: BinaryPlatform): { url: string; archive: BinaryArchive } {
|
|
71
|
+
const base = 'https://github.com/cloudflare/cloudflared/releases/latest/download/'
|
|
72
|
+
if (platform.os === 'darwin') return { url: `${base}cloudflared-darwin-${platform.arch}.tgz`, archive: 'tgz' }
|
|
73
|
+
if (platform.os === 'windows') return { url: `${base}cloudflared-windows-amd64.exe`, archive: 'exe' }
|
|
74
|
+
return { url: `${base}cloudflared-linux-${platform.arch}`, archive: 'none' }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Both download URLs for one platform WITH their archive shapes, defaults
|
|
78
|
+
* to the running process's. The archive metadata — not a URL suffix — is
|
|
79
|
+
* what tells the I/O half how to unpack. */
|
|
80
|
+
export function binaryUrls(
|
|
81
|
+
platform: BinaryPlatform = currentPlatform(),
|
|
82
|
+
): { caddy: string; caddyArchive: BinaryArchive; cloudflared: string; cloudflaredArchive: BinaryArchive } {
|
|
83
|
+
const caddy = caddyDownload(platform)
|
|
84
|
+
const cloudflared = cloudflaredDownload(platform)
|
|
85
|
+
return { caddy: caddy.url, caddyArchive: caddy.archive, cloudflared: cloudflared.url, cloudflaredArchive: cloudflared.archive }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Official manual-install pages, surfaced by the UI when a download fails. */
|
|
89
|
+
export const MANUAL_INSTALL_LINKS: Readonly<Record<IngressBinary, string>> = {
|
|
90
|
+
caddy: 'https://caddyserver.com/download',
|
|
91
|
+
cloudflared: 'https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Everything ingress lives under this one directory of DSH_HOME. */
|
|
95
|
+
export const INGRESS_DIR_NAME = 'zen-remote-ingress'
|
|
96
|
+
|
|
97
|
+
/** `<DSH_HOME>` as the rest of the plugin resolves it (config.ts's convention). */
|
|
98
|
+
export function dshHomeDefault(env: NodeJS.ProcessEnv = process.env): string {
|
|
99
|
+
return env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `<DSH_HOME>/zen-remote-ingress` — Caddyfile, data dir and bin/ sit here. */
|
|
103
|
+
export function ingressDir(dshHome: string): string {
|
|
104
|
+
return join(dshHome, INGRESS_DIR_NAME)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `<DSH_HOME>/zen-remote-ingress/bin` — both binaries install here. */
|
|
108
|
+
export function binDir(dshHome: string): string {
|
|
109
|
+
return join(ingressDir(dshHome), 'bin')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `<DSH_HOME>/zen-remote-ingress/Caddyfile` — generated, validated, then
|
|
113
|
+
* handed to `caddy run --config`. */
|
|
114
|
+
export function caddyfilePath(dshHome: string): string {
|
|
115
|
+
return join(ingressDir(dshHome), 'Caddyfile')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `<DSH_HOME>/zen-remote-ingress/caddy-data` — certificates and the ACME
|
|
119
|
+
* account live here, kept out of the default Caddy home. */
|
|
120
|
+
export function caddyDataDir(dshHome: string): string {
|
|
121
|
+
return join(ingressDir(dshHome), 'caddy-data')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The installed file name. Windows cannot spawn an extension-less image,
|
|
125
|
+
* so the `.exe` suffix is part of the install name there. */
|
|
126
|
+
export function binaryFileName(tool: IngressBinary, platform: BinaryPlatform = currentPlatform()): string {
|
|
127
|
+
if (platform.os === 'windows') return tool === 'caddy' ? 'caddy.exe' : 'cloudflared.exe'
|
|
128
|
+
return tool
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The absolute install path for one binary (where downloadBinary puts it). */
|
|
132
|
+
export function binaryInstallPath(dshHome: string, tool: IngressBinary, platform: BinaryPlatform = currentPlatform()): string {
|
|
133
|
+
return join(binDir(dshHome), binaryFileName(tool, platform))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Prebuilt Caddy + `github.com/caddy-dns/cloudflare` release assets hosted
|
|
137
|
+
* on this repository's `caddy-cloudflare` GitHub Release (built by
|
|
138
|
+
* `.github/workflows/caddy-assets.yml`). Unlike `caddyserver.com/api/download`
|
|
139
|
+
* — which compiles on the fly and throttles downloads to ~13 KB/s — these
|
|
140
|
+
* static GitHub Release assets download in seconds and work with mainland
|
|
141
|
+
* China GitHub acceleration mirrors (`gh-proxy.com` / `ghfast.top`). */
|
|
142
|
+
export function caddyPrebuiltDownload(platform: BinaryPlatform): { url: string; archive: BinaryArchive } {
|
|
143
|
+
const base = 'https://github.com/KyoMio/dsh-zen-remote/releases/download/caddy-cloudflare/'
|
|
144
|
+
if (platform.os === 'windows') return { url: `${base}caddy-windows-amd64.exe`, archive: 'exe' }
|
|
145
|
+
return { url: `${base}caddy-${platform.os}-${platform.arch}.tgz`, archive: 'tgz' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Mainland China GitHub release acceleration mirrors, tried first when the
|
|
149
|
+
* host environment is detected as mainland China (or tried as fallbacks if
|
|
150
|
+
* direct GitHub times out). */
|
|
151
|
+
export const GITHUB_MIRROR_PREFIXES = [
|
|
152
|
+
'https://gh-proxy.com/',
|
|
153
|
+
'https://ghfast.top/',
|
|
154
|
+
] as const
|
|
155
|
+
|
|
156
|
+
/** Ordered candidate downloads for one binary and platform:
|
|
157
|
+
* - When `preferCnMirror` is true, GitHub Release URLs through mainland
|
|
158
|
+
* mirrors come first, followed by direct GitHub Release URL, and finally
|
|
159
|
+
* (for Caddy) the upstream `caddyserver.com/api/download` fallback.
|
|
160
|
+
* - When `preferCnMirror` is false, direct GitHub Release comes first, then
|
|
161
|
+
* mainland mirrors, then the upstream build API fallback. */
|
|
162
|
+
export function binaryDownloadCandidates(
|
|
163
|
+
tool: IngressBinary,
|
|
164
|
+
platform: BinaryPlatform = currentPlatform(),
|
|
165
|
+
preferCnMirror = false,
|
|
166
|
+
): Array<{ url: string; archive: BinaryArchive }> {
|
|
167
|
+
const primary = tool === 'caddy' ? caddyPrebuiltDownload(platform) : cloudflaredDownload(platform)
|
|
168
|
+
const mirrored = GITHUB_MIRROR_PREFIXES.map((prefix) => ({
|
|
169
|
+
url: `${prefix}${primary.url}`,
|
|
170
|
+
archive: primary.archive,
|
|
171
|
+
}))
|
|
172
|
+
const ordered = preferCnMirror ? [...mirrored, primary] : [primary, ...mirrored]
|
|
173
|
+
if (tool === 'caddy') {
|
|
174
|
+
ordered.push(caddyDownload(platform))
|
|
175
|
+
}
|
|
176
|
+
return ordered
|
|
177
|
+
}
|