locadot 2.1.0-beta.2 → 2.1.0-beta.3
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 +7 -0
- package/README.md +6 -0
- package/dist/lib/remotes.js +24 -9
- package/dist/proxy/cors.js +6 -2
- package/dist/proxy/hub.js +44 -3
- package/dist/proxy/remote.js +4 -3
- package/dist/proxy/request.js +3 -1
- package/dist/proxy/response.js +2 -1
- package/dist/proxy/rewrite.js +3 -1
- package/dist/proxy/router.js +17 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
### Fixed
|
|
36
36
|
- The proxy can now tell a user's `LOCADOT_HTTP_PORT`/`LOCADOT_HTTPS_PORT` from the copies the CLI pins on every spawn
|
|
37
37
|
(`LOCADOT_USER_PORTS`), so saved ports aren't reported as env-overridden.
|
|
38
|
+
- `--cors` through remote access: a shared `--cors` mapping is now `--cors` on the receiver too (shown in `list` and the
|
|
39
|
+
dashboard, kept in step by `remote:sync` and `remote:update`). The sender used to rewrite its apps' origins in pages to its
|
|
40
|
+
*own* local URLs, which don't exist on the receiver. Now the receiver sends the names it uses (`X-Locadot-Names`), so a
|
|
41
|
+
page's API calls go to the receiver's name for that mapping (e.g. `api.alice.localhost` after a clash), on the receiver's
|
|
42
|
+
port. The upstream still sees the calling page's real origin. `<port>.<domain>.localhost` gets the CORS handling of a
|
|
43
|
+
`--cors` mapping on that port. The sender keeps names only for mappings the peer can see, and only as `*.localhost` URLs.
|
|
44
|
+
Both machines need this version for the rewriting; with an older receiver the sender behaves as before.
|
|
38
45
|
|
|
39
46
|
## 2.1.0-beta.0 (2026-09-27)
|
|
40
47
|
|
package/README.md
CHANGED
|
@@ -131,6 +131,12 @@ target, so an editor can reach anything the sender's machine can reach. Only giv
|
|
|
131
131
|
of the tokens are stored, and failed attempts are rate limited. Only admins can open the sender's dashboard through the hub.
|
|
132
132
|
The dashboard has **Remote access** and **Connected machines** cards for all of this.
|
|
133
133
|
|
|
134
|
+
`--cors` carries over: a sender's `--cors` mapping is `--cors` on the receiver too, with no extra flag. Preflights and CORS
|
|
135
|
+
headers work as they do locally, and origins in the sender's pages are rewritten to the receiver's own names and port (so
|
|
136
|
+
`http://localhost:8000` in a page becomes `http://api.alice.localhost` if that's the receiver's name for `api.localhost`).
|
|
137
|
+
An admin's `<port>.<domain>.localhost` gets the same treatment when a `--cors` mapping points at that port.
|
|
138
|
+
`remote:sync` picks up a sender turning `--cors` on or off.
|
|
139
|
+
|
|
134
140
|
### Certificates
|
|
135
141
|
|
|
136
142
|
| Command | What it does |
|
package/dist/lib/remotes.js
CHANGED
|
@@ -91,6 +91,8 @@ const isDomainLabel = (value) => {
|
|
|
91
91
|
exports.isDomainLabel = isDomainLabel;
|
|
92
92
|
/** Sender allows the peer onto its localhost, treating an older sender (no `localhost` field) as "yes" when admin. */
|
|
93
93
|
const localhostAllowed = (data, role) => typeof data.localhost === "boolean" ? data.localhost : role === "admin";
|
|
94
|
+
/** A shared --cors mapping is --cors here too; the sender still does the CORS work (see router forwardRemote). */
|
|
95
|
+
const inherited = (h) => (h.cors ? { cors: true } : {});
|
|
94
96
|
/** Domains already spoken for: other remotes' `domain`, and any single-label `*.localhost` registry host. */
|
|
95
97
|
const takenDomains = (excludeName) => {
|
|
96
98
|
const store = readFile();
|
|
@@ -212,7 +214,7 @@ class Remotes {
|
|
|
212
214
|
continue;
|
|
213
215
|
}
|
|
214
216
|
taken.add(local);
|
|
215
|
-
registry.hosts[local] = { target: remote.url, remote: { name, host: h.host }, createdAt: now, updatedAt: now };
|
|
217
|
+
registry.hosts[local] = { target: remote.url, remote: { name, host: h.host }, ...inherited(h), createdAt: now, updatedAt: now };
|
|
216
218
|
mapped.push({ local, host: h.host });
|
|
217
219
|
}
|
|
218
220
|
});
|
|
@@ -244,9 +246,14 @@ class Remotes {
|
|
|
244
246
|
const now = new Date().toISOString();
|
|
245
247
|
await registry_1.default.mutate((registry) => {
|
|
246
248
|
const already = new Set();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
const flags = new Map(hosts.map((h) => [h.host, h]));
|
|
250
|
+
for (const [local, entry] of Object.entries(registry.hosts)) {
|
|
251
|
+
if (entry.remote?.name !== name)
|
|
252
|
+
continue;
|
|
253
|
+
already.add(entry.remote.host);
|
|
254
|
+
const h = flags.get(entry.remote.host);
|
|
255
|
+
if (h && Boolean(h.cors) !== Boolean(entry.cors))
|
|
256
|
+
registry.hosts[local] = { ...entry, cors: h.cors || undefined };
|
|
250
257
|
}
|
|
251
258
|
const taken = new Set(Object.keys(registry.hosts));
|
|
252
259
|
for (const h of hosts) {
|
|
@@ -256,7 +263,7 @@ class Remotes {
|
|
|
256
263
|
if (!local)
|
|
257
264
|
continue;
|
|
258
265
|
taken.add(local);
|
|
259
|
-
registry.hosts[local] = { target: updated.url, remote: { name, host: h.host }, createdAt: now, updatedAt: now };
|
|
266
|
+
registry.hosts[local] = { target: updated.url, remote: { name, host: h.host }, ...inherited(h), createdAt: now, updatedAt: now };
|
|
260
267
|
}
|
|
261
268
|
});
|
|
262
269
|
const registry = registry_1.default.read();
|
|
@@ -270,13 +277,14 @@ class Remotes {
|
|
|
270
277
|
const local = localhost_1.default.requireHost(localHost);
|
|
271
278
|
const hostsRes = await call(`${apiBase(remote.url)}/hosts`, { token: remote.token });
|
|
272
279
|
const hosts = hostsRes.hosts;
|
|
273
|
-
|
|
280
|
+
const shared = hosts.find((h) => h.host === remoteHost);
|
|
281
|
+
if (!shared)
|
|
274
282
|
throw new hosts_1.NotFoundError(`"${remoteHost}" is not available on "${name}".`);
|
|
275
283
|
const now = new Date().toISOString();
|
|
276
284
|
await registry_1.default.mutate((registry) => {
|
|
277
285
|
if (registry.hosts[local])
|
|
278
286
|
throw new hosts_1.ConflictError(`❌ ${local} is already mapped to ${registry.hosts[local].target}.`);
|
|
279
|
-
registry.hosts[local] = { target: remote.url, remote: { name, host: remoteHost }, createdAt: now, updatedAt: now };
|
|
287
|
+
registry.hosts[local] = { target: remote.url, remote: { name, host: remoteHost }, ...inherited(shared), createdAt: now, updatedAt: now };
|
|
280
288
|
});
|
|
281
289
|
}
|
|
282
290
|
static setUrl(name, url) {
|
|
@@ -329,7 +337,7 @@ class Remotes {
|
|
|
329
337
|
const taken = new Set(Object.keys(registry.hosts));
|
|
330
338
|
const local = Remotes.localName(name, created.host, taken);
|
|
331
339
|
if (local)
|
|
332
|
-
registry.hosts[local] = { target: remote.url, remote: { name, host: created.host }, createdAt: now, updatedAt: now };
|
|
340
|
+
registry.hosts[local] = { target: remote.url, remote: { name, host: created.host }, ...inherited(created), createdAt: now, updatedAt: now };
|
|
333
341
|
});
|
|
334
342
|
return created;
|
|
335
343
|
}
|
|
@@ -340,7 +348,14 @@ class Remotes {
|
|
|
340
348
|
token: remote.token,
|
|
341
349
|
body: input,
|
|
342
350
|
});
|
|
343
|
-
|
|
351
|
+
const updated = data.host;
|
|
352
|
+
await registry_1.default.mutate((registry) => {
|
|
353
|
+
for (const [local, entry] of Object.entries(registry.hosts)) {
|
|
354
|
+
if (entry.remote?.name === name && entry.remote.host === host)
|
|
355
|
+
registry.hosts[local] = { ...entry, cors: updated.cors || undefined };
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return updated;
|
|
344
359
|
}
|
|
345
360
|
static async removeHost(name, host) {
|
|
346
361
|
const remote = Remotes.mustGet(name);
|
package/dist/proxy/cors.js
CHANGED
|
@@ -24,12 +24,16 @@ exports.preflightHeaders = preflightHeaders;
|
|
|
24
24
|
/**
|
|
25
25
|
* --cors: the upstream should see a request from a site it trusts, so origin/CSRF checks pass.
|
|
26
26
|
* A page on another mapped domain (signalsant.localhost calling api.signalsant.localhost) is
|
|
27
|
-
* sent as that domain's real origin; anything else as the target's own origin.
|
|
27
|
+
* sent as that domain's real origin; anything else as the target's own origin. A peer's page is on the
|
|
28
|
+
* receiver's name for one of our domains, so it is matched by those names, never by our own.
|
|
28
29
|
*/
|
|
29
30
|
const sameOriginHeaders = (req, entry, lookup) => {
|
|
31
|
+
const names = (0, request_1.peerNamesOf)(req);
|
|
32
|
+
const callerHost = (url) => names ? Object.keys(names).find((host) => new URL(names[host]).host === url.host) : url.hostname;
|
|
30
33
|
const upstreamOrigin = (value) => {
|
|
31
34
|
try {
|
|
32
|
-
const
|
|
35
|
+
const host = callerHost(new URL(value));
|
|
36
|
+
const caller = host ? lookup(host) : undefined;
|
|
33
37
|
if (caller)
|
|
34
38
|
return new URL(caller.target).origin;
|
|
35
39
|
}
|
package/dist/proxy/hub.js
CHANGED
|
@@ -87,6 +87,39 @@ const receiverHost = (req) => {
|
|
|
87
87
|
const match = /^([^:]+)(?::(\d{1,5}))?$/.exec(raw);
|
|
88
88
|
return match && localhost_1.default.isValidLocalhostDomain(match[1]) ? raw : undefined;
|
|
89
89
|
};
|
|
90
|
+
const MAX_NAMES = 16 * 1024;
|
|
91
|
+
/**
|
|
92
|
+
* `X-Locadot-Names` from the receiver: `<our host>=<its URL for it>,…`. Kept only for hosts this peer can see and
|
|
93
|
+
* `http(s)://*.localhost[:port]` URLs; they only ever end up in responses to that same peer.
|
|
94
|
+
*/
|
|
95
|
+
const peerNames = (req, visible) => {
|
|
96
|
+
const header = req.headers["x-locadot-names"];
|
|
97
|
+
if (typeof header !== "string" || header.length > MAX_NAMES)
|
|
98
|
+
return undefined;
|
|
99
|
+
const names = {};
|
|
100
|
+
for (const pair of header.split(",")) {
|
|
101
|
+
const [host, url] = pair.trim().toLowerCase().split("=");
|
|
102
|
+
const match = url ? /^https?:\/\/([^/:]+)(?::(\d{1,5}))?$/.exec(url) : null;
|
|
103
|
+
if (host && match && visible.includes(host) && localhost_1.default.isValidLocalhostDomain(match[1]))
|
|
104
|
+
names[host] = url;
|
|
105
|
+
}
|
|
106
|
+
return names;
|
|
107
|
+
};
|
|
108
|
+
/** Sender side: a --cors mapping whose target is this machine's `port`, so port access gets its cors too. */
|
|
109
|
+
const corsMappingFor = (hosts, port) => Object.keys(hosts).find((host) => {
|
|
110
|
+
const entry = hosts[host];
|
|
111
|
+
if (!entry.cors || entry.remote)
|
|
112
|
+
return false;
|
|
113
|
+
try {
|
|
114
|
+
const url = new URL(entry.target);
|
|
115
|
+
const own = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
116
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) && own === port;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const ownHosts = (hosts, peer) => links_1.default.visibleHosts(peer, Object.keys(hosts)).filter((host) => !hosts[host].remote);
|
|
90
123
|
const stripLocadotHeaders = (req) => {
|
|
91
124
|
for (const name of Object.keys(req.headers))
|
|
92
125
|
if (name.toLowerCase().startsWith("x-locadot-"))
|
|
@@ -127,7 +160,7 @@ function classify(req, publicHost, hosts, opts) {
|
|
|
127
160
|
if (!origin)
|
|
128
161
|
return { kind: "deny", status: 400, message: "Bad request" };
|
|
129
162
|
// Keep X-Locadot-Token: the dashboard page sends it on every mutating call.
|
|
130
|
-
for (const name of ["x-locadot-peer", "x-locadot-dashboard", "x-locadot-host", "x-locadot-port"])
|
|
163
|
+
for (const name of ["x-locadot-peer", "x-locadot-dashboard", "x-locadot-host", "x-locadot-port", "x-locadot-names"])
|
|
131
164
|
delete req.headers[name];
|
|
132
165
|
return { kind: "dashboard", peer, origin };
|
|
133
166
|
}
|
|
@@ -139,8 +172,15 @@ function classify(req, publicHost, hosts, opts) {
|
|
|
139
172
|
return { kind: "deny", status: 400, message: "Bad request" };
|
|
140
173
|
if (opts?.blockedPorts?.includes(port))
|
|
141
174
|
return { kind: "deny", status: 403, message: "Forbidden" };
|
|
175
|
+
const names = req.headers["x-locadot-names"] === undefined ? undefined : peerNames(req, ownHosts(hosts, peer));
|
|
176
|
+
const host = corsMappingFor(hosts, port);
|
|
177
|
+
// The page stays on <port>.<domain>.localhost rather than moving to the receiver's name for the mapping.
|
|
178
|
+
const self = receiverHost(req);
|
|
179
|
+
const scheme = names && Object.values(names)[0]?.split(":")[0];
|
|
180
|
+
if (host && names && self && scheme)
|
|
181
|
+
names[host] = `${scheme}://${self}`;
|
|
142
182
|
stripLocadotHeaders(req);
|
|
143
|
-
return { kind: "local", port, peer };
|
|
183
|
+
return { kind: "local", port, peer, names, host };
|
|
144
184
|
}
|
|
145
185
|
const hostHeader = req.headers["x-locadot-host"];
|
|
146
186
|
const host = typeof hostHeader === "string" ? hostHeader.trim().toLowerCase() : "";
|
|
@@ -152,8 +192,9 @@ function classify(req, publicHost, hosts, opts) {
|
|
|
152
192
|
if (!visible.includes(host)) {
|
|
153
193
|
return { kind: "deny", status: 403, message: "Forbidden" };
|
|
154
194
|
}
|
|
195
|
+
const names = req.headers["x-locadot-names"] === undefined ? undefined : peerNames(req, visible);
|
|
155
196
|
stripLocadotHeaders(req);
|
|
156
|
-
return { kind: "app", host, peer };
|
|
197
|
+
return { kind: "app", host, peer, names };
|
|
157
198
|
}
|
|
158
199
|
/* ---------- /_locadot/v1 API ---------- */
|
|
159
200
|
const send = (res, status, body) => {
|
package/dist/proxy/remote.js
CHANGED
|
@@ -32,7 +32,7 @@ const remotes = () => {
|
|
|
32
32
|
};
|
|
33
33
|
const remoteFor = (name) => remotes()[name];
|
|
34
34
|
exports.remoteFor = remoteFor;
|
|
35
|
-
const remoteOptions = (req, entry, remote) => ({
|
|
35
|
+
const remoteOptions = (req, entry, remote, names) => ({
|
|
36
36
|
target: remote.url,
|
|
37
37
|
changeOrigin: true,
|
|
38
38
|
// Don't leak the receiver's LAN addresses to the sender.
|
|
@@ -47,6 +47,7 @@ const remoteOptions = (req, entry, remote) => ({
|
|
|
47
47
|
"X-Original-Host": req.headers.host || "",
|
|
48
48
|
"X-Locadot-Peer": remote.token,
|
|
49
49
|
"X-Locadot-Host": entry.remote.host,
|
|
50
|
+
...(names ? { "X-Locadot-Names": names } : {}),
|
|
50
51
|
},
|
|
51
52
|
});
|
|
52
53
|
exports.remoteOptions = remoteOptions;
|
|
@@ -75,7 +76,7 @@ exports.localFor = localFor;
|
|
|
75
76
|
const canReachLocalhost = (remote) => remote.role === "admin" && remote.localhost !== false;
|
|
76
77
|
exports.canReachLocalhost = canReachLocalhost;
|
|
77
78
|
/** Without a port the sender serves its dashboard. */
|
|
78
|
-
const localOptions = (req, remote, port) => ({
|
|
79
|
+
const localOptions = (req, remote, port, names) => ({
|
|
79
80
|
target: remote.url,
|
|
80
81
|
changeOrigin: true,
|
|
81
82
|
xfwd: false,
|
|
@@ -88,7 +89,7 @@ const localOptions = (req, remote, port) => ({
|
|
|
88
89
|
headers: {
|
|
89
90
|
"X-Original-Host": req.headers.host || "",
|
|
90
91
|
"X-Locadot-Peer": remote.token,
|
|
91
|
-
...(port ? { "X-Locadot-Port": String(port) } : { "X-Locadot-Dashboard": "1" }),
|
|
92
|
+
...(port ? { "X-Locadot-Port": String(port), ...(names ? { "X-Locadot-Names": names } : {}) } : { "X-Locadot-Dashboard": "1" }),
|
|
92
93
|
},
|
|
93
94
|
});
|
|
94
95
|
exports.localOptions = localOptions;
|
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.peerOriginOf = exports.fromRemote = exports.fromTunnel = exports.mappedHost = exports.hostOf = exports.tag = void 0;
|
|
3
|
+
exports.isTls = exports.viaOf = 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 });
|
|
@@ -23,6 +23,8 @@ const fromRemote = (req) => Boolean(tags.get(req)?.remote);
|
|
|
23
23
|
exports.fromRemote = fromRemote;
|
|
24
24
|
const peerOriginOf = (req) => tags.get(req)?.peerOrigin;
|
|
25
25
|
exports.peerOriginOf = peerOriginOf;
|
|
26
|
+
const peerNamesOf = (req) => tags.get(req)?.peerNames;
|
|
27
|
+
exports.peerNamesOf = peerNamesOf;
|
|
26
28
|
const viaOf = (req) => tags.get(req)?.via;
|
|
27
29
|
exports.viaOf = viaOf;
|
|
28
30
|
/** Tunnel visitors are on https even though cloudflared talks plain http to us. */
|
package/dist/proxy/response.js
CHANGED
|
@@ -23,7 +23,8 @@ const handleProxyResponse = (proxyRes, req, res, hosts, publicUrl = () => undefi
|
|
|
23
23
|
return;
|
|
24
24
|
stripHopByHop(proxyRes.headers);
|
|
25
25
|
const entry = hosts[(0, request_1.mappedHost)(req)];
|
|
26
|
-
|
|
26
|
+
// A remote mapping's cors flag is inherited: the sender already applied it.
|
|
27
|
+
if (!entry?.cors || entry.remote)
|
|
27
28
|
return;
|
|
28
29
|
(0, cors_1.applyCors)(req, proxyRes.headers);
|
|
29
30
|
const via = (0, request_1.viaOf)(req);
|
package/dist/proxy/rewrite.js
CHANGED
|
@@ -25,9 +25,11 @@ const originMap = (req, hosts, publicUrl = () => undefined) => {
|
|
|
25
25
|
const tls = (0, request_1.isTls)(req);
|
|
26
26
|
const self = (0, request_1.mappedHost)(req);
|
|
27
27
|
const tunnel = (0, request_1.fromTunnel)(req);
|
|
28
|
+
// A peer's browser knows our hosts by its own names; older receivers don't send them.
|
|
29
|
+
const peer = (0, request_1.peerNamesOf)(req);
|
|
28
30
|
const pairs = [];
|
|
29
31
|
for (const [host, entry] of Object.entries(hosts)) {
|
|
30
|
-
const local = tunnel ? publicUrl(host) : (0, urls_1.urlFor)(host, tls);
|
|
32
|
+
const local = tunnel ? publicUrl(host) : peer ? peer[host] : (0, urls_1.urlFor)(host, tls);
|
|
31
33
|
if (!local)
|
|
32
34
|
continue;
|
|
33
35
|
try {
|
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() });
|
|
23
|
+
(0, request_1.tag)(req, { host: hub.host, remote: true, secure: ctx.hub.secure(), peerNames: hub.names });
|
|
24
24
|
return hub.host;
|
|
25
25
|
}
|
|
26
26
|
if (hub?.kind === "dashboard") {
|
|
@@ -28,8 +28,9 @@ const resolveHost = (req, ctx, hub) => {
|
|
|
28
28
|
return "localhost";
|
|
29
29
|
}
|
|
30
30
|
if (hub?.kind === "local") {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// A port behind a --cors mapping is served as that mapping, cors included.
|
|
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
34
|
return host;
|
|
34
35
|
}
|
|
35
36
|
const host = (0, request_1.hostOf)(req);
|
|
@@ -42,6 +43,16 @@ const dashboardUrl = (req) => `${(0, urls_1.urlFor)("localhost", (0, request_1.i
|
|
|
42
43
|
const passThrough = (req, entry) => Boolean(entry.cors);
|
|
43
44
|
/** Same-origin only, and a tunnel visitor may only reach public hosts (see guard.ts). */
|
|
44
45
|
const viaAllowed = (req, via) => (0, passthrough_1.isSameOrigin)(req) && (!(0, request_1.fromTunnel)(req) || (0, guard_1.isPublicHost)(via.host));
|
|
46
|
+
/**
|
|
47
|
+
* Receiver side: `<sender host>=<our URL for it>` for every mapping of this remote, so a --cors sender rewrites
|
|
48
|
+
* its apps' origins to the names this machine uses rather than its own.
|
|
49
|
+
*/
|
|
50
|
+
const peerNames = (req, ctx, name) => (ctx.remoteHosts?.(name) || [])
|
|
51
|
+
.flatMap((local) => {
|
|
52
|
+
const host = ctx.lookup(local)?.remote?.host;
|
|
53
|
+
return host ? [`${host}=${(0, urls_1.urlFor)(local, (0, request_1.isTls)(req))}`] : [];
|
|
54
|
+
})
|
|
55
|
+
.join(",");
|
|
45
56
|
const reason = (err) => err?.code || err?.message;
|
|
46
57
|
const remoteGone = (entry) => `the connection to ${entry.remote.name} was removed`;
|
|
47
58
|
/** Sender side: an admin peer reaching one of this machine's ports. */
|
|
@@ -84,7 +95,7 @@ function handleRequest(req, res, ctx) {
|
|
|
84
95
|
ctx.dashboard(req, res);
|
|
85
96
|
return;
|
|
86
97
|
}
|
|
87
|
-
const entry = hub?.kind === "local" ? localEntry(hub.port) : ctx.lookup(host);
|
|
98
|
+
const entry = hub?.kind === "local" && !hub.host ? localEntry(hub.port) : ctx.lookup(host);
|
|
88
99
|
if (!entry) {
|
|
89
100
|
const local = localTarget(req, ctx, host, hub);
|
|
90
101
|
if (local) {
|
|
@@ -155,7 +166,7 @@ function forwardRemote(req, res, ctx, host, entry) {
|
|
|
155
166
|
}
|
|
156
167
|
const started = Date.now();
|
|
157
168
|
res.once("finish", () => (0, stats_1.record)(ctx.stats, host, res.statusCode, Date.now() - started, res.statusCode >= 500));
|
|
158
|
-
ctx.proxy.web(req, res, (0, remote_1.remoteOptions)(req, entry, remote), (err) => {
|
|
169
|
+
ctx.proxy.web(req, res, (0, remote_1.remoteOptions)(req, entry, remote, peerNames(req, ctx, remote.name)), (err) => {
|
|
159
170
|
logger_1.default.warn(`${host} → ${remote.name} (${remote.url}): ${reason(err)}`);
|
|
160
171
|
if (res.headersSent) {
|
|
161
172
|
res.destroy();
|
|
@@ -182,7 +193,7 @@ function forwardLocal(req, res, ctx, host, local) {
|
|
|
182
193
|
const label = `${remote.name}: ${port ? `localhost:${port}` : "dashboard"}`;
|
|
183
194
|
const started = Date.now();
|
|
184
195
|
res.once("finish", () => (0, stats_1.record)(ctx.stats, host, res.statusCode, Date.now() - started, res.statusCode >= 500));
|
|
185
|
-
ctx.proxy.web(req, res, (0, remote_1.localOptions)(req, remote, port), (err) => {
|
|
196
|
+
ctx.proxy.web(req, res, (0, remote_1.localOptions)(req, remote, port, port ? peerNames(req, ctx, remote.name) : undefined), (err) => {
|
|
186
197
|
logger_1.default.warn(`${host} → ${label}: ${reason(err)}`);
|
|
187
198
|
if (res.headersSent) {
|
|
188
199
|
res.destroy();
|
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.3",
|
|
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",
|