locadot 1.6.0-beta.2 → 1.6.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 +2 -1
- package/README.md +4 -3
- package/dist/dashboard/api.js +2 -2
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/page.js +36 -12
- package/dist/index.js +2 -0
- package/dist/lib/commands.js +3 -3
- package/dist/lib/hosts.js +3 -2
- package/dist/lib/http.js +64 -3
- package/dist/server.js +37 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
The version in `package.json` is 1.6.0-beta.
|
|
5
|
+
The version in `package.json` is 1.6.0-beta.3 (published under the `beta` tag). The owner decides the bump. **2.0.0** is suggested because of the
|
|
6
6
|
breaking items below.
|
|
7
7
|
|
|
8
8
|
### Breaking
|
|
@@ -13,6 +13,7 @@ breaking items below.
|
|
|
13
13
|
- Bare `localhost` is reserved for the dashboard and can't be mapped.
|
|
14
14
|
|
|
15
15
|
### Added
|
|
16
|
+
- `--cors` per mapping (CLI, API and dashboard). `Origin`/`Referer` are sent as the target's own origin, CORS preflights are answered locally, and any origin may call the domain with credentials. (ENH-12)
|
|
16
17
|
- A control panel at `https://localhost` with a dark theme. You can add, edit and remove mappings, toggle CA trust and start-at-boot, see root/admin and privileged-port status,
|
|
17
18
|
tail and clear logs, and stop the proxy. (FEAT-05)
|
|
18
19
|
- A token-protected JSON API for scripts and AI agents (`POST/PUT/DELETE /api/hosts`, `/api/startup`, `/api/trust`, `/api/logs`, `/api/proxy/stop`),
|
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ Options for `add` / `update`:
|
|
|
54
54
|
| `-p, --port <port>` | Shorthand for `--target http://localhost:<port>`. |
|
|
55
55
|
| `-t, --target <url>` | Any http(s) upstream. A bare `host:port` means `http://host:port`. |
|
|
56
56
|
| `-k, --insecure` | Don't verify the TLS certificate of an `https` target (self-signed upstreams). |
|
|
57
|
+
| `--cors` / `--no-cors` | Bypass CORS for this domain. The upstream gets `Origin`/`Referer` as its own origin, preflights are answered locally, any origin may read responses (with credentials), and cookies become `SameSite=None` over HTTPS. URLs hard-coded in pages still go to the real domain. |
|
|
57
58
|
| `--no-start` | Save the mapping without starting the proxy. |
|
|
58
59
|
|
|
59
60
|
### Proxy
|
|
@@ -93,7 +94,7 @@ are answered by locadot itself, not proxied. The page is a dark control panel:
|
|
|
93
94
|
|
|
94
95
|
- **System:** whether the proxy runs as root/admin, whether ports below 1024 can be bound (with the Linux sysctl fix when they
|
|
95
96
|
can't), CA trust with an on/off toggle, start-at-boot with an on/off toggle, ports, bind addresses and state dir. It also has a **Stop proxy** button.
|
|
96
|
-
- **Proxies:** add a mapping (host + port / host:port / URL, and
|
|
97
|
+
- **Proxies:** add a mapping (host + port / host:port / URL, and optional "Insecure TLS" and "Bypass CORS" boxes), edit a target inline, or remove one. The table shows
|
|
97
98
|
each source → destination with up/down, status, latency, hits, errors, last access and average ms. It refreshes every 5 s.
|
|
98
99
|
- **Logs:** a live tail with refresh and clear.
|
|
99
100
|
- **CLI / API snippets** you can copy.
|
|
@@ -113,8 +114,8 @@ each time the proxy starts.
|
|
|
113
114
|
| `GET /api/status` | | Proxy info, uptime, host count, and `system` (`platform`, `isRoot`, `canBindPrivileged`, `unprivilegedPortStart`, `caTrusted`, `startup`, `stateDir`). |
|
|
114
115
|
| `GET /api/hosts` | | Every mapping with urls, probe (`up`, `status`, `ms`) and stats. |
|
|
115
116
|
| `GET /api/logs?lines=200` | | `{ lines: [...] }` |
|
|
116
|
-
| `POST /api/hosts` | `{ "host": "app.localhost", "target": "3000", "insecure": false }` | Add a mapping (201; 409 if it exists). |
|
|
117
|
-
| `PUT /api/hosts/:host` | `{ "target": "https://example.com", "insecure": false }` | Change a mapping (404 if unknown). |
|
|
117
|
+
| `POST /api/hosts` | `{ "host": "app.localhost", "target": "3000", "insecure": false, "cors": false }` | Add a mapping (201; 409 if it exists). |
|
|
118
|
+
| `PUT /api/hosts/:host` | `{ "target": "https://example.com", "insecure": false, "cors": true }` | Change a mapping (404 if unknown). |
|
|
118
119
|
| `DELETE /api/hosts/:host` | | Remove a mapping. |
|
|
119
120
|
| `POST /api/startup` | `{ "enabled": true }` | Start at boot on/off. |
|
|
120
121
|
| `POST /api/trust` | `{ "trusted": true }` | Trust or untrust the CA. |
|
package/dist/dashboard/api.js
CHANGED
|
@@ -165,7 +165,7 @@ async function route(req, url, ctx) {
|
|
|
165
165
|
const body = await readJson(req);
|
|
166
166
|
try {
|
|
167
167
|
if (path === "/api/hosts") {
|
|
168
|
-
const { host, entry } = await hosts_1.default.add({ host: body.host, target: body.target, insecure: optionalBool(body.insecure, "insecure") });
|
|
168
|
+
const { host, entry } = await hosts_1.default.add({ host: body.host, target: body.target, insecure: optionalBool(body.insecure, "insecure"), cors: optionalBool(body.cors, "cors") });
|
|
169
169
|
ctx.reload();
|
|
170
170
|
logger_1.default.info(`➕ dashboard: ${host} → ${entry.target}`);
|
|
171
171
|
return { status: 201, body: { ok: true, host, ...entry, url: hostUrl(host, ctx) } };
|
|
@@ -178,7 +178,7 @@ async function route(req, url, ctx) {
|
|
|
178
178
|
logger_1.default.info(`🗑️ dashboard: removed ${host}`);
|
|
179
179
|
return { status: 200, body: { ok: true, host } };
|
|
180
180
|
}
|
|
181
|
-
const updated = await hosts_1.default.update({ host, target: body.target, insecure: optionalBool(body.insecure, "insecure") });
|
|
181
|
+
const updated = await hosts_1.default.update({ host, target: body.target, insecure: optionalBool(body.insecure, "insecure"), cors: optionalBool(body.cors, "cors") });
|
|
182
182
|
ctx.reload();
|
|
183
183
|
logger_1.default.info(`✏️ dashboard: ${updated.host} → ${updated.entry.target}`);
|
|
184
184
|
return { status: 200, body: { ok: true, host: updated.host, ...updated.entry, url: hostUrl(updated.host, ctx) } };
|
package/dist/dashboard/index.js
CHANGED
package/dist/dashboard/page.js
CHANGED
|
@@ -305,6 +305,9 @@ tr:last-child td { border-bottom: none; }
|
|
|
305
305
|
width: 100%;
|
|
306
306
|
min-width: 160px;
|
|
307
307
|
}
|
|
308
|
+
.badges { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
309
|
+
.badge-cors { color: var(--accent-cyan); border-color: var(--accent-cyan); background: rgba(34, 211, 238, 0.1); }
|
|
310
|
+
td .inline-check + .inline-check { margin-top: 4px; }
|
|
308
311
|
.inline-check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
309
312
|
.dim { color: var(--muted-dim); }
|
|
310
313
|
|
|
@@ -449,6 +452,7 @@ const clientJs = `
|
|
|
449
452
|
var addHostInput = document.getElementById("add-host");
|
|
450
453
|
var addTargetInput = document.getElementById("add-target");
|
|
451
454
|
var addInsecureInput = document.getElementById("add-insecure");
|
|
455
|
+
var addCorsInput = document.getElementById("add-cors");
|
|
452
456
|
var addSubmitBtn = document.getElementById("add-submit");
|
|
453
457
|
var addErrorEl = document.getElementById("add-error");
|
|
454
458
|
|
|
@@ -757,9 +761,10 @@ const clientJs = `
|
|
|
757
761
|
}
|
|
758
762
|
if (hostVal.indexOf(".") === -1) hostVal = hostVal + ".localhost";
|
|
759
763
|
var insecureVal = !!addInsecureInput.checked;
|
|
764
|
+
var corsVal = !!addCorsInput.checked;
|
|
760
765
|
addSubmitBtn.disabled = true;
|
|
761
766
|
addSubmitBtn.classList.add("busy");
|
|
762
|
-
apiFetch("/api/hosts", { method: "POST", body: JSON.stringify({ host: hostVal, target: targetVal, insecure: insecureVal }) })
|
|
767
|
+
apiFetch("/api/hosts", { method: "POST", body: JSON.stringify({ host: hostVal, target: targetVal, insecure: insecureVal, cors: corsVal }) })
|
|
763
768
|
.then(function () {
|
|
764
769
|
addForm.reset();
|
|
765
770
|
showToast("Added " + hostVal, "success");
|
|
@@ -822,17 +827,24 @@ const clientJs = `
|
|
|
822
827
|
tr.appendChild(makeCell("\\u2192", "arrow"));
|
|
823
828
|
tr.appendChild(makeCell(row.target, "mono"));
|
|
824
829
|
|
|
825
|
-
var
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
830
|
+
var optionsTd = document.createElement("td");
|
|
831
|
+
var flags = [];
|
|
832
|
+
if (row.insecure) flags.push(["insecure", "Upstream TLS certificate is not verified"]);
|
|
833
|
+
if (row.cors) flags.push(["cors", "Origin/Referer rewritten to the target; any origin may call this host"]);
|
|
834
|
+
if (flags.length) {
|
|
835
|
+
optionsTd.className = "badges";
|
|
836
|
+
flags.forEach(function (flag) {
|
|
837
|
+
var badge = document.createElement("span");
|
|
838
|
+
badge.className = "badge" + (flag[0] === "cors" ? " badge-cors" : "");
|
|
839
|
+
badge.textContent = flag[0];
|
|
840
|
+
badge.title = flag[1];
|
|
841
|
+
optionsTd.appendChild(badge);
|
|
842
|
+
});
|
|
831
843
|
} else {
|
|
832
|
-
|
|
833
|
-
|
|
844
|
+
optionsTd.className = "dim";
|
|
845
|
+
optionsTd.textContent = "\\u2014";
|
|
834
846
|
}
|
|
835
|
-
tr.appendChild(
|
|
847
|
+
tr.appendChild(optionsTd);
|
|
836
848
|
|
|
837
849
|
var statusTd = document.createElement("td");
|
|
838
850
|
statusTd.appendChild(statusPill(row));
|
|
@@ -899,6 +911,14 @@ const clientJs = `
|
|
|
899
911
|
insecureLabel.appendChild(insecureInput);
|
|
900
912
|
insecureLabel.appendChild(document.createTextNode("insecure"));
|
|
901
913
|
insecureTd.appendChild(insecureLabel);
|
|
914
|
+
var corsLabel = document.createElement("label");
|
|
915
|
+
corsLabel.className = "inline-check";
|
|
916
|
+
var corsInput = document.createElement("input");
|
|
917
|
+
corsInput.type = "checkbox";
|
|
918
|
+
corsInput.checked = !!row.cors;
|
|
919
|
+
corsLabel.appendChild(corsInput);
|
|
920
|
+
corsLabel.appendChild(document.createTextNode("cors"));
|
|
921
|
+
insecureTd.appendChild(corsLabel);
|
|
902
922
|
tr.appendChild(insecureTd);
|
|
903
923
|
|
|
904
924
|
tr.appendChild(makeCell("\\u2014", "dim"));
|
|
@@ -927,7 +947,7 @@ const clientJs = `
|
|
|
927
947
|
cancelBtn.disabled = true;
|
|
928
948
|
apiFetch("/api/hosts/" + encodeURIComponent(row.host), {
|
|
929
949
|
method: "PUT",
|
|
930
|
-
body: JSON.stringify({ target: newTarget, insecure: !!insecureInput.checked })
|
|
950
|
+
body: JSON.stringify({ target: newTarget, insecure: !!insecureInput.checked, cors: !!corsInput.checked })
|
|
931
951
|
})
|
|
932
952
|
.then(function () {
|
|
933
953
|
editingHost = null;
|
|
@@ -1134,6 +1154,10 @@ function renderPage(nonce, token) {
|
|
|
1134
1154
|
<input type="checkbox" id="add-insecure" name="insecure">
|
|
1135
1155
|
<label for="add-insecure">Insecure TLS</label>
|
|
1136
1156
|
</div>
|
|
1157
|
+
<div class="field field-checkbox" title="Send Origin/Referer as the target's own and let any origin call this host">
|
|
1158
|
+
<input type="checkbox" id="add-cors" name="cors">
|
|
1159
|
+
<label for="add-cors">Bypass CORS</label>
|
|
1160
|
+
</div>
|
|
1137
1161
|
<button type="submit" id="add-submit" class="btn btn-primary">Add proxy</button>
|
|
1138
1162
|
</div>
|
|
1139
1163
|
<div id="add-error" class="field-error" role="alert" hidden></div>
|
|
@@ -1153,7 +1177,7 @@ function renderPage(nonce, token) {
|
|
|
1153
1177
|
<th scope="col">Host</th>
|
|
1154
1178
|
<th scope="col" class="visually-hidden">Flow</th>
|
|
1155
1179
|
<th scope="col">Target</th>
|
|
1156
|
-
<th scope="col">
|
|
1180
|
+
<th scope="col">Options</th>
|
|
1157
1181
|
<th scope="col">Status</th>
|
|
1158
1182
|
<th scope="col">Hits</th>
|
|
1159
1183
|
<th scope="col">Errors</th>
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ const destinationOptions = (command) => command
|
|
|
30
30
|
.option("-p, --port <port>", "Local port to forward to (shorthand for --target http://localhost:<port>)")
|
|
31
31
|
.option("-t, --target <url>", "Any upstream: 3000, 127.0.0.1:8080, http://192.168.1.5:8080, https://google.com")
|
|
32
32
|
.option("-k, --insecure", "Don't verify the TLS certificate of an https target")
|
|
33
|
+
.option("--cors", "Send Origin/Referer as the target's own and let any origin call this domain")
|
|
34
|
+
.option("--no-cors", "Turn --cors off again (update)")
|
|
33
35
|
.option("--no-start", "Only save the mapping; don't start the proxy");
|
|
34
36
|
program
|
|
35
37
|
.name("locadot")
|
package/dist/lib/commands.js
CHANGED
|
@@ -90,7 +90,7 @@ const warnIfDown = async (entry) => {
|
|
|
90
90
|
};
|
|
91
91
|
class Commands {
|
|
92
92
|
static async add(options) {
|
|
93
|
-
const { host, entry } = await hosts_1.default.add({ host: options.host, target: resolveTarget(options), insecure: options.insecure });
|
|
93
|
+
const { host, entry } = await hosts_1.default.add({ host: options.host, target: resolveTarget(options), insecure: options.insecure, cors: options.cors });
|
|
94
94
|
const { target } = entry;
|
|
95
95
|
await ensureRunning(options);
|
|
96
96
|
print(`✅ ${(0, exports.urlFor)(host)} → ${target}`);
|
|
@@ -100,7 +100,7 @@ class Commands {
|
|
|
100
100
|
await warnIfDown(entry);
|
|
101
101
|
}
|
|
102
102
|
static async update(options) {
|
|
103
|
-
const { host, entry } = await hosts_1.default.update({ host: options.host, target: resolveTarget(options), insecure: options.insecure });
|
|
103
|
+
const { host, entry } = await hosts_1.default.update({ host: options.host, target: resolveTarget(options), insecure: options.insecure, cors: options.cors });
|
|
104
104
|
const { target } = entry;
|
|
105
105
|
await ensureRunning(options);
|
|
106
106
|
print(`✅ Updated ${(0, exports.urlFor)(host)} → ${target}`);
|
|
@@ -122,7 +122,7 @@ class Commands {
|
|
|
122
122
|
}
|
|
123
123
|
const width = Math.max(...hosts.map(([host]) => (0, exports.urlFor)(host).length));
|
|
124
124
|
for (const [host, entry] of hosts) {
|
|
125
|
-
print(`${(0, exports.urlFor)(host).padEnd(width)} → ${entry.target}${entry.insecure ? " (insecure)" : ""}`);
|
|
125
|
+
print(`${(0, exports.urlFor)(host).padEnd(width)} → ${entry.target}${entry.insecure ? " (insecure)" : ""}${entry.cors ? " (cors)" : ""}`);
|
|
126
126
|
}
|
|
127
127
|
print(`☑️ Total: ${hosts.length}.`);
|
|
128
128
|
}
|
package/dist/lib/hosts.js
CHANGED
|
@@ -69,7 +69,7 @@ class HostOps {
|
|
|
69
69
|
if (existing) {
|
|
70
70
|
throw new ConflictError(`❌ ${host} is already mapped to ${existing.target}. Use \`locadot update --host ${host} ...\` instead.`);
|
|
71
71
|
}
|
|
72
|
-
registry.hosts[host] = { target, insecure: input.insecure || undefined, createdAt: now, updatedAt: now };
|
|
72
|
+
registry.hosts[host] = { target, insecure: input.insecure || undefined, cors: input.cors || undefined, createdAt: now, updatedAt: now };
|
|
73
73
|
return registry.hosts[host];
|
|
74
74
|
});
|
|
75
75
|
return { host, entry };
|
|
@@ -82,7 +82,8 @@ class HostOps {
|
|
|
82
82
|
if (!existing)
|
|
83
83
|
throw new NotFoundError(`${constants_1.default.proxyInfo.hostNotFound} (${host})`);
|
|
84
84
|
const insecure = input.insecure ?? existing.insecure;
|
|
85
|
-
|
|
85
|
+
const cors = input.cors ?? existing.cors;
|
|
86
|
+
registry.hosts[host] = { ...existing, target, insecure: insecure || undefined, cors: cors || undefined, updatedAt: new Date().toISOString() };
|
|
86
87
|
return registry.hosts[host];
|
|
87
88
|
});
|
|
88
89
|
return { host, entry };
|
package/dist/lib/http.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.hostOf = void 0;
|
|
6
|
+
exports.applyCors = exports.hostOf = void 0;
|
|
7
7
|
const template_1 = require("../constants/template");
|
|
8
8
|
const constants_1 = __importDefault(require("../constants"));
|
|
9
9
|
const logger_1 = __importDefault(require("../utils/logger"));
|
|
@@ -34,8 +34,54 @@ const proxyOptions = (req, entry) => ({
|
|
|
34
34
|
hostRewrite: req.headers.host,
|
|
35
35
|
protocolRewrite: isTls(req) ? "https" : "http",
|
|
36
36
|
cookieDomainRewrite: { "*": "" },
|
|
37
|
-
headers: { "X-Original-Host": req.headers.host || "" },
|
|
37
|
+
headers: { "X-Original-Host": req.headers.host || "", ...(entry.cors ? sameOriginHeaders(req, entry) : {}) },
|
|
38
38
|
});
|
|
39
|
+
// --cors: the upstream should see a request from its own site, so origin/CSRF checks pass.
|
|
40
|
+
const sameOriginHeaders = (req, entry) => {
|
|
41
|
+
const origin = new URL(entry.target).origin;
|
|
42
|
+
const headers = {};
|
|
43
|
+
if (req.headers.origin)
|
|
44
|
+
headers.Origin = origin;
|
|
45
|
+
if (req.headers.referer) {
|
|
46
|
+
try {
|
|
47
|
+
const referer = new URL(req.headers.referer);
|
|
48
|
+
headers.Referer = origin + referer.pathname + referer.search;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
headers.Referer = origin + "/";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return headers;
|
|
55
|
+
};
|
|
56
|
+
const isPreflight = (req) => req.method === "OPTIONS" && !!req.headers.origin && !!req.headers["access-control-request-method"];
|
|
57
|
+
/** Headers that let the calling page read the response, credentials included. */
|
|
58
|
+
const allowOrigin = (req) => req.headers.origin
|
|
59
|
+
? { "Access-Control-Allow-Origin": req.headers.origin, "Access-Control-Allow-Credentials": "true", Vary: "Origin" }
|
|
60
|
+
: { "Access-Control-Allow-Origin": "*" };
|
|
61
|
+
/**
|
|
62
|
+
* Rewrites an upstream response for a --cors host: our CORS headers replace the upstream's,
|
|
63
|
+
* and cookies become SameSite=None so a page on another origin can send them back.
|
|
64
|
+
*/
|
|
65
|
+
const applyCors = (req, headers) => {
|
|
66
|
+
for (const name of Object.keys(headers)) {
|
|
67
|
+
if (name.startsWith("access-control-"))
|
|
68
|
+
delete headers[name];
|
|
69
|
+
}
|
|
70
|
+
const exposed = Object.keys(headers).filter((name) => name !== "set-cookie" && name !== "vary");
|
|
71
|
+
headers["access-control-allow-origin"] = req.headers.origin || "*";
|
|
72
|
+
if (req.headers.origin) {
|
|
73
|
+
headers["access-control-allow-credentials"] = "true";
|
|
74
|
+
const vary = String(headers.vary || "");
|
|
75
|
+
if (!/(^|,)\s*(origin|\*)\s*(,|$)/i.test(vary))
|
|
76
|
+
headers.vary = vary ? `${vary}, Origin` : "Origin";
|
|
77
|
+
}
|
|
78
|
+
if (exposed.length)
|
|
79
|
+
headers["access-control-expose-headers"] = exposed.join(", ");
|
|
80
|
+
if (isTls(req) && headers["set-cookie"]) {
|
|
81
|
+
headers["set-cookie"] = headers["set-cookie"].map((cookie) => cookie.replace(/;\s*samesite=[^;]*/gi, "").replace(/;\s*secure\b(?!=)/gi, "") + "; SameSite=None; Secure");
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
exports.applyCors = applyCors;
|
|
39
85
|
const record = (stats, host, status, ms, error) => {
|
|
40
86
|
const current = stats.get(host) || { hits: 0, errors: 0 };
|
|
41
87
|
current.hits += 1;
|
|
@@ -60,6 +106,20 @@ class HttpModule {
|
|
|
60
106
|
res.end((0, template_1.proxyNotFound)(host, dashboardUrl(req)));
|
|
61
107
|
return;
|
|
62
108
|
}
|
|
109
|
+
// Answered here: upstreams often reject OPTIONS or don't send the headers the browser wants.
|
|
110
|
+
if (entry.cors && isPreflight(req)) {
|
|
111
|
+
const requested = req.headers["access-control-request-headers"];
|
|
112
|
+
res.writeHead(204, {
|
|
113
|
+
...allowOrigin(req),
|
|
114
|
+
"Access-Control-Allow-Methods": String(req.headers["access-control-request-method"]),
|
|
115
|
+
...(requested ? { "Access-Control-Allow-Headers": String(requested) } : {}),
|
|
116
|
+
...(req.headers["access-control-request-private-network"] ? { "Access-Control-Allow-Private-Network": "true" } : {}),
|
|
117
|
+
"Access-Control-Max-Age": "600",
|
|
118
|
+
"Content-Length": "0",
|
|
119
|
+
});
|
|
120
|
+
res.end();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
63
123
|
const started = Date.now();
|
|
64
124
|
res.once("finish", () => {
|
|
65
125
|
record(ctx.stats, host, res.statusCode, Date.now() - started, res.statusCode >= 500);
|
|
@@ -70,7 +130,8 @@ class HttpModule {
|
|
|
70
130
|
res.destroy();
|
|
71
131
|
return;
|
|
72
132
|
}
|
|
73
|
-
|
|
133
|
+
// With --cors the page should see a 502, not a CORS error.
|
|
134
|
+
res.writeHead(502, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", ...(entry.cors ? allowOrigin(req) : {}) });
|
|
74
135
|
res.end((0, template_1.upstreamDown)(host, entry.target, err?.code || err?.message || "error", dashboardUrl(req)));
|
|
75
136
|
});
|
|
76
137
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -12,7 +45,7 @@ const http_proxy_1 = __importDefault(require("http-proxy"));
|
|
|
12
45
|
const constants_1 = __importDefault(require("./constants"));
|
|
13
46
|
const registry_1 = __importDefault(require("./lib/registry"));
|
|
14
47
|
const locadot_file_1 = __importDefault(require("./lib/locadot-file"));
|
|
15
|
-
const http_2 =
|
|
48
|
+
const http_2 = __importStar(require("./lib/http"));
|
|
16
49
|
const localhost_1 = __importDefault(require("./lib/localhost"));
|
|
17
50
|
const file_1 = __importDefault(require("./utils/file"));
|
|
18
51
|
const logger_1 = __importDefault(require("./utils/logger"));
|
|
@@ -68,7 +101,7 @@ async function startCentralProxy() {
|
|
|
68
101
|
// Hop-by-hop headers describe the upstream connection, not ours. Apache sends
|
|
69
102
|
// `Connection: Upgrade, close` + `Upgrade: h2`, which made us close the browser's
|
|
70
103
|
// socket after every response (ERR_TOO_MANY_RETRIES on asset-heavy pages).
|
|
71
|
-
proxy.on("proxyRes", (proxyRes) => {
|
|
104
|
+
proxy.on("proxyRes", (proxyRes, req) => {
|
|
72
105
|
if (proxyRes.statusCode === 101)
|
|
73
106
|
return;
|
|
74
107
|
const listed = String(proxyRes.headers.connection || "")
|
|
@@ -78,6 +111,8 @@ async function startCentralProxy() {
|
|
|
78
111
|
for (const name of [...listed, "connection", "keep-alive", "upgrade", "proxy-connection"]) {
|
|
79
112
|
delete proxyRes.headers[name];
|
|
80
113
|
}
|
|
114
|
+
if (registry.hosts[(0, http_2.hostOf)(req)]?.cors)
|
|
115
|
+
(0, http_2.applyCors)(req, proxyRes.headers);
|
|
81
116
|
});
|
|
82
117
|
const info = {
|
|
83
118
|
pid: process.pid,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locadot",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.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",
|