1+ #!/usr/bin/env node
2+
3+ const http = require ( 'http' ) ;
4+ const https = require ( 'https' ) ;
5+ const url = require ( 'url' ) ;
6+ const path = require ( 'path' ) ;
7+ const fs = require ( 'fs' ) ;
8+ const httpProxy = require ( 'http-proxy' ) ;
9+
10+ // Default configuration
11+ let config = {
12+ port : 8000 ,
13+ host : '0.0.0.0' ,
14+ root : process . cwd ( ) ,
15+ cache : false ,
16+ cors : true ,
17+ silent : false
18+ } ;
19+
20+ // Parse command line arguments
21+ function parseArgs ( ) {
22+ const args = process . argv . slice ( 2 ) ;
23+
24+ for ( let i = 0 ; i < args . length ; i ++ ) {
25+ const arg = args [ i ] ;
26+
27+ if ( arg === '-p' && args [ i + 1 ] ) {
28+ config . port = parseInt ( args [ i + 1 ] ) ;
29+ i ++ ;
30+ } else if ( arg === '-a' && args [ i + 1 ] ) {
31+ config . host = args [ i + 1 ] ;
32+ i ++ ;
33+ } else if ( arg === '-c-1' ) {
34+ config . cache = false ;
35+ } else if ( arg === '-c' && args [ i + 1 ] ) {
36+ config . cache = parseInt ( args [ i + 1 ] ) > 0 ;
37+ i ++ ;
38+ } else if ( arg === '--cors' ) {
39+ config . cors = true ;
40+ } else if ( arg === '-S' || arg === '--silent' ) {
41+ config . silent = true ;
42+ } else if ( arg === '--log-ip' ) {
43+ config . logIp = true ;
44+ } else if ( ! arg . startsWith ( '-' ) ) {
45+ config . root = path . resolve ( arg ) ;
46+ }
47+ }
48+ }
49+
50+ // Create proxy server
51+ const proxy = httpProxy . createProxyServer ( {
52+ changeOrigin : true ,
53+ secure : true ,
54+ followRedirects : true
55+ } ) ;
56+
57+ // Handle proxy errors
58+ proxy . on ( 'error' , ( err , req , res ) => {
59+ console . error ( 'Proxy Error:' , err . message ) ;
60+ if ( ! res . headersSent ) {
61+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
62+ res . end ( JSON . stringify ( { error : 'Proxy Error' , message : err . message } ) ) ;
63+ }
64+ } ) ;
65+
66+ // Modify proxy request headers
67+ proxy . on ( 'proxyReq' , ( proxyReq , req , res ) => {
68+ // Transform localhost:8000 to appear as phcode.dev domain
69+ const originalHost = req . headers . host ;
70+ const originalReferer = req . headers . referer ;
71+ const originalOrigin = req . headers . origin ;
72+
73+ // Set target host
74+ proxyReq . setHeader ( 'Host' , 'account.phcode.dev' ) ;
75+
76+ // Transform referer from localhost:8000 to phcode.dev
77+ if ( originalReferer && originalReferer . includes ( 'localhost:8000' ) ) {
78+ const newReferer = originalReferer . replace ( / l o c a l h o s t : 8 0 0 0 / g, 'phcode.dev' ) ;
79+ proxyReq . setHeader ( 'Referer' , newReferer ) ;
80+ } else if ( ! originalReferer ) {
81+ proxyReq . setHeader ( 'Referer' , 'https://phcode.dev/' ) ;
82+ }
83+
84+ // Transform origin from localhost:8000 to phcode.dev
85+ if ( originalOrigin && originalOrigin . includes ( 'localhost:8000' ) ) {
86+ const newOrigin = originalOrigin . replace ( / l o c a l h o s t : 8 0 0 0 / g, 'phcode.dev' ) ;
87+ proxyReq . setHeader ( 'Origin' , newOrigin ) ;
88+ } else if ( ! originalOrigin ) {
89+ proxyReq . setHeader ( 'Origin' , 'https://phcode.dev' ) ;
90+ }
91+
92+ // Ensure HTTPS scheme
93+ proxyReq . setHeader ( 'X-Forwarded-Proto' , 'https' ) ;
94+ proxyReq . setHeader ( 'X-Forwarded-For' , req . connection . remoteAddress ) ;
95+
96+ } ) ;
97+
98+ // Modify proxy response headers
99+ proxy . on ( 'proxyRes' , ( proxyRes , req , res ) => {
100+ // Pass through cache control and other security headers
101+ // But translate any domain references back to localhost for the browser
102+
103+ const setCookieHeader = proxyRes . headers [ 'set-cookie' ] ;
104+ if ( setCookieHeader ) {
105+ // Transform any phcode.dev domain cookies back to localhost
106+ const modifiedCookies = setCookieHeader . map ( cookie => {
107+ return cookie . replace ( / d o m a i n = \. ? p h c o d e \. d e v / gi, 'domain=localhost' ) ;
108+ } ) ;
109+ proxyRes . headers [ 'set-cookie' ] = modifiedCookies ;
110+ }
111+
112+ // Ensure CORS headers if needed
113+ if ( config . cors ) {
114+ proxyRes . headers [ 'Access-Control-Allow-Origin' ] = '*' ;
115+ proxyRes . headers [ 'Access-Control-Allow-Methods' ] = 'GET, POST, PUT, DELETE, OPTIONS' ;
116+ proxyRes . headers [ 'Access-Control-Allow-Headers' ] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control' ;
117+ }
118+ } ) ;
119+
120+ // Get MIME type based on file extension
121+ function getMimeType ( filePath ) {
122+ const ext = path . extname ( filePath ) . toLowerCase ( ) ;
123+ const mimeTypes = {
124+ '.html' : 'text/html' ,
125+ '.htm' : 'text/html' ,
126+ '.css' : 'text/css' ,
127+ '.js' : 'application/javascript' ,
128+ '.json' : 'application/json' ,
129+ '.png' : 'image/png' ,
130+ '.jpg' : 'image/jpeg' ,
131+ '.jpeg' : 'image/jpeg' ,
132+ '.gif' : 'image/gif' ,
133+ '.svg' : 'image/svg+xml' ,
134+ '.ico' : 'image/x-icon' ,
135+ '.woff' : 'font/woff' ,
136+ '.woff2' : 'font/woff2' ,
137+ '.ttf' : 'font/ttf' ,
138+ '.eot' : 'application/vnd.ms-fontobject'
139+ } ;
140+ return mimeTypes [ ext ] || 'application/octet-stream' ;
141+ }
142+
143+ // Serve static files
144+ function serveStaticFile ( req , res , filePath ) {
145+ fs . stat ( filePath , ( err , stats ) => {
146+ if ( err ) {
147+ res . writeHead ( 404 , { 'Content-Type' : 'text/plain' } ) ;
148+ res . end ( 'File not found' ) ;
149+ return ;
150+ }
151+
152+ if ( stats . isDirectory ( ) ) {
153+ // Try to serve index.html from directory
154+ const indexPath = path . join ( filePath , 'index.html' ) ;
155+ fs . stat ( indexPath , ( err , indexStats ) => {
156+ if ( ! err && indexStats . isFile ( ) ) {
157+ serveStaticFile ( req , res , indexPath ) ;
158+ } else {
159+ // List directory contents
160+ fs . readdir ( filePath , ( err , files ) => {
161+ if ( err ) {
162+ res . writeHead ( 500 , { 'Content-Type' : 'text/plain' } ) ;
163+ res . end ( 'Error reading directory' ) ;
164+ return ;
165+ }
166+
167+ const html = `
168+ <!DOCTYPE html>
169+ <html>
170+ <head><title>Directory listing</title></head>
171+ <body>
172+ <h1>Directory listing for ${ req . url } </h1>
173+ <ul>
174+ ${ files . map ( file =>
175+ `<li><a href="${ path . join ( req . url , file ) } ">${ file } </a></li>`
176+ ) . join ( '' ) }
177+ </ul>
178+ </body>
179+ </html>
180+ ` ;
181+
182+ const headers = {
183+ 'Content-Type' : 'text/html' ,
184+ 'Content-Length' : Buffer . byteLength ( html )
185+ } ;
186+
187+ if ( ! config . cache ) {
188+ headers [ 'Cache-Control' ] = 'no-cache, no-store, must-revalidate' ;
189+ headers [ 'Pragma' ] = 'no-cache' ;
190+ headers [ 'Expires' ] = '0' ;
191+ }
192+
193+ if ( config . cors ) {
194+ headers [ 'Access-Control-Allow-Origin' ] = '*' ;
195+ headers [ 'Access-Control-Allow-Methods' ] = 'GET, POST, PUT, DELETE, OPTIONS' ;
196+ headers [ 'Access-Control-Allow-Headers' ] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control' ;
197+ }
198+
199+ res . writeHead ( 200 , headers ) ;
200+ res . end ( html ) ;
201+ } ) ;
202+ }
203+ } ) ;
204+ return ;
205+ }
206+
207+ // Serve file
208+ const mimeType = getMimeType ( filePath ) ;
209+ const headers = {
210+ 'Content-Type' : mimeType ,
211+ 'Content-Length' : stats . size
212+ } ;
213+
214+ if ( ! config . cache ) {
215+ headers [ 'Cache-Control' ] = 'no-cache, no-store, must-revalidate' ;
216+ headers [ 'Pragma' ] = 'no-cache' ;
217+ headers [ 'Expires' ] = '0' ;
218+ }
219+
220+ if ( config . cors ) {
221+ headers [ 'Access-Control-Allow-Origin' ] = '*' ;
222+ headers [ 'Access-Control-Allow-Methods' ] = 'GET, POST, PUT, DELETE, OPTIONS' ;
223+ headers [ 'Access-Control-Allow-Headers' ] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control' ;
224+ }
225+
226+ res . writeHead ( 200 , headers ) ;
227+
228+ const stream = fs . createReadStream ( filePath ) ;
229+ stream . pipe ( res ) ;
230+
231+ stream . on ( 'error' , ( err ) => {
232+ res . writeHead ( 500 , { 'Content-Type' : 'text/plain' } ) ;
233+ res . end ( 'Error reading file' ) ;
234+ } ) ;
235+ } ) ;
236+ }
237+
238+ // Create HTTP server
239+ const server = http . createServer ( ( req , res ) => {
240+ const parsedUrl = url . parse ( req . url , true ) ;
241+
242+ // Handle CORS preflight
243+ if ( req . method === 'OPTIONS' && config . cors ) {
244+ res . writeHead ( 200 , {
245+ 'Access-Control-Allow-Origin' : '*' ,
246+ 'Access-Control-Allow-Methods' : 'GET, POST, PUT, DELETE, OPTIONS' ,
247+ 'Access-Control-Allow-Headers' : 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control'
248+ } ) ;
249+ res . end ( ) ;
250+ return ;
251+ }
252+
253+ // Check if this is a proxy request
254+ if ( parsedUrl . pathname . startsWith ( '/proxy/accounts' ) ) {
255+ // Extract the path after /proxy/accounts
256+ const targetPath = parsedUrl . pathname . replace ( '/proxy/accounts' , '' ) ;
257+ const originalUrl = req . url ;
258+
259+ // Modify the request URL for the proxy
260+ req . url = targetPath + ( parsedUrl . search || '' ) ;
261+
262+ if ( ! config . silent ) {
263+ console . log ( `[PROXY] ${ req . method } ${ originalUrl } -> https://account.phcode.dev${ req . url } ` ) ;
264+ }
265+
266+ // Proxy the request
267+ proxy . web ( req , res , {
268+ target : 'https://account.phcode.dev' ,
269+ changeOrigin : true ,
270+ secure : true
271+ } ) ;
272+ return ;
273+ }
274+
275+ // Serve static files
276+ let filePath = path . join ( config . root , parsedUrl . pathname ) ;
277+
278+ // Security: prevent directory traversal
279+ const normalizedPath = path . normalize ( filePath ) ;
280+ if ( ! normalizedPath . startsWith ( config . root ) ) {
281+ res . writeHead ( 403 , { 'Content-Type' : 'text/plain' } ) ;
282+ res . end ( 'Forbidden' ) ;
283+ return ;
284+ }
285+
286+ if ( ! config . silent ) {
287+ const clientIp = req . headers [ 'x-forwarded-for' ] || req . connection . remoteAddress ;
288+ console . log ( `[${ new Date ( ) . toISOString ( ) } ] ${ req . method } ${ req . url } ${ config . logIp ? ` (${ clientIp } )` : '' } ` ) ;
289+ }
290+
291+ serveStaticFile ( req , res , filePath ) ;
292+ } ) ;
293+
294+ // Parse arguments and start server
295+ parseArgs ( ) ;
296+
297+ server . listen ( config . port , config . host , ( ) => {
298+ if ( ! config . silent ) {
299+ console . log ( `Starting up http-server, serving ${ config . root } ` ) ;
300+ console . log ( `Available on:` ) ;
301+ console . log ( ` http://${ config . host === '0.0.0.0' ? 'localhost' : config . host } :${ config . port } ` ) ;
302+ console . log ( `Proxy routes:` ) ;
303+ console . log ( ` /proxy/accounts/* -> https://account.phcode.dev/*` ) ;
304+ console . log ( 'Hit CTRL-C to stop the server' ) ;
305+ }
306+ } ) ;
307+
308+ // Handle graceful shutdown
309+ process . on ( 'SIGINT' , ( ) => {
310+ console . log ( '\nShutting down the server...' ) ;
311+ server . close ( ( ) => {
312+ process . exit ( 0 ) ;
313+ } ) ;
314+ } ) ;
315+
316+ process . on ( 'SIGTERM' , ( ) => {
317+ console . log ( '\nShutting down the server...' ) ;
318+ server . close ( ( ) => {
319+ process . exit ( 0 ) ;
320+ } ) ;
321+ } ) ;
0 commit comments