@@ -2,9 +2,27 @@ import http from 'http';
22import fetch from 'node-fetch' ;
33import { pipeline } from 'stream' ;
44import { promisify } from 'util' ;
5+ import { Registry , Counter , Histogram } from 'prom-client' ;
56
67const streamPipeline = promisify ( pipeline ) ;
78
9+ // Create a registry and metrics
10+ const register = new Registry ( ) ;
11+ const httpRequestsTotal = new Counter ( {
12+ name : 'http_requests_total' ,
13+ help : 'Total number of HTTP requests' ,
14+ labelNames : [ 'method' , 'status' ]
15+ } ) ;
16+ const httpRequestDuration = new Histogram ( {
17+ name : 'http_request_duration_seconds' ,
18+ help : 'HTTP request duration in seconds' ,
19+ labelNames : [ 'method' ]
20+ } ) ;
21+
22+ // Register the metrics
23+ register . registerMetric ( httpRequestsTotal ) ;
24+ register . registerMetric ( httpRequestDuration ) ;
25+
826const USER_AGENT =
927 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" ;
1028
@@ -21,17 +39,28 @@ const LAN_DENYLIST = [
2139 / ^ 1 0 \. / ,
2240 / ^ 1 9 2 \. 1 6 8 \. / ,
2341 / ^ 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 - 1 ] ) \. /
24- ] ;
42+ ] ;
2543
2644const server = http . createServer ( async ( req , res ) => {
45+ const start = process . hrtime ( ) ;
46+
47+ // Handle metrics endpoint
48+ if ( req . url === '/metrics' ) {
49+ res . setHeader ( 'Content-Type' , register . contentType ) ;
50+ res . end ( await register . metrics ( ) ) ;
51+ return ;
52+ }
53+
2754 const origin = req . headers . origin ;
2855 if ( ! ORIGIN_ALLOWLIST . includes ( origin ?. toLowerCase ( ) ) ) {
2956 res . writeHead ( 403 ) ;
57+ httpRequestsTotal . inc ( { method : req . method , status : 403 } ) ;
3058 return res . end ( 'Forbidden' ) ;
3159 }
3260
3361 if ( ! [ 'GET' , 'HEAD' , 'OPTIONS' ] . includes ( req . method ) ) {
3462 res . writeHead ( 405 ) ;
63+ httpRequestsTotal . inc ( { method : req . method , status : 405 } ) ;
3564 return res . end ( 'Method Not Allowed' ) ;
3665 }
3766
@@ -42,13 +71,15 @@ const server = http.createServer(async (req, res) => {
4271 'Access-Control-Allow-Headers' : '*' ,
4372 'Access-Control-Max-Age' : '86400' ,
4473 } ) ;
74+ httpRequestsTotal . inc ( { method : req . method , status : 204 } ) ;
4575 return res . end ( ) ;
4676 }
4777
4878 const urlObj = new URL ( req . url , `http://${ req . headers . host } ` ) ;
4979 const rawTarget = urlObj . searchParams . get ( 'url' ) ;
5080 if ( ! rawTarget ) {
5181 res . writeHead ( 400 ) ;
82+ httpRequestsTotal . inc ( { method : req . method , status : 400 } ) ;
5283 return res . end ( "Missing 'url' parameter" ) ;
5384 }
5485
@@ -58,14 +89,16 @@ const server = http.createServer(async (req, res) => {
5889 new URL ( targetUrl ) ; // validates format
5990 } catch {
6091 res . writeHead ( 400 ) ;
92+ httpRequestsTotal . inc ( { method : req . method , status : 400 } ) ;
6193 return res . end ( "Invalid URL" ) ;
6294 }
6395
6496 const targetHostname = new URL ( targetUrl ) . hostname ;
6597 if ( LAN_DENYLIST . some ( rx => rx . test ( targetHostname ) ) ) {
6698 res . writeHead ( 403 ) ;
99+ httpRequestsTotal . inc ( { method : req . method , status : 403 } ) ;
67100 return res . end ( 'Forbidden' ) ;
68- }
101+ }
69102
70103 try {
71104 // Clone headers and override necessary ones
@@ -93,14 +126,14 @@ const server = http.createServer(async (req, res) => {
93126 // Convert and sanitize response headers
94127 const rawHeaders = Object . fromEntries ( upstream . headers . entries ( ) ) ;
95128 for ( const key of Object . keys ( rawHeaders ) ) {
96- const lower = key . toLowerCase ( ) ;
97- if (
98- lower . startsWith ( 'access-control-' ) ||
99- lower === 'content-encoding' ||
100- lower === 'transfer-encoding'
101- ) {
102- delete rawHeaders [ key ] ;
103- }
129+ const lower = key . toLowerCase ( ) ;
130+ if (
131+ lower . startsWith ( 'access-control-' ) ||
132+ lower === 'content-encoding' ||
133+ lower === 'transfer-encoding'
134+ ) {
135+ delete rawHeaders [ key ] ;
136+ }
104137 }
105138
106139 rawHeaders [ 'Access-Control-Allow-Origin' ] = origin ;
@@ -114,15 +147,37 @@ const server = http.createServer(async (req, res) => {
114147 }
115148
116149 res . writeHead ( upstream . status , rawHeaders ) ;
150+ httpRequestsTotal . inc ( { method : req . method , status : upstream . status } ) ;
151+
152+ // Handle client disconnection
153+ req . on ( 'close' , ( ) => {
154+ if ( ! res . writableEnded ) {
155+ res . destroy ( ) ;
156+ }
157+ } ) ;
158+
117159 await streamPipeline ( upstream . body , res ) ;
118160 } catch ( e ) {
119161 console . error ( `Fetch failed: ${ e . message } ` ) ;
120162 console . error ( e . stack ) ;
121- res . writeHead ( 500 ) ;
122- res . end ( `Error: ${ e . message } ` ) ;
163+
164+ // Only send error response if headers haven't been sent
165+ if ( ! res . headersSent ) {
166+ res . writeHead ( 500 ) ;
167+ httpRequestsTotal . inc ( { method : req . method , status : 500 } ) ;
168+ res . end ( `Error: ${ e . message } ` ) ;
169+ } else {
170+ // If headers were sent, just destroy the response
171+ res . destroy ( ) ;
172+ }
173+ } finally {
174+ const [ seconds , nanoseconds ] = process . hrtime ( start ) ;
175+ const duration = seconds + nanoseconds / 1e9 ;
176+ httpRequestDuration . observe ( { method : req . method } , duration ) ;
123177 }
124178} ) ;
125179
126180server . listen ( 8080 , ( ) => {
127181 console . log ( 'CORS proxy listening on http://0.0.0.0:8080' ) ;
182+ console . log ( 'Metrics available at http://0.0.0.0:8080/metrics' ) ;
128183} ) ;
0 commit comments