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.
Files changed (3) hide show
  1. package/index.js +3 -3
  2. package/lib.js +59 -23
  3. 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 { port, host, apiKey, subdomain } = parseArgs(process.argv.slice(2));
9
+ const { target, host, apiKey, subdomain } = parseArgs(process.argv.slice(2));
10
10
 
11
- console.log(`${BOLD}runlocal${RESET} — expose localhost:${port} to the internet`);
12
- createConnection({ host, port, apiKey, subdomain, WebSocket });
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 port = 3000;
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 localhost to the internet. Works with runlocal.eu");
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
- port = parseInt(argv[i], 10);
88
+ target = parseTarget(argv[i]);
67
89
  }
68
90
  }
69
91
 
70
- return { port, host, apiKey, subdomain };
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, port, nextRef, log) {
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: "127.0.0.1",
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 = http.request(options, (proxyRes) => {
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 respBody = Buffer.concat(chunks).toString();
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: respBody,
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: `Could not connect to localhost:${port} — ${err.message}`,
197
+ body: Buffer.from(errBody, "utf8").toString("base64"),
198
+ body_encoding: "base64",
165
199
  },
166
200
  ])
167
201
  );
168
202
  });
169
203
 
170
- if (body && body.length > 0) {
171
- proxyReq.write(body);
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
- port,
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 localhost:${port}${RESET}`);
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, port, nextRef, log);
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, port, nextRef, log, activeWsConnections, WebSocket);
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, port, nextRef, log, activeWsConnections, WebSocket) {
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 localWsUrl = `ws://127.0.0.1:${port}${fullPath}`;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlocal",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Expose localhost to the internet — open source tunnel server",
5
5
  "bin": {
6
6
  "runlocal": "./index.js"