@rexaray008/nodeproxy 1.0.1 → 2.0.0
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 copy.md +283 -0
- package/README.md +279 -62
- package/bin/cli.js +204 -5
- package/lib/cloudflare.js +222 -0
- package/lib/config.js +55 -25
- package/lib/pm2.js +102 -0
- package/lib/server.js +35 -22
- package/nodeproxy.config.json +30 -19
- package/package.json +3 -6
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const { loadConfig, saveConfig, ensureConfig, DEFAULT_CONFIG_NAME } = require('../lib/config');
|
|
5
5
|
const { startServer } = require('../lib/server');
|
|
6
|
+
const cf = require('../lib/cloudflare');
|
|
7
|
+
const pm2 = require('../lib/pm2');
|
|
6
8
|
|
|
7
9
|
function usage() {
|
|
8
10
|
console.log(`
|
|
@@ -12,9 +14,43 @@ Usage:
|
|
|
12
14
|
nodeproxy start [--config <file>] Start the proxy (auto-creates a
|
|
13
15
|
default config on first run)
|
|
14
16
|
nodeproxy init [--config <file>] Just create the config file
|
|
15
|
-
nodeproxy add <host> <target> [--config <file>]
|
|
17
|
+
nodeproxy add <host> <target> [--port <n>] [--config <file>]
|
|
16
18
|
Add/update a route, e.g.
|
|
17
19
|
nodeproxy add example.com http://localhost:4000
|
|
20
|
+
Targets the first listener unless
|
|
21
|
+
--port picks a specific one.
|
|
22
|
+
nodeproxy listen <port> [--config <file>]
|
|
23
|
+
Add a brand new listener on a new
|
|
24
|
+
port, e.g. nodeproxy listen 9090
|
|
25
|
+
(then "nodeproxy add ... --port 9090")
|
|
26
|
+
|
|
27
|
+
nodeproxy tunnel setup --domain <domain> [--name <name>] [--port <n>]
|
|
28
|
+
One-time: install cloudflared if
|
|
29
|
+
needed, log in (opens a browser,
|
|
30
|
+
you click Authorize once), create
|
|
31
|
+
a tunnel, and point <domain> at
|
|
32
|
+
your local port (default 8080).
|
|
33
|
+
nodeproxy tunnel start [--name <name>] Run just the tunnel.
|
|
34
|
+
nodeproxy tunnel doctor --domain <domain>
|
|
35
|
+
Diagnose "works locally but not
|
|
36
|
+
on other devices" issues.
|
|
37
|
+
|
|
38
|
+
nodeproxy app add <name> --cmd "<command>" [--cwd <dir>]
|
|
39
|
+
Register your own project to be
|
|
40
|
+
started/kept alive automatically
|
|
41
|
+
(via pm2), e.g.
|
|
42
|
+
nodeproxy app add myapp --cmd "npm start" --cwd ./my-project
|
|
43
|
+
nodeproxy app list Show registered/running apps.
|
|
44
|
+
nodeproxy app logs <name> Tail logs for a registered app.
|
|
45
|
+
nodeproxy app stop <name> Stop a registered app.
|
|
46
|
+
|
|
47
|
+
nodeproxy start [--config <file>] [--tunnel] [--name <name>]
|
|
48
|
+
Starts any registered apps (via
|
|
49
|
+
pm2), then the proxy, then the
|
|
50
|
+
tunnel if --tunnel is given.
|
|
51
|
+
This is the one command that
|
|
52
|
+
brings everything up together.
|
|
53
|
+
|
|
18
54
|
nodeproxy help Show this message
|
|
19
55
|
|
|
20
56
|
One-command quick start:
|
|
@@ -46,29 +82,192 @@ function main() {
|
|
|
46
82
|
if (cmd === 'add') {
|
|
47
83
|
const host = args[1];
|
|
48
84
|
const target = args[2];
|
|
85
|
+
const portFlag = getFlag(args, '--port');
|
|
49
86
|
if (!host || !target) {
|
|
50
|
-
console.error('Usage: nodeproxy add <host> <target>');
|
|
87
|
+
console.error('Usage: nodeproxy add <host> <target> [--port <n>]');
|
|
51
88
|
process.exit(1);
|
|
52
89
|
}
|
|
53
90
|
const { config, file } = loadConfig(configPath);
|
|
54
|
-
const
|
|
91
|
+
const listener = portFlag
|
|
92
|
+
? config.servers.find(s => String(s.port) === String(portFlag))
|
|
93
|
+
: config.servers[0];
|
|
94
|
+
if (!listener) {
|
|
95
|
+
console.error(`No listener on port ${portFlag}. Create one first with: nodeproxy listen ${portFlag}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const existing = listener.routes.find(r => r.host === host);
|
|
55
99
|
if (existing) {
|
|
56
100
|
existing.targets = [target];
|
|
57
101
|
} else {
|
|
58
|
-
|
|
102
|
+
listener.routes.push({ host, targets: [target], staticDir: null });
|
|
103
|
+
}
|
|
104
|
+
saveConfig(config, configPath);
|
|
105
|
+
console.log(`[nodeproxy] Route added: ${host} -> ${target} on port ${listener.port} (${file})`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (cmd === 'listen') {
|
|
110
|
+
const port = Number(args[1]);
|
|
111
|
+
if (!port) {
|
|
112
|
+
console.error('Usage: nodeproxy listen <port>');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const { config, file } = loadConfig(configPath);
|
|
116
|
+
if (config.servers.some(s => s.port === port)) {
|
|
117
|
+
console.error(`Port ${port} already has a listener.`);
|
|
118
|
+
process.exit(1);
|
|
59
119
|
}
|
|
120
|
+
config.servers.push({
|
|
121
|
+
port,
|
|
122
|
+
https: { enabled: false, key: '', cert: '' },
|
|
123
|
+
routes: []
|
|
124
|
+
});
|
|
60
125
|
saveConfig(config, configPath);
|
|
61
|
-
console.log(`[nodeproxy]
|
|
126
|
+
console.log(`[nodeproxy] New listener added on port ${port} (${file}). Now add routes to it with: nodeproxy add <host> <target> --port ${port}`);
|
|
62
127
|
return;
|
|
63
128
|
}
|
|
64
129
|
|
|
130
|
+
if (cmd === 'tunnel') {
|
|
131
|
+
const sub = args[1];
|
|
132
|
+
const domain = getFlag(args, '--domain');
|
|
133
|
+
const name = getFlag(args, '--name') || (domain ? domain.replace(/\./g, '-') : 'nodeproxy-tunnel');
|
|
134
|
+
const port = Number(getFlag(args, '--port')) || 8080;
|
|
135
|
+
|
|
136
|
+
if (sub === 'setup') {
|
|
137
|
+
if (!domain) {
|
|
138
|
+
console.error('Usage: nodeproxy tunnel setup --domain <yourdomain.com> [--name <name>] [--port <n>]');
|
|
139
|
+
console.error('(Your domain must already be added to your Cloudflare account.)');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
if (!cf.ensureInstalled()) process.exit(1);
|
|
143
|
+
if (!cf.isLoggedIn()) {
|
|
144
|
+
cf.login();
|
|
145
|
+
} else {
|
|
146
|
+
console.log('[tunnel] Already logged in to Cloudflare on this machine.');
|
|
147
|
+
}
|
|
148
|
+
if (!cf.tunnelExists(name)) {
|
|
149
|
+
cf.createTunnel(name);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(`[tunnel] Tunnel "${name}" already exists — reusing it.`);
|
|
152
|
+
}
|
|
153
|
+
const tunnelId = cf.getTunnelId(name);
|
|
154
|
+
cf.writeIngressConfig(name, tunnelId, domain, port);
|
|
155
|
+
cf.routeDns(name, domain);
|
|
156
|
+
console.log(`\n[tunnel] Done! "${domain}" now points at your local port ${port}.`);
|
|
157
|
+
console.log(`[tunnel] From now on, just run: nodeproxy start --tunnel --name ${name}\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (sub === 'start') {
|
|
162
|
+
cf.runTunnel(name);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sub === 'doctor') {
|
|
167
|
+
if (!domain) {
|
|
168
|
+
console.error('Usage: nodeproxy tunnel doctor --domain <domain>');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
cf.diagnose(domain).then(cf.printDiagnosis);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.error('Usage: nodeproxy tunnel setup --domain <domain> | nodeproxy tunnel start [--name <name>] | nodeproxy tunnel doctor --domain <domain>');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (cmd === 'app') {
|
|
180
|
+
const sub = args[1];
|
|
181
|
+
|
|
182
|
+
if (sub === 'add') {
|
|
183
|
+
const name = args[2];
|
|
184
|
+
const appCmd = getFlag(args, '--cmd');
|
|
185
|
+
const cwd = getFlag(args, '--cwd');
|
|
186
|
+
if (!name || !appCmd) {
|
|
187
|
+
console.error('Usage: nodeproxy app add <name> --cmd "<command>" [--cwd <dir>]');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const { config, file } = loadConfig(configPath);
|
|
191
|
+
const existing = config.apps.find(a => a.name === name);
|
|
192
|
+
const entry = { name, cmd: appCmd, cwd: cwd || process.cwd() };
|
|
193
|
+
if (existing) {
|
|
194
|
+
Object.assign(existing, entry);
|
|
195
|
+
} else {
|
|
196
|
+
config.apps.push(entry);
|
|
197
|
+
}
|
|
198
|
+
saveConfig(config, configPath);
|
|
199
|
+
console.log(`[nodeproxy] Registered app "${name}" (${appCmd}) in ${file}`);
|
|
200
|
+
console.log('[nodeproxy] It will now start automatically with "nodeproxy start".');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (sub === 'list') {
|
|
205
|
+
const { config } = loadConfig(configPath);
|
|
206
|
+
if (config.apps.length === 0) {
|
|
207
|
+
console.log('No apps registered. Add one with: nodeproxy app add <name> --cmd "<command>"');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const running = pm2.isInstalled() ? pm2.listApps() : [];
|
|
211
|
+
for (const a of config.apps) {
|
|
212
|
+
const proc = running.find(p => p.name === a.name);
|
|
213
|
+
const status = proc ? proc.pm2_env.status : 'not running';
|
|
214
|
+
console.log(`- ${a.name}: ${a.cmd} (${a.cwd}) [${status}]`);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (sub === 'stop') {
|
|
220
|
+
const name = args[2];
|
|
221
|
+
if (!name) {
|
|
222
|
+
console.error('Usage: nodeproxy app stop <name>');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
pm2.stopApp(name);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (sub === 'logs') {
|
|
230
|
+
const name = args[2];
|
|
231
|
+
if (!name) {
|
|
232
|
+
console.error('Usage: nodeproxy app logs <name>');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
pm2.logs(name);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.error('Usage: nodeproxy app add <name> --cmd "<command>" [--cwd <dir>] | list | stop <name> | logs <name>');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
65
243
|
if (cmd === 'start') {
|
|
66
244
|
const { config, file } = loadConfig(configPath);
|
|
67
245
|
console.log(`[nodeproxy] Using config: ${file}`);
|
|
68
246
|
if (file.endsWith(DEFAULT_CONFIG_NAME)) {
|
|
69
247
|
console.log('[nodeproxy] (edit this file, or use "nodeproxy add <host> <target>", to change routing)');
|
|
70
248
|
}
|
|
249
|
+
|
|
250
|
+
if (config.apps && config.apps.length > 0) {
|
|
251
|
+
if (!pm2.ensureInstalled()) {
|
|
252
|
+
console.error('[nodeproxy] Could not set up pm2 — registered apps will not be started automatically.');
|
|
253
|
+
} else {
|
|
254
|
+
for (const app of config.apps) {
|
|
255
|
+
pm2.startApp(app);
|
|
256
|
+
}
|
|
257
|
+
pm2.save();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
71
261
|
startServer(config);
|
|
262
|
+
|
|
263
|
+
if (args.includes('--tunnel')) {
|
|
264
|
+
const name = getFlag(args, '--name') || 'nodeproxy-tunnel';
|
|
265
|
+
if (!cf.isInstalled()) {
|
|
266
|
+
console.error('[tunnel] cloudflared is not installed. Run "nodeproxy tunnel setup --domain <domain>" first.');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
cf.runTunnel(name);
|
|
270
|
+
}
|
|
72
271
|
return;
|
|
73
272
|
}
|
|
74
273
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const CF_DIR = path.join(os.homedir(), '.cloudflared');
|
|
9
|
+
|
|
10
|
+
function isInstalled() {
|
|
11
|
+
try {
|
|
12
|
+
execSync('cloudflared --version', { stdio: 'ignore' });
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function installInstructions() {
|
|
20
|
+
const platform = os.platform();
|
|
21
|
+
if (platform === 'darwin') {
|
|
22
|
+
return 'brew install cloudflared';
|
|
23
|
+
}
|
|
24
|
+
if (platform === 'linux') {
|
|
25
|
+
return [
|
|
26
|
+
'# Debian/Ubuntu (x86_64):',
|
|
27
|
+
'curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb && sudo dpkg -i cloudflared.deb',
|
|
28
|
+
'',
|
|
29
|
+
'# Or as a plain binary (any distro, x86_64):',
|
|
30
|
+
'sudo curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && sudo chmod +x /usr/local/bin/cloudflared'
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
if (platform === 'win32') {
|
|
34
|
+
return 'winget install --id Cloudflare.cloudflared\n# or: choco install cloudflared';
|
|
35
|
+
}
|
|
36
|
+
return 'See https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Best-effort automatic install. Returns true on success.
|
|
40
|
+
function tryAutoInstall() {
|
|
41
|
+
const platform = os.platform();
|
|
42
|
+
try {
|
|
43
|
+
if (platform === 'darwin') {
|
|
44
|
+
console.log('[tunnel] Installing cloudflared via Homebrew...');
|
|
45
|
+
execSync('brew install cloudflared', { stdio: 'inherit' });
|
|
46
|
+
return isInstalled();
|
|
47
|
+
}
|
|
48
|
+
if (platform === 'linux') {
|
|
49
|
+
console.log('[tunnel] Installing cloudflared binary to /usr/local/bin ...');
|
|
50
|
+
execSync(
|
|
51
|
+
'curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared && ' +
|
|
52
|
+
'chmod +x /tmp/cloudflared && ' +
|
|
53
|
+
'(sudo mv /tmp/cloudflared /usr/local/bin/cloudflared || mv /tmp/cloudflared /usr/local/bin/cloudflared)',
|
|
54
|
+
{ stdio: 'inherit' }
|
|
55
|
+
);
|
|
56
|
+
return isInstalled();
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.warn('[tunnel] Automatic install failed:', err.message);
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ensureInstalled() {
|
|
65
|
+
if (isInstalled()) return true;
|
|
66
|
+
console.log('[tunnel] cloudflared not found — attempting automatic install...');
|
|
67
|
+
if (tryAutoInstall()) {
|
|
68
|
+
console.log('[tunnel] cloudflared installed successfully.');
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
console.error('\n[tunnel] Could not auto-install cloudflared. Install it manually with:\n');
|
|
72
|
+
console.error(installInstructions());
|
|
73
|
+
console.error('\nThen re-run this command.\n');
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isLoggedIn() {
|
|
78
|
+
return fs.existsSync(path.join(CF_DIR, 'cert.pem'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Opens a browser for the one-time Cloudflare account authorization.
|
|
82
|
+
// This step is inherently interactive (OAuth) and cannot be scripted away.
|
|
83
|
+
function login() {
|
|
84
|
+
console.log('\n[tunnel] Opening your browser to authorize this machine with your Cloudflare account.');
|
|
85
|
+
console.log('[tunnel] (One-time step. After this, everything is fully automatic.)\n');
|
|
86
|
+
execSync('cloudflared tunnel login', { stdio: 'inherit' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tunnelExists(name) {
|
|
90
|
+
try {
|
|
91
|
+
const out = execSync('cloudflared tunnel list -o json', { encoding: 'utf8' });
|
|
92
|
+
const list = JSON.parse(out);
|
|
93
|
+
return list.some(t => t.name === name);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createTunnel(name) {
|
|
100
|
+
console.log(`[tunnel] Creating tunnel "${name}"...`);
|
|
101
|
+
execSync(`cloudflared tunnel create ${name}`, { stdio: 'inherit' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getTunnelId(name) {
|
|
105
|
+
const out = execSync('cloudflared tunnel list -o json', { encoding: 'utf8' });
|
|
106
|
+
const list = JSON.parse(out);
|
|
107
|
+
const match = list.find(t => t.name === name);
|
|
108
|
+
return match ? match.id : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function routeDns(name, domain) {
|
|
112
|
+
console.log(`[tunnel] Pointing ${domain} at tunnel "${name}"...`);
|
|
113
|
+
execSync(`cloudflared tunnel route dns ${name} ${domain}`, { stdio: 'inherit' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Writes the cloudflared ingress config that maps the public domain
|
|
117
|
+
// to the local nodeproxy port.
|
|
118
|
+
function writeIngressConfig(name, tunnelId, domain, localPort) {
|
|
119
|
+
const credFile = path.join(CF_DIR, `${tunnelId}.json`);
|
|
120
|
+
const configYml = [
|
|
121
|
+
`tunnel: ${tunnelId}`,
|
|
122
|
+
`credentials-file: ${credFile}`,
|
|
123
|
+
'ingress:',
|
|
124
|
+
` - hostname: ${domain}`,
|
|
125
|
+
` service: http://localhost:${localPort}`,
|
|
126
|
+
' - service: http_status:404',
|
|
127
|
+
''
|
|
128
|
+
].join('\n');
|
|
129
|
+
const configPath = path.join(CF_DIR, `${name}.yml`);
|
|
130
|
+
fs.writeFileSync(configPath, configYml);
|
|
131
|
+
return configPath;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Runs the tunnel as a background child process. Returns the child so
|
|
135
|
+
// the caller can manage its lifecycle alongside the local proxy.
|
|
136
|
+
function runTunnel(name) {
|
|
137
|
+
const configPath = path.join(CF_DIR, `${name}.yml`);
|
|
138
|
+
console.log(`[tunnel] Starting cloudflared tunnel "${name}"...`);
|
|
139
|
+
const child = spawn('cloudflared', ['tunnel', '--config', configPath, 'run', name], {
|
|
140
|
+
stdio: 'inherit'
|
|
141
|
+
});
|
|
142
|
+
child.on('exit', (code) => {
|
|
143
|
+
console.log(`[tunnel] cloudflared exited with code ${code}`);
|
|
144
|
+
});
|
|
145
|
+
return child;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dns = require('dns').promises;
|
|
149
|
+
|
|
150
|
+
// Diagnoses the single most common cause of "works on my machine but not
|
|
151
|
+
// on other devices/networks": the DNS record Cloudflare created for the
|
|
152
|
+
// tunnel is set to "DNS only" (grey cloud) instead of "Proxied" (orange
|
|
153
|
+
// cloud). Tunnel hostnames only resolve publicly when proxied — an
|
|
154
|
+
// unproxied record either fails to resolve for everyone else, or resolves
|
|
155
|
+
// to an address only reachable from networks that already have a route to
|
|
156
|
+
// it (which can look like "it works for me" if you're testing from
|
|
157
|
+
// somewhere with cached/local resolution).
|
|
158
|
+
async function diagnose(domain) {
|
|
159
|
+
const results = { domain, checks: [] };
|
|
160
|
+
|
|
161
|
+
let cname = null;
|
|
162
|
+
try {
|
|
163
|
+
const records = await dns.resolveCname(domain);
|
|
164
|
+
cname = records[0];
|
|
165
|
+
results.checks.push({ ok: true, label: 'CNAME record found', detail: cname });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
results.checks.push({ ok: false, label: 'No CNAME record found', detail: err.code });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (cname && !cname.includes('cfargotunnel.com')) {
|
|
171
|
+
results.checks.push({
|
|
172
|
+
ok: false,
|
|
173
|
+
label: 'CNAME does not point at a Cloudflare Tunnel',
|
|
174
|
+
detail: `Points at "${cname}" instead of a *.cfargotunnel.com address. Re-run "nodeproxy tunnel setup" to fix routing.`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const addrs = await dns.resolve4(domain);
|
|
180
|
+
results.checks.push({ ok: true, label: 'Resolves to an IP address publicly', detail: addrs.join(', ') });
|
|
181
|
+
results.publiclyResolvable = true;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
results.publiclyResolvable = false;
|
|
184
|
+
results.checks.push({
|
|
185
|
+
ok: false,
|
|
186
|
+
label: 'Does NOT resolve to an IP address publicly',
|
|
187
|
+
detail: `${err.code}. This is almost always because the DNS record is set to "DNS only" (grey cloud) in the Cloudflare dashboard instead of "Proxied" (orange cloud). Fix: Cloudflare dashboard → DNS → click the cloud icon next to this record so it turns orange, then wait ~1 minute and retry.`
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function printDiagnosis(results) {
|
|
195
|
+
console.log(`\n[tunnel doctor] Checking ${results.domain} ...\n`);
|
|
196
|
+
for (const c of results.checks) {
|
|
197
|
+
console.log(`${c.ok ? '✓' : '✗'} ${c.label}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
198
|
+
}
|
|
199
|
+
if (results.publiclyResolvable) {
|
|
200
|
+
console.log('\n[tunnel doctor] DNS looks correctly configured for public access.');
|
|
201
|
+
console.log('If it still only works on your own machine, check: is "nodeproxy start --tunnel" actually running right now? The tunnel only carries traffic while that process is alive.\n');
|
|
202
|
+
} else {
|
|
203
|
+
console.log('\n[tunnel doctor] This is why other devices can\'t reach it — see the fix above.\n');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
isInstalled,
|
|
209
|
+
ensureInstalled,
|
|
210
|
+
installInstructions,
|
|
211
|
+
isLoggedIn,
|
|
212
|
+
login,
|
|
213
|
+
tunnelExists,
|
|
214
|
+
createTunnel,
|
|
215
|
+
getTunnelId,
|
|
216
|
+
routeDns,
|
|
217
|
+
writeIngressConfig,
|
|
218
|
+
runTunnel,
|
|
219
|
+
diagnose,
|
|
220
|
+
printDiagnosis,
|
|
221
|
+
CF_DIR
|
|
222
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -7,34 +7,64 @@ const DEFAULT_CONFIG_NAME = 'nodeproxy.config.json';
|
|
|
7
7
|
|
|
8
8
|
function defaultConfig() {
|
|
9
9
|
return {
|
|
10
|
-
//
|
|
11
|
-
|
|
10
|
+
// Request logging to stdout (applies to all listeners)
|
|
11
|
+
logging: true,
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
// Backend project(s) that nodeproxy will start and keep alive for you
|
|
14
|
+
// via pm2, so you don't have to manually run "npm start" yourself
|
|
15
|
+
// before the proxy has anything to forward to. Optional — leave this
|
|
16
|
+
// empty if you're already running your project some other way.
|
|
17
|
+
apps: [
|
|
18
|
+
// { "name": "myapp", "cmd": "npm start", "cwd": "/path/to/project" }
|
|
19
|
+
],
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
21
|
+
// One entry per port you want the proxy to listen on.
|
|
22
|
+
// Each listener has its own independent set of host-based routes,
|
|
23
|
+
// so project A can live on 8080 and project B on 9090, fully
|
|
24
|
+
// separate from each other.
|
|
25
|
+
servers: [
|
|
26
|
+
{
|
|
27
|
+
port: 8080,
|
|
28
|
+
https: {
|
|
29
|
+
enabled: false,
|
|
30
|
+
key: '', // path to privkey.pem
|
|
31
|
+
cert: '' // path to fullchain.pem
|
|
32
|
+
},
|
|
33
|
+
// Virtual hosts / routes for THIS listener.
|
|
34
|
+
// "host" supports "*" as a catch-all for local testing.
|
|
35
|
+
// "targets" is a list -> automatic round-robin load balancing.
|
|
36
|
+
routes: [
|
|
37
|
+
{
|
|
38
|
+
host: '*',
|
|
39
|
+
targets: ['http://localhost:3000'],
|
|
40
|
+
// If set, static files are served from this directory when
|
|
41
|
+
// no upstream target responds (or as the sole behavior if
|
|
42
|
+
// targets is empty).
|
|
43
|
+
staticDir: null
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
51
|
+
// Accepts either the current multi-listener format ({ servers: [...] })
|
|
52
|
+
// or the old single-listener format ({ port, https, routes }) and always
|
|
53
|
+
// returns the multi-listener shape, so old config files keep working.
|
|
54
|
+
function normalizeConfig(config) {
|
|
55
|
+
if (Array.isArray(config.servers)) {
|
|
56
|
+
if (!Array.isArray(config.apps)) config.apps = [];
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
60
|
+
return {
|
|
61
|
+
logging: config.logging !== false,
|
|
62
|
+
apps: Array.isArray(config.apps) ? config.apps : [],
|
|
63
|
+
servers: [
|
|
31
64
|
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// no upstream target responds (or as the sole behavior if
|
|
36
|
-
// targets is empty).
|
|
37
|
-
staticDir: null
|
|
65
|
+
port: config.port || 8080,
|
|
66
|
+
https: config.https || { enabled: false, key: '', cert: '' },
|
|
67
|
+
routes: config.routes || []
|
|
38
68
|
}
|
|
39
69
|
]
|
|
40
70
|
};
|
|
@@ -58,7 +88,7 @@ function loadConfig(customPath) {
|
|
|
58
88
|
const file = ensureConfig(customPath);
|
|
59
89
|
const raw = fs.readFileSync(file, 'utf8');
|
|
60
90
|
try {
|
|
61
|
-
return { config: JSON.parse(raw), file };
|
|
91
|
+
return { config: normalizeConfig(JSON.parse(raw)), file };
|
|
62
92
|
} catch (err) {
|
|
63
93
|
throw new Error(`Failed to parse ${file}: ${err.message}`);
|
|
64
94
|
}
|
|
@@ -70,4 +100,4 @@ function saveConfig(config, customPath) {
|
|
|
70
100
|
return file;
|
|
71
101
|
}
|
|
72
102
|
|
|
73
|
-
module.exports = { defaultConfig, configPath, ensureConfig, loadConfig, saveConfig, DEFAULT_CONFIG_NAME };
|
|
103
|
+
module.exports = { defaultConfig, normalizeConfig, configPath, ensureConfig, loadConfig, saveConfig, DEFAULT_CONFIG_NAME };
|
package/lib/pm2.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function isInstalled() {
|
|
6
|
+
try {
|
|
7
|
+
execSync('pm2 -v', { stdio: 'ignore' });
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureInstalled() {
|
|
15
|
+
if (isInstalled()) return true;
|
|
16
|
+
console.log('[pm2] pm2 not found — installing globally via npm...');
|
|
17
|
+
try {
|
|
18
|
+
execSync('npm install -g pm2', { stdio: 'inherit' });
|
|
19
|
+
return isInstalled();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('[pm2] Automatic install failed:', err.message);
|
|
22
|
+
console.error('[pm2] Install it manually with: npm install -g pm2');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function listApps() {
|
|
28
|
+
try {
|
|
29
|
+
const out = execSync('pm2 jlist', { encoding: 'utf8' });
|
|
30
|
+
return JSON.parse(out);
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isRunning(name) {
|
|
37
|
+
return listApps().some(p => p.name === name && p.pm2_env && p.pm2_env.status === 'online');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Starts (or restarts, if already registered) an app under pm2.
|
|
41
|
+
// { name, cmd, cwd } — cmd can be "npm start", "node server.js", etc.
|
|
42
|
+
function startApp({ name, cmd, cwd }) {
|
|
43
|
+
const exists = listApps().some(p => p.name === name);
|
|
44
|
+
if (exists) {
|
|
45
|
+
console.log(`[pm2] "${name}" already registered — restarting it.`);
|
|
46
|
+
const res = spawnSync('pm2', ['restart', name], { stdio: 'inherit' });
|
|
47
|
+
return res.status === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`[pm2] Starting "${name}" (${cmd}) in ${cwd || process.cwd()} ...`);
|
|
51
|
+
// Split "npm start" style commands into interpreter + args so pm2 can
|
|
52
|
+
// manage them directly rather than as a raw shell string.
|
|
53
|
+
const parts = cmd.trim().split(/\s+/);
|
|
54
|
+
const bin = parts[0];
|
|
55
|
+
const args = parts.slice(1);
|
|
56
|
+
|
|
57
|
+
const pm2Args = ['start', bin, '--name', name];
|
|
58
|
+
if (cwd) pm2Args.push('--cwd', cwd);
|
|
59
|
+
if (args.length) pm2Args.push('--', ...args);
|
|
60
|
+
|
|
61
|
+
const res = spawnSync('pm2', pm2Args, { stdio: 'inherit' });
|
|
62
|
+
return res.status === 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stopApp(name) {
|
|
66
|
+
spawnSync('pm2', ['stop', name], { stdio: 'inherit' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function deleteApp(name) {
|
|
70
|
+
spawnSync('pm2', ['delete', name], { stdio: 'inherit' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logs(name) {
|
|
74
|
+
spawnSync('pm2', ['logs', name], { stdio: 'inherit' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Persists the current pm2 process list so it comes back after a reboot,
|
|
78
|
+
// PROVIDED "pm2 startup" has also been run once (that step needs sudo and
|
|
79
|
+
// is printed to the user rather than run automatically).
|
|
80
|
+
function save() {
|
|
81
|
+
spawnSync('pm2', ['save'], { stdio: 'inherit' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printStartupInstructions() {
|
|
85
|
+
console.log('\n[pm2] To make your app(s) survive a machine reboot, run this once:');
|
|
86
|
+
console.log(' pm2 startup');
|
|
87
|
+
console.log(' (then copy/run the sudo command it prints)');
|
|
88
|
+
console.log(' pm2 save\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
isInstalled,
|
|
93
|
+
ensureInstalled,
|
|
94
|
+
listApps,
|
|
95
|
+
isRunning,
|
|
96
|
+
startApp,
|
|
97
|
+
stopApp,
|
|
98
|
+
deleteApp,
|
|
99
|
+
logs,
|
|
100
|
+
save,
|
|
101
|
+
printStartupInstructions
|
|
102
|
+
};
|