4
4
IncomingMessage ,
5
5
ClientRequest ,
6
6
ServerResponse ,
7
+ IncomingHttpHeaders ,
7
8
} from 'node:http' ;
8
9
import {
9
10
createServer as createHttpsServer ,
@@ -16,6 +17,7 @@ import { readdir, readFile } from 'node:fs/promises';
16
17
import { join } from 'node:path' ;
17
18
import { EventEmitter } from 'node:events' ;
18
19
import { existsSync } from 'node:fs' ;
20
+ import { Socket } from 'node:net' ;
19
21
20
22
export class ProxyEntry {
21
23
readonly domain : string ;
@@ -41,7 +43,7 @@ interface ProxySettingsFromFile extends Partial<ProxySettings> {
41
43
export type MinimalProxyEntry = Partial < ProxyEntry > & Pick < ProxyEntry , 'domain' > ;
42
44
43
45
export class ProxySettings {
44
- readonly certificatesFolder : string = String ( process . env . PROXY_CERTS_FOLDER ) ;
46
+ readonly certificatesFolder : string = String ( process . env . PROXY_CERTS_FOLDER || process . cwd ( ) ) ;
45
47
readonly certificateFile : string = 'fullchain.pem' ;
46
48
readonly keyFile : string = 'privkey.pem' ;
47
49
readonly httpPort : number = Number ( process . env . HTTP_PORT ) || 80 ;
@@ -56,6 +58,12 @@ export class ProxySettings {
56
58
}
57
59
}
58
60
61
+ export type ProxyIncomingMessage = IncomingMessage & {
62
+ originHost : string ;
63
+ originlUrl : URL | null ;
64
+ proxyEntry : ProxyEntry ;
65
+ } ;
66
+
59
67
export class ProxyServer extends EventEmitter {
60
68
protected certs : Record < string , SecureContext > = { } ;
61
69
protected proxies : Array < MinimalProxyEntry > = [ ] ;
@@ -79,8 +87,8 @@ export class ProxyServer extends EventEmitter {
79
87
const ssl = this . getSslOptions ( ) ;
80
88
81
89
this . servers = [
82
- httpPort && createHttpServer ( ( req , res ) => this . handleRequest ( req , res , false ) ) . listen ( httpPort ) ,
83
- httpsPort && createHttpsServer ( ssl , ( req , res ) => this . handleRequest ( req , res , true ) ) . listen ( httpsPort ) ,
90
+ httpPort && this . setupServer ( createHttpServer ( ) , false ) . listen ( httpPort ) ,
91
+ httpsPort && this . setupServer ( createHttpsServer ( ssl ) , true ) . listen ( httpsPort ) ,
84
92
] . filter ( Boolean ) ;
85
93
}
86
94
@@ -115,31 +123,134 @@ export class ProxyServer extends EventEmitter {
115
123
return this ;
116
124
}
117
125
118
- handleRequest ( req : IncomingMessage , res : ServerResponse , isSsl ?: boolean ) {
126
+ onRequest ( _req : IncomingMessage , res : ServerResponse , isSsl : boolean ) {
127
+ const req = this . matchProxy ( _req ) ;
128
+ const { proxyEntry } = req ;
129
+
130
+ if ( ! proxyEntry ) {
131
+ return ;
132
+ }
133
+
134
+ const proxyRequest = this . createRequest ( req , res , isSsl ) ;
135
+
136
+ if ( ! proxyRequest ) {
137
+ return ;
138
+ }
139
+
140
+ req . on ( 'data' , ( chunk ) => proxyRequest . write ( chunk ) ) ;
141
+ req . on ( 'end' , ( ) => proxyRequest . end ( ) ) ;
142
+
143
+ proxyRequest . on ( 'error' , ( error ) => this . handleError ( error , res ) ) ;
144
+ proxyRequest . on ( 'response' , ( proxyRes ) => {
145
+ this . setHeaders ( proxyRes , res ) ;
146
+
147
+ const isCorsSimple = req . method !== 'OPTIONS' && proxyEntry . cors && req . headers . origin ;
148
+ if ( isCorsSimple ) {
149
+ this . setCorsHeaders ( req , res ) ;
150
+ }
151
+
152
+ res . writeHead ( proxyRes . statusCode , proxyRes . statusMessage ) ;
153
+
154
+ proxyRes . on ( 'data' , ( chunk ) => res . write ( chunk ) ) ;
155
+ proxyRes . on ( 'end' , ( ) => res . end ( ) ) ;
156
+ } ) ;
157
+ }
158
+
159
+ onUpgrade ( _req : IncomingMessage , socket : Socket , head : any , isSsl : boolean ) {
160
+ const notValid = _req . method !== 'GET' || ! _req . headers . upgrade || _req . headers . upgrade . toLowerCase ( ) !== 'websocket' ;
161
+ const req = this . matchProxy ( _req ) ;
162
+
163
+ if ( notValid || ! req . proxyEntry ) {
164
+ socket . destroy ( ) ;
165
+ return ;
166
+ }
167
+
168
+ const proxyReq = this . createRequest ( req , socket as any , isSsl ) ;
169
+
170
+ socket . setTimeout ( 0 ) ;
171
+ socket . setNoDelay ( true ) ;
172
+ socket . setKeepAlive ( true , 0 ) ;
173
+
174
+ if ( head && head . length ) {
175
+ socket . unshift ( head ) ;
176
+ }
177
+
178
+ proxyReq . on ( 'error' , ( error ) => this . emit ( 'proxyerror' , error ) ) ;
179
+ proxyReq . on ( 'upgrade' , ( proxyRes , proxySocket , proxyHead ) => {
180
+ proxySocket . on ( 'error' , ( error ) => this . emit ( 'proxyerror' , error ) ) ;
181
+
182
+ socket . on ( 'error' , ( error ) => {
183
+ this . emit ( 'proxyerror' , error ) ;
184
+ proxySocket . end ( ) ;
185
+ } ) ;
186
+
187
+ if ( proxyHead && proxyHead . length ) {
188
+ proxySocket . unshift ( proxyHead ) ;
189
+ }
190
+
191
+ socket . write ( this . createWebSocketResponseHeaders ( proxyRes . headers ) ) ;
192
+
193
+ proxySocket . pipe ( socket ) . pipe ( proxySocket ) ;
194
+ } ) ;
195
+
196
+ return proxyReq . end ( ) ;
197
+ }
198
+
199
+ protected createWebSocketResponseHeaders ( headers : IncomingHttpHeaders ) {
200
+ return (
201
+ Object . entries ( headers )
202
+ . reduce (
203
+ function ( head , next ) {
204
+ const [ key , value ] = next ;
205
+
206
+ if ( ! Array . isArray ( value ) ) {
207
+ head . push ( `${ key } : ${ value } ` ) ;
208
+ return head ;
209
+ }
210
+
211
+ for ( const next of value ) {
212
+ head . push ( `${ key } : ${ next } ` ) ;
213
+ }
214
+
215
+ return head ;
216
+ } ,
217
+ [ 'HTTP/1.1 101 Switching Protocols' ]
218
+ )
219
+ . join ( '\r\n' ) + '\r\n\r\n'
220
+ ) ;
221
+ }
222
+
223
+ protected matchProxy ( req : IncomingMessage ) {
119
224
const originHost = [ req . headers [ 'x-forwarded-for' ] , req . headers . host ] . filter ( Boolean ) [ 0 ] ;
120
225
const originlUrl = originHost ? new URL ( 'http://' + originHost ) : null ;
121
226
const proxyEntry = originlUrl ? this . findProxyEntry ( originlUrl . hostname , req . url ) : null ;
122
227
228
+ Object . assign ( req , { originHost, originlUrl, proxyEntry } ) ;
229
+
230
+ return req as ProxyIncomingMessage ;
231
+ }
232
+
233
+ protected createRequest ( req : ProxyIncomingMessage , res : ServerResponse , isSsl : boolean ) {
234
+ const { originHost, originlUrl, proxyEntry } = req ;
235
+
123
236
if ( this . settings . enableDebug ) {
124
- const _end = res . end ;
125
- res . end = ( ...args ) => {
237
+ res . on ( 'finish' , ( ) => {
126
238
console . log (
127
239
'[%s] %s %s [%s] => %d %s' ,
128
240
new Date ( ) . toISOString ( ) . slice ( 0 , 19 ) ,
129
241
req . method ,
130
242
req . url ,
131
243
originHost ,
132
244
res . statusCode ,
133
- proxyEntry ?. target || '(none)' ,
245
+ proxyEntry ?. target || '(none)'
134
246
) ;
135
-
136
- return _end . apply ( res , args ) ;
137
- } ;
247
+ } ) ;
138
248
}
139
249
140
250
if ( ! ( originlUrl && proxyEntry ) ) {
141
251
if ( this . settings . fallback ) {
142
- return this . settings . fallback ( req , res ) ;
252
+ this . settings . fallback ( req , res ) ;
253
+ return ;
143
254
}
144
255
145
256
res . writeHead ( 404 , 'Not found' ) ;
@@ -199,7 +310,8 @@ export class ProxyServer extends EventEmitter {
199
310
targetUrl . pathname = targetUrl . pathname . replace ( proxyEntry . path , '' ) ;
200
311
}
201
312
202
- const proxyRequest = ( targetUrl . protocol === 'https:' ? httpsRequest : httpRequest ) ( targetUrl , { method : req . method } ) ;
313
+ const requestOptions = { method : req . method } ;
314
+ const proxyRequest = ( targetUrl . protocol === 'https:' ? httpsRequest : httpRequest ) ( targetUrl , requestOptions ) ;
203
315
this . setHeaders ( req , proxyRequest ) ;
204
316
205
317
if ( proxyEntry . headers ) {
@@ -217,23 +329,14 @@ export class ProxyServer extends EventEmitter {
217
329
proxyRequest . setHeader ( 'x-forwarded-proto' , isSsl ? 'https' : 'http' ) ;
218
330
proxyRequest . setHeader ( 'forwarded' , 'host=' + req . headers . host + ';proto=' + ( isSsl ? 'https' : 'http' ) ) ;
219
331
220
- req . on ( 'data' , ( chunk ) => proxyRequest . write ( chunk ) ) ;
221
- req . on ( 'end' , ( ) => proxyRequest . end ( ) ) ;
222
-
223
- proxyRequest . on ( 'error' , ( error ) => this . handleError ( error , res ) ) ;
224
- proxyRequest . on ( 'response' , ( proxyRes ) => {
225
- this . setHeaders ( proxyRes , res ) ;
226
-
227
- const isCorsSimple = req . method !== 'OPTIONS' && proxyEntry . cors && req . headers . origin ;
228
- if ( isCorsSimple ) {
229
- this . setCorsHeaders ( req , res ) ;
230
- }
332
+ return proxyRequest ;
333
+ }
231
334
232
- res . writeHead ( proxyRes . statusCode , proxyRes . statusMessage ) ;
335
+ protected setupServer ( server : any , isSsl : boolean ) {
336
+ server . on ( 'request' , ( req , res ) => this . onRequest ( req , res , isSsl ) ) ;
337
+ server . on ( 'upgrade' , ( req , socket , head ) => this . onUpgrade ( req , socket , head , isSsl ) ) ;
233
338
234
- proxyRes . on ( 'data' , ( chunk ) => res . write ( chunk ) ) ;
235
- proxyRes . on ( 'end' , ( ) => res . end ( ) ) ;
236
- } ) ;
339
+ return server ;
237
340
}
238
341
239
342
protected async loadCertificate ( folder : string ) {
@@ -285,9 +388,8 @@ export class ProxyServer extends EventEmitter {
285
388
const byDomain = this . proxies . filter (
286
389
( p ) =>
287
390
p . domain === domainFromRequest ||
288
- ( p . domain . startsWith ( "*." ) &&
289
- ( p . domain . slice ( 2 ) === requestParentDomain ||
290
- p . domain . slice ( 2 ) === domainFromRequest ) )
391
+ ( p . domain . startsWith ( '*.' ) &&
392
+ ( p . domain . slice ( 2 ) === requestParentDomain || p . domain . slice ( 2 ) === domainFromRequest ) )
291
393
) ;
292
394
293
395
if ( byDomain . length === 1 ) {
@@ -301,9 +403,11 @@ export class ProxyServer extends EventEmitter {
301
403
// without path
302
404
// example.com => [target]
303
405
304
- return byDomain . find ( ( p ) => p . path && ( requestPath === p . path || requestPath . startsWith ( p . path + '/' ) ) )
305
- || byDomain . find ( ( p ) => ! p . path )
306
- || null ;
406
+ return (
407
+ byDomain . find ( ( p ) => p . path && ( requestPath === p . path || requestPath . startsWith ( p . path + '/' ) ) ) ||
408
+ byDomain . find ( ( p ) => ! p . path ) ||
409
+ null
410
+ ) ;
307
411
}
308
412
309
413
protected getSslOptions ( ) : HttpsServerOptions {
@@ -384,12 +488,12 @@ export class ProxyServer extends EventEmitter {
384
488
export async function loadConfig ( path ?: string , optional = false ) : Promise < ProxySettingsFromFile > {
385
489
if ( ! path ) {
386
490
const candidates = [
387
- join ( process . cwd ( ) , " proxy.config.mjs" ) ,
388
- join ( process . cwd ( ) , " proxy.config.js" ) ,
389
- join ( process . cwd ( ) , " proxy.config.json" ) ,
491
+ join ( process . cwd ( ) , ' proxy.config.mjs' ) ,
492
+ join ( process . cwd ( ) , ' proxy.config.js' ) ,
493
+ join ( process . cwd ( ) , ' proxy.config.json' ) ,
390
494
] ;
391
495
392
- path = candidates . find ( path => existsSync ( path ) ) ;
496
+ path = candidates . find ( ( path ) => existsSync ( path ) ) ;
393
497
}
394
498
395
499
if ( ! path || ! existsSync ( path ) ) {
@@ -401,9 +505,7 @@ export async function loadConfig(path?: string, optional = false): Promise<Proxy
401
505
}
402
506
403
507
if ( path . endsWith ( '.json' ) ) {
404
- return new ProxySettings (
405
- JSON . parse ( await readFile ( path , "utf-8" ) )
406
- ) ;
508
+ return new ProxySettings ( JSON . parse ( await readFile ( path , 'utf-8' ) ) ) ;
407
509
}
408
510
409
511
const mod = await import ( path ) ;
@@ -412,4 +514,4 @@ export async function loadConfig(path?: string, optional = false): Promise<Proxy
412
514
413
515
export function defineConfig ( config : ProxySettings ) {
414
516
return new ProxySettings ( config ) ;
415
- }
517
+ }
0 commit comments