runlocal 0.8.0 → 0.10.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/index.js +3 -3
- package/lib.js +59 -23
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const { parseArgs, createConnection } = require("./lib");
|
|
|
6
6
|
const BOLD = "\x1b[1m";
|
|
7
7
|
const RESET = "\x1b[0m";
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const { target, host, apiKey, subdomain } = parseArgs(process.argv.slice(2));
|
|
10
10
|
|
|
11
|
-
console.log(`${BOLD}runlocal${RESET} — expose
|
|
12
|
-
createConnection({ host,
|
|
11
|
+
console.log(`${BOLD}runlocal${RESET} — expose ${target.display} to the internet`);
|
|
12
|
+
createConnection({ host, target, apiKey, subdomain, WebSocket });
|
package/lib.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const http = require("http");
|
|
2
|
+
const https = require("https");
|
|
2
3
|
const fs = require("fs");
|
|
3
4
|
const path = require("path");
|
|
4
5
|
const os = require("os");
|
|
@@ -25,8 +26,27 @@ function readApiKeyFile() {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function parseTarget(value) {
|
|
30
|
+
if (/^https?:\/\//.test(value)) {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
return {
|
|
33
|
+
hostname: url.hostname,
|
|
34
|
+
port: url.port ? parseInt(url.port, 10) : (url.protocol === "https:" ? 443 : 80),
|
|
35
|
+
protocol: url.protocol,
|
|
36
|
+
display: value.replace(/\/$/, ""),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const port = parseInt(value, 10);
|
|
40
|
+
return {
|
|
41
|
+
hostname: "127.0.0.1",
|
|
42
|
+
port,
|
|
43
|
+
protocol: "http:",
|
|
44
|
+
display: `localhost:${port}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
function parseArgs(argv) {
|
|
29
|
-
let
|
|
49
|
+
let target = parseTarget("3000");
|
|
30
50
|
let host = process.env.RUNLOCAL_HOST || "wss://runlocal.eu";
|
|
31
51
|
let apiKey = process.env.RUNLATER_API_KEY || readApiKeyFile();
|
|
32
52
|
let subdomain = null;
|
|
@@ -38,9 +58,9 @@ function parseArgs(argv) {
|
|
|
38
58
|
} else if (argv[i] === "--subdomain" && argv[i + 1]) {
|
|
39
59
|
subdomain = argv[++i];
|
|
40
60
|
} else if (argv[i] === "--help" || argv[i] === "-h") {
|
|
41
|
-
console.log("Usage: runlocal <port> [options]");
|
|
61
|
+
console.log("Usage: runlocal <port|url> [options]");
|
|
42
62
|
console.log("");
|
|
43
|
-
console.log(" Expose
|
|
63
|
+
console.log(" Expose a local server to the internet. Works with runlocal.eu");
|
|
44
64
|
console.log(" or any self-hosted runlocal server.");
|
|
45
65
|
console.log("");
|
|
46
66
|
console.log("Options:");
|
|
@@ -55,6 +75,8 @@ function parseArgs(argv) {
|
|
|
55
75
|
console.log("");
|
|
56
76
|
console.log("Examples:");
|
|
57
77
|
console.log(" npx runlocal 3000 Random subdomain");
|
|
78
|
+
console.log(" npx runlocal https://10.8.0.1 Proxy any URL");
|
|
79
|
+
console.log(" npx runlocal http://myapp.local:8080 Custom host and port");
|
|
58
80
|
console.log(" npx runlocal 3000 --api-key pk_xxx Stable subdomain");
|
|
59
81
|
console.log(" npx runlocal 3000 --subdomain my-api Custom subdomain");
|
|
60
82
|
console.log(" npx runlocal 3000 --server wss://tunnel.example.com Self-hosted");
|
|
@@ -63,11 +85,11 @@ function parseArgs(argv) {
|
|
|
63
85
|
console.log("Hosted version: https://runlocal.eu");
|
|
64
86
|
process.exit(0);
|
|
65
87
|
} else if (!argv[i].startsWith("-")) {
|
|
66
|
-
|
|
88
|
+
target = parseTarget(argv[i]);
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
return {
|
|
92
|
+
return { target, host, apiKey, subdomain };
|
|
71
93
|
}
|
|
72
94
|
|
|
73
95
|
function filterHeaders(headers) {
|
|
@@ -83,16 +105,23 @@ function filterHeaders(headers) {
|
|
|
83
105
|
}
|
|
84
106
|
|
|
85
107
|
function buildWsUrl(host, apiKey, subdomain) {
|
|
86
|
-
const params = new URLSearchParams({ vsn: "2.0.0" });
|
|
108
|
+
const params = new URLSearchParams({ vsn: "2.0.0", caps: "binary-bodies" });
|
|
87
109
|
if (apiKey) params.set("api_key", apiKey);
|
|
88
110
|
if (subdomain) params.set("subdomain", subdomain);
|
|
89
111
|
return `${host}/tunnel/websocket?${params.toString()}`;
|
|
90
112
|
}
|
|
91
113
|
|
|
92
|
-
function handleRequest(ws, joinRef, topic, payload,
|
|
93
|
-
const { request_id, method, path, query_string, headers, body } = payload;
|
|
114
|
+
function handleRequest(ws, joinRef, topic, payload, target, nextRef, log) {
|
|
115
|
+
const { request_id, method, path, query_string, headers, body, body_encoding } = payload;
|
|
94
116
|
const fullPath = query_string ? `${path}?${query_string}` : path;
|
|
95
117
|
|
|
118
|
+
let requestBody = null;
|
|
119
|
+
if (body && body.length > 0) {
|
|
120
|
+
requestBody = body_encoding === "base64"
|
|
121
|
+
? Buffer.from(body, "base64")
|
|
122
|
+
: Buffer.from(body);
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
const timestamp = new Date().toLocaleTimeString();
|
|
97
126
|
log(
|
|
98
127
|
`${DIM}${timestamp}${RESET} ${BOLD}${method}${RESET} ${fullPath}`
|
|
@@ -100,19 +129,21 @@ function handleRequest(ws, joinRef, topic, payload, port, nextRef, log) {
|
|
|
100
129
|
|
|
101
130
|
const reqHeaders = filterHeaders(headers);
|
|
102
131
|
|
|
132
|
+
const requester = target.protocol === "https:" ? https : http;
|
|
103
133
|
const options = {
|
|
104
|
-
hostname:
|
|
105
|
-
port: port,
|
|
134
|
+
hostname: target.hostname,
|
|
135
|
+
port: target.port,
|
|
106
136
|
path: fullPath,
|
|
107
137
|
method: method,
|
|
108
138
|
headers: reqHeaders,
|
|
139
|
+
rejectUnauthorized: false,
|
|
109
140
|
};
|
|
110
141
|
|
|
111
|
-
const proxyReq =
|
|
142
|
+
const proxyReq = requester.request(options, (proxyRes) => {
|
|
112
143
|
const chunks = [];
|
|
113
144
|
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
114
145
|
proxyRes.on("end", () => {
|
|
115
|
-
const
|
|
146
|
+
const respBuffer = Buffer.concat(chunks);
|
|
116
147
|
const respHeaders = [];
|
|
117
148
|
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
118
149
|
if (Array.isArray(v)) {
|
|
@@ -140,7 +171,8 @@ function handleRequest(ws, joinRef, topic, payload, port, nextRef, log) {
|
|
|
140
171
|
request_id,
|
|
141
172
|
status: proxyRes.statusCode,
|
|
142
173
|
headers: respHeaders,
|
|
143
|
-
body:
|
|
174
|
+
body: respBuffer.toString("base64"),
|
|
175
|
+
body_encoding: "base64",
|
|
144
176
|
},
|
|
145
177
|
])
|
|
146
178
|
);
|
|
@@ -151,6 +183,7 @@ function handleRequest(ws, joinRef, topic, payload, port, nextRef, log) {
|
|
|
151
183
|
log(
|
|
152
184
|
`${DIM}${timestamp}${RESET} ${RED}ERR${RESET} ${fullPath} — ${err.message}`
|
|
153
185
|
);
|
|
186
|
+
const errBody = `Could not connect to ${target.display} — ${err.message}`;
|
|
154
187
|
ws.send(
|
|
155
188
|
JSON.stringify([
|
|
156
189
|
joinRef,
|
|
@@ -161,14 +194,15 @@ function handleRequest(ws, joinRef, topic, payload, port, nextRef, log) {
|
|
|
161
194
|
request_id,
|
|
162
195
|
status: 502,
|
|
163
196
|
headers: [["content-type", "text/plain"]],
|
|
164
|
-
body:
|
|
197
|
+
body: Buffer.from(errBody, "utf8").toString("base64"),
|
|
198
|
+
body_encoding: "base64",
|
|
165
199
|
},
|
|
166
200
|
])
|
|
167
201
|
);
|
|
168
202
|
});
|
|
169
203
|
|
|
170
|
-
if (
|
|
171
|
-
proxyReq.write(
|
|
204
|
+
if (requestBody && requestBody.length > 0) {
|
|
205
|
+
proxyReq.write(requestBody);
|
|
172
206
|
}
|
|
173
207
|
proxyReq.end();
|
|
174
208
|
}
|
|
@@ -176,7 +210,7 @@ function handleRequest(ws, joinRef, topic, payload, port, nextRef, log) {
|
|
|
176
210
|
function createConnection(options) {
|
|
177
211
|
const {
|
|
178
212
|
host,
|
|
179
|
-
|
|
213
|
+
target,
|
|
180
214
|
apiKey,
|
|
181
215
|
subdomain,
|
|
182
216
|
WebSocket,
|
|
@@ -252,7 +286,7 @@ function createConnection(options) {
|
|
|
252
286
|
}
|
|
253
287
|
|
|
254
288
|
log("");
|
|
255
|
-
log(` ${DIM}Forwarding to
|
|
289
|
+
log(` ${DIM}Forwarding to ${target.display}${RESET}`);
|
|
256
290
|
log(` ${DIM}Inspect requests at ${RESET}${CYAN}${inspectUrl}${RESET}`);
|
|
257
291
|
log(` ${DIM}Press Ctrl+C to stop${RESET}`);
|
|
258
292
|
|
|
@@ -268,12 +302,12 @@ function createConnection(options) {
|
|
|
268
302
|
}
|
|
269
303
|
|
|
270
304
|
if (event === "http_request") {
|
|
271
|
-
handleRequest(ws, joinRef, topic, payload,
|
|
305
|
+
handleRequest(ws, joinRef, topic, payload, target, nextRef, log);
|
|
272
306
|
return;
|
|
273
307
|
}
|
|
274
308
|
|
|
275
309
|
if (event === "ws_upgrade") {
|
|
276
|
-
handleWsUpgrade(ws, joinRef, topic, payload,
|
|
310
|
+
handleWsUpgrade(ws, joinRef, topic, payload, target, nextRef, log, activeWsConnections, WebSocket);
|
|
277
311
|
return;
|
|
278
312
|
}
|
|
279
313
|
|
|
@@ -322,14 +356,15 @@ function createConnection(options) {
|
|
|
322
356
|
return { ws, getJoinRef: () => joinRef, nextRef };
|
|
323
357
|
}
|
|
324
358
|
|
|
325
|
-
function handleWsUpgrade(ws, joinRef, topic, payload,
|
|
359
|
+
function handleWsUpgrade(ws, joinRef, topic, payload, target, nextRef, log, activeWsConnections, WebSocket) {
|
|
326
360
|
const { ws_id, path: wsPath, query_string, headers } = payload;
|
|
327
361
|
const fullPath = query_string ? `${wsPath}?${query_string}` : wsPath;
|
|
328
362
|
const timestamp = new Date().toLocaleTimeString();
|
|
329
363
|
|
|
330
364
|
log(`${DIM}${timestamp}${RESET} ${BOLD}WS${RESET} ${fullPath}`);
|
|
331
365
|
|
|
332
|
-
const
|
|
366
|
+
const wsProtocol = target.protocol === "https:" ? "wss:" : "ws:";
|
|
367
|
+
const localWsUrl = `${wsProtocol}//${target.hostname}:${target.port}${fullPath}`;
|
|
333
368
|
|
|
334
369
|
const reqHeaders = {};
|
|
335
370
|
if (headers) {
|
|
@@ -345,7 +380,7 @@ function handleWsUpgrade(ws, joinRef, topic, payload, port, nextRef, log, active
|
|
|
345
380
|
|
|
346
381
|
let localWs;
|
|
347
382
|
try {
|
|
348
|
-
localWs = new WebSocket(localWsUrl, { headers: reqHeaders });
|
|
383
|
+
localWs = new WebSocket(localWsUrl, { headers: reqHeaders, rejectUnauthorized: false });
|
|
349
384
|
} catch (err) {
|
|
350
385
|
log(`${DIM}${timestamp}${RESET} ${RED}WS ERR${RESET} ${fullPath} — ${err.message}`);
|
|
351
386
|
ws.send(JSON.stringify([joinRef, nextRef(), topic, "ws_close", { ws_id }]));
|
|
@@ -403,6 +438,7 @@ function handleWsClose(payload, activeWsConnections) {
|
|
|
403
438
|
|
|
404
439
|
module.exports = {
|
|
405
440
|
parseArgs,
|
|
441
|
+
parseTarget,
|
|
406
442
|
filterHeaders,
|
|
407
443
|
buildWsUrl,
|
|
408
444
|
handleRequest,
|