unplugin-https-reverse-proxy 4.0.3 → 4.1.0-beta.10

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.
Files changed (62) hide show
  1. package/README.md +30 -6
  2. package/dist/{chunk-RSBXFJ3Z.js → chunk-C5A7XLKL.js} +9 -2
  3. package/dist/{chunk-2P7SUCQ3.js → chunk-EY3HXBJ4.js} +0 -26
  4. package/dist/chunk-RKHJN6KY.js +53 -0
  5. package/dist/{chunk-MD5CVZP7.js → chunk-U7KAIPZC.js} +982 -797
  6. package/dist/chunk-WOMR3ZLM.js +17 -0
  7. package/dist/esbuild.cjs +1086 -867
  8. package/dist/esbuild.js +5 -3
  9. package/dist/hmr-sse-bridge.cjs +12 -4
  10. package/dist/hmr-sse-bridge.d.cts +4 -3
  11. package/dist/hmr-sse-bridge.d.ts +4 -3
  12. package/dist/hmr-sse-bridge.js +5 -3
  13. package/dist/index.cjs +1086 -867
  14. package/dist/index.js +5 -3
  15. package/dist/nuxt.cjs +1057 -852
  16. package/dist/nuxt.js +6 -9
  17. package/dist/privilege-state.cjs +44 -0
  18. package/dist/privilege-state.d.cts +7 -0
  19. package/dist/privilege-state.d.ts +7 -0
  20. package/dist/privilege-state.js +10 -0
  21. package/dist/privileged-helper.cjs +824 -0
  22. package/dist/privileged-helper.d.cts +2 -0
  23. package/dist/privileged-helper.d.ts +2 -0
  24. package/dist/privileged-helper.js +797 -0
  25. package/dist/rollup.cjs +1086 -867
  26. package/dist/rollup.js +5 -3
  27. package/dist/rspack.cjs +1086 -867
  28. package/dist/rspack.js +5 -3
  29. package/dist/utils.cjs +0 -28
  30. package/dist/utils.d.cts +1 -3
  31. package/dist/utils.d.ts +1 -3
  32. package/dist/utils.js +1 -5
  33. package/dist/vite.cjs +1086 -867
  34. package/dist/vite.js +5 -3
  35. package/dist/webpack.cjs +1086 -867
  36. package/dist/webpack.js +5 -3
  37. package/package.json +1 -4
  38. package/src/caddy/caddyfile.ts +3 -0
  39. package/src/caddy/config.ts +195 -53
  40. package/src/caddy/constants.ts +30 -1
  41. package/src/caddy/index.ts +239 -206
  42. package/src/caddy/privileged.ts +124 -0
  43. package/src/caddy/utils.ts +0 -43
  44. package/src/hmr-sse-bridge.ts +14 -3
  45. package/src/index.ts +52 -25
  46. package/src/nuxt.ts +1 -6
  47. package/src/privilege-state.ts +23 -0
  48. package/src/privileged/bootstrap.ts +289 -0
  49. package/src/privileged/client.ts +288 -0
  50. package/src/privileged/entry.ts +73 -0
  51. package/src/privileged/hosts-file.ts +142 -0
  52. package/src/privileged/launch.ts +332 -0
  53. package/src/privileged/path-guard.ts +74 -0
  54. package/src/privileged/ports.ts +45 -0
  55. package/src/privileged/protocol.ts +111 -0
  56. package/src/privileged/server.ts +578 -0
  57. package/src/privileged/session.ts +169 -0
  58. package/src/privileged/validation.ts +38 -0
  59. package/src/privileged/windows-elevation.ts +80 -0
  60. package/src/privileged-helper.ts +123 -0
  61. package/src/utils.ts +0 -28
  62. package/src/host/index.ts +0 -52
@@ -0,0 +1,124 @@
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
+ /** Admin API socket; `ca.trust` talks to it instead of the TCP default. */
86
+ caddyAdminSocket?: string
87
+ /** Shown before the password prompt so the request is not a surprise. */
88
+ reason?: { target: string, https: boolean }
89
+ }
90
+
91
+ /** Where Caddy keeps its PKI, per platform. */
92
+ export function caddyStoragePath(): string {
93
+ return process.platform === 'win32'
94
+ ? join(process.env.AppData ?? '', 'Caddy')
95
+ : join(process.env.HOME ?? '', 'Library/Application Support/Caddy')
96
+ }
97
+
98
+ /**
99
+ * Opens (or reuses) the privileged helper for this runtime directory.
100
+ * Only the helper runs as root: the plugin process, its caches and its
101
+ * `node_modules` keep the developer's identity.
102
+ */
103
+ export function openPrivilegedSession(
104
+ runtimeDir: string,
105
+ options: PrivilegedSessionOptions,
106
+ ): Promise<OpenedPrivilegedSession> {
107
+ const env = resolvePrivilegedEnv()
108
+ return openHelperSession({
109
+ runtimeDir,
110
+ helperEntry: env.helperEntry,
111
+ helperArgs: env.helperArgs,
112
+ direct: options.direct,
113
+ // Caddy keeps its PKI under the user's home; the helper runs it as root, so
114
+ // it must know which tree to hand back afterwards.
115
+ caddyStoragePath: caddyStoragePath(),
116
+ // The binary and Caddyfile are fixed here: the privileged helper must never
117
+ // execute a path supplied over the socket.
118
+ caddyBin: options.caddyBin,
119
+ caddyConfigPath: options.caddyConfigPath,
120
+ caddyAdminSocket: options.caddyAdminSocket,
121
+ reason: options.reason,
122
+ log: options.log,
123
+ })
124
+ }
@@ -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%'
@@ -134,14 +134,25 @@ function handleClientMessage(
134
134
  })
135
135
  }
136
136
 
137
+ export function shouldUseHmrSseBridge(protocol: string, userAgent: string): boolean {
138
+ if (protocol !== 'https:')
139
+ return false
140
+
141
+ const isApplePlatform = /Macintosh|iPhone|iPad|iPod/.test(userAgent)
142
+ const isSafari = /Version\//.test(userAgent)
143
+ && /Safari\//.test(userAgent)
144
+ && !/(?:Chrome|Chromium|CriOS|Edg|EdgiOS|OPR|OPiOS|Firefox|FxiOS)\//.test(userAgent)
145
+
146
+ return isApplePlatform && isSafari
147
+ }
148
+
137
149
  /**
138
- * Client-side script that patches WebSocket to use SSE+fetch on iOS Safari.
150
+ * Client-side script that patches WebSocket to use SSE+fetch on macOS/iOS Safari.
139
151
  * This script is injected via Vite's transformIndexHtml hook.
140
152
  */
141
153
  export const hmrSseBridgeClientScript = `
142
154
  (function() {
143
- // Activate when served over HTTPS to bypass WSS self-signed cert issues
144
- if (location.protocol !== 'https:') return;
155
+ if (!(${shouldUseHmrSseBridge.toString()})(location.protocol, navigator.userAgent)) return;
145
156
 
146
157
  var OriginalWebSocket = window.WebSocket;
147
158
 
package/src/index.ts CHANGED
@@ -7,7 +7,9 @@ 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 { consola, generateQrcode, getIpv4List, isAdmin, once } from './utils'
10
+ import { isPrivilegeDenied, markPrivilegeDenied } from './privilege-state'
11
+ import { PrivilegeDeniedError } from './privileged/launch'
12
+ import { consola, generateQrcode, getIpv4List, once } from './utils'
11
13
 
12
14
  let config: ResolvedConfig
13
15
 
@@ -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
- caddy = new CaddyInstant()
68
- caddy.run(source, target, {
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
  })
@@ -96,10 +113,6 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
96
113
  if (!enable)
97
114
  return
98
115
 
99
- if (!isAdmin()) {
100
- consola.warn('please run as administrator')
101
- return
102
- }
103
116
  if (!target) {
104
117
  consola.fail('please provide target')
105
118
  return
@@ -123,17 +136,17 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
123
136
 
124
137
  server.printUrls = () => vitePrintUrls(options, source, target, base, _printUrls)
125
138
 
126
- // Setup SSE bridge for iOS Safari WSS workaround
139
+ // Setup SSE bridge for macOS/iOS Safari WSS workaround
127
140
  if (options.https) {
128
141
  setupHmrSseBridge(server)
129
142
  hmrSseBridgeActive = true
130
- consola.info('HMR SSE bridge enabled for iOS Safari WSS workaround')
143
+ consola.info('HMR SSE bridge enabled for macOS/iOS Safari WSS workaround')
131
144
  }
132
145
  },
133
146
  transformIndexHtml() {
134
147
  if (!hmrSseBridgeActive)
135
148
  return []
136
- // Inject client-side script to patch WebSocket on iOS Safari
149
+ // Inject client-side script to patch WebSocket on macOS/iOS Safari
137
150
  return [{
138
151
  tag: 'script',
139
152
  children: hmrSseBridgeClientScript,
@@ -153,11 +166,6 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
153
166
  if (!enable)
154
167
  return
155
168
 
156
- if (!isAdmin()) {
157
- consola.warn('please run as administrator')
158
- return
159
- }
160
-
161
169
  if (!target) {
162
170
  consola.fail('please provide target')
163
171
  return
@@ -177,6 +185,9 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
177
185
  }
178
186
  const source = `${devServer.host}:${devServer.port}`
179
187
 
188
+ if (isPrivilegeDenied())
189
+ return
190
+
180
191
  if (caddy)
181
192
  return
182
193
 
@@ -204,9 +215,10 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
204
215
  options.https = false
205
216
  }
206
217
 
207
- caddy = new CaddyInstant()
218
+ const instance = new CaddyInstant()
219
+ caddy = instance
208
220
 
209
- caddy.run(source, target, {
221
+ instance.run(source, target, {
210
222
  base,
211
223
  ...options,
212
224
  }).then(() => {
@@ -216,6 +228,14 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
216
228
  consola.success(` ${c.green('➜')} ${c.bold('run caddy reverse proxy success')}: ${colorUrl(`${options.https ? 'https' : 'http'}://${target}${base}`)}`)
217
229
  })
218
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
+ }
219
239
  throw e
220
240
  })
221
241
  },
@@ -231,11 +251,6 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
231
251
  if (!enable)
232
252
  return
233
253
 
234
- if (!isAdmin()) {
235
- consola.warn('please run as administrator')
236
- return
237
- }
238
-
239
254
  if (!target) {
240
255
  consola.fail('please provide target')
241
256
  return
@@ -257,10 +272,11 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
257
272
 
258
273
  const source = `${devServer.host}:${devServer.port}`
259
274
 
260
- if (caddy)
275
+ if (isPrivilegeDenied())
261
276
  return
262
277
 
263
- caddy = new CaddyInstant()
278
+ if (caddy)
279
+ return
264
280
 
265
281
  let base = '/'
266
282
  const publicPath = compiler.options.output?.publicPath as unknown
@@ -272,7 +288,10 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
272
288
  if (base !== '/' && !base.endsWith('/'))
273
289
  base = `${base}/`
274
290
 
275
- caddy.run(source, target, {
291
+ const instance = new CaddyInstant()
292
+ caddy = instance
293
+
294
+ instance.run(source, target, {
276
295
  base,
277
296
  ...options,
278
297
  }).then(async () => {
@@ -280,6 +299,14 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
280
299
 
281
300
  consola.success(` ${c.green('➜')} ${c.bold('run caddy reverse proxy success')}: ${colorUrl(`${options.https ? 'https' : 'http'}://${target}${base}`)}`)
282
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
+ }
283
310
  throw e
284
311
  })
285
312
  },
package/src/nuxt.ts CHANGED
@@ -2,7 +2,7 @@ import type { Options } from './types'
2
2
  import process from 'node:process'
3
3
  import { defineNuxtModule } from '@nuxt/kit'
4
4
  import { vitePrintUrls } from '.'
5
- import { consola, isAdmin } from './utils'
5
+ import { consola } from './utils'
6
6
  import '@nuxt/schema'
7
7
 
8
8
  export interface ModuleOptions extends Options {
@@ -26,11 +26,6 @@ export default defineNuxtModule<ModuleOptions>({
26
26
  if (!enable)
27
27
  return
28
28
 
29
- if (!isAdmin()) {
30
- consola.warn('please run as administrator')
31
- return
32
- }
33
-
34
29
  if (!target) {
35
30
  consola.fail('please provide target')
36
31
  return
@@ -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
+ }