@stacksjs/rpx 0.11.19 → 0.11.21
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/acme-challenge.d.ts +13 -0
- package/dist/auth.d.ts +21 -0
- package/dist/bin/cli.js +252 -200
- package/dist/cert-inspect.d.ts +5 -3
- package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
- package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
- package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
- package/dist/chunk-qs36t2rf.js +224 -0
- package/dist/daemon.d.ts +4 -1
- package/dist/https.d.ts +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +7 -7
- package/dist/proxy-handler.d.ts +18 -1
- package/dist/proxy-pool.d.ts +5 -0
- package/dist/redirect.d.ts +40 -0
- package/dist/registry.d.ts +2 -1
- package/dist/site-resolver.d.ts +106 -0
- package/dist/site-splash.d.ts +19 -0
- package/dist/site-supervisor.d.ts +56 -0
- package/dist/start.d.ts +31 -8
- package/dist/types.d.ts +64 -0
- package/package.json +6 -7
- package/dist/chunk-rs8gqpax.js +0 -174
- package/src/cert-inspect.ts +0 -69
- package/src/colors.ts +0 -13
- package/src/config.ts +0 -45
- package/src/daemon-runner.ts +0 -180
- package/src/daemon.ts +0 -1209
- package/src/dns-state.ts +0 -116
- package/src/dns.ts +0 -568
- package/src/host-match.ts +0 -52
- package/src/host-routes.ts +0 -147
- package/src/hosts.ts +0 -283
- package/src/https.ts +0 -905
- package/src/index.ts +0 -161
- package/src/logger.ts +0 -19
- package/src/macos-trust.ts +0 -175
- package/src/on-demand.ts +0 -264
- package/src/origin-guard.ts +0 -127
- package/src/port-manager.ts +0 -183
- package/src/process-manager.ts +0 -164
- package/src/proxy-handler.ts +0 -387
- package/src/proxy-pool.ts +0 -1003
- package/src/registry.ts +0 -366
- package/src/sni.ts +0 -93
- package/src/start.ts +0 -1421
- package/src/static-files.ts +0 -201
- package/src/types.ts +0 -267
- package/src/utils.ts +0 -243
package/src/host-match.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-based route matching with wildcard support.
|
|
3
|
-
*
|
|
4
|
-
* The routing table is keyed by host pattern. A pattern is either an exact
|
|
5
|
-
* hostname (`api.example.com`) or a wildcard (`*.example.com`). Lookup prefers
|
|
6
|
-
* an exact match, then the most-specific (deepest-suffix) wildcard.
|
|
7
|
-
*
|
|
8
|
-
* Kept dependency-free and pure so it's reusable from both the daemon and the
|
|
9
|
-
* in-process multi-proxy path, and trivially unit-testable.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export function isWildcardPattern(pattern: string): boolean {
|
|
13
|
-
return pattern.startsWith('*.')
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* True if `hostname` matches the wildcard `pattern` (`*.suffix`). A wildcard
|
|
18
|
-
* matches exactly one or more leading labels — `*.example.com` matches
|
|
19
|
-
* `a.example.com` and `a.b.example.com`, but NOT the bare apex `example.com`.
|
|
20
|
-
*/
|
|
21
|
-
export function matchesWildcard(hostname: string, pattern: string): boolean {
|
|
22
|
-
if (!isWildcardPattern(pattern))
|
|
23
|
-
return false
|
|
24
|
-
const suffix = pattern.slice(1) // '*.example.com' → '.example.com'
|
|
25
|
-
return hostname.length > suffix.length && hostname.endsWith(suffix)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Find the route value for `hostname` in a host-keyed map. Exact match wins;
|
|
30
|
-
* otherwise the matching wildcard with the longest (most-specific) suffix wins.
|
|
31
|
-
* Returns `undefined` when nothing matches.
|
|
32
|
-
*/
|
|
33
|
-
export function matchHost<T>(table: Map<string, T>, hostname: string): T | undefined {
|
|
34
|
-
const exact = table.get(hostname)
|
|
35
|
-
if (exact !== undefined)
|
|
36
|
-
return exact
|
|
37
|
-
|
|
38
|
-
let best: T | undefined
|
|
39
|
-
let bestLen = -1
|
|
40
|
-
for (const [pattern, value] of table) {
|
|
41
|
-
if (!isWildcardPattern(pattern))
|
|
42
|
-
continue
|
|
43
|
-
if (matchesWildcard(hostname, pattern)) {
|
|
44
|
-
const len = pattern.length - 1 // length of the matched suffix
|
|
45
|
-
if (len > bestLen) {
|
|
46
|
-
bestLen = len
|
|
47
|
-
best = value
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return best
|
|
52
|
-
}
|
package/src/host-routes.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path-aware routing within a single host.
|
|
3
|
-
*
|
|
4
|
-
* `host-match.ts` answers "which host pattern owns this hostname?" (exact wins,
|
|
5
|
-
* then most-specific wildcard). That is sufficient when every host maps to a
|
|
6
|
-
* single backend. But a single domain often needs to serve several things at
|
|
7
|
-
* once — e.g. `stacksjs.com/api/*` proxied to an app on `:3000`,
|
|
8
|
-
* `stacksjs.com/docs*` served from `/var/www/docs`, and `stacksjs.com/` served
|
|
9
|
-
* from `/var/www/public`. That requires a second routing dimension: the
|
|
10
|
-
* request **path**.
|
|
11
|
-
*
|
|
12
|
-
* This module layers path routing on top of host routing without disturbing
|
|
13
|
-
* host-only routing:
|
|
14
|
-
* - Each host pattern maps to a list of `(path, route)` entries.
|
|
15
|
-
* - Lookup first resolves the host (reusing `matchHost` semantics), then picks
|
|
16
|
-
* the entry whose `path` is the longest prefix of the request pathname.
|
|
17
|
-
* - A host with a single entry whose `path` is `'/'` (or empty) behaves
|
|
18
|
-
* exactly like the old host-only table — full backward compatibility.
|
|
19
|
-
*
|
|
20
|
-
* Kept dependency-free and pure so it's reusable from both the daemon and the
|
|
21
|
-
* in-process multi-proxy path, and trivially unit-testable.
|
|
22
|
-
*/
|
|
23
|
-
import { isWildcardPattern, matchesWildcard } from './host-match'
|
|
24
|
-
|
|
25
|
-
/** One path-scoped route under a host. */
|
|
26
|
-
export interface PathRoute<T> {
|
|
27
|
-
/**
|
|
28
|
-
* Path prefix this route owns, e.g. `'/api'`. `'/'` (or `''`) is the host
|
|
29
|
-
* default that catches everything not claimed by a more specific prefix.
|
|
30
|
-
*/
|
|
31
|
-
path: string
|
|
32
|
-
/** The route value (e.g. a {@link import('./proxy-handler').ProxyRoute}). */
|
|
33
|
-
route: T
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A host-keyed routing table where each host owns an ordered set of
|
|
38
|
-
* path-scoped routes. Build it with {@link buildHostRoutes}.
|
|
39
|
-
*/
|
|
40
|
-
export type HostRoutes<T> = Map<string, Array<PathRoute<T>>>
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Normalize a path prefix to a leading-slash, no-trailing-slash form so prefix
|
|
44
|
-
* comparisons are predictable. `''`/`undefined`/`'/'` all normalize to `'/'`
|
|
45
|
-
* (the host default). `'/api/'` → `'/api'`, `'docs'` → `'/docs'`.
|
|
46
|
-
*/
|
|
47
|
-
export function normalizePathPrefix(path: string | undefined): string {
|
|
48
|
-
if (!path || path === '/')
|
|
49
|
-
return '/'
|
|
50
|
-
let p = path.trim()
|
|
51
|
-
if (!p.startsWith('/'))
|
|
52
|
-
p = `/${p}`
|
|
53
|
-
// Strip trailing slashes (but keep the root '/').
|
|
54
|
-
p = p.replace(/\/+$/, '')
|
|
55
|
-
return p === '' ? '/' : p
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* True if `pathname` is matched by the prefix `prefix`. The root prefix `'/'`
|
|
60
|
-
* matches everything. A non-root prefix matches when the pathname equals it
|
|
61
|
-
* (`/api`), or continues with a `/` (`/api/x`) — so `/api` does NOT match
|
|
62
|
-
* `/apifoo`, only a real path-segment boundary.
|
|
63
|
-
*/
|
|
64
|
-
export function pathPrefixMatches(pathname: string, prefix: string): boolean {
|
|
65
|
-
if (prefix === '/')
|
|
66
|
-
return true
|
|
67
|
-
if (pathname === prefix)
|
|
68
|
-
return true
|
|
69
|
-
return pathname.startsWith(`${prefix}/`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Build a {@link HostRoutes} table from a flat list of entries. Entries are
|
|
74
|
-
* grouped by host; within each host the path-routes are sorted longest-prefix
|
|
75
|
-
* first so {@link matchHostRoute} can take the first match. If two entries
|
|
76
|
-
* collide on the same (host, path) the later one wins (matching `Map.set`).
|
|
77
|
-
*/
|
|
78
|
-
export function buildHostRoutes<T>(
|
|
79
|
-
entries: Array<{ host: string, path?: string, route: T }>,
|
|
80
|
-
): HostRoutes<T> {
|
|
81
|
-
const byHost = new Map<string, Map<string, T>>()
|
|
82
|
-
for (const e of entries) {
|
|
83
|
-
const prefix = normalizePathPrefix(e.path)
|
|
84
|
-
let paths = byHost.get(e.host)
|
|
85
|
-
if (!paths) {
|
|
86
|
-
paths = new Map<string, T>()
|
|
87
|
-
byHost.set(e.host, paths)
|
|
88
|
-
}
|
|
89
|
-
paths.set(prefix, e.route)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const table: HostRoutes<T> = new Map()
|
|
93
|
-
for (const [host, paths] of byHost) {
|
|
94
|
-
const list: Array<PathRoute<T>> = []
|
|
95
|
-
for (const [path, route] of paths)
|
|
96
|
-
list.push({ path, route })
|
|
97
|
-
// Longest prefix first; '/' (length 1) naturally sorts last as the default.
|
|
98
|
-
list.sort((a, b) => b.path.length - a.path.length)
|
|
99
|
-
table.set(host, list)
|
|
100
|
-
}
|
|
101
|
-
return table
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Find the path-route list for `hostname` in a {@link HostRoutes} table. Exact
|
|
106
|
-
* host match wins; otherwise the most-specific (deepest-suffix) wildcard wins —
|
|
107
|
-
* mirroring {@link import('./host-match').matchHost}.
|
|
108
|
-
*/
|
|
109
|
-
export function matchHostList<T>(table: HostRoutes<T>, hostname: string): Array<PathRoute<T>> | undefined {
|
|
110
|
-
const exact = table.get(hostname)
|
|
111
|
-
if (exact !== undefined)
|
|
112
|
-
return exact
|
|
113
|
-
|
|
114
|
-
let best: Array<PathRoute<T>> | undefined
|
|
115
|
-
let bestLen = -1
|
|
116
|
-
for (const [pattern, value] of table) {
|
|
117
|
-
if (!isWildcardPattern(pattern))
|
|
118
|
-
continue
|
|
119
|
-
if (matchesWildcard(hostname, pattern)) {
|
|
120
|
-
const len = pattern.length - 1
|
|
121
|
-
if (len > bestLen) {
|
|
122
|
-
bestLen = len
|
|
123
|
-
best = value
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return best
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Resolve a (hostname, pathname) pair to a single route value. First the host
|
|
132
|
-
* is resolved ({@link matchHostList}); then the longest matching path prefix
|
|
133
|
-
* within that host wins. Returns `undefined` when no host matches, or a host
|
|
134
|
-
* matches but no path prefix (including the `'/'` default) covers the request.
|
|
135
|
-
*/
|
|
136
|
-
export function matchHostRoute<T>(table: HostRoutes<T>, hostname: string, pathname: string): T | undefined {
|
|
137
|
-
const list = matchHostList(table, hostname)
|
|
138
|
-
if (!list)
|
|
139
|
-
return undefined
|
|
140
|
-
// `list` is pre-sorted longest-prefix-first, so the first match is the most
|
|
141
|
-
// specific one ('/' is last and matches everything as the host default).
|
|
142
|
-
for (const entry of list) {
|
|
143
|
-
if (pathPrefixMatches(pathname, entry.path))
|
|
144
|
-
return entry.route
|
|
145
|
-
}
|
|
146
|
-
return undefined
|
|
147
|
-
}
|
package/src/hosts.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { exec } from 'node:child_process'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import * as process from 'node:process'
|
|
6
|
-
import { promisify } from 'node:util'
|
|
7
|
-
import { debugLog, getSudoPassword, isProcessElevated } from './utils'
|
|
8
|
-
|
|
9
|
-
const execAsync = promisify(exec)
|
|
10
|
-
|
|
11
|
-
/** `.localhost` names resolve to loopback per RFC 6761 — no /etc/hosts entry needed. */
|
|
12
|
-
export function isLoopbackDevelopmentHost(host: string): boolean {
|
|
13
|
-
const normalized = host.trim().toLowerCase()
|
|
14
|
-
return normalized === 'localhost'
|
|
15
|
-
|| normalized.endsWith('.localhost')
|
|
16
|
-
|| normalized.endsWith('.localhost.')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const hostsFilePath: string = process.platform === 'win32'
|
|
20
|
-
? path.join(process.env.windir || 'C:\\Windows', 'System32', 'drivers', 'etc', 'hosts')
|
|
21
|
-
: '/etc/hosts'
|
|
22
|
-
|
|
23
|
-
// Flag to track if we've already received sudo privileges in this session
|
|
24
|
-
let sudoPrivilegesAcquired = false
|
|
25
|
-
|
|
26
|
-
// Single function to execute sudo commands, with caching for permissions.
|
|
27
|
-
// Wraps in sh -c so pipes/redirects all run under sudo.
|
|
28
|
-
async function execSudo(command: string): Promise<string> {
|
|
29
|
-
if (process.platform === 'win32')
|
|
30
|
-
throw new Error('Administrator privileges required on Windows')
|
|
31
|
-
|
|
32
|
-
if (isProcessElevated()) {
|
|
33
|
-
const { stdout } = await execAsync(command)
|
|
34
|
-
return stdout
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const sudoPassword = getSudoPassword()
|
|
38
|
-
const escaped = command.replace(/'/g, `'\\''`)
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
if (sudoPassword) {
|
|
42
|
-
const { stdout } = await execAsync(`echo '${sudoPassword}' | sudo -S sh -c '${escaped}' 2>/dev/null`)
|
|
43
|
-
sudoPrivilegesAcquired = true
|
|
44
|
-
return stdout
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (sudoPrivilegesAcquired) {
|
|
48
|
-
try {
|
|
49
|
-
const { stdout } = await execAsync(`sudo -n sh -c '${escaped}'`)
|
|
50
|
-
return stdout
|
|
51
|
-
}
|
|
52
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
53
|
-
catch (error) {
|
|
54
|
-
debugLog('hosts', 'Cached sudo privileges expired, requesting again', true)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const { stdout } = await execAsync(`sudo -n sh -c '${escaped}'`)
|
|
60
|
-
sudoPrivilegesAcquired = true
|
|
61
|
-
return stdout
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
throw new Error('sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)')
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
throw new Error(`Failed to execute sudo command: ${(error as Error).message}`)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function addHosts(hosts: string[], verbose?: boolean): Promise<void> {
|
|
73
|
-
const needsHostsFile = hosts.filter(h => !isLoopbackDevelopmentHost(h))
|
|
74
|
-
const skipped = hosts.filter(h => isLoopbackDevelopmentHost(h))
|
|
75
|
-
if (skipped.length > 0) {
|
|
76
|
-
debugLog('hosts', `Skipping /etc/hosts for loopback dev names: ${skipped.join(', ')}`, verbose)
|
|
77
|
-
}
|
|
78
|
-
if (needsHostsFile.length === 0)
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
debugLog('hosts', `Adding hosts: ${needsHostsFile.join(', ')}`, verbose)
|
|
82
|
-
debugLog('hosts', `Using hosts file at: ${hostsFilePath}`, verbose)
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// Read existing hosts file content
|
|
86
|
-
let existingContent: string
|
|
87
|
-
try {
|
|
88
|
-
existingContent = await fs.promises.readFile(hostsFilePath, 'utf-8')
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// /etc/hosts typically requires elevated permissions — fall back to sudo
|
|
92
|
-
debugLog('hosts', 'Reading hosts file requires elevated permissions, using sudo', verbose)
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
existingContent = await execSudo(`cat "${hostsFilePath}"`)
|
|
96
|
-
}
|
|
97
|
-
catch (sudoErr) {
|
|
98
|
-
console.log(' Could not read hosts file — skipping hosts setup')
|
|
99
|
-
debugLog('hosts', `sudo read also failed: ${sudoErr}`, verbose)
|
|
100
|
-
throw new Error(`Cannot read hosts file: ${sudoErr}`)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Prepare new entries, only including those that don't exist
|
|
105
|
-
const newEntries = needsHostsFile.filter((host) => {
|
|
106
|
-
const ipv4Entry = `127.0.0.1 ${host}`
|
|
107
|
-
const ipv6Entry = `::1 ${host}`
|
|
108
|
-
return !existingContent.includes(ipv4Entry) && !existingContent.includes(ipv6Entry)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
if (newEntries.length === 0) {
|
|
112
|
-
debugLog('hosts', 'All hosts already exist in hosts file', verbose)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Create content for new entries
|
|
117
|
-
const hostEntries = newEntries.map(host =>
|
|
118
|
-
`\n# Added by rpx\n127.0.0.1 ${host}\n::1 ${host}`,
|
|
119
|
-
).join('\n')
|
|
120
|
-
|
|
121
|
-
const tmpFile = path.join(os.tmpdir(), `rpx-hosts-${Date.now()}.tmp`)
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
// Write to temporary file
|
|
125
|
-
await fs.promises.writeFile(tmpFile, existingContent + hostEntries, 'utf8')
|
|
126
|
-
|
|
127
|
-
// Use tee with sudo to write the content to hosts file
|
|
128
|
-
await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`)
|
|
129
|
-
console.log(` Hosts updated: ${newEntries.join(', ')}`)
|
|
130
|
-
}
|
|
131
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
132
|
-
catch (error) {
|
|
133
|
-
// Don't throw — just tell the user what to add manually
|
|
134
|
-
console.log(' Could not update hosts file automatically')
|
|
135
|
-
console.log(' Add these entries to /etc/hosts:')
|
|
136
|
-
newEntries.forEach((host) => {
|
|
137
|
-
console.log(` 127.0.0.1 ${host}`)
|
|
138
|
-
console.log(` ::1 ${host}`)
|
|
139
|
-
})
|
|
140
|
-
console.log(` Or run: sudo nano ${hostsFilePath}`)
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
try {
|
|
144
|
-
await fs.promises.unlink(tmpFile)
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// Ignore cleanup errors
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
const error = err as Error
|
|
153
|
-
debugLog('hosts', `Failed to manage hosts file: ${error.message}`, verbose)
|
|
154
|
-
// Don't throw - hosts file management is best-effort
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function removeHosts(hosts: string[], verbose?: boolean): Promise<void> {
|
|
159
|
-
debugLog('hosts', `Removing hosts: ${hosts.join(', ')}`, verbose)
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
// Read existing hosts file content
|
|
163
|
-
let content: string
|
|
164
|
-
try {
|
|
165
|
-
content = await fs.promises.readFile(hostsFilePath, 'utf-8')
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
debugLog('hosts', 'Reading hosts file requires elevated permissions, using sudo', verbose)
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
content = await execSudo(`cat "${hostsFilePath}"`)
|
|
172
|
-
}
|
|
173
|
-
catch (sudoErr) {
|
|
174
|
-
debugLog('hosts', `sudo read also failed: ${sudoErr}`, verbose)
|
|
175
|
-
throw new Error(`Cannot read hosts file: ${sudoErr}`)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const lines = content.split('\n')
|
|
180
|
-
let modified = false
|
|
181
|
-
|
|
182
|
-
// Filter out our added entries and their comments
|
|
183
|
-
const filteredLines = lines.filter((line) => {
|
|
184
|
-
// Check if this line contains one of our hosts
|
|
185
|
-
const isHostLine = hosts.some(host =>
|
|
186
|
-
line.includes(` ${host}`)
|
|
187
|
-
&& (line.includes('127.0.0.1') || line.includes('::1')),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
if (isHostLine) {
|
|
191
|
-
modified = true
|
|
192
|
-
return false
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// If it's our comment line, remove it
|
|
196
|
-
if (line.trim() === '# Added by rpx') {
|
|
197
|
-
modified = true
|
|
198
|
-
return false
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return true
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// If nothing was removed, we're done
|
|
205
|
-
if (!modified) {
|
|
206
|
-
debugLog('hosts', 'No matching hosts found to remove', verbose)
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Remove empty lines at the end of the file
|
|
211
|
-
while (filteredLines[filteredLines.length - 1]?.trim() === '')
|
|
212
|
-
filteredLines.pop()
|
|
213
|
-
|
|
214
|
-
// Ensure file ends with a single newline
|
|
215
|
-
const newContent = `${filteredLines.join('\n')}\n`
|
|
216
|
-
|
|
217
|
-
const tmpFile = path.join(os.tmpdir(), `rpx-hosts-${Date.now()}.tmp`)
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
// Write to temporary file
|
|
221
|
-
await fs.promises.writeFile(tmpFile, newContent, 'utf8')
|
|
222
|
-
|
|
223
|
-
// Use tee with sudo to write the content to hosts file
|
|
224
|
-
await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`)
|
|
225
|
-
debugLog('hosts', 'Hosts removed successfully', verbose)
|
|
226
|
-
}
|
|
227
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
228
|
-
catch (error) {
|
|
229
|
-
debugLog('hosts', 'Could not clean up hosts file automatically', verbose)
|
|
230
|
-
}
|
|
231
|
-
finally {
|
|
232
|
-
try {
|
|
233
|
-
// Clean up the temp file
|
|
234
|
-
await fs.promises.unlink(tmpFile)
|
|
235
|
-
}
|
|
236
|
-
catch (unlinkErr) {
|
|
237
|
-
// Ignore cleanup errors
|
|
238
|
-
debugLog('hosts', `Failed to remove temporary file: ${unlinkErr}`, verbose)
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
debugLog('hosts', `Failed to clean up hosts file: ${(err as Error).message}`, verbose)
|
|
244
|
-
// Don't throw - hosts file cleanup is best-effort
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export async function checkHosts(hosts: string[], verbose?: boolean): Promise<boolean[]> {
|
|
249
|
-
debugLog('hosts', `Checking hosts: ${hosts}`, verbose)
|
|
250
|
-
|
|
251
|
-
let content: string
|
|
252
|
-
try {
|
|
253
|
-
content = await fs.promises.readFile(hostsFilePath, 'utf-8')
|
|
254
|
-
}
|
|
255
|
-
catch (readErr) {
|
|
256
|
-
debugLog('hosts', `Error reading hosts file: ${readErr}`, verbose)
|
|
257
|
-
|
|
258
|
-
// Try with sudo using SUDO_PASSWORD if available
|
|
259
|
-
try {
|
|
260
|
-
const sudoPassword = getSudoPassword()
|
|
261
|
-
let cmd: string
|
|
262
|
-
if (sudoPassword) {
|
|
263
|
-
cmd = `echo '${sudoPassword}' | sudo -S cat "${hostsFilePath}" 2>/dev/null`
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
cmd = `sudo -n cat "${hostsFilePath}" 2>/dev/null || cat "${hostsFilePath}" 2>/dev/null || echo ""`
|
|
267
|
-
}
|
|
268
|
-
const { stdout } = await execAsync(cmd)
|
|
269
|
-
content = stdout
|
|
270
|
-
}
|
|
271
|
-
catch (sudoErr) {
|
|
272
|
-
// Can't read hosts file - assume entries don't exist
|
|
273
|
-
debugLog('hosts', `Cannot read hosts file, assuming entries don't exist: ${sudoErr}`, verbose)
|
|
274
|
-
return hosts.map(() => false)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return hosts.map((host) => {
|
|
279
|
-
const ipv4Entry = `127.0.0.1 ${host}`
|
|
280
|
-
const ipv6Entry = `::1 ${host}`
|
|
281
|
-
return content.includes(ipv4Entry) || content.includes(ipv6Entry)
|
|
282
|
-
})
|
|
283
|
-
}
|