unplugin-https-reverse-proxy 4.0.4 → 4.1.0-beta.2
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 +30 -6
- package/dist/{chunk-5R33PUJP.js → chunk-7CA3EIIX.js} +681 -223
- package/dist/{chunk-2P7SUCQ3.js → chunk-EY3HXBJ4.js} +0 -26
- package/dist/chunk-RKHJN6KY.js +53 -0
- package/dist/esbuild.cjs +781 -300
- package/dist/esbuild.js +3 -2
- package/dist/index.cjs +781 -300
- package/dist/index.js +3 -2
- package/dist/nuxt.cjs +779 -290
- package/dist/nuxt.js +5 -9
- package/dist/privileged-helper.cjs +668 -0
- package/dist/privileged-helper.d.cts +2 -0
- package/dist/privileged-helper.d.ts +2 -0
- package/dist/privileged-helper.js +641 -0
- package/dist/rollup.cjs +781 -300
- package/dist/rollup.js +3 -2
- package/dist/rspack.cjs +781 -300
- package/dist/rspack.js +3 -2
- package/dist/utils.cjs +0 -28
- package/dist/utils.d.cts +1 -3
- package/dist/utils.d.ts +1 -3
- package/dist/utils.js +1 -5
- package/dist/vite.cjs +781 -300
- package/dist/vite.js +3 -2
- package/dist/webpack.cjs +781 -300
- package/dist/webpack.js +3 -2
- package/package.json +1 -4
- package/src/caddy/caddyfile.ts +1 -0
- package/src/caddy/index.ts +183 -110
- package/src/caddy/privileged.ts +118 -0
- package/src/caddy/utils.ts +0 -43
- package/src/index.ts +1 -15
- package/src/nuxt.ts +1 -6
- package/src/privileged/bootstrap.ts +174 -0
- package/src/privileged/client.ts +287 -0
- package/src/privileged/entry.ts +45 -0
- package/src/privileged/hosts-file.ts +142 -0
- package/src/privileged/launch.ts +193 -0
- package/src/privileged/ports.ts +45 -0
- package/src/privileged/protocol.ts +111 -0
- package/src/privileged/server.ts +465 -0
- package/src/privileged/session.ts +114 -0
- package/src/privileged/validation.ts +38 -0
- package/src/privileged/windows-elevation.ts +80 -0
- package/src/privileged-helper.ts +109 -0
- package/src/utils.ts +0 -28
- package/src/host/index.ts +0 -52
package/dist/webpack.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
unpluginFactory
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7CA3EIIX.js";
|
|
4
|
+
import "./chunk-EY3HXBJ4.js";
|
|
4
5
|
import "./chunk-C5A7XLKL.js";
|
|
5
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-RKHJN6KY.js";
|
|
6
7
|
|
|
7
8
|
// src/webpack.ts
|
|
8
9
|
import { createWebpackPlugin } from "unplugin";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unplugin-https-reverse-proxy",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.1.0-beta.2",
|
|
5
5
|
"description": "",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/zcf0508/unplugin-https-reverse-proxy#readme",
|
|
@@ -119,7 +119,6 @@
|
|
|
119
119
|
"console-table-printer": "^2.12.1",
|
|
120
120
|
"dotenv-flow": "^4.1.0",
|
|
121
121
|
"got-cjs": "^12.5.4",
|
|
122
|
-
"hostile": "^1.4.0",
|
|
123
122
|
"http-proxy-agent": "^7.0.2",
|
|
124
123
|
"https-proxy-agent": "^7.0.6",
|
|
125
124
|
"ora": "5",
|
|
@@ -133,8 +132,6 @@
|
|
|
133
132
|
"@nuxt/schema": "^3.16.1",
|
|
134
133
|
"@rsbuild/core": "^0.4.15",
|
|
135
134
|
"@rspack/core": "^0.5.9",
|
|
136
|
-
"@types/hostile": "^1.3.5",
|
|
137
|
-
"@types/kill-port": "^2.0.3",
|
|
138
135
|
"@types/node": "^20.11.19",
|
|
139
136
|
"@types/proper-lockfile": "^4.1.4",
|
|
140
137
|
"@types/qrcode-terminal": "^0.12.2",
|
package/src/caddy/caddyfile.ts
CHANGED
package/src/caddy/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Buffer } from 'node:buffer'
|
|
2
|
+
import type { OpenedPrivilegedSession } from './privileged'
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
2
4
|
import { chmodSync, createWriteStream, existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
5
|
import { readFile, unlink, writeFile } from 'node:fs/promises'
|
|
4
6
|
import { platform } from 'node:os'
|
|
@@ -9,25 +11,23 @@ import { got } from 'got-cjs'
|
|
|
9
11
|
import { HttpProxyAgent } from 'http-proxy-agent'
|
|
10
12
|
import { HttpsProxyAgent } from 'https-proxy-agent'
|
|
11
13
|
import * as lockfile from 'proper-lockfile'
|
|
12
|
-
import {
|
|
13
|
-
import { chmodRecursive, consola, once } from '../utils'
|
|
14
|
+
import { consola, once } from '../utils'
|
|
14
15
|
import { genCaddyfile, validateTemplateContext } from './caddyfile'
|
|
15
16
|
import { useCaddyConfig } from './config'
|
|
16
17
|
import { caddyFilePath, caddyLockFilePath, caddyPath, supportList, TEMP_DIR } from './constants'
|
|
17
18
|
import { isCaddyReloadCancellation } from './log'
|
|
19
|
+
import { HelperCaddyProcess, openPrivilegedSession } from './privileged'
|
|
18
20
|
import { formatTlsHealthFailure, waitForLocalTls } from './tls-health'
|
|
19
|
-
import {
|
|
21
|
+
import { logProgress, logProgressOver } from './utils'
|
|
20
22
|
|
|
21
23
|
export async function download(): Promise<string> {
|
|
22
24
|
if (await testCaddy())
|
|
23
25
|
return caddyPath
|
|
24
26
|
|
|
25
27
|
return new Promise<string>((resolve, reject) => {
|
|
26
|
-
// https://caddyserver.com/api/download?os=darwin&arch=amd64
|
|
27
|
-
|
|
28
28
|
if (!existsSync(TEMP_DIR)) {
|
|
29
29
|
mkdirSync(TEMP_DIR, { recursive: true })
|
|
30
|
-
chmodSync(TEMP_DIR,
|
|
30
|
+
chmodSync(TEMP_DIR, 0o700)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const file = createWriteStream(caddyPath)
|
|
@@ -55,8 +55,7 @@ export async function download(): Promise<string> {
|
|
|
55
55
|
const httpsAgent = httpsProxy ? new HttpsProxyAgent(httpsProxy) : undefined
|
|
56
56
|
|
|
57
57
|
const chmodCaddyOnce = once(() => {
|
|
58
|
-
|
|
59
|
-
chmodSync(caddyPath, 0o777)
|
|
58
|
+
chmodSync(caddyPath, 0o755)
|
|
60
59
|
})
|
|
61
60
|
|
|
62
61
|
got.stream(dowmloadLink, {
|
|
@@ -71,30 +70,32 @@ export async function download(): Promise<string> {
|
|
|
71
70
|
logProgressOver()
|
|
72
71
|
if (process.platform === 'win32')
|
|
73
72
|
return resolve(caddyPath)
|
|
74
|
-
|
|
75
|
-
chmodSync(caddyPath, 0o777)
|
|
73
|
+
chmodSync(caddyPath, 0o755)
|
|
76
74
|
resolve(caddyPath)
|
|
77
75
|
}).on('error', (err) => {
|
|
78
|
-
// consola.error(err)
|
|
79
76
|
reject(err)
|
|
80
77
|
})
|
|
81
78
|
})
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Checks for a usable binary by running it with no arguments: Caddy prints its
|
|
83
|
+
* usage and exits, so this neither binds a port nor needs privileges.
|
|
84
|
+
*/
|
|
85
|
+
export function testCaddy(): Promise<boolean> {
|
|
86
|
+
return new Promise((resolve) => {
|
|
86
87
|
if (!existsSync(caddyPath))
|
|
87
88
|
return resolve(false)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
chmodSync(caddyPath, 0o755)
|
|
92
|
+
}
|
|
93
|
+
catch {}
|
|
94
|
+
|
|
95
|
+
execFile(caddyPath, [], { timeout: 10_000 }, (error, stdout) => {
|
|
96
|
+
if (error && !stdout)
|
|
97
|
+
return resolve(false)
|
|
98
|
+
resolve(stdout.length > 0)
|
|
98
99
|
})
|
|
99
100
|
})
|
|
100
101
|
}
|
|
@@ -129,32 +130,67 @@ function splitHostPort(addr: string): { host: string, port: string } {
|
|
|
129
130
|
return { host: addr.slice(0, lastColon), port: addr.slice(lastColon + 1) }
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/** Injection point for tests: opens the privileged helper without sudo. */
|
|
134
|
+
export interface CaddyDependencies {
|
|
135
|
+
openSession: (runtimeDir: string) => Promise<OpenedPrivilegedSession>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const defaultDependencies: CaddyDependencies = {
|
|
139
|
+
openSession: runtimeDir => openPrivilegedSession(runtimeDir, {
|
|
140
|
+
log: message => consola.debug(message),
|
|
141
|
+
// Both paths are fixed here, before any socket traffic: the privileged
|
|
142
|
+
// helper must never execute something named in a request.
|
|
143
|
+
caddyBin: caddyPath,
|
|
144
|
+
caddyConfigPath: caddyFilePath,
|
|
145
|
+
}),
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
export class CaddyInstant {
|
|
133
149
|
private inited = false
|
|
134
150
|
private stoped = false
|
|
151
|
+
private readonly deps: CaddyDependencies
|
|
135
152
|
|
|
136
153
|
/** has locked when start */
|
|
137
154
|
private locked = false
|
|
138
155
|
private caddyfile: string | undefined
|
|
139
156
|
|
|
140
|
-
private _caddyChild:
|
|
157
|
+
private _caddyChild: HelperCaddyProcess | undefined
|
|
158
|
+
private _session: OpenedPrivilegedSession | undefined
|
|
141
159
|
|
|
142
160
|
private _source: string | undefined
|
|
143
161
|
private _target: string | undefined
|
|
144
|
-
private _stopEffects: Array<
|
|
145
|
-
|
|
146
|
-
constructor() {
|
|
147
|
-
testCaddy().then(async () => {
|
|
148
|
-
this.inited = true
|
|
149
|
-
})
|
|
162
|
+
private _stopEffects: Array<(() => void)> = []
|
|
150
163
|
|
|
164
|
+
constructor(deps: Partial<CaddyDependencies> = {}) {
|
|
165
|
+
this.deps = { ...defaultDependencies, ...deps }
|
|
151
166
|
this.locked = this.checkLock()
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
checkLock(): boolean {
|
|
155
170
|
if (!existsSync(caddyLockFilePath))
|
|
156
171
|
writeFileSync(caddyLockFilePath, '')
|
|
157
|
-
|
|
172
|
+
try {
|
|
173
|
+
return lockfile.checkSync(caddyLockFilePath)
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Connects to the privileged helper, starting it once if needed. It is
|
|
182
|
+
* reused across instances so a second dev server does not ask for a
|
|
183
|
+
* password again.
|
|
184
|
+
*/
|
|
185
|
+
private async session(): Promise<OpenedPrivilegedSession> {
|
|
186
|
+
// A crashed helper leaves the cached client permanently disconnected; drop
|
|
187
|
+
// it so the next call starts a fresh helper instead of failing forever.
|
|
188
|
+
if (this._session && !this._session.client.connected)
|
|
189
|
+
this._session = undefined
|
|
190
|
+
|
|
191
|
+
if (!this._session)
|
|
192
|
+
this._session = await this.deps.openSession(TEMP_DIR)
|
|
193
|
+
return this._session
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
async getCaddyfile(): Promise<string> {
|
|
@@ -189,6 +225,34 @@ export class CaddyInstant {
|
|
|
189
225
|
this.inited = true
|
|
190
226
|
}
|
|
191
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Hands the Caddy storage directory back to the developer. Local CA data
|
|
230
|
+
* created by an earlier root-owned run must stay readable for `tls internal`
|
|
231
|
+
* to keep serving the same certificate the browser already trusts.
|
|
232
|
+
*/
|
|
233
|
+
/**
|
|
234
|
+
* Installs Caddy's local root CA so the browser stops warning about
|
|
235
|
+
* `tls internal` certificates. Generating the CA and trusting it are separate
|
|
236
|
+
* steps: a new CA appears whenever the previous one expires or the storage is
|
|
237
|
+
* replaced, so this runs on every start instead of once.
|
|
238
|
+
*
|
|
239
|
+
* The helper owns both the binary path and the command, so nothing here can
|
|
240
|
+
* be turned into an arbitrary root execution.
|
|
241
|
+
*/
|
|
242
|
+
private async trustLocalCa(session: OpenedPrivilegedSession): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
const result = await session.client.call('ca.trust')
|
|
245
|
+
if (result.trusted)
|
|
246
|
+
consola.debug(`Caddy local CA is trusted\n${result.output}`)
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
consola.warn(
|
|
250
|
+
`could not install the Caddy root certificate automatically: ${(error as Error).message}\n`
|
|
251
|
+
+ `Run \`${caddyPath} trust\` manually if the browser warns about the certificate.`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
192
256
|
private async ensureTlsReady(targetHost: string): Promise<void> {
|
|
193
257
|
const health = await waitForLocalTls(targetHost)
|
|
194
258
|
if (!health.healthy)
|
|
@@ -249,11 +313,13 @@ export class CaddyInstant {
|
|
|
249
313
|
if (!this.inited)
|
|
250
314
|
await this.init()
|
|
251
315
|
|
|
316
|
+
const session = await this.session()
|
|
317
|
+
|
|
252
318
|
const sourceHost = splitHostPort(source).host
|
|
253
319
|
const targetHost = splitHostPort(target).host
|
|
254
|
-
|
|
255
|
-
throw new Error('update host failed')
|
|
320
|
+
await session.client.call('hosts.add', { ip: sourceHost, host: targetHost })
|
|
256
321
|
consola.success('update host success\n')
|
|
322
|
+
|
|
257
323
|
const normalizeBase = (b: string): string => {
|
|
258
324
|
let out = b || '/'
|
|
259
325
|
if (!out.startsWith('/'))
|
|
@@ -296,10 +362,10 @@ export class CaddyInstant {
|
|
|
296
362
|
writeFileSync(caddyLockFilePath, '')
|
|
297
363
|
|
|
298
364
|
if (!this.locked) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
await
|
|
365
|
+
// Ports 80/443 may still be held by a previous, uncleanly stopped run.
|
|
366
|
+
await session.client.call('port.free', { port: 443 })
|
|
367
|
+
if (process.platform === 'win32' || !https)
|
|
368
|
+
await session.client.call('port.free', { port: 80 })
|
|
303
369
|
}
|
|
304
370
|
|
|
305
371
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -311,8 +377,13 @@ export class CaddyInstant {
|
|
|
311
377
|
startupCheckStarted = true
|
|
312
378
|
|
|
313
379
|
try {
|
|
314
|
-
if (https)
|
|
380
|
+
if (https) {
|
|
381
|
+
// `caddy trust` reads the CA through the admin API, so it can only
|
|
382
|
+
// run once Caddy is serving. The helper does it as root because the
|
|
383
|
+
// system trust store is not writable by the developer account.
|
|
384
|
+
await this.trustLocalCa(session)
|
|
315
385
|
await this.ensureTlsReady(targetHost)
|
|
386
|
+
}
|
|
316
387
|
resolve()
|
|
317
388
|
}
|
|
318
389
|
catch (error) {
|
|
@@ -344,25 +415,6 @@ export class CaddyInstant {
|
|
|
344
415
|
|
|
345
416
|
// @ts-expect-error override
|
|
346
417
|
process.exit = async (code?: number) => {
|
|
347
|
-
const port = Number(splitHostPort(source).port)
|
|
348
|
-
if (!Number.isNaN(port) && await tryPort(port))
|
|
349
|
-
await kill(port)
|
|
350
|
-
|
|
351
|
-
// fix `Error: EPERM: operation not permitted`
|
|
352
|
-
const pwd = process.cwd()
|
|
353
|
-
const viteCacheDir = `${pwd}/node_modules/.vite`
|
|
354
|
-
if (existsSync(viteCacheDir))
|
|
355
|
-
await chmodRecursive(viteCacheDir, 0o777).catch(() => { })
|
|
356
|
-
const viteTempCacheDir = `${pwd}/node_modules/.vite-temp`
|
|
357
|
-
if (existsSync(viteTempCacheDir))
|
|
358
|
-
await chmodRecursive(viteTempCacheDir, 0o777).catch(() => { })
|
|
359
|
-
const nuxtCacheDir = `${pwd}/.nuxt`
|
|
360
|
-
if (existsSync(nuxtCacheDir))
|
|
361
|
-
await chmodRecursive(nuxtCacheDir, 0o777).catch(() => { })
|
|
362
|
-
const webpackCacheDir = `${pwd}/node_modules/.cache`
|
|
363
|
-
if (existsSync(webpackCacheDir))
|
|
364
|
-
await chmodRecursive(webpackCacheDir, 0o777).catch(() => { })
|
|
365
|
-
|
|
366
418
|
if (this.stoped)
|
|
367
419
|
return originalExit(code)
|
|
368
420
|
|
|
@@ -382,56 +434,67 @@ export class CaddyInstant {
|
|
|
382
434
|
}
|
|
383
435
|
|
|
384
436
|
if (!this.locked) {
|
|
385
|
-
this._caddyChild = process.platform !== 'win32'
|
|
386
|
-
? spawn('sudo', ['-E', caddyPath, 'run', '--config', caddyFilePath, '--watch'])
|
|
387
|
-
: spawn(caddyPath, ['run', '--config', caddyFilePath, '--watch'])
|
|
388
|
-
|
|
389
437
|
lockfile.lockSync(caddyLockFilePath)
|
|
438
|
+
setupCleanup()
|
|
390
439
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
440
|
+
void (async () => {
|
|
441
|
+
try {
|
|
442
|
+
const child = new HelperCaddyProcess(session.client)
|
|
443
|
+
this._caddyChild = child
|
|
394
444
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
445
|
+
await session.client.call('caddy.start')
|
|
446
|
+
|
|
447
|
+
child.on('exit', (code: number | null) => {
|
|
448
|
+
if (code !== null && code !== 0)
|
|
449
|
+
reject(new Error(`Caddy exited with code ${code}`))
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
child.stderr.on('data', async (data: Buffer) => {
|
|
453
|
+
const lines = data.toString().split('\n').map((line: string) => line.trim())
|
|
454
|
+
for (const line of lines) {
|
|
455
|
+
// caddy log
|
|
456
|
+
// eslint-disable-next-line no-console
|
|
457
|
+
showCaddyLog && line && console.info(line)
|
|
458
|
+
if (isCaddyReloadCancellation(line)) {
|
|
459
|
+
consola.info('Caddy restarted before TLS issuance completed; retrying.\n')
|
|
460
|
+
continue
|
|
411
461
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
462
|
+
try {
|
|
463
|
+
if (line.includes('Error:') || (line && JSON.parse(line).level === 'error')) {
|
|
464
|
+
if (line.includes('parsing caddyfile tokens')) {
|
|
465
|
+
consola.error('Caddyfile syntax error, please contact the author:\n', `${this.caddyfile}\n\n${line}\n`)
|
|
466
|
+
await this.baseCleanup()
|
|
467
|
+
process.exit()
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
consola.error(`${line}\n`)
|
|
471
|
+
return reject(line)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
416
474
|
}
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
catch {}
|
|
475
|
+
catch {}
|
|
420
476
|
|
|
421
|
-
|
|
422
|
-
|
|
477
|
+
if (line.includes('config file changed'))
|
|
478
|
+
consola.success('Detect added a new reverse proxy config, restarted.\n')
|
|
423
479
|
|
|
424
|
-
|
|
480
|
+
if (line.includes('caddy proxying') || line.includes('serving initial'))
|
|
481
|
+
void resolveWhenReady()
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
486
|
+
consola.info(data.toString())
|
|
425
487
|
void resolveWhenReady()
|
|
488
|
+
})
|
|
426
489
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
490
|
+
catch (error) {
|
|
491
|
+
// The hosts entry was written before Caddy started; a failed start
|
|
492
|
+
// must not leave it behind while the dev server keeps running.
|
|
493
|
+
if (!this.locked)
|
|
494
|
+
await this.baseCleanup(restore)
|
|
495
|
+
reject(error)
|
|
496
|
+
}
|
|
497
|
+
})()
|
|
435
498
|
}
|
|
436
499
|
else {
|
|
437
500
|
consola.info('Caddy is running in another process.')
|
|
@@ -453,26 +516,36 @@ export class CaddyInstant {
|
|
|
453
516
|
|
|
454
517
|
async baseCleanup(restore: boolean = true): Promise<void> {
|
|
455
518
|
this._stopEffects.forEach(fn => fn())
|
|
519
|
+
this._stopEffects.length = 0
|
|
456
520
|
|
|
457
521
|
try {
|
|
458
|
-
if (restore && this._source && this._target) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
522
|
+
if (restore && this._source && this._target && this._session) {
|
|
523
|
+
const sourceHost = splitHostPort(this._source).host
|
|
524
|
+
const targetHost = splitHostPort(this._target).host
|
|
525
|
+
await this._session.client.call('hosts.remove', { ip: sourceHost, host: targetHost })
|
|
526
|
+
if (sourceHost === 'localhost')
|
|
527
|
+
await this._session.client.call('hosts.remove', { ip: '127.0.0.1', host: targetHost })
|
|
528
|
+
consola.success('Restore host success.\n')
|
|
464
529
|
}
|
|
465
530
|
}
|
|
466
531
|
catch (e) {
|
|
467
|
-
|
|
468
|
-
|
|
532
|
+
// The helper restores its own entries when the connection drops, so a
|
|
533
|
+
// closed session here is not a failed cleanup.
|
|
534
|
+
if (this._session?.client.connected === false) {
|
|
535
|
+
consola.debug('hosts entries are restored by the privileged helper on disconnect')
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
consola.error(e)
|
|
539
|
+
consola.fail('Restore host failed.\n')
|
|
540
|
+
}
|
|
469
541
|
}
|
|
542
|
+
|
|
470
543
|
if (!this.locked && !this.stoped) {
|
|
471
544
|
await this.stopCaddy()
|
|
472
545
|
try {
|
|
473
|
-
await lockfile.unlock(caddyLockFilePath).catch(() => {
|
|
474
|
-
await unlink(caddyFilePath).catch(() => {
|
|
475
|
-
await unlink(caddyLockFilePath).catch(() => {
|
|
546
|
+
await lockfile.unlock(caddyLockFilePath).catch(() => {})
|
|
547
|
+
await unlink(caddyFilePath).catch(() => {})
|
|
548
|
+
await unlink(caddyLockFilePath).catch(() => {})
|
|
476
549
|
if (caddyConfig) {
|
|
477
550
|
await caddyConfig.cleanUp()
|
|
478
551
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { PrivilegedClient } from '../privileged/client'
|
|
2
|
+
import type { HelperEvent } from '../privileged/protocol'
|
|
3
|
+
import { Buffer } from 'node:buffer'
|
|
4
|
+
import { EventEmitter } from 'node:events'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import process from 'node:process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { resolveHelperEntry } from '../privileged/entry'
|
|
9
|
+
import { openHelperSession } from '../privileged/session'
|
|
10
|
+
|
|
11
|
+
export interface PrivilegedEnv {
|
|
12
|
+
/** Executable started as root, resolved from the running bundle. */
|
|
13
|
+
helperEntry: string
|
|
14
|
+
helperArgs: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves what the helper needs to be started from the current bundle.
|
|
19
|
+
* `__dirname` exists in both the CJS and ESM outputs (tsup injects a shim).
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePrivilegedEnv(): PrivilegedEnv {
|
|
22
|
+
const moduleDir = typeof __dirname === 'string'
|
|
23
|
+
? __dirname
|
|
24
|
+
: dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const resolved = resolveHelperEntry({ moduleDir })
|
|
26
|
+
return { helperEntry: resolved.entry, helperArgs: resolved.args }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Re-emits helper events as the `stdout`/`stderr` events the log parsing expects. */
|
|
30
|
+
export class HelperCaddyProcess extends EventEmitter {
|
|
31
|
+
readonly stdout = new EventEmitter()
|
|
32
|
+
readonly stderr = new EventEmitter()
|
|
33
|
+
private finished = false
|
|
34
|
+
private readonly unsubscribe: () => void
|
|
35
|
+
|
|
36
|
+
constructor(private readonly client: PrivilegedClient) {
|
|
37
|
+
super()
|
|
38
|
+
this.unsubscribe = client.onEvent(event => this.handleEvent(event))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private handleEvent(event: HelperEvent): void {
|
|
42
|
+
if (event.type !== 'event')
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
if (event.kind === 'caddy.stdout') {
|
|
46
|
+
this.stdout.emit('data', Buffer.from(event.data))
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (event.kind === 'caddy.stderr') {
|
|
51
|
+
this.stderr.emit('data', Buffer.from(event.data))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (event.kind === 'caddy.exit') {
|
|
56
|
+
this.finished = true
|
|
57
|
+
this.emit('exit', event.code, event.signal)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Asks the helper to stop Caddy; the helper also stops it on disconnect. */
|
|
62
|
+
kill(): void {
|
|
63
|
+
this.unsubscribe()
|
|
64
|
+
if (this.finished)
|
|
65
|
+
return
|
|
66
|
+
this.finished = true
|
|
67
|
+
void this.client.call('caddy.stop').catch(() => {})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface OpenedPrivilegedSession {
|
|
72
|
+
client: PrivilegedClient
|
|
73
|
+
reused: boolean
|
|
74
|
+
close: () => Promise<void>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface PrivilegedSessionOptions {
|
|
78
|
+
log?: (message: string) => void
|
|
79
|
+
/** Runs the helper as the current user; tests use this to avoid sudo. */
|
|
80
|
+
direct?: boolean
|
|
81
|
+
/** Caddy binary the helper may run; fixed before the session opens. */
|
|
82
|
+
caddyBin: string
|
|
83
|
+
/** Caddyfile the helper may load. */
|
|
84
|
+
caddyConfigPath: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Where Caddy keeps its PKI, per platform. */
|
|
88
|
+
export function caddyStoragePath(): string {
|
|
89
|
+
return process.platform === 'win32'
|
|
90
|
+
? join(process.env.AppData ?? '', 'Caddy')
|
|
91
|
+
: join(process.env.HOME ?? '', 'Library/Application Support/Caddy')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Opens (or reuses) the privileged helper for this runtime directory.
|
|
96
|
+
* Only the helper runs as root: the plugin process, its caches and its
|
|
97
|
+
* `node_modules` keep the developer's identity.
|
|
98
|
+
*/
|
|
99
|
+
export function openPrivilegedSession(
|
|
100
|
+
runtimeDir: string,
|
|
101
|
+
options: PrivilegedSessionOptions,
|
|
102
|
+
): Promise<OpenedPrivilegedSession> {
|
|
103
|
+
const env = resolvePrivilegedEnv()
|
|
104
|
+
return openHelperSession({
|
|
105
|
+
runtimeDir,
|
|
106
|
+
helperEntry: env.helperEntry,
|
|
107
|
+
helperArgs: env.helperArgs,
|
|
108
|
+
direct: options.direct,
|
|
109
|
+
// Caddy keeps its PKI under the user's home; the helper runs it as root, so
|
|
110
|
+
// it must know which tree to hand back afterwards.
|
|
111
|
+
caddyStoragePath: caddyStoragePath(),
|
|
112
|
+
// The binary and Caddyfile are fixed here: the privileged helper must never
|
|
113
|
+
// execute a path supplied over the socket.
|
|
114
|
+
caddyBin: options.caddyBin,
|
|
115
|
+
caddyConfigPath: options.caddyConfigPath,
|
|
116
|
+
log: options.log,
|
|
117
|
+
})
|
|
118
|
+
}
|
package/src/caddy/utils.ts
CHANGED
|
@@ -1,51 +1,8 @@
|
|
|
1
|
-
import { exec } from 'node:child_process'
|
|
2
|
-
import { createServer } from 'node:net'
|
|
3
1
|
import ora from 'ora'
|
|
4
2
|
import { consola } from '../utils'
|
|
5
3
|
|
|
6
4
|
let spinner: ora.Ora
|
|
7
5
|
|
|
8
|
-
/**
|
|
9
|
-
* @returns whether the port is in use
|
|
10
|
-
*/
|
|
11
|
-
export function tryPort(port: number): Promise<boolean> {
|
|
12
|
-
return new Promise<boolean>((resolve, _reject) => {
|
|
13
|
-
try {
|
|
14
|
-
const server = createServer().listen(port)
|
|
15
|
-
server.on('listening', () => {
|
|
16
|
-
server.close()
|
|
17
|
-
resolve(false)
|
|
18
|
-
})
|
|
19
|
-
server.on('error', (err) => {
|
|
20
|
-
// @ts-expect-error err.code
|
|
21
|
-
if (err.code === 'EADDRINUSE')
|
|
22
|
-
resolve(true)
|
|
23
|
-
server.close()
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
resolve(true)
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* npx free-port :port
|
|
34
|
-
*/
|
|
35
|
-
export async function kill(port: number): Promise<void> {
|
|
36
|
-
return new Promise<void>((resolve, reject) => {
|
|
37
|
-
exec(`npx @maxbo/free-port ${port} -s`, (err, stdout, _stderr) => {
|
|
38
|
-
if (err) {
|
|
39
|
-
reject(err)
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
consola.log(stdout)
|
|
43
|
-
resolve()
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
6
|
function getNumber(number: number): string {
|
|
50
7
|
if (number <= 0)
|
|
51
8
|
return '0%'
|