unplugin-https-reverse-proxy 4.1.0-beta.2 → 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/dist/{chunk-7CA3EIIX.js → chunk-HBXMM25C.js} +101 -23
- package/dist/chunk-WOMR3ZLM.js +17 -0
- package/dist/esbuild.cjs +103 -20
- package/dist/esbuild.js +2 -1
- package/dist/index.cjs +103 -20
- package/dist/index.js +2 -1
- package/dist/nuxt.cjs +77 -16
- package/dist/nuxt.js +2 -1
- 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/rollup.cjs +103 -20
- package/dist/rollup.js +2 -1
- package/dist/rspack.cjs +103 -20
- package/dist/rspack.js +2 -1
- package/dist/vite.cjs +103 -20
- package/dist/vite.js +2 -1
- package/dist/webpack.cjs +103 -20
- package/dist/webpack.js +2 -1
- package/package.json +1 -1
- package/src/caddy/index.ts +7 -6
- package/src/caddy/privileged.ts +3 -0
- package/src/index.ts +48 -7
- package/src/privilege-state.ts +23 -0
- package/src/privileged/entry.ts +34 -6
- package/src/privileged/launch.ts +46 -4
- package/src/privileged/session.ts +3 -0
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import c from 'picocolors'
|
|
|
7
7
|
import { createUnplugin } from 'unplugin'
|
|
8
8
|
import { CaddyInstant } from './caddy'
|
|
9
9
|
import { hmrSseBridgeClientScript, setupHmrSseBridge } from './hmr-sse-bridge'
|
|
10
|
+
import { isPrivilegeDenied, markPrivilegeDenied } from './privilege-state'
|
|
11
|
+
import { PrivilegeDeniedError } from './privileged/launch'
|
|
10
12
|
import { consola, generateQrcode, getIpv4List, once } from './utils'
|
|
11
13
|
|
|
12
14
|
let config: ResolvedConfig
|
|
@@ -62,10 +64,15 @@ export const vitePrintUrls = once((
|
|
|
62
64
|
) => {
|
|
63
65
|
_printUrls?.()
|
|
64
66
|
|
|
67
|
+
// Declined earlier in this process; a hot restart must not ask again.
|
|
68
|
+
if (isPrivilegeDenied())
|
|
69
|
+
return
|
|
70
|
+
|
|
65
71
|
if (caddy)
|
|
66
72
|
return
|
|
67
|
-
|
|
68
|
-
caddy
|
|
73
|
+
const instance = new CaddyInstant()
|
|
74
|
+
caddy = instance
|
|
75
|
+
instance.run(source, target, {
|
|
69
76
|
base,
|
|
70
77
|
...options,
|
|
71
78
|
}).then(async () => {
|
|
@@ -73,6 +80,16 @@ export const vitePrintUrls = once((
|
|
|
73
80
|
|
|
74
81
|
consola.success(` ${c.green('➜')} ${c.bold('run caddy reverse proxy success')}: ${colorUrl(`${options.https ? 'https' : 'http'}://${target}${base}`)}`)
|
|
75
82
|
}).catch((e) => {
|
|
83
|
+
// Declining the password is a choice, not a crash: drop the half-started
|
|
84
|
+
// instance so a later attempt can prompt again.
|
|
85
|
+
if (caddy === instance)
|
|
86
|
+
caddy = undefined as unknown as CaddyInstant
|
|
87
|
+
if (e instanceof PrivilegeDeniedError) {
|
|
88
|
+
markPrivilegeDenied()
|
|
89
|
+
consola.warn('reverse proxy skipped: administrator permission was not granted.')
|
|
90
|
+
consola.info('Restart the dev server to enable it again.')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
76
93
|
throw e
|
|
77
94
|
})
|
|
78
95
|
})
|
|
@@ -168,6 +185,9 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
168
185
|
}
|
|
169
186
|
const source = `${devServer.host}:${devServer.port}`
|
|
170
187
|
|
|
188
|
+
if (isPrivilegeDenied())
|
|
189
|
+
return
|
|
190
|
+
|
|
171
191
|
if (caddy)
|
|
172
192
|
return
|
|
173
193
|
|
|
@@ -195,9 +215,10 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
195
215
|
options.https = false
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
|
|
218
|
+
const instance = new CaddyInstant()
|
|
219
|
+
caddy = instance
|
|
199
220
|
|
|
200
|
-
|
|
221
|
+
instance.run(source, target, {
|
|
201
222
|
base,
|
|
202
223
|
...options,
|
|
203
224
|
}).then(() => {
|
|
@@ -207,6 +228,14 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
207
228
|
consola.success(` ${c.green('➜')} ${c.bold('run caddy reverse proxy success')}: ${colorUrl(`${options.https ? 'https' : 'http'}://${target}${base}`)}`)
|
|
208
229
|
})
|
|
209
230
|
}).catch((e) => {
|
|
231
|
+
if (caddy === instance)
|
|
232
|
+
caddy = undefined as unknown as CaddyInstant
|
|
233
|
+
if (e instanceof PrivilegeDeniedError) {
|
|
234
|
+
markPrivilegeDenied()
|
|
235
|
+
consola.warn('reverse proxy skipped: administrator permission was not granted.')
|
|
236
|
+
consola.info('Restart the dev server to enable it again.')
|
|
237
|
+
return
|
|
238
|
+
}
|
|
210
239
|
throw e
|
|
211
240
|
})
|
|
212
241
|
},
|
|
@@ -243,10 +272,11 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
243
272
|
|
|
244
273
|
const source = `${devServer.host}:${devServer.port}`
|
|
245
274
|
|
|
246
|
-
if (
|
|
275
|
+
if (isPrivilegeDenied())
|
|
247
276
|
return
|
|
248
277
|
|
|
249
|
-
|
|
278
|
+
if (caddy)
|
|
279
|
+
return
|
|
250
280
|
|
|
251
281
|
let base = '/'
|
|
252
282
|
const publicPath = compiler.options.output?.publicPath as unknown
|
|
@@ -258,7 +288,10 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
258
288
|
if (base !== '/' && !base.endsWith('/'))
|
|
259
289
|
base = `${base}/`
|
|
260
290
|
|
|
261
|
-
|
|
291
|
+
const instance = new CaddyInstant()
|
|
292
|
+
caddy = instance
|
|
293
|
+
|
|
294
|
+
instance.run(source, target, {
|
|
262
295
|
base,
|
|
263
296
|
...options,
|
|
264
297
|
}).then(async () => {
|
|
@@ -266,6 +299,14 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
266
299
|
|
|
267
300
|
consola.success(` ${c.green('➜')} ${c.bold('run caddy reverse proxy success')}: ${colorUrl(`${options.https ? 'https' : 'http'}://${target}${base}`)}`)
|
|
268
301
|
}).catch((e) => {
|
|
302
|
+
if (caddy === instance)
|
|
303
|
+
caddy = undefined as unknown as CaddyInstant
|
|
304
|
+
if (e instanceof PrivilegeDeniedError) {
|
|
305
|
+
markPrivilegeDenied()
|
|
306
|
+
consola.warn('reverse proxy skipped: administrator permission was not granted.')
|
|
307
|
+
consola.info('Restart the dev server to enable it again.')
|
|
308
|
+
return
|
|
309
|
+
}
|
|
269
310
|
throw e
|
|
270
311
|
})
|
|
271
312
|
},
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the user dismissed the password prompt in this process.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module so the flag outlives a hot restart. Vite rebuilds the
|
|
5
|
+
* dev server in place (`Object.assign(server, newServer)`) rather than starting
|
|
6
|
+
* a new process, so module scope is the only place that distinguishes "saved
|
|
7
|
+
* the config again" from "started the dev server again".
|
|
8
|
+
*/
|
|
9
|
+
let denied = false
|
|
10
|
+
|
|
11
|
+
/** Records that the user declined; every entry point checks this first. */
|
|
12
|
+
export function markPrivilegeDenied(): void {
|
|
13
|
+
denied = true
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isPrivilegeDenied(): boolean {
|
|
17
|
+
return denied
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Test hook: restores the initial state between cases. */
|
|
21
|
+
export function resetPrivilegeDenied(): void {
|
|
22
|
+
denied = false
|
|
23
|
+
}
|
package/src/privileged/entry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
5
5
|
export interface HelperEntry {
|
|
@@ -26,20 +26,48 @@ export interface ResolveHelperEntryOptions {
|
|
|
26
26
|
* Published builds ship a compiled `privileged-helper.js` and start it with
|
|
27
27
|
* node; running from the repository uses `tsx` so the TypeScript source works
|
|
28
28
|
* without a build step.
|
|
29
|
+
*
|
|
30
|
+
* The running module sits in a subdirectory (`src/caddy`, `src/privileged`),
|
|
31
|
+
* while the helper lives one level up in `src`, so both lookups walk up instead
|
|
32
|
+
* of assuming a fixed depth.
|
|
29
33
|
*/
|
|
30
34
|
export function resolveHelperEntry(options: ResolveHelperEntryOptions): HelperEntry {
|
|
31
35
|
const { moduleDir, nodeBin = process.execPath, exists = existsSync } = options
|
|
32
36
|
|
|
33
37
|
for (const name of ['privileged-helper.js', 'privileged-helper.cjs', 'privileged-helper.mjs']) {
|
|
34
|
-
const candidate =
|
|
35
|
-
if (
|
|
38
|
+
const candidate = findUp(moduleDir, name, exists)
|
|
39
|
+
if (candidate)
|
|
36
40
|
return { entry: nodeBin, args: [candidate] }
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
const source =
|
|
40
|
-
const tsxCli =
|
|
41
|
-
if (
|
|
43
|
+
const source = findUp(moduleDir, 'privileged-helper.ts', exists)
|
|
44
|
+
const tsxCli = findUp(moduleDir, 'node_modules/tsx/dist/cli.mjs', exists)
|
|
45
|
+
if (source && tsxCli)
|
|
42
46
|
return { entry: nodeBin, args: [tsxCli, source] }
|
|
43
47
|
|
|
44
48
|
throw new Error(`privileged helper entry not found in ${moduleDir}. Build the package first.`)
|
|
45
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Walks up from `from` looking for a relative path.
|
|
53
|
+
*
|
|
54
|
+
* Stops at the filesystem root so a helper outside the project is never picked
|
|
55
|
+
* up by accident.
|
|
56
|
+
*/
|
|
57
|
+
function findUp(
|
|
58
|
+
from: string,
|
|
59
|
+
relative: string,
|
|
60
|
+
exists: (path: string) => boolean,
|
|
61
|
+
): string | undefined {
|
|
62
|
+
let current = from
|
|
63
|
+
while (true) {
|
|
64
|
+
const candidate = join(current, relative)
|
|
65
|
+
if (exists(candidate))
|
|
66
|
+
return candidate
|
|
67
|
+
|
|
68
|
+
const parent = dirname(current)
|
|
69
|
+
if (parent === current)
|
|
70
|
+
return undefined
|
|
71
|
+
current = parent
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/privileged/launch.ts
CHANGED
|
@@ -39,6 +39,25 @@ export interface LaunchOptions {
|
|
|
39
39
|
caddyBin: string
|
|
40
40
|
/** Caddyfile the helper is allowed to load. */
|
|
41
41
|
caddyConfigPath: string
|
|
42
|
+
/** Shown before the password prompt so the request is not a surprise. */
|
|
43
|
+
reason?: { target: string, https: boolean }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Explains why a password is about to be requested. Without this the prompt
|
|
48
|
+
* appears out of nowhere in the middle of `npm run dev` and looks like something
|
|
49
|
+
* went wrong.
|
|
50
|
+
*/
|
|
51
|
+
export function describePasswordReason(target: string, https: boolean): string[] {
|
|
52
|
+
const port = https ? '443' : '80'
|
|
53
|
+
return [
|
|
54
|
+
`reverse proxy needs administrator permission for ${target}`,
|
|
55
|
+
` · bind port ${port}${https ? ` for https://${target}` : ''}`,
|
|
56
|
+
` · add ${target} to the hosts file`,
|
|
57
|
+
'',
|
|
58
|
+
'This is the only prompt; the rest of the dev server keeps running as you.',
|
|
59
|
+
'Press Ctrl+C at the prompt to skip the proxy and keep the dev server.',
|
|
60
|
+
]
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
/**
|
|
@@ -47,23 +66,44 @@ export interface LaunchOptions {
|
|
|
47
66
|
* The credential cache is keyed by terminal, so a later `sudo -n` only finds
|
|
48
67
|
* this entry when the child stays in the same session. That is why the helper
|
|
49
68
|
* is spawned without `detached`.
|
|
69
|
+
*
|
|
70
|
+
* Returns `denied` when the user dismissed the prompt, so the caller can keep
|
|
71
|
+
* the dev server running instead of treating it as a crash.
|
|
50
72
|
*/
|
|
51
|
-
export function authenticateSudo(
|
|
73
|
+
export function authenticateSudo(
|
|
74
|
+
sudoCommand = 'sudo',
|
|
75
|
+
options: { target?: string, https?: boolean } = {},
|
|
76
|
+
): { ok: boolean, denied?: boolean, message?: string } {
|
|
52
77
|
if (platform() === 'win32')
|
|
53
78
|
return { ok: true }
|
|
54
79
|
|
|
55
80
|
if (spawnSync(sudoCommand, ['-n', 'true'], { stdio: 'ignore' }).status === 0)
|
|
56
81
|
return { ok: true }
|
|
57
82
|
|
|
83
|
+
if (options.target) {
|
|
84
|
+
for (const line of describePasswordReason(options.target, options.https ?? false))
|
|
85
|
+
process.stderr.write(`${line}\n`)
|
|
86
|
+
process.stderr.write('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
if (spawnSync(sudoCommand, ['-v'], { stdio: 'inherit' }).status === 0)
|
|
59
90
|
return { ok: true }
|
|
60
91
|
|
|
61
92
|
return {
|
|
62
93
|
ok: false,
|
|
94
|
+
denied: true,
|
|
63
95
|
message: 'administrator permission is required to edit the hosts file and bind ports 80/443',
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
/** The user dismissed the password prompt; this is a choice, not a failure. */
|
|
100
|
+
export class PrivilegeDeniedError extends Error {
|
|
101
|
+
constructor(message: string) {
|
|
102
|
+
super(message)
|
|
103
|
+
this.name = 'PrivilegeDeniedError'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
export interface PrivilegedHelperHandle {
|
|
68
108
|
client: PrivilegedClient
|
|
69
109
|
close: () => Promise<void>
|
|
@@ -92,9 +132,11 @@ export async function startPrivilegedHelper(options: LaunchOptions): Promise<Pri
|
|
|
92
132
|
} = options
|
|
93
133
|
|
|
94
134
|
if (!skipAuth && !direct) {
|
|
95
|
-
const auth = authenticateSudo(sudoCommand)
|
|
96
|
-
if (!auth.ok)
|
|
97
|
-
|
|
135
|
+
const auth = authenticateSudo(sudoCommand, options.reason)
|
|
136
|
+
if (!auth.ok) {
|
|
137
|
+
const message = auth.message ?? 'administrator permission is required'
|
|
138
|
+
throw auth.denied ? new PrivilegeDeniedError(message) : new Error(message)
|
|
139
|
+
}
|
|
98
140
|
}
|
|
99
141
|
|
|
100
142
|
const forwarded = collectForwardedEnv(env)
|
|
@@ -24,6 +24,8 @@ export interface SessionScope {
|
|
|
24
24
|
caddyBin: string
|
|
25
25
|
/** Caddyfile the helper may load. */
|
|
26
26
|
caddyConfigPath: string
|
|
27
|
+
/** Shown before the password prompt so the request is not a surprise. */
|
|
28
|
+
reason?: { target: string, https: boolean }
|
|
27
29
|
log?: (message: string) => void
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -81,6 +83,7 @@ export async function openHelperSession(scope: SessionScope): Promise<OpenedSess
|
|
|
81
83
|
caddyStoragePath: scope.caddyStoragePath,
|
|
82
84
|
caddyBin: scope.caddyBin,
|
|
83
85
|
caddyConfigPath: scope.caddyConfigPath,
|
|
86
|
+
reason: scope.reason,
|
|
84
87
|
log: scope.log,
|
|
85
88
|
})
|
|
86
89
|
|