@rexaray008/nodeproxy 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # nodeproxy
2
+
3
+ A tiny, zero-dependency reverse proxy / web server for Node.js — think "a mini nginx" you can read in one sitting. No native code, no config-language DSL, just JSON.
4
+
5
+ ## Features
6
+ - Host-based virtual routing (`api.example.com` → one backend, `app.example.com` → another)
7
+ - Round-robin load balancing across multiple upstream targets per host
8
+ - Wildcard `*` catch-all host for local dev
9
+ - Static file fallback (serve a folder directly, no backend needed)
10
+ - HTTPS support (bring your own cert/key)
11
+ - Zero npm dependencies — pure Node.js `http`/`https` core modules
12
+
13
+ ## One-command quick start
14
+
15
+ ```bash
16
+ npx nodeproxy start
17
+ ```
18
+
19
+ That's it. On first run this:
20
+ 1. Creates `nodeproxy.config.json` in the current directory with sensible defaults
21
+ 2. Starts listening on port `8080`, forwarding everything to `http://localhost:3000`
22
+
23
+ Edit `nodeproxy.config.json` and restart to change routing, or use the CLI helper below.
24
+
25
+ ### Install globally instead
26
+ ```bash
27
+ npm install -g nodeproxy
28
+ nodeproxy start
29
+ ```
30
+ (publish this folder to npm first, or just `npm link` it locally — see below)
31
+
32
+ ### Run without installing (from this folder)
33
+ ```bash
34
+ node bin/cli.js start
35
+ ```
36
+
37
+ ## Configuring routes
38
+
39
+ ```bash
40
+ # Route traffic for a host to a backend
41
+ nodeproxy add example.com http://localhost:4000
42
+
43
+ # Add a second target on the same host for load balancing
44
+ # (just edit the "targets" array in nodeproxy.config.json directly)
45
+ ```
46
+
47
+ Example `nodeproxy.config.json`:
48
+
49
+ ```json
50
+ {
51
+ "port": 8080,
52
+ "https": { "enabled": false, "port": 8443, "key": "", "cert": "" },
53
+ "logging": true,
54
+ "routes": [
55
+ { "host": "app.example.com", "targets": ["http://localhost:4000", "http://localhost:4001"], "staticDir": null },
56
+ { "host": "static.example.com", "targets": [], "staticDir": "./public" },
57
+ { "host": "*", "targets": ["http://localhost:3000"], "staticDir": null }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ - `targets` with 2+ entries = automatic round-robin load balancing
63
+ - `staticDir` set + empty `targets` = pure static file server for that host
64
+ - `host: "*"` = catch-all, used when no other host matches (handy for local dev where you don't care about the `Host` header)
65
+
66
+ ## HTTPS
67
+
68
+ ```json
69
+ "https": {
70
+ "enabled": true,
71
+ "port": 8443,
72
+ "key": "/path/to/privkey.pem",
73
+ "cert": "/path/to/fullchain.pem"
74
+ }
75
+ ```
76
+
77
+ Get free certs from Let's Encrypt (e.g. via `certbot`) and point `key`/`cert` at the files. HTTP and HTTPS run side-by-side.
78
+
79
+ ## Local install / publish
80
+
81
+ ```bash
82
+ npm link # makes the `nodeproxy` command available globally on this machine
83
+ # or
84
+ npm publish # to publish it as a real npm package
85
+ ```
86
+
87
+ ## Project layout
88
+ ```
89
+ bin/cli.js CLI entrypoint (start / init / add / help)
90
+ lib/server.js Core proxy engine (routing, load balancing, static serving)
91
+ lib/config.js Config file loading/creation
92
+ ```
93
+
94
+ ## Limitations (intentional — this is a learning-scale project, not production nginx)
95
+ - No automatic Let's Encrypt / ACME renewal — bring your own certs
96
+ - No SNI-based multi-cert HTTPS (one cert for all HTTPS hosts)
97
+ - No rate limiting, caching, or WAF features
98
+ - Round-robin only (no least-connections/weighted balancing)
99
+
100
+ These are all reasonable next steps if you want to extend it.
package/bin/cli.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { loadConfig, saveConfig, ensureConfig, DEFAULT_CONFIG_NAME } = require('../lib/config');
5
+ const { startServer } = require('../lib/server');
6
+
7
+ function usage() {
8
+ console.log(`
9
+ nodeproxy - a tiny reverse proxy / web server (like a mini nginx)
10
+
11
+ Usage:
12
+ nodeproxy start [--config <file>] Start the proxy (auto-creates a
13
+ default config on first run)
14
+ nodeproxy init [--config <file>] Just create the config file
15
+ nodeproxy add <host> <target> [--config <file>]
16
+ Add/update a route, e.g.
17
+ nodeproxy add example.com http://localhost:4000
18
+ nodeproxy help Show this message
19
+
20
+ One-command quick start:
21
+ npx nodeproxy start
22
+ `);
23
+ }
24
+
25
+ function getFlag(args, name) {
26
+ const i = args.indexOf(name);
27
+ if (i === -1) return undefined;
28
+ return args[i + 1];
29
+ }
30
+
31
+ function main() {
32
+ const args = process.argv.slice(2);
33
+ const cmd = args[0] || 'start';
34
+ const configPath = getFlag(args, '--config');
35
+
36
+ if (cmd === 'help' || cmd === '-h' || cmd === '--help') {
37
+ return usage();
38
+ }
39
+
40
+ if (cmd === 'init') {
41
+ const file = ensureConfig(configPath);
42
+ console.log(`[nodeproxy] Config ready at ${file}`);
43
+ return;
44
+ }
45
+
46
+ if (cmd === 'add') {
47
+ const host = args[1];
48
+ const target = args[2];
49
+ if (!host || !target) {
50
+ console.error('Usage: nodeproxy add <host> <target>');
51
+ process.exit(1);
52
+ }
53
+ const { config, file } = loadConfig(configPath);
54
+ const existing = config.routes.find(r => r.host === host);
55
+ if (existing) {
56
+ existing.targets = [target];
57
+ } else {
58
+ config.routes.push({ host, targets: [target], staticDir: null });
59
+ }
60
+ saveConfig(config, configPath);
61
+ console.log(`[nodeproxy] Route added: ${host} -> ${target} (${file})`);
62
+ return;
63
+ }
64
+
65
+ if (cmd === 'start') {
66
+ const { config, file } = loadConfig(configPath);
67
+ console.log(`[nodeproxy] Using config: ${file}`);
68
+ if (file.endsWith(DEFAULT_CONFIG_NAME)) {
69
+ console.log('[nodeproxy] (edit this file, or use "nodeproxy add <host> <target>", to change routing)');
70
+ }
71
+ startServer(config);
72
+ return;
73
+ }
74
+
75
+ console.error(`Unknown command: ${cmd}`);
76
+ usage();
77
+ process.exit(1);
78
+ }
79
+
80
+ main();
package/lib/config.js ADDED
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DEFAULT_CONFIG_NAME = 'nodeproxy.config.json';
7
+
8
+ function defaultConfig() {
9
+ return {
10
+ // Port the proxy itself listens on
11
+ port: 8080,
12
+
13
+ // Optional HTTPS. Leave "enabled": false until you have real certs.
14
+ https: {
15
+ enabled: false,
16
+ port: 8443,
17
+ key: '', // path to privkey.pem
18
+ cert: '' // path to fullchain.pem
19
+ },
20
+
21
+ // Request logging to stdout
22
+ logging: true,
23
+
24
+ // Gzip/deflate pass-through is automatic (we just forward headers),
25
+ // nothing to configure there.
26
+
27
+ // Virtual hosts / routes.
28
+ // "host" supports "*" as a catch-all for local testing.
29
+ // "targets" is a list -> automatic round-robin load balancing.
30
+ routes: [
31
+ {
32
+ host: '*',
33
+ targets: ['http://localhost:3000'],
34
+ // If set, static files are served from this directory when
35
+ // no upstream target responds (or as the sole behavior if
36
+ // targets is empty).
37
+ staticDir: null
38
+ }
39
+ ]
40
+ };
41
+ }
42
+
43
+ function configPath(customPath) {
44
+ return customPath
45
+ ? path.resolve(process.cwd(), customPath)
46
+ : path.resolve(process.cwd(), DEFAULT_CONFIG_NAME);
47
+ }
48
+
49
+ function ensureConfig(customPath) {
50
+ const file = configPath(customPath);
51
+ if (!fs.existsSync(file)) {
52
+ fs.writeFileSync(file, JSON.stringify(defaultConfig(), null, 2) + '\n');
53
+ }
54
+ return file;
55
+ }
56
+
57
+ function loadConfig(customPath) {
58
+ const file = ensureConfig(customPath);
59
+ const raw = fs.readFileSync(file, 'utf8');
60
+ try {
61
+ return { config: JSON.parse(raw), file };
62
+ } catch (err) {
63
+ throw new Error(`Failed to parse ${file}: ${err.message}`);
64
+ }
65
+ }
66
+
67
+ function saveConfig(config, customPath) {
68
+ const file = configPath(customPath);
69
+ fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
70
+ return file;
71
+ }
72
+
73
+ module.exports = { defaultConfig, configPath, ensureConfig, loadConfig, saveConfig, DEFAULT_CONFIG_NAME };
package/lib/server.js ADDED
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { URL } = require('url');
8
+
9
+ const MIME = {
10
+ '.html': 'text/html; charset=utf-8',
11
+ '.htm': 'text/html; charset=utf-8',
12
+ '.css': 'text/css; charset=utf-8',
13
+ '.js': 'application/javascript; charset=utf-8',
14
+ '.json': 'application/json; charset=utf-8',
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.svg': 'image/svg+xml',
20
+ '.ico': 'image/x-icon',
21
+ '.txt': 'text/plain; charset=utf-8',
22
+ '.woff': 'font/woff',
23
+ '.woff2': 'font/woff2'
24
+ };
25
+
26
+ // --- round-robin state, one counter per route ---
27
+ const counters = new WeakMap();
28
+
29
+ function pickTarget(route) {
30
+ if (!route.targets || route.targets.length === 0) return null;
31
+ let i = counters.get(route) || 0;
32
+ const target = route.targets[i % route.targets.length];
33
+ counters.set(route, i + 1);
34
+ return target;
35
+ }
36
+
37
+ function matchRoute(routes, hostHeader) {
38
+ const host = (hostHeader || '').split(':')[0].toLowerCase();
39
+ let exact = null;
40
+ let wildcard = null;
41
+ for (const r of routes) {
42
+ if (r.host === host) exact = r;
43
+ if (r.host === '*') wildcard = r;
44
+ }
45
+ return exact || wildcard || null;
46
+ }
47
+
48
+ function log(config, ...args) {
49
+ if (config.logging) {
50
+ console.log(new Date().toISOString(), ...args);
51
+ }
52
+ }
53
+
54
+ function serveStatic(dir, reqUrl, res) {
55
+ const safePath = path.normalize(path.join(dir, decodeURIComponent(reqUrl.split('?')[0])));
56
+ if (!safePath.startsWith(path.resolve(dir))) {
57
+ res.writeHead(403);
58
+ return res.end('Forbidden');
59
+ }
60
+ let filePath = safePath;
61
+ fs.stat(filePath, (err, stat) => {
62
+ if (err) {
63
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
64
+ return res.end('404 Not Found');
65
+ }
66
+ if (stat.isDirectory()) {
67
+ filePath = path.join(filePath, 'index.html');
68
+ }
69
+ const ext = path.extname(filePath);
70
+ fs.readFile(filePath, (err2, data) => {
71
+ if (err2) {
72
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
73
+ return res.end('404 Not Found');
74
+ }
75
+ res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
76
+ res.end(data);
77
+ });
78
+ });
79
+ }
80
+
81
+ function proxyRequest(target, req, res, config) {
82
+ const targetUrl = new URL(target);
83
+ const options = {
84
+ hostname: targetUrl.hostname,
85
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
86
+ path: req.url,
87
+ method: req.method,
88
+ headers: Object.assign({}, req.headers, {
89
+ 'x-forwarded-for': req.socket.remoteAddress,
90
+ 'x-forwarded-proto': req.socket.encrypted ? 'https' : 'http',
91
+ host: targetUrl.host
92
+ })
93
+ };
94
+
95
+ const client = targetUrl.protocol === 'https:' ? https : http;
96
+
97
+ const proxyReq = client.request(options, (proxyRes) => {
98
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
99
+ proxyRes.pipe(res, { end: true });
100
+ });
101
+
102
+ proxyReq.on('error', (err) => {
103
+ log(config, 'upstream error:', err.message);
104
+ if (!res.headersSent) {
105
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
106
+ }
107
+ res.end('502 Bad Gateway');
108
+ });
109
+
110
+ req.pipe(proxyReq, { end: true });
111
+ }
112
+
113
+ function makeRequestHandler(config) {
114
+ return function requestHandler(req, res) {
115
+ const route = matchRoute(config.routes, req.headers.host);
116
+
117
+ log(config, req.method, req.headers.host || '-', req.url, '->', route ? route.host : 'no-match');
118
+
119
+ if (!route) {
120
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
121
+ return res.end('404 Not Found (no matching route for this host)');
122
+ }
123
+
124
+ const target = pickTarget(route);
125
+
126
+ if (target) {
127
+ return proxyRequest(target, req, res, config);
128
+ }
129
+
130
+ if (route.staticDir) {
131
+ return serveStatic(route.staticDir, req.url, res);
132
+ }
133
+
134
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
135
+ res.end('502 Bad Gateway (no target configured for this host)');
136
+ };
137
+ }
138
+
139
+ function startServer(config) {
140
+ const handler = makeRequestHandler(config);
141
+ const servers = [];
142
+
143
+ const httpServer = http.createServer(handler);
144
+ httpServer.listen(config.port, () => {
145
+ console.log(`[nodeproxy] HTTP listening on port ${config.port}`);
146
+ });
147
+ servers.push(httpServer);
148
+
149
+ if (config.https && config.https.enabled) {
150
+ if (!config.https.key || !config.https.cert) {
151
+ console.warn('[nodeproxy] https.enabled is true but key/cert paths are missing — skipping HTTPS.');
152
+ } else {
153
+ const httpsOptions = {
154
+ key: fs.readFileSync(config.https.key),
155
+ cert: fs.readFileSync(config.https.cert)
156
+ };
157
+ const httpsServer = https.createServer(httpsOptions, handler);
158
+ httpsServer.listen(config.https.port, () => {
159
+ console.log(`[nodeproxy] HTTPS listening on port ${config.https.port}`);
160
+ });
161
+ servers.push(httpsServer);
162
+ }
163
+ }
164
+
165
+ return servers;
166
+ }
167
+
168
+ module.exports = { startServer, matchRoute, pickTarget };
@@ -0,0 +1,26 @@
1
+ {
2
+ "port": 8080,
3
+ "https": {
4
+ "enabled": false,
5
+ "port": 8443,
6
+ "key": "",
7
+ "cert": ""
8
+ },
9
+ "logging": true,
10
+ "routes": [
11
+ {
12
+ "host": "*",
13
+ "targets": [
14
+ "http://localhost:3000"
15
+ ],
16
+ "staticDir": null
17
+ },
18
+ {
19
+ "host": "https://hirenray.rest",
20
+ "targets": [
21
+ "http://localhost:3000"
22
+ ],
23
+ "staticDir": null
24
+ }
25
+ ]
26
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@rexaray008/nodeproxy",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.1",
7
+ "description": "A lightweight, zero-dependency reverse proxy / web server (like a mini nginx) for Node.js. One command to install, configure and run.",
8
+ "main": "lib/server.js",
9
+ "bin": {
10
+ "nodeproxy": "bin/cli.js"
11
+ },
12
+ "scripts": {
13
+ "start": "node bin/cli.js start"
14
+ },
15
+ "engines": {
16
+ "node": ">=16"
17
+ },
18
+ "license": "MIT",
19
+ "dependencies": {}
20
+ }