Skip to content

Commit 2ea367a

Browse files
committed
chore: proxy to account.phcode.dev for localhost dev login
1 parent a98663c commit 2ea367a

File tree

3 files changed

+327
-4
lines changed

3 files changed

+327
-4
lines changed

package-lock.json

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"gulp-useref": "^5.0.0",
3232
"gulp-webserver": "^0.9.1",
3333
"gulp-zip": "^5.1.0",
34+
"http-proxy": "^1.18.1",
3435
"http-server": "14.1.0",
3536
"husky": "^7.0.4",
3637
"jasmine-core": "^4.2.0",
@@ -71,10 +72,10 @@
7172
"_patchVersionBump": "gulp patchVersionBump",
7273
"_minorVersionBump": "gulp minorVersionBump",
7374
"_majorVersionBump": "gulp majorVersionBump",
74-
"serve": "http-server . -p 8000 -c-1",
75+
"serve": "node serve-proxy.js . -p 8000 -c-1",
7576
"_serveWithWebCacheHelp": "echo !!!Make sure to npm run release:dev/stageing/prod before testing the cache!!!",
7677
"serveWithWebCache": "npm run _releaseWebCache && npm run _serveWithWebCacheHelp && http-server ./dist -p 8000 -c-1",
77-
"serveExternal": "http-server . -p 8000 -a 0.0.0.0 --log-ip true -c-1",
78+
"serveExternal": "node serve-proxy.js . -p 8000 -a 0.0.0.0 --log-ip -c-1",
7879
"createJSDocs": "node build/api-docs-generator.js && git add docs",
7980
"_translateStrings": "gulp translateStrings",
8081
"_minify": "r.js -o require.min.config.js && echo this is untested see https://stackoverflow.com/questions/14337970/minifying-requirejs-javascript-codebase-to-a-single-file"

serve-proxy.js

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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(/localhost:8000/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(/localhost:8000/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(/domain=\.?phcode\.dev/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

Comments
 (0)