@@ -14,10 +14,34 @@ import http from "http";
1414import https from "https" ;
1515import path from "path" ;
1616import net from "net" ;
17+ import zlib from "zlib" ;
1718import { execSync } from "child_process" ;
1819import httpProxy from "http-proxy" ;
1920const { createProxyServer } = httpProxy ;
2021
22+ // ============================================================================
23+ // Performance: Keep-alive agent for WireGuard / direct HTTP proxy targets.
24+ // Reuses TCP connections to PC2 nodes instead of opening one per request.
25+ // NOT used for Boson ActiveProxy (requires Connection: close).
26+ // ============================================================================
27+ const keepAliveAgent = new http . Agent ( {
28+ keepAlive : true ,
29+ maxSockets : 64 ,
30+ maxFreeSockets : 16 ,
31+ timeout : 60_000 ,
32+ } ) ;
33+
34+ // ============================================================================
35+ // Performance: Compression + cache header configuration
36+ // ============================================================================
37+ const COMPRESSIBLE_TYPES = / ^ t e x t \/ | \/ j s o n | \/ j a v a s c r i p t | \/ x m l | \+ x m l | \/ s v g / ;
38+ const MIN_COMPRESS_SIZE = 1024 ; // Don't compress responses under 1KB
39+
40+ const STATIC_CACHE_RULES = {
41+ long : { maxAge : 604800 , extensions : / \. ( p n g | j p e ? g | g i f | w e b p | i c o | w o f f 2 ? | t t f | e o t ) $ / i } ,
42+ medium : { maxAge : 86400 , extensions : / \. ( j s | c s s | s v g | m a p ) $ / i } ,
43+ } ;
44+
2145// Configuration (supports environment variables for multi-gateway deployment)
2246const CONFIG = {
2347 // Gateway identity for multi-instance deployments
@@ -1367,6 +1391,56 @@ proxy.on("error", (err, req, res) => {
13671391 }
13681392} ) ;
13691393
1394+ // ============================================================================
1395+ // Performance: Intercept proxy responses to add compression + cache headers.
1396+ // Operates transparently -- the PC2 node receives identical requests and its
1397+ // response headers are preserved. We only supplement missing Cache-Control and
1398+ // compress text-based responses that the browser can accept compressed.
1399+ // ============================================================================
1400+ proxy . on ( "proxyRes" , ( proxyRes , req , res ) => {
1401+ const reqUrl = req . url || '' ;
1402+ const contentType = proxyRes . headers [ 'content-type' ] || '' ;
1403+
1404+ // --- Cache headers for static assets (only if node didn't set its own) ---
1405+ if ( ! proxyRes . headers [ 'cache-control' ] ) {
1406+ for ( const rule of Object . values ( STATIC_CACHE_RULES ) ) {
1407+ if ( rule . extensions . test ( reqUrl ) ) {
1408+ proxyRes . headers [ 'cache-control' ] = `public, max-age=${ rule . maxAge } ` ;
1409+ break ;
1410+ }
1411+ }
1412+ }
1413+
1414+ // --- Gzip compression for text-based responses ---
1415+ const acceptEncoding = req . headers [ 'accept-encoding' ] || '' ;
1416+ const alreadyEncoded = ! ! proxyRes . headers [ 'content-encoding' ] ;
1417+ const isCompressible = COMPRESSIBLE_TYPES . test ( contentType ) ;
1418+ const contentLength = parseInt ( proxyRes . headers [ 'content-length' ] || '0' , 10 ) ;
1419+ const tooSmall = contentLength > 0 && contentLength < MIN_COMPRESS_SIZE ;
1420+ const supportsGzip = acceptEncoding . includes ( 'gzip' ) ;
1421+
1422+ if ( ! alreadyEncoded && isCompressible && ! tooSmall && supportsGzip ) {
1423+ // Remove content-length (unknown after compression) and set encoding
1424+ delete proxyRes . headers [ 'content-length' ] ;
1425+ proxyRes . headers [ 'content-encoding' ] = 'gzip' ;
1426+ proxyRes . headers [ 'vary' ] = 'Accept-Encoding' ;
1427+
1428+ // Pipe through gzip -- override res.write/end to compress on the fly
1429+ const gzipStream = zlib . createGzip ( { level : 6 } ) ;
1430+ const originalWrite = res . write . bind ( res ) ;
1431+ const originalEnd = res . end . bind ( res ) ;
1432+
1433+ gzipStream . on ( 'data' , ( chunk ) => originalWrite ( chunk ) ) ;
1434+ gzipStream . on ( 'end' , ( ) => originalEnd ( ) ) ;
1435+
1436+ res . write = ( chunk ) => gzipStream . write ( chunk ) ;
1437+ res . end = ( chunk ) => {
1438+ if ( chunk ) gzipStream . write ( chunk ) ;
1439+ gzipStream . end ( ) ;
1440+ } ;
1441+ }
1442+ } ) ;
1443+
13701444// Extract username from hostname
13711445function extractUsername ( hostname ) {
13721446 if ( ! hostname ) return null ;
@@ -1458,10 +1532,11 @@ async function handleRequest(req, res) {
14581532 return ;
14591533 }
14601534
1461- // Direct HTTP proxy (includes WireGuard tunnel endpoints at 10.100.x.x)
1535+ // Direct HTTP proxy (includes WireGuard tunnel endpoints at 10.100.x.x).
1536+ // Uses keep-alive agent to reuse TCP connections to the PC2 node.
14621537 const viaWG = isWireGuardIP ( nodeInfo . endpoint ) ;
14631538 console . log ( `[Gateway] Proxying ${ username } -> ${ nodeInfo . endpoint } ${ viaWG ? ' (WireGuard tunnel)' : '' } ` ) ;
1464- proxy . web ( req , res , { target : nodeInfo . endpoint } ) ;
1539+ proxy . web ( req , res , { target : nodeInfo . endpoint , agent : keepAliveAgent } ) ;
14651540}
14661541
14671542// API request handler
0 commit comments