unplugin-https-reverse-proxy 4.0.4 → 4.1.0-beta.3
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-2P7SUCQ3.js → chunk-EY3HXBJ4.js} +0 -26
- package/dist/{chunk-5R33PUJP.js → chunk-HBXMM25C.js} +765 -229
- package/dist/chunk-RKHJN6KY.js +53 -0
- package/dist/chunk-WOMR3ZLM.js +17 -0
- package/dist/esbuild.cjs +870 -306
- package/dist/esbuild.js +4 -2
- package/dist/index.cjs +870 -306
- package/dist/index.js +4 -2
- package/dist/nuxt.cjs +842 -292
- package/dist/nuxt.js +6 -9
- package/dist/privilege-state.cjs +44 -0
- package/dist/privilege-state.d.cts +7 -0
- package/dist/privilege-state.d.ts +7 -0
- package/dist/privilege-state.js +10 -0
- 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 +870 -306
- package/dist/rollup.js +4 -2
- package/dist/rspack.cjs +870 -306
- package/dist/rspack.js +4 -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 +870 -306
- package/dist/vite.js +4 -2
- package/dist/webpack.cjs +870 -306
- package/dist/webpack.js +4 -2
- package/package.json +1 -4
- package/src/caddy/caddyfile.ts +1 -0
- package/src/caddy/index.ts +184 -110
- package/src/caddy/privileged.ts +121 -0
- package/src/caddy/utils.ts +0 -43
- package/src/index.ts +49 -22
- package/src/nuxt.ts +1 -6
- package/src/privilege-state.ts +23 -0
- package/src/privileged/bootstrap.ts +174 -0
- package/src/privileged/client.ts +287 -0
- package/src/privileged/entry.ts +73 -0
- package/src/privileged/hosts-file.ts +142 -0
- package/src/privileged/launch.ts +235 -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 +117 -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,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
unpluginFactory
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HBXMM25C.js";
|
|
4
|
+
import "./chunk-EY3HXBJ4.js";
|
|
4
5
|
import "./chunk-C5A7XLKL.js";
|
|
5
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-WOMR3ZLM.js";
|
|
7
|
+
import "./chunk-RKHJN6KY.js";
|
|
6
8
|
|
|
7
9
|
// src/webpack.ts
|
|
8
10
|
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.3",
|
|
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,68 @@ 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, reason?: { target: string, https: boolean }) => Promise<OpenedPrivilegedSession>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const defaultDependencies: CaddyDependencies = {
|
|
139
|
+
openSession: (runtimeDir, reason) => 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
|
+
reason,
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
export class CaddyInstant {
|
|
133
150
|
private inited = false
|
|
134
151
|
private stoped = false
|
|
152
|
+
private readonly deps: CaddyDependencies
|
|
135
153
|
|
|
136
154
|
/** has locked when start */
|
|
137
155
|
private locked = false
|
|
138
156
|
private caddyfile: string | undefined
|
|
139
157
|
|
|
140
|
-
private _caddyChild:
|
|
158
|
+
private _caddyChild: HelperCaddyProcess | undefined
|
|
159
|
+
private _session: OpenedPrivilegedSession | undefined
|
|
141
160
|
|
|
142
161
|
private _source: string | undefined
|
|
143
162
|
private _target: string | undefined
|
|
144
|
-
private _stopEffects: Array<
|
|
145
|
-
|
|
146
|
-
constructor() {
|
|
147
|
-
testCaddy().then(async () => {
|
|
148
|
-
this.inited = true
|
|
149
|
-
})
|
|
163
|
+
private _stopEffects: Array<(() => void)> = []
|
|
150
164
|
|
|
165
|
+
constructor(deps: Partial<CaddyDependencies> = {}) {
|
|
166
|
+
this.deps = { ...defaultDependencies, ...deps }
|
|
151
167
|
this.locked = this.checkLock()
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
checkLock(): boolean {
|
|
155
171
|
if (!existsSync(caddyLockFilePath))
|
|
156
172
|
writeFileSync(caddyLockFilePath, '')
|
|
157
|
-
|
|
173
|
+
try {
|
|
174
|
+
return lockfile.checkSync(caddyLockFilePath)
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Connects to the privileged helper, starting it once if needed. It is
|
|
183
|
+
* reused across instances so a second dev server does not ask for a
|
|
184
|
+
* password again.
|
|
185
|
+
*/
|
|
186
|
+
private async session(reason?: { target: string, https: boolean }): Promise<OpenedPrivilegedSession> {
|
|
187
|
+
// A crashed helper leaves the cached client permanently disconnected; drop
|
|
188
|
+
// it so the next call starts a fresh helper instead of failing forever.
|
|
189
|
+
if (this._session && !this._session.client.connected)
|
|
190
|
+
this._session = undefined
|
|
191
|
+
|
|
192
|
+
if (!this._session)
|
|
193
|
+
this._session = await this.deps.openSession(TEMP_DIR, reason)
|
|
194
|
+
return this._session
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
async getCaddyfile(): Promise<string> {
|
|
@@ -189,6 +226,34 @@ export class CaddyInstant {
|
|
|
189
226
|
this.inited = true
|
|
190
227
|
}
|
|
191
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Hands the Caddy storage directory back to the developer. Local CA data
|
|
231
|
+
* created by an earlier root-owned run must stay readable for `tls internal`
|
|
232
|
+
* to keep serving the same certificate the browser already trusts.
|
|
233
|
+
*/
|
|
234
|
+
/**
|
|
235
|
+
* Installs Caddy's local root CA so the browser stops warning about
|
|
236
|
+
* `tls internal` certificates. Generating the CA and trusting it are separate
|
|
237
|
+
* steps: a new CA appears whenever the previous one expires or the storage is
|
|
238
|
+
* replaced, so this runs on every start instead of once.
|
|
239
|
+
*
|
|
240
|
+
* The helper owns both the binary path and the command, so nothing here can
|
|
241
|
+
* be turned into an arbitrary root execution.
|
|
242
|
+
*/
|
|
243
|
+
private async trustLocalCa(session: OpenedPrivilegedSession): Promise<void> {
|
|
244
|
+
try {
|
|
245
|
+
const result = await session.client.call('ca.trust')
|
|
246
|
+
if (result.trusted)
|
|
247
|
+
consola.debug(`Caddy local CA is trusted\n${result.output}`)
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
consola.warn(
|
|
251
|
+
`could not install the Caddy root certificate automatically: ${(error as Error).message}\n`
|
|
252
|
+
+ `Run \`${caddyPath} trust\` manually if the browser warns about the certificate.`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
192
257
|
private async ensureTlsReady(targetHost: string): Promise<void> {
|
|
193
258
|
const health = await waitForLocalTls(targetHost)
|
|
194
259
|
if (!health.healthy)
|
|
@@ -251,9 +316,11 @@ export class CaddyInstant {
|
|
|
251
316
|
|
|
252
317
|
const sourceHost = splitHostPort(source).host
|
|
253
318
|
const targetHost = splitHostPort(target).host
|
|
254
|
-
|
|
255
|
-
|
|
319
|
+
|
|
320
|
+
const session = await this.session({ target: targetHost, https })
|
|
321
|
+
await session.client.call('hosts.add', { ip: sourceHost, host: targetHost })
|
|
256
322
|
consola.success('update host success\n')
|
|
323
|
+
|
|
257
324
|
const normalizeBase = (b: string): string => {
|
|
258
325
|
let out = b || '/'
|
|
259
326
|
if (!out.startsWith('/'))
|
|
@@ -296,10 +363,10 @@ export class CaddyInstant {
|
|
|
296
363
|
writeFileSync(caddyLockFilePath, '')
|
|
297
364
|
|
|
298
365
|
if (!this.locked) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
await
|
|
366
|
+
// Ports 80/443 may still be held by a previous, uncleanly stopped run.
|
|
367
|
+
await session.client.call('port.free', { port: 443 })
|
|
368
|
+
if (process.platform === 'win32' || !https)
|
|
369
|
+
await session.client.call('port.free', { port: 80 })
|
|
303
370
|
}
|
|
304
371
|
|
|
305
372
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -311,8 +378,13 @@ export class CaddyInstant {
|
|
|
311
378
|
startupCheckStarted = true
|
|
312
379
|
|
|
313
380
|
try {
|
|
314
|
-
if (https)
|
|
381
|
+
if (https) {
|
|
382
|
+
// `caddy trust` reads the CA through the admin API, so it can only
|
|
383
|
+
// run once Caddy is serving. The helper does it as root because the
|
|
384
|
+
// system trust store is not writable by the developer account.
|
|
385
|
+
await this.trustLocalCa(session)
|
|
315
386
|
await this.ensureTlsReady(targetHost)
|
|
387
|
+
}
|
|
316
388
|
resolve()
|
|
317
389
|
}
|
|
318
390
|
catch (error) {
|
|
@@ -344,25 +416,6 @@ export class CaddyInstant {
|
|
|
344
416
|
|
|
345
417
|
// @ts-expect-error override
|
|
346
418
|
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
419
|
if (this.stoped)
|
|
367
420
|
return originalExit(code)
|
|
368
421
|
|
|
@@ -382,56 +435,67 @@ export class CaddyInstant {
|
|
|
382
435
|
}
|
|
383
436
|
|
|
384
437
|
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
438
|
lockfile.lockSync(caddyLockFilePath)
|
|
439
|
+
setupCleanup()
|
|
390
440
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
441
|
+
void (async () => {
|
|
442
|
+
try {
|
|
443
|
+
const child = new HelperCaddyProcess(session.client)
|
|
444
|
+
this._caddyChild = child
|
|
394
445
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
446
|
+
await session.client.call('caddy.start')
|
|
447
|
+
|
|
448
|
+
child.on('exit', (code: number | null) => {
|
|
449
|
+
if (code !== null && code !== 0)
|
|
450
|
+
reject(new Error(`Caddy exited with code ${code}`))
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
child.stderr.on('data', async (data: Buffer) => {
|
|
454
|
+
const lines = data.toString().split('\n').map((line: string) => line.trim())
|
|
455
|
+
for (const line of lines) {
|
|
456
|
+
// caddy log
|
|
457
|
+
// eslint-disable-next-line no-console
|
|
458
|
+
showCaddyLog && line && console.info(line)
|
|
459
|
+
if (isCaddyReloadCancellation(line)) {
|
|
460
|
+
consola.info('Caddy restarted before TLS issuance completed; retrying.\n')
|
|
461
|
+
continue
|
|
411
462
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
463
|
+
try {
|
|
464
|
+
if (line.includes('Error:') || (line && JSON.parse(line).level === 'error')) {
|
|
465
|
+
if (line.includes('parsing caddyfile tokens')) {
|
|
466
|
+
consola.error('Caddyfile syntax error, please contact the author:\n', `${this.caddyfile}\n\n${line}\n`)
|
|
467
|
+
await this.baseCleanup()
|
|
468
|
+
process.exit()
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
consola.error(`${line}\n`)
|
|
472
|
+
return reject(line)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
416
475
|
}
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
catch {}
|
|
476
|
+
catch {}
|
|
420
477
|
|
|
421
|
-
|
|
422
|
-
|
|
478
|
+
if (line.includes('config file changed'))
|
|
479
|
+
consola.success('Detect added a new reverse proxy config, restarted.\n')
|
|
423
480
|
|
|
424
|
-
|
|
481
|
+
if (line.includes('caddy proxying') || line.includes('serving initial'))
|
|
482
|
+
void resolveWhenReady()
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
487
|
+
consola.info(data.toString())
|
|
425
488
|
void resolveWhenReady()
|
|
489
|
+
})
|
|
426
490
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
491
|
+
catch (error) {
|
|
492
|
+
// The hosts entry was written before Caddy started; a failed start
|
|
493
|
+
// must not leave it behind while the dev server keeps running.
|
|
494
|
+
if (!this.locked)
|
|
495
|
+
await this.baseCleanup(restore)
|
|
496
|
+
reject(error)
|
|
497
|
+
}
|
|
498
|
+
})()
|
|
435
499
|
}
|
|
436
500
|
else {
|
|
437
501
|
consola.info('Caddy is running in another process.')
|
|
@@ -453,26 +517,36 @@ export class CaddyInstant {
|
|
|
453
517
|
|
|
454
518
|
async baseCleanup(restore: boolean = true): Promise<void> {
|
|
455
519
|
this._stopEffects.forEach(fn => fn())
|
|
520
|
+
this._stopEffects.length = 0
|
|
456
521
|
|
|
457
522
|
try {
|
|
458
|
-
if (restore && this._source && this._target) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
523
|
+
if (restore && this._source && this._target && this._session) {
|
|
524
|
+
const sourceHost = splitHostPort(this._source).host
|
|
525
|
+
const targetHost = splitHostPort(this._target).host
|
|
526
|
+
await this._session.client.call('hosts.remove', { ip: sourceHost, host: targetHost })
|
|
527
|
+
if (sourceHost === 'localhost')
|
|
528
|
+
await this._session.client.call('hosts.remove', { ip: '127.0.0.1', host: targetHost })
|
|
529
|
+
consola.success('Restore host success.\n')
|
|
464
530
|
}
|
|
465
531
|
}
|
|
466
532
|
catch (e) {
|
|
467
|
-
|
|
468
|
-
|
|
533
|
+
// The helper restores its own entries when the connection drops, so a
|
|
534
|
+
// closed session here is not a failed cleanup.
|
|
535
|
+
if (this._session?.client.connected === false) {
|
|
536
|
+
consola.debug('hosts entries are restored by the privileged helper on disconnect')
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
consola.error(e)
|
|
540
|
+
consola.fail('Restore host failed.\n')
|
|
541
|
+
}
|
|
469
542
|
}
|
|
543
|
+
|
|
470
544
|
if (!this.locked && !this.stoped) {
|
|
471
545
|
await this.stopCaddy()
|
|
472
546
|
try {
|
|
473
|
-
await lockfile.unlock(caddyLockFilePath).catch(() => {
|
|
474
|
-
await unlink(caddyFilePath).catch(() => {
|
|
475
|
-
await unlink(caddyLockFilePath).catch(() => {
|
|
547
|
+
await lockfile.unlock(caddyLockFilePath).catch(() => {})
|
|
548
|
+
await unlink(caddyFilePath).catch(() => {})
|
|
549
|
+
await unlink(caddyLockFilePath).catch(() => {})
|
|
476
550
|
if (caddyConfig) {
|
|
477
551
|
await caddyConfig.cleanUp()
|
|
478
552
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
/** Shown before the password prompt so the request is not a surprise. */
|
|
86
|
+
reason?: { target: string, https: boolean }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Where Caddy keeps its PKI, per platform. */
|
|
90
|
+
export function caddyStoragePath(): string {
|
|
91
|
+
return process.platform === 'win32'
|
|
92
|
+
? join(process.env.AppData ?? '', 'Caddy')
|
|
93
|
+
: join(process.env.HOME ?? '', 'Library/Application Support/Caddy')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Opens (or reuses) the privileged helper for this runtime directory.
|
|
98
|
+
* Only the helper runs as root: the plugin process, its caches and its
|
|
99
|
+
* `node_modules` keep the developer's identity.
|
|
100
|
+
*/
|
|
101
|
+
export function openPrivilegedSession(
|
|
102
|
+
runtimeDir: string,
|
|
103
|
+
options: PrivilegedSessionOptions,
|
|
104
|
+
): Promise<OpenedPrivilegedSession> {
|
|
105
|
+
const env = resolvePrivilegedEnv()
|
|
106
|
+
return openHelperSession({
|
|
107
|
+
runtimeDir,
|
|
108
|
+
helperEntry: env.helperEntry,
|
|
109
|
+
helperArgs: env.helperArgs,
|
|
110
|
+
direct: options.direct,
|
|
111
|
+
// Caddy keeps its PKI under the user's home; the helper runs it as root, so
|
|
112
|
+
// it must know which tree to hand back afterwards.
|
|
113
|
+
caddyStoragePath: caddyStoragePath(),
|
|
114
|
+
// The binary and Caddyfile are fixed here: the privileged helper must never
|
|
115
|
+
// execute a path supplied over the socket.
|
|
116
|
+
caddyBin: options.caddyBin,
|
|
117
|
+
caddyConfigPath: options.caddyConfigPath,
|
|
118
|
+
reason: options.reason,
|
|
119
|
+
log: options.log,
|
|
120
|
+
})
|
|
121
|
+
}
|
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%'
|