unplugin-https-reverse-proxy 4.0.4 → 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.
- package/README.md +30 -6
- package/dist/{chunk-2P7SUCQ3.js → chunk-EY3HXBJ4.js} +0 -26
- package/dist/chunk-RKHJN6KY.js +53 -0
- package/dist/{chunk-5R33PUJP.js → chunk-U7KAIPZC.js} +981 -796
- package/dist/chunk-WOMR3ZLM.js +17 -0
- package/dist/esbuild.cjs +1077 -864
- package/dist/esbuild.js +4 -2
- package/dist/index.cjs +1077 -864
- package/dist/index.js +4 -2
- package/dist/nuxt.cjs +1049 -850
- 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 +824 -0
- package/dist/privileged-helper.d.cts +2 -0
- package/dist/privileged-helper.d.ts +2 -0
- package/dist/privileged-helper.js +797 -0
- package/dist/rollup.cjs +1077 -864
- package/dist/rollup.js +4 -2
- package/dist/rspack.cjs +1077 -864
- 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 +1077 -864
- package/dist/vite.js +4 -2
- package/dist/webpack.cjs +1077 -864
- package/dist/webpack.js +4 -2
- package/package.json +1 -4
- package/src/caddy/caddyfile.ts +3 -0
- package/src/caddy/config.ts +195 -53
- package/src/caddy/constants.ts +30 -1
- package/src/caddy/index.ts +239 -206
- package/src/caddy/privileged.ts +124 -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 +289 -0
- package/src/privileged/client.ts +288 -0
- package/src/privileged/entry.ts +73 -0
- package/src/privileged/hosts-file.ts +142 -0
- package/src/privileged/launch.ts +332 -0
- package/src/privileged/path-guard.ts +74 -0
- package/src/privileged/ports.ts +45 -0
- package/src/privileged/protocol.ts +111 -0
- package/src/privileged/server.ts +578 -0
- package/src/privileged/session.ts +169 -0
- package/src/privileged/validation.ts +38 -0
- package/src/privileged/windows-elevation.ts +80 -0
- package/src/privileged-helper.ts +123 -0
- package/src/utils.ts +0 -28
- package/src/host/index.ts +0 -52
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { PrivilegedClient } from './client'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { platform } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import * as lockfile from 'proper-lockfile'
|
|
8
|
+
import { connectExistingHelper, createHelperToken, startPrivilegedHelper } from './launch'
|
|
9
|
+
|
|
10
|
+
export interface SessionScope {
|
|
11
|
+
/** Runtime directory shared by the plugin, e.g. `TEMP_DIR`. */
|
|
12
|
+
runtimeDir: string
|
|
13
|
+
/** Directory of the running bundle; used to locate the helper entry. */
|
|
14
|
+
helperEntry: string
|
|
15
|
+
helperArgs?: string[]
|
|
16
|
+
env?: NodeJS.ProcessEnv
|
|
17
|
+
sudoCommand?: string
|
|
18
|
+
/** Skips the interactive credential check; used by tests. */
|
|
19
|
+
skipAuth?: boolean
|
|
20
|
+
/** Runs the helper as the current user; used by tests without privileges. */
|
|
21
|
+
direct?: boolean
|
|
22
|
+
/** Caddy storage directory; the helper hands it back to the user on stop. */
|
|
23
|
+
caddyStoragePath?: string
|
|
24
|
+
/** Caddy binary the helper may run; the client cannot change it later. */
|
|
25
|
+
caddyBin: string
|
|
26
|
+
/** Caddyfile the helper may load. */
|
|
27
|
+
caddyConfigPath: string
|
|
28
|
+
/** Admin API socket; used by `ca.trust` instead of the TCP default. */
|
|
29
|
+
caddyAdminSocket?: string
|
|
30
|
+
/** Shown before the password prompt so the request is not a surprise. */
|
|
31
|
+
reason?: { target: string, https: boolean }
|
|
32
|
+
log?: (message: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OpenedSession {
|
|
36
|
+
client: PrivilegedClient
|
|
37
|
+
/** True when an already-running helper was reused. */
|
|
38
|
+
reused: boolean
|
|
39
|
+
close: () => Promise<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function tokenPath(runtimeDir: string): string {
|
|
43
|
+
return join(runtimeDir, 'helper.token')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sessionSocketPath(runtimeDir: string): string {
|
|
47
|
+
if (platform() === 'win32')
|
|
48
|
+
return `\\\\.\\pipe\\uhrp-${Buffer.from(runtimeDir).toString('hex').slice(0, 40)}`
|
|
49
|
+
return join(runtimeDir, 'helper.sock')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Connects to the helper already serving this runtime directory, or starts a
|
|
54
|
+
* new one. Reuse matters for multi-instance dev servers: the second instance
|
|
55
|
+
* attaches to the running Caddy instead of asking for a password again.
|
|
56
|
+
*
|
|
57
|
+
* The check-then-start sequence runs under a lock. Two projects starting at the
|
|
58
|
+
* same moment would otherwise both fail to find a socket, both spawn a helper,
|
|
59
|
+
* and the second would delete the first one's socket while it was still
|
|
60
|
+
* serving.
|
|
61
|
+
*/
|
|
62
|
+
export async function openHelperSession(scope: SessionScope): Promise<OpenedSession> {
|
|
63
|
+
const socketPath = sessionSocketPath(scope.runtimeDir)
|
|
64
|
+
const release = await acquireStartLock(scope.runtimeDir)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const reused = await tryReuse(socketPath, scope)
|
|
68
|
+
if (reused)
|
|
69
|
+
return reused
|
|
70
|
+
|
|
71
|
+
return await startNew(socketPath, scope)
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
release()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function tryReuse(socketPath: string, scope: SessionScope): Promise<OpenedSession | undefined> {
|
|
79
|
+
if (!existsSync(socketPath) && platform() !== 'win32')
|
|
80
|
+
return undefined
|
|
81
|
+
|
|
82
|
+
const token = await readToken(scope.runtimeDir)
|
|
83
|
+
if (!token)
|
|
84
|
+
return undefined
|
|
85
|
+
|
|
86
|
+
const reused = await connectExistingHelper(socketPath, token)
|
|
87
|
+
if (!reused)
|
|
88
|
+
return undefined
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
client: reused,
|
|
92
|
+
reused: true,
|
|
93
|
+
close: async () => {
|
|
94
|
+
await reused.close()
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function startNew(socketPath: string, scope: SessionScope): Promise<OpenedSession> {
|
|
100
|
+
const token = createHelperToken()
|
|
101
|
+
const started = await startPrivilegedHelper({
|
|
102
|
+
socketPath,
|
|
103
|
+
token,
|
|
104
|
+
helperEntry: scope.helperEntry,
|
|
105
|
+
helperArgs: scope.helperArgs,
|
|
106
|
+
sudoCommand: scope.sudoCommand,
|
|
107
|
+
skipAuth: scope.skipAuth,
|
|
108
|
+
direct: scope.direct,
|
|
109
|
+
env: scope.env,
|
|
110
|
+
caddyStoragePath: scope.caddyStoragePath,
|
|
111
|
+
caddyBin: scope.caddyBin,
|
|
112
|
+
caddyConfigPath: scope.caddyConfigPath,
|
|
113
|
+
caddyAdminSocket: scope.caddyAdminSocket,
|
|
114
|
+
reason: scope.reason,
|
|
115
|
+
log: scope.log,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await writeToken(scope.runtimeDir, token)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
client: started.client,
|
|
122
|
+
reused: false,
|
|
123
|
+
close: async () => {
|
|
124
|
+
await started.close()
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Serialises "is a helper running? if not, start one" across processes.
|
|
131
|
+
*
|
|
132
|
+
* The lock file is separate from the socket: the socket only exists once a
|
|
133
|
+
* helper is listening, while this lock guards the window before that.
|
|
134
|
+
*/
|
|
135
|
+
async function acquireStartLock(runtimeDir: string): Promise<() => void> {
|
|
136
|
+
const lockPath = join(runtimeDir, 'helper.start.lock')
|
|
137
|
+
try {
|
|
138
|
+
await writeFile(lockPath, '', { flag: 'a' })
|
|
139
|
+
}
|
|
140
|
+
catch {}
|
|
141
|
+
|
|
142
|
+
const release = await lockfile.lock(lockPath, {
|
|
143
|
+
realpath: false,
|
|
144
|
+
stale: 30_000,
|
|
145
|
+
retries: { retries: 100, minTimeout: 50, maxTimeout: 200 },
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
void release().catch(() => {})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readToken(runtimeDir: string): Promise<string | undefined> {
|
|
154
|
+
try {
|
|
155
|
+
const content = await readFile(tokenPath(runtimeDir), 'utf8')
|
|
156
|
+
const token = content.trim()
|
|
157
|
+
return token === '' ? undefined : token
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function writeToken(runtimeDir: string, token: string): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
await writeFile(tokenPath(runtimeDir), token, { mode: 0o600 })
|
|
167
|
+
}
|
|
168
|
+
catch {}
|
|
169
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isIP } from 'node:net'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates values that cross the socket. Everything reaching the privileged
|
|
5
|
+
* helper is untrusted, and a hosts line in particular is a plain-text file: an
|
|
6
|
+
* unvalidated hostname containing a newline could inject entries this plugin
|
|
7
|
+
* does not own and therefore never cleans up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Ports the plugin may free. Wide enough for the two the proxy binds. */
|
|
11
|
+
export const ALLOWED_FREE_PORTS = new Set([80, 443])
|
|
12
|
+
|
|
13
|
+
const HOSTNAME_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i
|
|
14
|
+
const IPV6_IN_HOSTS = /^[0-9a-f:.]+$/i
|
|
15
|
+
|
|
16
|
+
export function isSafeHost(host: unknown): host is string {
|
|
17
|
+
if (typeof host !== 'string' || host.length === 0 || host.length > 253)
|
|
18
|
+
return false
|
|
19
|
+
// A hosts entry is whitespace separated, so any whitespace or control
|
|
20
|
+
// character would split the line or start a new one.
|
|
21
|
+
if (/[\s#\0]/.test(host))
|
|
22
|
+
return false
|
|
23
|
+
return HOSTNAME_PATTERN.test(host)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isSafeIp(ip: unknown): ip is string {
|
|
27
|
+
if (typeof ip !== 'string')
|
|
28
|
+
return false
|
|
29
|
+
if (isIP(ip) !== 0)
|
|
30
|
+
return true
|
|
31
|
+
// `::1` and friends are caught by isIP; this keeps the check explicit for
|
|
32
|
+
// hosts files that spell IPv6 without brackets.
|
|
33
|
+
return IPV6_IN_HOSTS.test(ip)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isAllowedFreePort(port: unknown): port is number {
|
|
37
|
+
return typeof port === 'number' && Number.isInteger(port) && ALLOWED_FREE_PORTS.has(port)
|
|
38
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Windows has no `sudo`: elevation goes through UAC, and the elevated process
|
|
7
|
+
* is a new process that cannot inherit the parent's stdio. The helper talks
|
|
8
|
+
* over a named pipe, so it only needs to be started detached.
|
|
9
|
+
*
|
|
10
|
+
* `Start-Process -Verb RunAs` is the documented way to trigger the UAC prompt
|
|
11
|
+
* without a helper binary or a dependency on `sudo-prompt`.
|
|
12
|
+
*/
|
|
13
|
+
export interface WindowsElevationOptions {
|
|
14
|
+
/** Executable to run elevated; normally the node runtime. */
|
|
15
|
+
file: string
|
|
16
|
+
/** Arguments for that executable. */
|
|
17
|
+
args: string[]
|
|
18
|
+
/** Directory to start in; defaults to the parent's. */
|
|
19
|
+
cwd?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Quotes an argument for PowerShell's single-quoted string syntax. */
|
|
23
|
+
export function quotePowerShell(value: string): string {
|
|
24
|
+
return `'${value.replace(/'/g, '\'\'')}'`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the PowerShell command that starts `file` elevated. Exported so the
|
|
29
|
+
* escaping can be unit-tested without touching a real UAC prompt.
|
|
30
|
+
*/
|
|
31
|
+
export function buildElevationCommand(options: WindowsElevationOptions): string {
|
|
32
|
+
const argumentList = options.args.map(quotePowerShell).join(',')
|
|
33
|
+
const parts = [
|
|
34
|
+
'Start-Process',
|
|
35
|
+
'-FilePath',
|
|
36
|
+
quotePowerShell(options.file),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if (argumentList)
|
|
40
|
+
parts.push('-ArgumentList', argumentList)
|
|
41
|
+
|
|
42
|
+
if (options.cwd)
|
|
43
|
+
parts.push('-WorkingDirectory', quotePowerShell(options.cwd))
|
|
44
|
+
|
|
45
|
+
parts.push('-Verb', 'RunAs')
|
|
46
|
+
parts.push('-WindowStyle', 'Hidden')
|
|
47
|
+
|
|
48
|
+
return parts.join(' ')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Starts the helper elevated on Windows. Resolves once PowerShell has accepted
|
|
53
|
+
* the request; the helper's own readiness is observed through the named pipe.
|
|
54
|
+
*/
|
|
55
|
+
export function spawnElevatedOnWindows(options: WindowsElevationOptions): Promise<ChildProcess> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const script = buildElevationCommand(options)
|
|
58
|
+
const child = spawn(
|
|
59
|
+
'powershell.exe',
|
|
60
|
+
['-NoProfile', '-NonInteractive', '-Command', script],
|
|
61
|
+
{ stdio: 'ignore', windowsHide: true },
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// PowerShell exits once UAC is answered; a non-zero code means the user
|
|
65
|
+
// dismissed the prompt or elevation was denied.
|
|
66
|
+
child.once('error', reject)
|
|
67
|
+
child.once('exit', (code) => {
|
|
68
|
+
if (code === 0)
|
|
69
|
+
return resolve(child)
|
|
70
|
+
reject(new Error(
|
|
71
|
+
`administrator permission was not granted (PowerShell exited with ${code}). `
|
|
72
|
+
+ 'Accept the UAC prompt to let the plugin edit the hosts file and bind ports 80/443.',
|
|
73
|
+
))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isWindowsElevationAvailable(): boolean {
|
|
79
|
+
return process.platform === 'win32'
|
|
80
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { startHelper } from './privileged/bootstrap'
|
|
3
|
+
|
|
4
|
+
interface ParsedArgs {
|
|
5
|
+
socket: string
|
|
6
|
+
token: string
|
|
7
|
+
hosts?: string
|
|
8
|
+
caddyBin: string
|
|
9
|
+
caddyConfig: string
|
|
10
|
+
ownerUid?: number
|
|
11
|
+
ownerGid?: number
|
|
12
|
+
caddyStorage?: string
|
|
13
|
+
caddyAdmin?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
17
|
+
let socket = ''
|
|
18
|
+
let token = ''
|
|
19
|
+
let hosts: string | undefined
|
|
20
|
+
let caddyBin = ''
|
|
21
|
+
let caddyConfig = ''
|
|
22
|
+
let ownerUid: number | undefined
|
|
23
|
+
let ownerGid: number | undefined
|
|
24
|
+
let caddyStorage: string | undefined
|
|
25
|
+
let caddyAdmin: string | undefined
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index < argv.length; index++) {
|
|
28
|
+
const current = argv[index]
|
|
29
|
+
if (current === '--socket')
|
|
30
|
+
socket = argv[++index] ?? ''
|
|
31
|
+
else if (current === '--token')
|
|
32
|
+
token = argv[++index] ?? ''
|
|
33
|
+
else if (current === '--hosts')
|
|
34
|
+
hosts = argv[++index] ?? undefined
|
|
35
|
+
else if (current === '--caddy')
|
|
36
|
+
caddyBin = argv[++index] ?? ''
|
|
37
|
+
else if (current === '--caddy-config')
|
|
38
|
+
caddyConfig = argv[++index] ?? ''
|
|
39
|
+
else if (current === '--owner-uid')
|
|
40
|
+
ownerUid = Number(argv[++index])
|
|
41
|
+
else if (current === '--owner-gid')
|
|
42
|
+
ownerGid = Number(argv[++index])
|
|
43
|
+
else if (current === '--caddy-storage')
|
|
44
|
+
caddyStorage = argv[++index] ?? undefined
|
|
45
|
+
else if (current === '--caddy-admin')
|
|
46
|
+
caddyAdmin = argv[++index] ?? undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!socket || !token)
|
|
50
|
+
throw new Error('usage: privileged-helper --socket <path> --token <token> --caddy <bin> --caddy-config <path>')
|
|
51
|
+
if (!caddyBin || !caddyConfig)
|
|
52
|
+
throw new Error('privileged-helper requires --caddy and --caddy-config')
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
socket,
|
|
56
|
+
token,
|
|
57
|
+
hosts,
|
|
58
|
+
caddyBin,
|
|
59
|
+
caddyConfig,
|
|
60
|
+
ownerUid: Number.isInteger(ownerUid) ? ownerUid : undefined,
|
|
61
|
+
ownerGid: Number.isInteger(ownerGid) ? ownerGid : undefined,
|
|
62
|
+
caddyStorage,
|
|
63
|
+
caddyAdmin,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Entry point of the process that runs with elevated privileges.
|
|
69
|
+
*
|
|
70
|
+
* It owns no product logic. The Caddy binary and Caddyfile are fixed on the
|
|
71
|
+
* command line, so requests on the socket can only say *when* to act, never
|
|
72
|
+
* what* to execute.
|
|
73
|
+
*/
|
|
74
|
+
async function main(): Promise<void> {
|
|
75
|
+
const args = parseArgs(process.argv.slice(2))
|
|
76
|
+
|
|
77
|
+
const helper = await startHelper({
|
|
78
|
+
socketPath: args.socket,
|
|
79
|
+
token: args.token,
|
|
80
|
+
hostsPath: args.hosts,
|
|
81
|
+
caddyBin: args.caddyBin,
|
|
82
|
+
caddyConfigPath: args.caddyConfig,
|
|
83
|
+
ownerUid: args.ownerUid,
|
|
84
|
+
ownerGid: args.ownerGid,
|
|
85
|
+
caddyStoragePath: args.caddyStorage,
|
|
86
|
+
caddyAdminSocket: args.caddyAdmin,
|
|
87
|
+
caddyEnv: {
|
|
88
|
+
HOME: process.env.HOME ?? '',
|
|
89
|
+
// Keep Caddy's storage next to the invoking user's home even though the
|
|
90
|
+
// process runs as root.
|
|
91
|
+
XDG_DATA_HOME: process.env.XDG_DATA_HOME ?? '',
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Terminal signals reach the whole foreground process group, so they also hit
|
|
96
|
+
// this helper when one of several dev servers stops. Acting on them would tear
|
|
97
|
+
// down the shared Caddy and hosts entries the remaining projects still use.
|
|
98
|
+
// The socket closing is the only signal this helper acts on.
|
|
99
|
+
//
|
|
100
|
+
// Ctrl+C, Ctrl+\, Ctrl+Z and a terminal closing all arrive this way: SIGINT,
|
|
101
|
+
// SIGQUIT, SIGTSTP and SIGHUP. Ignoring SIGTSTP also keeps the helper from
|
|
102
|
+
// being suspended, which would stall every attached project's RPC.
|
|
103
|
+
for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTSTP', 'SIGHUP'] as const)
|
|
104
|
+
process.on(signal, () => {})
|
|
105
|
+
|
|
106
|
+
// SIGTERM is different: IDE and process-tree runners send it to shut the tool
|
|
107
|
+
// down for real. It still must not drop a live session, so it only tears down
|
|
108
|
+
// when no dev server is attached.
|
|
109
|
+
process.on('SIGTERM', () => {
|
|
110
|
+
if (helper.runtime.clientCount() > 0)
|
|
111
|
+
return
|
|
112
|
+
void helper.runtime.dispose().finally(() => process.exit(0))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// A closed stdout (the parent died) must not interrupt the cleanup path.
|
|
116
|
+
process.stdout.on('error', () => {})
|
|
117
|
+
process.stderr.on('error', () => {})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main().catch((error: Error) => {
|
|
121
|
+
process.stderr.write(`[uhrp-helper] fatal: ${error.message}\n`)
|
|
122
|
+
process.exit(1)
|
|
123
|
+
})
|
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import { chmod, readdir, stat } from 'node:fs/promises'
|
|
3
1
|
import { networkInterfaces } from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import process from 'node:process'
|
|
6
2
|
import { createConsola } from 'consola'
|
|
7
3
|
import c from 'picocolors'
|
|
8
4
|
import qrcode from 'qrcode-terminal'
|
|
@@ -11,15 +7,6 @@ export const consola = createConsola({
|
|
|
11
7
|
level: 3,
|
|
12
8
|
}).withTag('reverse-proxy')
|
|
13
9
|
|
|
14
|
-
export async function chmodRecursive(_path: string, mode: number): Promise<void> {
|
|
15
|
-
await chmod(_path, mode)
|
|
16
|
-
if ((await stat(_path)).isDirectory()) {
|
|
17
|
-
const children = await readdir(_path)
|
|
18
|
-
for (const child of children)
|
|
19
|
-
await chmodRecursive(path.join(_path, child), mode)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
10
|
export function once<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => ReturnType<T> {
|
|
24
11
|
let called = false
|
|
25
12
|
return (...args: any[]) => {
|
|
@@ -30,21 +17,6 @@ export function once<T extends (...args: any[]) => any>(fn: T): (...args: Parame
|
|
|
30
17
|
}
|
|
31
18
|
}
|
|
32
19
|
|
|
33
|
-
export function isAdmin(): boolean {
|
|
34
|
-
if (process.platform === 'win32') {
|
|
35
|
-
try {
|
|
36
|
-
execSync('net session', { stdio: 'ignore' })
|
|
37
|
-
return true
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
return !!(process.getuid && process.getuid() === 0)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
20
|
export function getIpv4List(): string[] {
|
|
49
21
|
const interfaces = networkInterfaces()
|
|
50
22
|
|
package/src/host/index.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { exec } from 'node:child_process'
|
|
2
|
-
import { chmodSync } from 'node:fs'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
import { promisify } from 'node:util'
|
|
5
|
-
|
|
6
|
-
import hostile from 'hostile'
|
|
7
|
-
import { consola } from '../utils'
|
|
8
|
-
|
|
9
|
-
const _setHost = promisify(hostile.set)
|
|
10
|
-
const _removeHost = promisify(hostile.remove)
|
|
11
|
-
|
|
12
|
-
function getPathOfSystemHostsPath(): string {
|
|
13
|
-
return process.platform === 'win32'
|
|
14
|
-
? `${process.env.windir || 'C:\\WINDOWS'}\\system32\\drivers\\etc\\hosts`
|
|
15
|
-
: '/etc/hosts'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function syncHost(): Promise<boolean> {
|
|
19
|
-
return new Promise<boolean>((resolve, _reject) => {
|
|
20
|
-
exec(
|
|
21
|
-
process.platform === 'win32'
|
|
22
|
-
? 'ipconfig /flushdns'
|
|
23
|
-
: 'sudo -E killall -HUP mDNSResponder',
|
|
24
|
-
(error, _stdout, _stderr) => {
|
|
25
|
-
if (error) {
|
|
26
|
-
consola.error(`exec error: ${error}`)
|
|
27
|
-
return resolve(false)
|
|
28
|
-
}
|
|
29
|
-
return resolve(true)
|
|
30
|
-
},
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function addHost(ip: string, host: string): Promise<boolean> {
|
|
36
|
-
if (process.platform !== 'win32')
|
|
37
|
-
chmodSync(getPathOfSystemHostsPath(), 0o777)
|
|
38
|
-
await _setHost('127.0.0.1', 'localhost')
|
|
39
|
-
await _setHost(ip, host)
|
|
40
|
-
if (ip === 'localhost')
|
|
41
|
-
await _setHost('127.0.0.1', host)
|
|
42
|
-
return await syncHost()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function removeHost(ip: string, host: string): Promise<boolean> {
|
|
46
|
-
if (process.platform !== 'win32')
|
|
47
|
-
chmodSync(getPathOfSystemHostsPath(), 0o777)
|
|
48
|
-
await _removeHost(ip, host)
|
|
49
|
-
if (ip === 'localhost')
|
|
50
|
-
await _removeHost('127.0.0.1', host)
|
|
51
|
-
return await syncHost()
|
|
52
|
-
}
|