@@ -11,6 +11,7 @@ import { bold, dim, green, log } from '@stacksjs/cli'
11
11
import { version } from '../package.json'
12
12
import { generateCertficate } from './certificate'
13
13
import { config } from './config'
14
+ import { debugLog } from './utils'
14
15
15
16
// Keep track of all running servers for cleanup
16
17
const activeServers : Set < http . Server | https . Server > = new Set ( )
@@ -19,6 +20,7 @@ const activeServers: Set<http.Server | https.Server> = new Set()
19
20
* Cleanup function to close all servers and exit gracefully
20
21
*/
21
22
function cleanup ( ) {
23
+ debugLog ( 'cleanup' , 'Starting cleanup process' )
22
24
console . log ( `\n` )
23
25
log . info ( 'Shutting down proxy servers...' )
24
26
@@ -29,9 +31,11 @@ function cleanup() {
29
31
)
30
32
31
33
Promise . all ( closePromises ) . then ( ( ) => {
34
+ debugLog ( 'cleanup' , 'All servers closed successfully' )
32
35
log . success ( 'All proxy servers shut down successfully' )
33
36
process . exit ( 0 )
34
37
} ) . catch ( ( err ) => {
38
+ debugLog ( 'cleanup' , `Error during cleanup: ${ err . message } ` )
35
39
log . error ( 'Error during shutdown:' , err )
36
40
process . exit ( 1 )
37
41
} )
@@ -41,6 +45,7 @@ function cleanup() {
41
45
process . on ( 'SIGINT' , cleanup )
42
46
process . on ( 'SIGTERM' , cleanup )
43
47
process . on ( 'uncaughtException' , ( err ) => {
48
+ debugLog ( 'process' , `Uncaught exception: ${ err . message } ` )
44
49
log . error ( 'Uncaught exception:' , err )
45
50
cleanup ( )
46
51
} )
@@ -49,14 +54,20 @@ process.on('uncaughtException', (err) => {
49
54
* Load SSL certificates from files or use provided strings
50
55
*/
51
56
async function loadSSLConfig ( options : ReverseProxyOption ) : Promise < SSLConfig | null > {
57
+ debugLog ( 'ssl' , 'Loading SSL configuration' )
58
+
52
59
// If HTTPS is explicitly disabled, return null
53
- if ( options . https === false )
60
+ if ( options . https === false ) {
61
+ debugLog ( 'ssl' , 'HTTPS is disabled, skipping SSL configuration' )
54
62
return null
63
+ }
55
64
56
65
// If HTTPS is true and paths are specified, they must exist
57
66
if ( options . https === true && ( options . keyPath || options . certPath ) ) {
67
+ debugLog ( 'ssl' , 'Loading SSL certificates from files' )
58
68
if ( ! options . keyPath || ! options . certPath ) {
59
69
const missing = ! options . keyPath ? 'keyPath' : 'certPath'
70
+ debugLog ( 'ssl' , `Missing required SSL path: ${ missing } ` )
60
71
throw new Error ( `SSL Configuration requires both keyPath and certPath. Missing: ${ missing } ` )
61
72
}
62
73
@@ -76,14 +87,17 @@ async function loadSSLConfig(options: ReverseProxyOption): Promise<SSLConfig | n
76
87
// If caCertPath is specified, it must exist too
77
88
let ca : string | undefined
78
89
if ( options . caCertPath ) {
90
+ debugLog ( 'ssl' , 'Loading CA certificate' )
79
91
try {
80
92
ca = await fs . promises . readFile ( options . caCertPath , 'utf8' )
81
93
}
82
94
catch ( err ) {
95
+ debugLog ( 'ssl' , `Failed to read CA certificate: ${ ( err as Error ) . message } ` )
83
96
throw new Error ( `Failed to read CA certificate: ${ ( err as Error ) . message } ` )
84
97
}
85
98
}
86
99
100
+ debugLog ( 'ssl' , 'SSL certificates loaded successfully' )
87
101
return { key, cert, ...( ca ? { ca } : { } ) }
88
102
}
89
103
catch ( err ) {
@@ -92,40 +106,48 @@ async function loadSSLConfig(options: ReverseProxyOption): Promise<SSLConfig | n
92
106
? `File not found: ${ error . path } `
93
107
: error . message
94
108
109
+ debugLog ( 'ssl' , `SSL Configuration Error: ${ detail } ` )
95
110
throw new Error ( `SSL Configuration Error:\n${ detail } .\nHTTPS was explicitly enabled but certificate files are missing.` )
96
111
}
97
112
}
98
113
99
114
// If HTTPS is true but no paths specified, check for direct cert content
100
115
if ( options . https === true && options . key && options . cert ) {
116
+ debugLog ( 'ssl' , 'Using provided SSL certificate strings' )
101
117
return {
102
118
key : options . key ,
103
119
cert : options . cert ,
104
120
}
105
121
}
106
122
107
- // If HTTPS is true but no certificates provided at all, throw error
108
- if ( options . https === true )
123
+ // If HTTPS is true but no certificates provided at all, generate certificate
124
+ if ( options . https === true ) {
125
+ debugLog ( 'ssl' , 'Generating self-signed certificate' )
109
126
return await generateCertficate ( options )
127
+ }
110
128
111
129
// Default to no SSL
130
+ debugLog ( 'ssl' , 'No SSL configuration provided' )
112
131
return null
113
132
}
114
133
115
134
/**
116
135
* Check if a port is in use
117
136
*/
118
137
function isPortInUse ( port : number , hostname : string ) : Promise < boolean > {
138
+ debugLog ( 'port' , `Checking if port ${ port } is in use on ${ hostname } ` )
119
139
return new Promise ( ( resolve ) => {
120
140
const server = net . createServer ( )
121
141
122
142
server . once ( 'error' , ( err : NodeJS . ErrnoException ) => {
123
143
if ( err . code === 'EADDRINUSE' ) {
144
+ debugLog ( 'port' , `Port ${ port } is in use` )
124
145
resolve ( true )
125
146
}
126
147
} )
127
148
128
149
server . once ( 'listening' , ( ) => {
150
+ debugLog ( 'port' , `Port ${ port } is available` )
129
151
server . close ( )
130
152
resolve ( false )
131
153
} )
@@ -138,46 +160,50 @@ function isPortInUse(port: number, hostname: string): Promise<boolean> {
138
160
* Find next available port
139
161
*/
140
162
async function findAvailablePort ( startPort : number , hostname : string ) : Promise < number > {
163
+ debugLog ( 'port' , `Finding available port starting from ${ startPort } ` )
141
164
let port = startPort
142
165
while ( await isPortInUse ( port , hostname ) ) {
143
- log . debug ( `Port ${ port } is in use, trying ${ port + 1 } ` )
166
+ debugLog ( 'port' , `Port ${ port } is in use, trying ${ port + 1 } ` )
144
167
port ++
145
168
}
169
+ debugLog ( 'port' , `Found available port: ${ port } ` )
146
170
return port
147
171
}
148
172
149
173
/**
150
174
* Test connection to a server
151
175
*/
152
176
async function testConnection ( hostname : string , port : number ) : Promise < void > {
177
+ debugLog ( 'connection' , `Testing connection to ${ hostname } :${ port } ` )
153
178
return new Promise < void > ( ( resolve , reject ) => {
154
- log . debug ( `Testing connection to ${ hostname } :${ port } ...` )
155
-
156
179
const socket = net . connect ( {
157
180
host : hostname ,
158
181
port,
159
182
timeout : 5000 , // 5 second timeout
160
183
} )
161
184
162
185
socket . once ( 'connect' , ( ) => {
163
- log . debug ( `Successfully connected to ${ hostname } :${ port } ` )
186
+ debugLog ( 'connection' , `Successfully connected to ${ hostname } :${ port } ` )
164
187
socket . end ( )
165
188
resolve ( )
166
189
} )
167
190
168
191
socket . once ( 'timeout' , ( ) => {
192
+ debugLog ( 'connection' , `Connection to ${ hostname } :${ port } timed out` )
169
193
socket . destroy ( )
170
194
reject ( new Error ( `Connection to ${ hostname } :${ port } timed out` ) )
171
195
} )
172
196
173
197
socket . once ( 'error' , ( err ) => {
198
+ debugLog ( 'connection' , `Failed to connect to ${ hostname } :${ port } : ${ err . message } ` )
174
199
socket . destroy ( )
175
200
reject ( new Error ( `Failed to connect to ${ hostname } :${ port } : ${ err . message } ` ) )
176
201
} )
177
202
} )
178
203
}
179
204
180
205
export async function startServer ( options ?: ReverseProxyOption ) : Promise < void > {
206
+ debugLog ( 'server' , `Starting server with options: ${ JSON . stringify ( options ) } ` )
181
207
// Merge with default config
182
208
const mergedOptions : ReverseProxyOption = {
183
209
...config ,
@@ -187,6 +213,8 @@ export async function startServer(options?: ReverseProxyOption): Promise<void> {
187
213
const { fromUrl, toUrl, protocol } = normalizeUrls ( mergedOptions )
188
214
const fromPort = Number . parseInt ( fromUrl . port ) || ( protocol . includes ( 'https:' ) ? 443 : 80 )
189
215
216
+ debugLog ( 'server' , `Normalized URLs - From: ${ fromUrl } , To: ${ toUrl } , Protocol: ${ protocol } ` )
217
+
190
218
// Load SSL config first to fail fast if certificates are missing
191
219
const sslConfig = await loadSSLConfig ( mergedOptions )
192
220
@@ -195,6 +223,7 @@ export async function startServer(options?: ReverseProxyOption): Promise<void> {
195
223
await testConnection ( fromUrl . hostname , fromPort )
196
224
}
197
225
catch ( err ) {
226
+ debugLog ( 'server' , `Connection test failed: ${ ( err as Error ) . message } ` )
198
227
log . error ( ( err as Error ) . message )
199
228
process . exit ( 1 )
200
229
}
@@ -222,9 +251,11 @@ async function createProxyServer(
222
251
ssl : SSLConfig | null ,
223
252
options : ReverseProxyOption ,
224
253
) : Promise < void > {
254
+ debugLog ( 'proxy' , `Creating proxy server ${ from } -> ${ to } ` )
225
255
const useHttps = options . https === true
226
256
227
257
const requestHandler = ( req : http . IncomingMessage , res : http . ServerResponse ) => {
258
+ debugLog ( 'request' , `${ req . method } ${ req . url } ` )
228
259
const proxyOptions = {
229
260
hostname : sourceUrl . hostname ,
230
261
port : fromPort ,
@@ -237,6 +268,7 @@ async function createProxyServer(
237
268
}
238
269
239
270
const proxyReq = http . request ( proxyOptions , ( proxyRes ) => {
271
+ debugLog ( 'response' , `${ proxyRes . statusCode } ${ req . url } ` )
240
272
// Add security headers
241
273
const headers = {
242
274
...proxyRes . headers ,
@@ -249,6 +281,7 @@ async function createProxyServer(
249
281
} )
250
282
251
283
proxyReq . on ( 'error' , ( err ) => {
284
+ debugLog ( 'proxy' , `Proxy request failed: ${ err . message } ` )
252
285
log . error ( 'Proxy request failed:' , err )
253
286
res . writeHead ( 502 )
254
287
res . end ( `Proxy Error: ${ err . message } ` )
@@ -286,19 +319,15 @@ async function createProxyServer(
286
319
287
320
if ( ssl ) {
288
321
server . on ( 'secureConnection' , ( tlsSocket ) => {
289
- log . debug ( 'TLS Connection:' , {
290
- protocol : tlsSocket . getProtocol ?.( ) ,
291
- cipher : tlsSocket . getCipher ?.( ) ,
292
- authorized : tlsSocket . authorized ,
293
- authError : tlsSocket . authorizationError ,
294
- } )
322
+ debugLog ( 'tls' , `TLS Connection - Protocol: ${ tlsSocket . getProtocol ?.( ) } , Cipher: ${ tlsSocket . getCipher ?.( ) } , Authorized: ${ tlsSocket . authorized } ` )
295
323
} )
296
324
}
297
325
298
326
activeServers . add ( server )
299
327
300
328
return new Promise ( ( resolve , reject ) => {
301
329
server . listen ( listenPort , hostname , ( ) => {
330
+ debugLog ( 'server' , `Server listening on port ${ listenPort } ` )
302
331
console . log ( '' )
303
332
console . log ( ` ${ green ( bold ( 'reverse-proxy' ) ) } ${ green ( `v${ version } ` ) } ` )
304
333
console . log ( '' )
@@ -316,11 +345,15 @@ async function createProxyServer(
316
345
resolve ( )
317
346
} )
318
347
319
- server . on ( 'error' , reject )
348
+ server . on ( 'error' , ( err ) => {
349
+ debugLog ( 'server' , `Server error: ${ ( err as Error ) . message } ` )
350
+ reject ( err )
351
+ } )
320
352
} )
321
353
}
322
354
323
355
export async function setupReverseProxy ( options : ProxySetupOptions ) : Promise < void > {
356
+ debugLog ( 'setup' , `Setting up reverse proxy with options: ${ JSON . stringify ( options ) } ` )
324
357
const { from, to, fromPort, sourceUrl, ssl } = options
325
358
const httpPort = 80
326
359
const httpsPort = 443
@@ -329,39 +362,48 @@ export async function setupReverseProxy(options: ProxySetupOptions): Promise<voi
329
362
330
363
try {
331
364
if ( useHttps && ssl ) {
365
+ debugLog ( 'setup' , 'Checking HTTP port for redirect server' )
332
366
const isHttpPortBusy = await isPortInUse ( httpPort , hostname )
333
367
if ( ! isHttpPortBusy ) {
368
+ debugLog ( 'setup' , 'Starting HTTP redirect server' )
334
369
startHttpRedirectServer ( )
335
370
}
336
371
else {
372
+ debugLog ( 'setup' , 'Port 80 is in use, skipping HTTP redirect server' )
337
373
log . warn ( 'Port 80 is in use, HTTP to HTTPS redirect will not be available' )
338
374
}
339
375
}
340
376
341
377
const targetPort = useHttps ? httpsPort : httpPort
378
+ debugLog ( 'setup' , `Checking if target port ${ targetPort } is available` )
342
379
const isTargetPortBusy = await isPortInUse ( targetPort , hostname )
343
380
344
381
if ( isTargetPortBusy ) {
382
+ debugLog ( 'setup' , `Target port ${ targetPort } is busy, finding alternative` )
345
383
const availablePort = await findAvailablePort ( useHttps ? 8443 : 8080 , hostname )
346
384
log . warn ( `Port ${ targetPort } is in use. Using port ${ availablePort } instead.` )
347
385
log . info ( `You can use 'sudo lsof -i :${ targetPort } ' (Unix) or 'netstat -ano | findstr :${ targetPort } ' (Windows) to check what's using the port.` )
348
386
349
387
await createProxyServer ( from , to , fromPort , availablePort , hostname , sourceUrl , ssl , options )
350
388
}
351
389
else {
390
+ debugLog ( 'setup' , `Creating proxy server on port ${ targetPort } ` )
352
391
await createProxyServer ( from , to , fromPort , targetPort , hostname , sourceUrl , ssl , options )
353
392
}
354
393
}
355
394
catch ( err ) {
395
+ debugLog ( 'setup' , `Failed to setup reverse proxy: ${ ( err as Error ) . message } ` )
356
396
log . error ( `Failed to setup reverse proxy: ${ ( err as Error ) . message } ` )
357
397
cleanup ( )
358
398
}
359
399
}
360
400
361
401
export function startHttpRedirectServer ( ) : void {
402
+ debugLog ( 'redirect' , 'Starting HTTP redirect server' )
362
403
const server = http
363
404
. createServer ( ( req , res ) => {
364
405
const host = req . headers . host || ''
406
+ debugLog ( 'redirect' , `Redirecting ${ req . url } to https://${ host } ${ req . url } ` )
365
407
res . writeHead ( 301 , {
366
408
Location : `https://${ host } ${ req . url } ` ,
367
409
} )
@@ -373,13 +415,15 @@ export function startHttpRedirectServer(): void {
373
415
}
374
416
375
417
export function startProxy ( options ?: ReverseProxyOption ) : void {
418
+ debugLog ( 'proxy' , `Starting proxy with options: ${ JSON . stringify ( options ) } ` )
376
419
const finalOptions = {
377
420
...config ,
378
421
...options ,
379
422
}
380
423
381
424
// Remove SSL-related properties if HTTPS is disabled
382
425
if ( finalOptions . https === false ) {
426
+ debugLog ( 'proxy' , 'HTTPS disabled, removing SSL options' )
383
427
delete finalOptions . keyPath
384
428
delete finalOptions . certPath
385
429
delete finalOptions . caCertPath
@@ -399,21 +443,27 @@ export function startProxy(options?: ReverseProxyOption): void {
399
443
} )
400
444
401
445
startServer ( finalOptions ) . catch ( ( err ) => {
446
+ debugLog ( 'proxy' , `Failed to start proxy: ${ err . message } ` )
402
447
log . error ( `Failed to start proxy: ${ err . message } ` )
403
448
cleanup ( )
404
449
} )
405
450
}
406
451
407
452
export function startProxies ( options ?: ReverseProxyOptions ) : void {
453
+ debugLog ( 'proxies' , 'Starting multiple proxies' )
408
454
if ( Array . isArray ( options ) ) {
455
+ debugLog ( 'proxies' , `Starting ${ options . length } proxies` )
409
456
Promise . all ( options . map ( option => startServer ( option ) ) )
410
457
. catch ( ( err ) => {
458
+ debugLog ( 'proxies' , `Failed to start proxies: ${ err . message } ` )
411
459
log . error ( 'Failed to start proxies:' , err )
412
460
cleanup ( )
413
461
} )
414
462
}
415
463
else if ( options ) {
464
+ debugLog ( 'proxies' , 'Starting single proxy' )
416
465
startServer ( options ) . catch ( ( err ) => {
466
+ debugLog ( 'proxies' , `Failed to start proxy: ${ err . message } ` )
417
467
log . error ( 'Failed to start proxy:' , err )
418
468
cleanup ( )
419
469
} )
@@ -424,13 +474,16 @@ export function startProxies(options?: ReverseProxyOptions): void {
424
474
* Create normalized URLs with correct protocol
425
475
*/
426
476
function normalizeUrls ( options : ReverseProxyOption ) {
477
+ debugLog ( 'urls' , 'Normalizing URLs' )
427
478
const useHttps = options . https === true
428
479
const protocol = useHttps ? 'https://' : 'http://'
429
480
430
481
// Use default values if from/to are undefined
431
482
const fromString = options . from || 'localhost:5173'
432
483
const toString = options . to || 'stacks.localhost'
433
484
485
+ debugLog ( 'urls' , `Original URLs - From: ${ fromString } , To: ${ toString } ` )
486
+
434
487
const fromUrl = new URL ( fromString . startsWith ( 'http' )
435
488
? fromString
436
489
: `${ protocol } ${ fromString } ` )
@@ -439,6 +492,8 @@ function normalizeUrls(options: ReverseProxyOption) {
439
492
? toString
440
493
: `${ protocol } ${ toString } ` )
441
494
495
+ debugLog ( 'urls' , `Normalized URLs - From: ${ fromUrl } , To: ${ toUrl } , Protocol: ${ protocol } ` )
496
+
442
497
return {
443
498
fromUrl,
444
499
toUrl,
0 commit comments