@stacksjs/rpx 0.11.4 → 0.11.7
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 +82 -0
- package/dist/bin/cli.js +243 -8
- package/dist/chunk-6z1nzq0x.js +1 -0
- package/dist/chunk-jpf41gb9.js +49 -0
- package/dist/chunk-qcdcnadb.js +1 -0
- package/dist/daemon-runner.d.ts +29 -0
- package/dist/daemon.d.ts +99 -0
- package/dist/https.d.ts +8 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +155 -0
- package/dist/process-manager.d.ts +1 -0
- package/dist/proxy-handler.d.ts +15 -0
- package/dist/registry.d.ts +74 -0
- package/dist/start.d.ts +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +13 -1
- package/package.json +11 -9
- package/src/daemon-runner.ts +148 -0
- package/src/daemon.ts +496 -0
- package/src/https.ts +105 -64
- package/src/index.ts +42 -0
- package/src/process-manager.ts +2 -2
- package/src/proxy-handler.ts +99 -0
- package/src/registry.ts +346 -0
- package/src/start.ts +66 -84
- package/src/types.ts +21 -1
- package/src/utils.ts +78 -1
- package/dist/chunk-3y886wa5.js +0 -1
- package/dist/chunk-61re8msk.js +0 -1
- package/dist/chunk-94pvxvt5.js +0 -1
- package/dist/chunk-dz3837t8.js +0 -45
- package/dist/chunk-g5db14m7.js +0 -19
- package/dist/chunk-gbny098p.js +0 -2
- package/dist/chunk-pbbtnqsx.js +0 -123
- package/dist/dns.d.ts +0 -21
- package/dist/src/index.js +0 -1
package/src/https.ts
CHANGED
|
@@ -13,6 +13,50 @@ import { debugLog, execSudoSync, getPrimaryDomain, isMultiProxyConfig, isMultiPr
|
|
|
13
13
|
|
|
14
14
|
let cachedSSLConfig: { key: string, cert: string, ca?: string } | null = null
|
|
15
15
|
|
|
16
|
+
// Canonical filenames for the shared Root CA. The CA is a singleton across all
|
|
17
|
+
// rpx-managed domains so that browsers only need to trust it once. Host certs
|
|
18
|
+
// for individual domains are issued from this CA on demand.
|
|
19
|
+
const ROOT_CA_CERT_FILENAME = 'rpx-root-ca.crt'
|
|
20
|
+
const ROOT_CA_KEY_FILENAME = 'rpx-root-ca.key'
|
|
21
|
+
|
|
22
|
+
export interface RootCAPaths {
|
|
23
|
+
caCertPath: string
|
|
24
|
+
caKeyPath: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the canonical Root CA cert + key paths inside `basePath`.
|
|
29
|
+
*/
|
|
30
|
+
export function getRootCAPaths(basePath: string): RootCAPaths {
|
|
31
|
+
return {
|
|
32
|
+
caCertPath: join(basePath, ROOT_CA_CERT_FILENAME),
|
|
33
|
+
caKeyPath: join(basePath, ROOT_CA_KEY_FILENAME),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load a previously-persisted Root CA (cert + private key). Returns `null` if
|
|
39
|
+
* either file is missing or unreadable, signalling that a fresh CA needs to be
|
|
40
|
+
* created.
|
|
41
|
+
*/
|
|
42
|
+
async function loadRootCA(paths: RootCAPaths, verbose?: boolean): Promise<{ certificate: string, privateKey: string } | null> {
|
|
43
|
+
try {
|
|
44
|
+
const [certificate, privateKey] = await Promise.all([
|
|
45
|
+
fs.readFile(paths.caCertPath, 'utf8'),
|
|
46
|
+
fs.readFile(paths.caKeyPath, 'utf8'),
|
|
47
|
+
])
|
|
48
|
+
if (!certificate.includes('-----BEGIN CERTIFICATE-----') || !privateKey.includes('PRIVATE KEY-----')) {
|
|
49
|
+
debugLog('ssl', `Root CA files at ${paths.caCertPath} look malformed, will regenerate`, verbose)
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
return { certificate, privateKey }
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
debugLog('ssl', `No existing Root CA at ${paths.caCertPath} (${(err as NodeJS.ErrnoException).code || err}), will create one`, verbose)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
16
60
|
/**
|
|
17
61
|
* Resolves SSL paths based on configuration
|
|
18
62
|
*/
|
|
@@ -246,14 +290,7 @@ async function forceTrustCertificateMacOS(certPath: string): Promise<boolean> {
|
|
|
246
290
|
return true
|
|
247
291
|
}
|
|
248
292
|
catch {
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${certPath}"`)
|
|
252
|
-
return true
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return false
|
|
256
|
-
}
|
|
293
|
+
return false
|
|
257
294
|
}
|
|
258
295
|
}
|
|
259
296
|
catch {
|
|
@@ -274,15 +311,37 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
|
|
|
274
311
|
|
|
275
312
|
debugLog('ssl', `Generating certificate for domains: ${domains.join(', ')}`, options.verbose)
|
|
276
313
|
|
|
277
|
-
// Generate Root CA first
|
|
278
|
-
const rootCAConfig = httpsConfig(options, options.verbose)
|
|
279
|
-
|
|
280
|
-
if (options.verbose)
|
|
281
|
-
log.info('Generating Root CA certificate...')
|
|
282
|
-
const caCert = await createRootCA(rootCAConfig)
|
|
283
|
-
|
|
284
|
-
// Generate the host certificate with all domains
|
|
285
314
|
const hostConfig = httpsConfig(options, options.verbose)
|
|
315
|
+
const sslDir = hostConfig.basePath || join(homedir(), '.stacks', 'ssl')
|
|
316
|
+
await fs.mkdir(sslDir, { recursive: true })
|
|
317
|
+
const rootCAPaths = getRootCAPaths(sslDir)
|
|
318
|
+
|
|
319
|
+
// Reuse the persisted Root CA when present so the user only has to trust it
|
|
320
|
+
// once. A fresh CA gets created (and persisted) on first run.
|
|
321
|
+
let caCert = await loadRootCA(rootCAPaths, options.verbose)
|
|
322
|
+
let caIsNew = false
|
|
323
|
+
if (!caCert) {
|
|
324
|
+
if (options.verbose)
|
|
325
|
+
log.info('Generating Root CA certificate (one-time)...')
|
|
326
|
+
caCert = await createRootCA(hostConfig)
|
|
327
|
+
try {
|
|
328
|
+
await Promise.all([
|
|
329
|
+
fs.writeFile(rootCAPaths.caCertPath, caCert.certificate),
|
|
330
|
+
fs.writeFile(rootCAPaths.caKeyPath, caCert.privateKey, { mode: 0o600 }),
|
|
331
|
+
])
|
|
332
|
+
caIsNew = true
|
|
333
|
+
debugLog('ssl', `Persisted Root CA at ${rootCAPaths.caCertPath}`, options.verbose)
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
debugLog('ssl', `Error saving Root CA files: ${err}`, options.verbose)
|
|
337
|
+
throw new Error(`Failed to save Root CA files: ${err}`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
debugLog('ssl', `Reusing existing Root CA from ${rootCAPaths.caCertPath}`, options.verbose)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Issue the host cert with all SANs from the (possibly reused) CA.
|
|
286
345
|
if (options.verbose)
|
|
287
346
|
log.info(`Generating host certificate for: ${domains.join(', ')}`)
|
|
288
347
|
|
|
@@ -294,14 +353,9 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
|
|
|
294
353
|
},
|
|
295
354
|
})
|
|
296
355
|
|
|
297
|
-
//
|
|
298
|
-
//
|
|
356
|
+
// Persist host cert + key. Also write a copy of the CA cert at the per-domain
|
|
357
|
+
// `caCertPath` for back-compat with anything that still reads from there.
|
|
299
358
|
try {
|
|
300
|
-
// Ensure the SSL directory exists
|
|
301
|
-
const sslDir = hostConfig.basePath || join(homedir(), '.stacks', 'ssl')
|
|
302
|
-
await fs.mkdir(sslDir, { recursive: true })
|
|
303
|
-
|
|
304
|
-
// Write certificate files
|
|
305
359
|
await Promise.all([
|
|
306
360
|
fs.writeFile(hostConfig.certPath, hostCert.certificate),
|
|
307
361
|
fs.writeFile(hostConfig.keyPath, hostCert.privateKey),
|
|
@@ -315,15 +369,19 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
|
|
|
315
369
|
throw new Error(`Failed to save certificate files: ${err}`)
|
|
316
370
|
}
|
|
317
371
|
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
372
|
+
// The CA is the trust anchor — only it needs to live in the trust store.
|
|
373
|
+
// Skip the install step when the CA is already trusted (a freshly minted CA
|
|
374
|
+
// is never trusted yet; a reused one might still need a one-time install if
|
|
375
|
+
// someone wiped their keychain).
|
|
376
|
+
const caTrusted = caIsNew
|
|
377
|
+
? false
|
|
378
|
+
: await isCertTrusted(rootCAPaths.caCertPath, { verbose: options.verbose, regenerateUntrustedCerts: true })
|
|
379
|
+
|
|
380
|
+
if (caTrusted) {
|
|
381
|
+
debugLog('ssl', 'Root CA already trusted, skipping trust store update', options.verbose)
|
|
323
382
|
if (options.verbose)
|
|
324
|
-
log.success('
|
|
383
|
+
log.success('Root CA is already trusted in system trust store')
|
|
325
384
|
|
|
326
|
-
// Cache the SSL config for reuse
|
|
327
385
|
cachedSSLConfig = {
|
|
328
386
|
key: hostCert.privateKey,
|
|
329
387
|
cert: hostCert.certificate,
|
|
@@ -342,50 +400,34 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
|
|
|
342
400
|
|
|
343
401
|
let isTrusted = false
|
|
344
402
|
|
|
345
|
-
// We
|
|
403
|
+
// We install only the Root CA — host certs derive their trust from it.
|
|
346
404
|
if (process.platform === 'darwin') {
|
|
347
405
|
try {
|
|
348
|
-
|
|
349
|
-
// Combine both certificate trust operations into a single sudo command to avoid multiple password prompts
|
|
350
|
-
const combinedCmd = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${hostConfig.caCertPath}" && security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${hostConfig.certPath}"`
|
|
351
|
-
execSudoSync(combinedCmd)
|
|
406
|
+
execSudoSync(`security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${rootCAPaths.caCertPath}"`)
|
|
352
407
|
if (options.verbose)
|
|
353
|
-
log.success('Successfully added CA
|
|
354
|
-
|
|
355
|
-
// Also add to login keychain (no sudo needed)
|
|
356
|
-
try {
|
|
357
|
-
execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${hostConfig.certPath}"`, { stdio: 'pipe' })
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
// Ignore login keychain errors - system keychain is sufficient
|
|
361
|
-
}
|
|
408
|
+
log.success('Successfully added Root CA to system trust store')
|
|
362
409
|
|
|
363
410
|
isTrusted = true
|
|
364
411
|
|
|
365
|
-
//
|
|
366
|
-
const
|
|
367
|
-
const scriptPath = join(sslScriptDir, 'trust-rpx-cert.sh')
|
|
412
|
+
// Helper script for manual re-trust if the user ever wipes their keychain.
|
|
413
|
+
const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
|
|
368
414
|
const scriptContent = `#!/bin/bash
|
|
369
|
-
echo "Trusting RPX
|
|
370
|
-
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${
|
|
371
|
-
|
|
372
|
-
echo "Certificates trusted! Please restart your browser."
|
|
415
|
+
echo "Trusting RPX Root CA"
|
|
416
|
+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${rootCAPaths.caCertPath}"
|
|
417
|
+
echo "Root CA trusted! Please restart your browser."
|
|
373
418
|
echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
|
|
374
419
|
`
|
|
375
420
|
await fs.writeFile(scriptPath, scriptContent, { mode: 0o755 }) // Make it executable
|
|
376
421
|
}
|
|
377
422
|
catch (err) {
|
|
378
423
|
if (options.verbose)
|
|
379
|
-
log.warn(`Could not add
|
|
424
|
+
log.warn(`Could not add Root CA to trust store automatically: ${err}`)
|
|
380
425
|
|
|
381
|
-
|
|
382
|
-
const sslScriptDir = hostConfig.basePath || join(homedir(), '.stacks', 'ssl')
|
|
383
|
-
const scriptPath = join(sslScriptDir, 'trust-rpx-cert.sh')
|
|
426
|
+
const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
|
|
384
427
|
const scriptContent = `#!/bin/bash
|
|
385
|
-
echo "Trusting RPX
|
|
386
|
-
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${
|
|
387
|
-
|
|
388
|
-
echo "Certificates trusted! Please restart your browser."
|
|
428
|
+
echo "Trusting RPX Root CA"
|
|
429
|
+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${rootCAPaths.caCertPath}"
|
|
430
|
+
echo "Root CA trusted! Please restart your browser."
|
|
389
431
|
echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
|
|
390
432
|
`
|
|
391
433
|
await fs.writeFile(scriptPath, scriptContent, { mode: 0o755 })
|
|
@@ -404,10 +446,9 @@ echo "If you still see certificate warnings, type 'thisisunsafe' on the warning
|
|
|
404
446
|
// Create a more reliable trust script
|
|
405
447
|
const trustScript = `
|
|
406
448
|
mkdir -p "${certDir}" 2>/dev/null || true
|
|
407
|
-
cp "${
|
|
408
|
-
cp "${hostConfig.certPath}" "${certDir}/"
|
|
449
|
+
cp "${rootCAPaths.caCertPath}" "${certDir}/"
|
|
409
450
|
update-ca-certificates
|
|
410
|
-
echo "RPX
|
|
451
|
+
echo "RPX Root CA installed. Please restart your browser."
|
|
411
452
|
`
|
|
412
453
|
// Use a temp file for the script
|
|
413
454
|
const tmpScript = join(os.tmpdir(), `rpx-trust-${Date.now()}.sh`)
|
|
@@ -444,12 +485,12 @@ echo "RPX certificates installed. Please restart your browser."
|
|
|
444
485
|
try {
|
|
445
486
|
// Windows is different - use a PowerShell approach
|
|
446
487
|
const winScript = `
|
|
447
|
-
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("${
|
|
488
|
+
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("${rootCAPaths.caCertPath.replace(/\//g, '\\')}")
|
|
448
489
|
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("ROOT", "LocalMachine")
|
|
449
490
|
$store.Open("ReadWrite")
|
|
450
491
|
$store.Add($cert)
|
|
451
492
|
$store.Close()
|
|
452
|
-
Write-Host "
|
|
493
|
+
Write-Host "Root CA trusted successfully!"
|
|
453
494
|
`
|
|
454
495
|
const psPath = join(os.tmpdir(), 'rpx-trust.ps1')
|
|
455
496
|
await fs.writeFile(psPath, winScript)
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,48 @@ export {
|
|
|
22
22
|
|
|
23
23
|
export { DefaultPortManager, findAvailablePort, isPortInUse, portManager } from './port-manager'
|
|
24
24
|
|
|
25
|
+
export {
|
|
26
|
+
gcStaleEntries,
|
|
27
|
+
getRegistryDir,
|
|
28
|
+
isPidAlive,
|
|
29
|
+
isValidId,
|
|
30
|
+
readAll,
|
|
31
|
+
readEntry,
|
|
32
|
+
removeEntry,
|
|
33
|
+
watchRegistry,
|
|
34
|
+
writeEntry,
|
|
35
|
+
} from './registry'
|
|
36
|
+
|
|
37
|
+
export type { RegistryEntry, WatchHandle, WatchOptions } from './registry'
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
acquireDaemonLock,
|
|
41
|
+
defaultDaemonSpawnCommand,
|
|
42
|
+
ensureDaemonRunning,
|
|
43
|
+
getDaemonPidPath,
|
|
44
|
+
getDaemonRpxDir,
|
|
45
|
+
isDaemonRunning,
|
|
46
|
+
readDaemonPid,
|
|
47
|
+
releaseDaemonLock,
|
|
48
|
+
runDaemon,
|
|
49
|
+
stopDaemon,
|
|
50
|
+
} from './daemon'
|
|
51
|
+
|
|
52
|
+
export type {
|
|
53
|
+
DaemonHandle,
|
|
54
|
+
DaemonOptions,
|
|
55
|
+
EnsureDaemonOptions,
|
|
56
|
+
EnsureDaemonResult,
|
|
57
|
+
StopDaemonOptions,
|
|
58
|
+
StopDaemonResult,
|
|
59
|
+
} from './daemon'
|
|
60
|
+
|
|
61
|
+
export { createProxyFetchHandler } from './proxy-handler'
|
|
62
|
+
export type { GetRoute, ProxyFetchHandler, ProxyRoute } from './proxy-handler'
|
|
63
|
+
|
|
64
|
+
export { deriveIdFromTarget, runViaDaemon } from './daemon-runner'
|
|
65
|
+
export type { DaemonRunnerOptions, DaemonRunnerProxy } from './daemon-runner'
|
|
66
|
+
|
|
25
67
|
export { cleanup } from './start'
|
|
26
68
|
|
|
27
69
|
export { startProxies, startProxy, startServer } from './start'
|
package/src/process-manager.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { StartOptions } from './types'
|
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import * as process from 'node:process'
|
|
5
5
|
import { log } from './logger'
|
|
6
|
-
import { debugLog } from './utils'
|
|
6
|
+
import { debugLog, safeStringify } from './utils'
|
|
7
7
|
|
|
8
8
|
export interface ManagedProcess {
|
|
9
9
|
command: string
|
|
@@ -28,7 +28,7 @@ export class ProcessManager {
|
|
|
28
28
|
debugLog('start', `Starting process ${id}:`, verbose)
|
|
29
29
|
debugLog('start', ` Command: ${cmd} ${args.join(' ')}`, verbose)
|
|
30
30
|
debugLog('start', ` Working directory: ${cwd}`, verbose)
|
|
31
|
-
debugLog('start', ` Environment variables: ${
|
|
31
|
+
debugLog('start', ` Environment variables: ${safeStringify(options.env)}`, verbose)
|
|
32
32
|
|
|
33
33
|
const childProcess = spawn(cmd, args, {
|
|
34
34
|
cwd,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fetch handler used by the shared :443 server. Both the in-process
|
|
3
|
+
* multi-proxy mode in `start.ts` and the long-running daemon delegate to this
|
|
4
|
+
* module so routing semantics stay in one place.
|
|
5
|
+
*
|
|
6
|
+
* Routes are looked up via a caller-supplied `getRoute(hostname)` callback.
|
|
7
|
+
* The callback indirection lets each caller use whatever data structure makes
|
|
8
|
+
* sense (a fixed Map at startup, or a hot-swappable registry view) without
|
|
9
|
+
* coupling this module to either.
|
|
10
|
+
*/
|
|
11
|
+
import type { PathRewrite } from './types'
|
|
12
|
+
import { debugLog } from './utils'
|
|
13
|
+
import { resolvePathRewrite } from './utils'
|
|
14
|
+
|
|
15
|
+
export interface ProxyRoute {
|
|
16
|
+
/** Upstream `host:port` to forward requests to (e.g. `localhost:5173`). */
|
|
17
|
+
sourceHost: string
|
|
18
|
+
/** Strip `.html` suffix and 301 to clean URLs. */
|
|
19
|
+
cleanUrls?: boolean
|
|
20
|
+
/** Set the `origin` header to the target. */
|
|
21
|
+
changeOrigin?: boolean
|
|
22
|
+
/** Per-route path rewrites (vite/nginx-style prefix routing). */
|
|
23
|
+
pathRewrites?: PathRewrite[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type GetRoute = (hostname: string) => ProxyRoute | undefined
|
|
27
|
+
|
|
28
|
+
export type ProxyFetchHandler = (req: Request) => Promise<Response>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a Bun.serve-compatible `fetch` handler that routes requests based on
|
|
32
|
+
* the `Host` header. Returns 404 when no route matches and 502 on upstream
|
|
33
|
+
* failures.
|
|
34
|
+
*/
|
|
35
|
+
export function createProxyFetchHandler(getRoute: GetRoute, verbose?: boolean): ProxyFetchHandler {
|
|
36
|
+
return async (req: Request): Promise<Response> => {
|
|
37
|
+
const url = new URL(req.url)
|
|
38
|
+
const hostHeader = req.headers.get('host') || ''
|
|
39
|
+
// Strip port (`stacks.localhost:443` → `stacks.localhost`).
|
|
40
|
+
const hostname = hostHeader.split(':')[0]
|
|
41
|
+
|
|
42
|
+
const route = getRoute(hostname)
|
|
43
|
+
if (!route) {
|
|
44
|
+
debugLog('request', `No route found for host: ${hostname}`, verbose)
|
|
45
|
+
return new Response(`No proxy configured for ${hostname}`, { status: 404 })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let targetHost = route.sourceHost
|
|
49
|
+
let targetPath = url.pathname
|
|
50
|
+
|
|
51
|
+
// Per-route path rewrites: prefix preserved by default, matching Vite /
|
|
52
|
+
// nginx / http-proxy-middleware semantics. See `resolvePathRewrite`.
|
|
53
|
+
const rewriteMatch = resolvePathRewrite(url.pathname, route.pathRewrites)
|
|
54
|
+
if (rewriteMatch) {
|
|
55
|
+
targetHost = rewriteMatch.targetHost
|
|
56
|
+
targetPath = rewriteMatch.targetPath
|
|
57
|
+
debugLog('request', `Path rewrite: ${url.pathname} → ${targetHost}${targetPath}`, verbose)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const targetUrl = `http://${targetHost}${targetPath}${url.search}`
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const headers = new Headers(req.headers)
|
|
64
|
+
headers.set('host', targetHost)
|
|
65
|
+
if (route.changeOrigin)
|
|
66
|
+
headers.set('origin', `http://${route.sourceHost}`)
|
|
67
|
+
headers.set('x-forwarded-for', '127.0.0.1')
|
|
68
|
+
headers.set('x-forwarded-proto', 'https')
|
|
69
|
+
headers.set('x-forwarded-host', hostname)
|
|
70
|
+
|
|
71
|
+
const response = await fetch(targetUrl, {
|
|
72
|
+
method: req.method,
|
|
73
|
+
headers,
|
|
74
|
+
body: req.body,
|
|
75
|
+
redirect: 'manual',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Strip `.html` and 301 to the clean URL when enabled.
|
|
79
|
+
if (route.cleanUrls && url.pathname.endsWith('.html')) {
|
|
80
|
+
const cleanPath = url.pathname.replace(/\.html$/, '')
|
|
81
|
+
return new Response(null, {
|
|
82
|
+
status: 301,
|
|
83
|
+
headers: { Location: cleanPath },
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const responseHeaders = new Headers(response.headers)
|
|
88
|
+
return new Response(response.body, {
|
|
89
|
+
status: response.status,
|
|
90
|
+
statusText: response.statusText,
|
|
91
|
+
headers: responseHeaders,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
debugLog('request', `Proxy error for ${hostname}: ${err}`, verbose)
|
|
96
|
+
return new Response(`Proxy Error: ${err}`, { status: 502 })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|