Skip to content

Commit 7420091

Browse files
committed
feat: ensure multiple proxies work
1 parent b519c5b commit 7420091

8 files changed

+166
-98
lines changed

README.md

+39-5
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ startProxy(config)
6565
### CLI
6666

6767
```bash
68-
reverse-proxy --from localhost:3000 --to my-project.localhost
69-
reverse-proxy --from localhost:8080 --to my-project.test --keyPath ./key.pem --certPath ./cert.pem
70-
reverse-proxy --help
71-
reverse-proxy --version
68+
rpx --from localhost:3000 --to my-project.localhost
69+
rpx --from localhost:8080 --to my-project.test --keyPath ./key.pem --certPath ./cert.pem
70+
rpx --help
71+
rpx --version
7272
```
7373

7474
## Configuration
@@ -77,7 +77,7 @@ The Reverse Proxy can be configured using a `reverse-proxy.config.ts` _(or `reve
7777

7878
```ts
7979
// reverse-proxy.config.{ts,js}
80-
import type { ReverseProxyOptions } from './src/types'
80+
import type { ReverseProxyOptions } from '@stacksjs/rpx'
8181
import os from 'node:os'
8282
import path from 'node:path'
8383

@@ -106,6 +106,40 @@ const config: ReverseProxyOptions = {
106106
export default config
107107
```
108108

109+
In case you are trying to start multiple proxies, you may use this configuration:
110+
111+
```ts
112+
// reverse-proxy.config.{ts,js}
113+
import type { ReverseProxyOptions } from '@stacksjs/rpx'
114+
import os from 'node:os'
115+
import path from 'node:path'
116+
117+
const config: ReverseProxyOptions = {
118+
https: { // https: true -> also works with sensible defaults
119+
caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
120+
certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
121+
keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
122+
},
123+
124+
etcHostsCleanup: true,
125+
126+
proxies: [
127+
{
128+
from: 'localhost:5173',
129+
to: 'my-app.localhost',
130+
},
131+
{
132+
from: 'localhost:5174',
133+
to: 'my-api.local',
134+
},
135+
],
136+
137+
verbose: true,
138+
}
139+
140+
export default config
141+
```
142+
109143
_Then run:_
110144

111145
```bash

bun.lockb

0 Bytes
Binary file not shown.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"preview:docs": "vitepress preview docs"
6262
},
6363
"dependencies": {
64-
"@stacksjs/tlsx": "^0.7.5"
64+
"@stacksjs/tlsx": "^0.7.6"
6565
},
6666
"devDependencies": {
6767
"@stacksjs/cli": "^0.68.2",

reverse-proxy.config.ts

+21-30
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,28 @@
11
import type { ReverseProxyOptions } from './src/types'
2+
import os from 'node:os'
3+
import path from 'node:path'
24

3-
const config: ReverseProxyOptions = [
4-
{
5-
from: 'localhost:5173',
6-
to: 'test.localhost',
7-
https: true,
8-
// https: {
9-
// domain: 'stacks.localhost',
10-
// hostCertCN: 'stacks.localhost',
11-
// caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
12-
// certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
13-
// keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
14-
// altNameIPs: ['127.0.0.1'],
15-
// altNameURIs: ['localhost'],
16-
// organizationName: 'stacksjs.org',
17-
// countryName: 'US',
18-
// stateName: 'California',
19-
// localityName: 'Playa Vista',
20-
// commonName: 'stacks.localhost',
21-
// validityDays: 180,
22-
// verbose: false,
23-
// },
24-
verbose: true,
5+
const config: ReverseProxyOptions = {
6+
https: {
7+
caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
8+
certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
9+
keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
2510
},
26-
{
27-
from: 'localhost:5174',
28-
to: 'test.local',
29-
https: true,
30-
etcHostsCleanup: true,
31-
verbose: true,
32-
},
33-
]
11+
etcHostsCleanup: true,
12+
proxies: [
13+
{
14+
from: 'localhost:5173',
15+
to: 'test.localhost',
16+
},
17+
{
18+
from: 'localhost:5174',
19+
to: 'test.local',
20+
},
21+
],
22+
verbose: true,
23+
}
3424

25+
// alternatively, you can use the following configuration
3526
// const config = {
3627
// from: 'localhost:5173',
3728
// to: 'test2.localhost',

src/config.ts

-11
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,9 @@ export const config: ReverseProxyConfigs = await loadConfig({
1010
from: 'localhost:5173',
1111
to: 'stacks.localhost',
1212
https: {
13-
domain: 'stacks.localhost',
14-
hostCertCN: 'stacks.localhost',
1513
caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
1614
certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
1715
keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
18-
altNameIPs: ['127.0.0.1'],
19-
altNameURIs: ['localhost'],
20-
organizationName: 'stacksjs.org',
21-
countryName: 'US',
22-
stateName: 'California',
23-
localityName: 'Playa Vista',
24-
commonName: 'stacks.localhost',
25-
validityDays: 180,
26-
verbose: false,
2716
},
2817
etcHostsCleanup: true,
2918
verbose: true,

src/https.ts

+59-36
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1-
import type { ReverseProxyOption, ReverseProxyOptions, TlsConfig } from './types'
1+
import type { CustomTlsConfig, MultiReverseProxyConfig, ReverseProxyConfigs, TlsConfig } from './types'
22
import os from 'node:os'
33
import path from 'node:path'
44
import { log } from '@stacksjs/cli'
55
import { addCertToSystemTrustStoreAndSaveCert, createRootCA, generateCertificate as generateCert } from '@stacksjs/tlsx'
6+
import { config } from './config'
67
import { debugLog } from './utils'
78

89
let cachedSSLConfig: { key: string, cert: string, ca?: string } | null = null
910

10-
function extractDomains(options: ReverseProxyOptions): string[] {
11-
if (Array.isArray(options)) {
12-
return options.map((opt) => {
13-
const domain = opt.to || 'stacks.localhost'
11+
function isMultiProxyConfig(options: ReverseProxyConfigs): options is MultiReverseProxyConfig {
12+
return 'proxies' in options
13+
}
14+
15+
function extractDomains(options: ReverseProxyConfigs): string[] {
16+
if (isMultiProxyConfig(options)) {
17+
return options.proxies.map((proxy) => {
18+
const domain = proxy.to || 'stacks.localhost'
1419
return domain.startsWith('http') ? new URL(domain).hostname : domain
1520
})
1621
}
22+
1723
const domain = options.to || 'stacks.localhost'
1824
return [domain.startsWith('http') ? new URL(domain).hostname : domain]
1925
}
2026

2127
// Generate wildcard patterns for a domain
2228
function generateWildcardPatterns(domain: string): string[] {
2329
const patterns = new Set<string>()
24-
patterns.add(domain) // Add exact domain
30+
patterns.add(domain)
2531

2632
// Split domain into parts (e.g., "test.local" -> ["test", "local"])
2733
const parts = domain.split('.')
@@ -33,9 +39,26 @@ function generateWildcardPatterns(domain: string): string[] {
3339
return Array.from(patterns)
3440
}
3541

36-
function generateBaseConfig(domains: string[], verbose?: boolean): TlsConfig {
42+
function generateBaseConfig(options: ReverseProxyConfigs, verbose?: boolean): TlsConfig {
43+
const domains = extractDomains(options)
3744
const sslBase = path.join(os.homedir(), '.stacks', 'ssl')
38-
45+
console.log('config.https', config.https)
46+
const httpsConfig: Partial<CustomTlsConfig> = options.https === true
47+
? {
48+
caCertPath: path.join(sslBase, 'rpx-ca.crt'),
49+
certPath: path.join(sslBase, 'rpx.crt'),
50+
keyPath: path.join(sslBase, 'rpx.key'),
51+
}
52+
: typeof config.https === 'object'
53+
? {
54+
...options.https,
55+
...config.https,
56+
}
57+
: {}
58+
59+
debugLog('ssl', `Extracted domains: ${domains.join(', ')}`, verbose)
60+
debugLog('ssl', `Using SSL base path: ${sslBase}`, verbose)
61+
debugLog('ssl', `Using HTTPS config: ${JSON.stringify(httpsConfig)}`, verbose)
3962
// Generate all possible SANs, including wildcards
4063
const allPatterns = new Set<string>()
4164
domains.forEach((domain) => {
@@ -54,31 +77,29 @@ function generateBaseConfig(domains: string[], verbose?: boolean): TlsConfig {
5477
debugLog('ssl', `Generated domain patterns: ${uniqueDomains.join(', ')}`, verbose)
5578

5679
// Create a single object that contains all the config
57-
const config: TlsConfig = {
80+
return {
81+
// Use the first domain for the certificate CN
5882
domain: domains[0],
5983
hostCertCN: domains[0],
60-
caCertPath: path.join(sslBase, 'rpx-root-ca.crt'),
61-
certPath: path.join(sslBase, 'rpx-certificate.crt'),
62-
keyPath: path.join(sslBase, 'rpx-certificate.key'),
63-
altNameIPs: ['127.0.0.1', '::1'],
64-
// altNameURIs needs to be an empty array as we're using DNS names instead
65-
altNameURIs: [],
66-
// The real domains go in the commonName and subject alternative names
67-
commonName: domains[0],
68-
organizationName: 'RPX Local Development',
69-
countryName: 'US',
70-
stateName: 'California',
71-
localityName: 'Playa Vista',
72-
validityDays: 825,
84+
caCertPath: httpsConfig?.caCertPath ?? path.join(sslBase, 'rpx-ca.crt'),
85+
certPath: httpsConfig?.certPath ?? path.join(sslBase, 'rpx.crt'),
86+
keyPath: httpsConfig?.keyPath ?? path.join(sslBase, 'rpx.key'),
87+
altNameIPs: httpsConfig?.altNameIPs ?? ['127.0.0.1', '::1'],
88+
altNameURIs: httpsConfig?.altNameURIs ?? [],
89+
// Include all domains in the SAN
90+
commonName: httpsConfig?.commonName ?? domains[0],
91+
organizationName: httpsConfig?.organizationName ?? 'Local Development',
92+
countryName: httpsConfig?.countryName ?? 'US',
93+
stateName: httpsConfig?.stateName ?? 'California',
94+
localityName: httpsConfig?.localityName ?? 'Playa Vista',
95+
validityDays: httpsConfig?.validityDays ?? 825,
7396
verbose: verbose ?? false,
74-
// Add Subject Alternative Names as DNS names
97+
// Add all domains as Subject Alternative Names
7598
subjectAltNames: uniqueDomains.map(domain => ({
7699
type: 2, // DNS type
77100
value: domain,
78101
})),
79-
}
80-
81-
return config
102+
} satisfies TlsConfig
82103
}
83104

84105
function generateRootCAConfig(): TlsConfig {
@@ -102,21 +123,23 @@ function generateRootCAConfig(): TlsConfig {
102123
}
103124
}
104125

105-
export function httpsConfig(options: ReverseProxyOption | ReverseProxyOptions): TlsConfig {
106-
const domains = extractDomains(options)
107-
const verbose = Array.isArray(options) ? options[0]?.verbose : options.verbose
108-
109-
return generateBaseConfig(domains, verbose)
126+
export function httpsConfig(options: ReverseProxyConfigs): TlsConfig {
127+
return generateBaseConfig(options, options.verbose)
110128
}
111129

112-
export async function generateCertificate(options: ReverseProxyOption | ReverseProxyOptions): Promise<void> {
130+
export async function generateCertificate(options: ReverseProxyConfigs): Promise<void> {
113131
if (cachedSSLConfig) {
114-
debugLog('ssl', 'Using cached SSL configuration', Array.isArray(options) ? options[0]?.verbose : options.verbose)
132+
const verbose = isMultiProxyConfig(options) ? options.verbose : options.verbose
133+
debugLog('ssl', 'Using cached SSL configuration', verbose)
115134
return
116135
}
117136

118-
const domains = extractDomains(options)
119-
const verbose = Array.isArray(options) ? options[0]?.verbose : options.verbose
137+
// Get all unique domains from the configuration
138+
const domains = isMultiProxyConfig(options)
139+
? [options.proxies[0].to, ...options.proxies.map(proxy => proxy.to)] // Include the first domain from proxies array
140+
: [options.to]
141+
142+
const verbose = isMultiProxyConfig(options) ? options.verbose : options.verbose
120143

121144
debugLog('ssl', `Generating certificate for domains: ${domains.join(', ')}`, verbose)
122145

@@ -126,7 +149,7 @@ export async function generateCertificate(options: ReverseProxyOption | ReverseP
126149
const caCert = await createRootCA(rootCAConfig)
127150

128151
// Generate the host certificate with all domains
129-
const hostConfig = generateBaseConfig(domains, verbose)
152+
const hostConfig = generateBaseConfig(options, verbose)
130153
log.info(`Generating host certificate for: ${domains.join(', ')}`)
131154

132155
const hostCert = await generateCert({

src/start.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable no-console */
22
import type { SecureServerOptions } from 'node:http2'
33
import type { ServerOptions } from 'node:https'
4-
import type { ProxySetupOptions, ReverseProxyConfig, ReverseProxyOption, ReverseProxyOptions, SSLConfig } from './types'
4+
import type { BaseReverseProxyConfig, MultiReverseProxyConfig, ProxySetupOptions, ReverseProxyConfigs, ReverseProxyOption, ReverseProxyOptions, SingleReverseProxyConfig, SSLConfig } from './types'
55
import * as fs from 'node:fs'
66
import * as http from 'node:http'
77
import * as https from 'node:https'
@@ -216,7 +216,7 @@ async function testConnection(hostname: string, port: number, verbose?: boolean)
216216
})
217217
}
218218

219-
export async function startServer(options: ReverseProxyConfig): Promise<void> {
219+
export async function startServer(options: SingleReverseProxyConfig): Promise<void> {
220220
debugLog('server', `Starting server with options: ${JSON.stringify(options)}`, options.verbose)
221221

222222
// Parse URLs early to get the hostnames
@@ -538,7 +538,7 @@ export function startHttpRedirectServer(verbose?: boolean): void {
538538
export function startProxy(options: ReverseProxyOption): void {
539539
debugLog('proxy', `Starting proxy with options: ${JSON.stringify(options)}`, options?.verbose)
540540

541-
const serverOptions: ReverseProxyConfig = {
541+
const serverOptions: SingleReverseProxyConfig = {
542542
from: options?.from || 'localhost:5173',
543543
to: options?.to || 'stacks.localhost',
544544
https: httpsConfig(options),
@@ -563,16 +563,25 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
563563
if (!options)
564564
return
565565

566-
debugLog('proxies', `Starting proxies setup`, Array.isArray(options) ? options[0]?.verbose : options.verbose)
566+
debugLog('proxies', 'Starting proxies setup', isMultiProxyConfig(options) ? options.verbose : options.verbose)
567567

568-
// Convert single option to array for consistent handling
569-
const proxyOptions = Array.isArray(options) ? options : [options]
568+
console.log('options', options)
570569

571-
// Generate certificate once for all domains
572-
if (proxyOptions.some(opt => opt.https)) {
573-
await generateCertificate(proxyOptions)
570+
if (options.https) {
571+
await generateCertificate(options as ReverseProxyConfigs)
574572
}
575573

574+
// Convert configurations to a flat array of proxy configs
575+
const proxyOptions = isMultiProxyConfig(options)
576+
? options.proxies.map(proxy => ({
577+
...proxy,
578+
https: options.https,
579+
etcHostsCleanup: options.etcHostsCleanup,
580+
verbose: options.verbose,
581+
_cachedSSLConfig: options._cachedSSLConfig,
582+
}))
583+
: [options]
584+
576585
// Now start all proxies with the cached SSL config
577586
for (const option of proxyOptions) {
578587
try {
@@ -601,3 +610,7 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
601610
}
602611
}
603612
}
613+
614+
function isMultiProxyConfig(options: ReverseProxyConfigs): options is MultiReverseProxyConfig {
615+
return 'proxies' in options
616+
}

0 commit comments

Comments
 (0)