Skip to content

Commit 657f5e1

Browse files
improve node CORS proxy, general polish (#5)
- persist timer job timestamps to DB for more accurate scheduling - retention bug fixes
1 parent 64ae12a commit 657f5e1

File tree

21 files changed

+242
-185
lines changed

21 files changed

+242
-185
lines changed

.github/workflows/deploy.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ name: Release
33
on:
44
push:
55
branches: [main]
6+
paths-ignore:
7+
- '.vscode/**'
8+
- 'cors-proxies/**'
9+
- 'README.md'
10+
- '.gitignore'
11+
- '.editorconfig'
12+
- '**/*.md'
13+
- '**/*.sh'
614

715
jobs:
816
release:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Podds is a [Progressive Web Application (PWA)](https://en.wikipedia.org/wiki/Pro
5555
- [Svelte Bottom Sheet](https://github.com/AuxiDev/svelte-bottom-sheet)
5656
- [Lucide Icons](https://lucide.dev/)
5757
- [Pattern Monster](https://pattern.monster/)
58+
- [text divider](https://thenounproject.com/browse/icons/term/text-divider) by iconcheese from the Noun Project
5859

5960
## CORS Proxies
6061

cors-proxies/node/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
FROM node:slim
22
WORKDIR /app
33
COPY server.js .
4-
RUN npm install node-fetch
4+
COPY package.json .
5+
RUN npm install
56
CMD ["node", "server.js"]

cors-proxies/node/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "cors-proxy",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"dependencies": {
6+
"node-fetch": "^3.3.2",
7+
"prom-client": "^15.1.0"
8+
}
9+
}

cors-proxies/node/server.js

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,27 @@ import http from 'http';
22
import fetch from 'node-fetch';
33
import { pipeline } from 'stream';
44
import { promisify } from 'util';
5+
import { Registry, Counter, Histogram } from 'prom-client';
56

67
const 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+
826
const 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
/^10\./,
2240
/^192\.168\./,
2341
/^172\.(1[6-9]|2[0-9]|3[0-1])\./
24-
];
42+
];
2543

2644
const 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

126180
server.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
});

src/app.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@
103103

104104
--primary-black: color-mix(in oklch, var(--primary) 90%, black);
105105
--primary-grey-dark: color-mix(in oklch, var(--primary) 60%, var(--grey-900));
106+
--primary-grey-darker: color-mix(in oklch, var(--primary) 10%, var(--grey-900));
106107
--primary-grey-light: color-mix(in oklch, var(--primary-more) 30%, var(--grey-300));
108+
--primary-grey-lighter: color-mix(in oklch, var(--primary) 3%, white);
107109

108110
--primary-faint: light-dark(var(--primary-200), var(--primary-dark-200));
109111
--primary-less: light-dark(var(--primary-400), var(--primary-dark-400));

0 commit comments

Comments
 (0)