locadot 2.1.0-beta.3 → 2.1.0-beta.4
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/CHANGELOG.md +4 -0
- package/README.md +2 -0
- package/dist/proxy/guard.js +12 -1
- package/dist/proxy/hub.js +4 -2
- package/dist/proxy/passthrough.js +7 -3
- package/dist/proxy/request.js +3 -1
- package/dist/proxy/router.js +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
Senders must run this version too; an older sender answers `<domain>.localhost` with a 404.
|
|
34
34
|
|
|
35
35
|
### Fixed
|
|
36
|
+
- `--cors` pages that call an API on a bare `localhost:<port>` (e.g. `VITE_SERVER_URL=http://localhost:3100`) now send those
|
|
37
|
+
calls through the page's own origin (`/__locadot/x/http/localhost:3100/…`) like any other cross-origin call. Opened through
|
|
38
|
+
remote access, the API used to be called on the *viewer's* localhost, where it doesn't exist. A peer's pass-through reaches
|
|
39
|
+
the sender's localhost only with localhost access (admin, `hub:localhost on`), and never the proxy's own ports.
|
|
36
40
|
- The proxy can now tell a user's `LOCADOT_HTTP_PORT`/`LOCADOT_HTTPS_PORT` from the copies the CLI pins on every spawn
|
|
37
41
|
(`LOCADOT_USER_PORTS`), so saved ports aren't reported as env-overridden.
|
|
38
42
|
- `--cors` through remote access: a shared `--cors` mapping is now `--cors` on the receiver too (shown in `list` and the
|
package/README.md
CHANGED
|
@@ -295,6 +295,8 @@ locadot add --host signalsant.localhost --target https://signalsant.com --cors
|
|
|
295
295
|
`XMLHttpRequest`, `EventSource`, `WebSocket` and `sendBeacon` calls to other origins through the page's own origin
|
|
296
296
|
(`/__locadot/x/https/api.signalsant.com/…`). The browser sees a same-origin request, so CORS never applies, and locadot
|
|
297
297
|
forwards it with `Origin: https://signalsant.com`. Only the page itself can use it: requests from other sites get a 403.
|
|
298
|
+
Calls to a bare `localhost:<port>` (an API or dev server next to the app) go the same way, so the page works when opened
|
|
299
|
+
from another machine through remote access too. There, only admin peers reach the sender's localhost this way.
|
|
298
300
|
- **Cookies.** The page's cookies are forwarded only to the same site (`api.signalsant.com`), never to third parties.
|
|
299
301
|
Cookies set by third parties are dropped.
|
|
300
302
|
- **Mapped domains.** If you also map a domain (`api.signalsant.localhost` → `https://api.signalsant.com --cors`), its URLs in
|
package/dist/proxy/guard.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.publicOnlyAgents = exports.isPublicHost = exports.isPublicAddress = void 0;
|
|
6
|
+
exports.publicOnlyAgents = exports.isPublicHost = exports.isLoopbackHost = exports.isPublicAddress = void 0;
|
|
7
7
|
const dns_1 = __importDefault(require("dns"));
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
9
|
const https_1 = __importDefault(require("https"));
|
|
@@ -54,6 +54,17 @@ const isPublicAddress = (address) => {
|
|
|
54
54
|
return net_1.default.isIPv6(address) && !blocked.check(address, "ipv6");
|
|
55
55
|
};
|
|
56
56
|
exports.isPublicAddress = isPublicAddress;
|
|
57
|
+
/** localhost, *.localhost, 127.0.0.0/8, ::1 or 0.0.0.0: this machine itself. */
|
|
58
|
+
const isLoopbackHost = (host) => {
|
|
59
|
+
const name = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase().replace(/\.$/, "");
|
|
60
|
+
if (name === "localhost" || name.endsWith(".localhost"))
|
|
61
|
+
return true;
|
|
62
|
+
const v4 = net_1.default.isIPv4(name) ? name : net_1.default.isIPv6(name) ? embeddedV4(name) : undefined;
|
|
63
|
+
if (v4)
|
|
64
|
+
return v4.startsWith("127.") || v4 === "0.0.0.0";
|
|
65
|
+
return name === "::1" || name === "::";
|
|
66
|
+
};
|
|
67
|
+
exports.isLoopbackHost = isLoopbackHost;
|
|
57
68
|
/** A host the pass-through may call for a tunnel visitor, judged before any DNS lookup. */
|
|
58
69
|
const isPublicHost = (host) => {
|
|
59
70
|
const name = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase();
|
package/dist/proxy/hub.js
CHANGED
|
@@ -180,7 +180,7 @@ function classify(req, publicHost, hosts, opts) {
|
|
|
180
180
|
if (host && names && self && scheme)
|
|
181
181
|
names[host] = `${scheme}://${self}`;
|
|
182
182
|
stripLocadotHeaders(req);
|
|
183
|
-
return { kind: "local", port, peer, names, host };
|
|
183
|
+
return { kind: "local", port, peer, names, host, loopback: opts?.blockedPorts ?? [] };
|
|
184
184
|
}
|
|
185
185
|
const hostHeader = req.headers["x-locadot-host"];
|
|
186
186
|
const host = typeof hostHeader === "string" ? hostHeader.trim().toLowerCase() : "";
|
|
@@ -194,7 +194,9 @@ function classify(req, publicHost, hosts, opts) {
|
|
|
194
194
|
}
|
|
195
195
|
const names = req.headers["x-locadot-names"] === undefined ? undefined : peerNames(req, visible);
|
|
196
196
|
stripLocadotHeaders(req);
|
|
197
|
-
|
|
197
|
+
// A --cors page's pass-through calls to localhost:<port> reach this machine only for peers allowed its localhost.
|
|
198
|
+
const loopback = links_1.default.can(peer, "localhost") && opts?.localhost !== false ? opts?.blockedPorts ?? [] : undefined;
|
|
199
|
+
return { kind: "app", host, peer, names, loopback };
|
|
198
200
|
}
|
|
199
201
|
/* ---------- /_locadot/v1 API ---------- */
|
|
200
202
|
const send = (res, status, body) => {
|
|
@@ -56,12 +56,16 @@ exports.shimScript = `(() => {
|
|
|
56
56
|
if (window.__locadotShim) return;
|
|
57
57
|
window.__locadotShim = true;
|
|
58
58
|
const VIA = ${JSON.stringify(VIA)};
|
|
59
|
+
const portOf = (url) => url.port || (url.protocol === "https:" || url.protocol === "wss:" ? "443" : "80");
|
|
59
60
|
const route = (input) => {
|
|
60
61
|
try {
|
|
61
62
|
const url = new URL(String(input), location.href);
|
|
62
63
|
if (!/^(https?|wss?):$/.test(url.protocol)) return null;
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (url.hostname.endsWith(".localhost") || url.host === location.host) return null;
|
|
65
|
+
// Bare localhost:<port> (an app's API or dev server) exists only where the app runs, so it goes through the
|
|
66
|
+
// page's origin too; a remote viewer's own localhost doesn't have it. The page's own proxy port stays direct.
|
|
67
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
68
|
+
if (loopback && portOf(url) === portOf(location)) return null;
|
|
65
69
|
const ws = url.protocol === "ws:" || url.protocol === "wss:";
|
|
66
70
|
const base = ws ? (location.protocol === "https:" ? "wss://" : "ws://") + location.host : location.origin;
|
|
67
71
|
return base + VIA + url.protocol.slice(0, -1) + "/" + url.host + url.pathname + url.search + url.hash;
|
|
@@ -150,7 +154,7 @@ const rewriteViaResponse = (headers, via, target) => {
|
|
|
150
154
|
else {
|
|
151
155
|
try {
|
|
152
156
|
const url = new URL(location, `${via.scheme}://${via.host}/`);
|
|
153
|
-
if (/^https?:$/.test(url.protocol) &&
|
|
157
|
+
if (/^https?:$/.test(url.protocol) && !url.hostname.endsWith(".localhost"))
|
|
154
158
|
headers.location = (0, exports.viaPath)(url) + url.hash;
|
|
155
159
|
}
|
|
156
160
|
catch { }
|
package/dist/proxy/request.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isTls = exports.viaOf = exports.peerNamesOf = exports.peerOriginOf = exports.fromRemote = exports.fromTunnel = exports.mappedHost = exports.hostOf = exports.tag = void 0;
|
|
3
|
+
exports.isTls = exports.viaOf = exports.loopbackOf = exports.peerNamesOf = exports.peerOriginOf = exports.fromRemote = exports.fromTunnel = exports.mappedHost = exports.hostOf = exports.tag = void 0;
|
|
4
4
|
const tags = new WeakMap();
|
|
5
5
|
const tag = (req, values) => {
|
|
6
6
|
tags.set(req, { ...tags.get(req), ...values });
|
|
@@ -25,6 +25,8 @@ const peerOriginOf = (req) => tags.get(req)?.peerOrigin;
|
|
|
25
25
|
exports.peerOriginOf = peerOriginOf;
|
|
26
26
|
const peerNamesOf = (req) => tags.get(req)?.peerNames;
|
|
27
27
|
exports.peerNamesOf = peerNamesOf;
|
|
28
|
+
const loopbackOf = (req) => tags.get(req)?.loopback;
|
|
29
|
+
exports.loopbackOf = loopbackOf;
|
|
28
30
|
const viaOf = (req) => tags.get(req)?.via;
|
|
29
31
|
exports.viaOf = viaOf;
|
|
30
32
|
/** Tunnel visitors are on https even though cloudflared talks plain http to us. */
|
package/dist/proxy/router.js
CHANGED
|
@@ -20,7 +20,7 @@ const stats_1 = require("./stats");
|
|
|
20
20
|
/** Resolves a tunnel's public host to its mapping and tags the request, before any routing. */
|
|
21
21
|
const resolveHost = (req, ctx, hub) => {
|
|
22
22
|
if (hub?.kind === "app") {
|
|
23
|
-
(0, request_1.tag)(req, { host: hub.host, remote: true, secure: ctx.hub.secure(), peerNames: hub.names });
|
|
23
|
+
(0, request_1.tag)(req, { host: hub.host, remote: true, secure: ctx.hub.secure(), peerNames: hub.names, loopback: hub.loopback });
|
|
24
24
|
return hub.host;
|
|
25
25
|
}
|
|
26
26
|
if (hub?.kind === "dashboard") {
|
|
@@ -30,7 +30,7 @@ const resolveHost = (req, ctx, hub) => {
|
|
|
30
30
|
if (hub?.kind === "local") {
|
|
31
31
|
// A port behind a --cors mapping is served as that mapping, cors included.
|
|
32
32
|
const host = hub.host ?? `localhost:${hub.port}`;
|
|
33
|
-
(0, request_1.tag)(req, { host, remote: true, secure: ctx.hub.secure(), peerNames: hub.names });
|
|
33
|
+
(0, request_1.tag)(req, { host, remote: true, secure: ctx.hub.secure(), peerNames: hub.names, loopback: hub.loopback });
|
|
34
34
|
return host;
|
|
35
35
|
}
|
|
36
36
|
const host = (0, request_1.hostOf)(req);
|
|
@@ -41,8 +41,16 @@ const resolveHost = (req, ctx, hub) => {
|
|
|
41
41
|
};
|
|
42
42
|
const dashboardUrl = (req) => `${(0, urls_1.urlFor)("localhost", (0, request_1.isTls)(req))}/`;
|
|
43
43
|
const passThrough = (req, entry) => Boolean(entry.cors);
|
|
44
|
-
/**
|
|
45
|
-
const
|
|
44
|
+
/** A hub peer reaches this machine's localhost:<port> only with localhost access, and never the proxy's own ports. */
|
|
45
|
+
const peerMayReach = (req, via) => {
|
|
46
|
+
if (!(0, request_1.fromRemote)(req) || !(0, guard_1.isLoopbackHost)(via.host))
|
|
47
|
+
return true;
|
|
48
|
+
const allowed = (0, request_1.loopbackOf)(req);
|
|
49
|
+
const port = Number(via.host.match(/:(\d+)$/)?.[1] || (via.scheme === "https" || via.scheme === "wss" ? 443 : 80));
|
|
50
|
+
return Boolean(allowed && !allowed.includes(port));
|
|
51
|
+
};
|
|
52
|
+
/** Same-origin only, a tunnel visitor may only reach public hosts (see guard.ts), and a peer only its share of localhost. */
|
|
53
|
+
const viaAllowed = (req, via) => (0, passthrough_1.isSameOrigin)(req) && (!(0, request_1.fromTunnel)(req) || (0, guard_1.isPublicHost)(via.host)) && peerMayReach(req, via);
|
|
46
54
|
/**
|
|
47
55
|
* Receiver side: `<sender host>=<our URL for it>` for every mapping of this remote, so a --cors sender rewrites
|
|
48
56
|
* its apps' origins to the names this machine uses rather than its own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locadot",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.4",
|
|
4
4
|
"description": "Secure your local development environment with HTTPS and custom domains like dev.localhost.",
|
|
5
5
|
"homepage": "https://www.npmjs.com/package/locadot",
|
|
6
6
|
"main": "dist/index.js",
|