locadot 1.6.0-beta.3 → 1.6.0-beta.5
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 +8 -2
- package/README.md +44 -8
- package/dist/cli/commands/doctor.js +71 -0
- package/dist/cli/commands/files.js +89 -0
- package/dist/cli/commands/hosts.js +108 -0
- package/dist/cli/commands/index.js +43 -0
- package/dist/cli/commands/proxy.js +100 -0
- package/dist/cli/commands/system.js +50 -0
- package/dist/cli/commands/tunnel.js +108 -0
- package/dist/cli/shared.js +20 -0
- package/dist/constants/index.js +19 -5
- package/dist/dashboard/api.js +28 -5
- package/dist/dashboard/index.js +5 -4
- package/dist/dashboard/page.js +345 -189
- package/dist/index.js +15 -5
- package/dist/lib/hosts.js +14 -9
- package/dist/lib/localhost.js +7 -0
- package/dist/{proxy.js → lib/proxy-control.js} +12 -6
- package/dist/lib/urls.js +13 -0
- package/dist/proxy/cors.js +77 -0
- package/dist/proxy/options.js +34 -0
- package/dist/proxy/passthrough.js +174 -0
- package/dist/proxy/request.js +26 -0
- package/dist/proxy/response.js +40 -0
- package/dist/proxy/rewrite.js +94 -0
- package/dist/proxy/router.js +117 -0
- package/dist/proxy/stats.js +14 -0
- package/dist/proxy/tunnel.js +197 -0
- package/dist/server/index.js +86 -0
- package/dist/server/lifecycle.js +37 -0
- package/dist/server/listeners.js +62 -0
- package/dist/server/state.js +36 -0
- package/dist/server/token.js +22 -0
- package/package.json +1 -1
- package/dist/lib/commands.js +0 -307
- package/dist/lib/http.js +0 -166
- package/dist/server.js +0 -220
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
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.5 (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
|
+
### Changed
|
|
9
|
+
- Internal restructure into `cli/commands/*`, `server/*` and `proxy/*` modules. There are no behaviour changes; see `tasks/README.md`.
|
|
10
|
+
|
|
8
11
|
### Breaking
|
|
9
12
|
- The proxy binds to `127.0.0.1` and `::1` only. Set `LOCADOT_BIND=0.0.0.0` for the old LAN-exposed behaviour.
|
|
10
13
|
- The registry format is now v2 (`{version, hosts:{host:{target,…}}}`). The old `{host: port}` files are migrated
|
|
@@ -13,7 +16,10 @@ breaking items below.
|
|
|
13
16
|
- Bare `localhost` is reserved for the dashboard and can't be mapped.
|
|
14
17
|
|
|
15
18
|
### Added
|
|
16
|
-
-
|
|
19
|
+
- `locadot tunnel --host <h>` (with `--off`, and `tunnel:install`) shares a mapping on a public `https://*.trycloudflare.com` URL through a Cloudflare quick tunnel, with no account needed. `cloudflared` is downloaded on demand into the state dir. The dashboard has a Cloudflare Tunnel card, a Public URL column and Share/Unshare per row. The API accepts `PUT /api/hosts/:host {"tunnel": true}` and has `POST /api/cloudflared/install`. The `--cors` pass-through is disabled for tunnel visitors.
|
|
20
|
+
- The dashboard is redesigned: a neutral palette that follows the system light/dark theme, a responsive grid and a single Traffic column.
|
|
21
|
+
- `locadot start --port <http> --https-port <https>` (also on `restart`) runs the proxy on other ports than 80/443. The choice is saved in the state dir, so `add`, `status`, `open` and start-at-boot keep using it; `--port 80 --https-port 443` goes back. Running it while the proxy is up on other ports restarts it on the new ones.
|
|
22
|
+
- `--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. Absolute URLs of other mapped domains in text responses are rewritten to their `.localhost` names, and a mapped caller is sent as its real origin, so a site plus its API both work locally. Pages also get a small script that sends calls to any other origin through the page's own origin (`/__locadot/x/…`), so no mapping per API or third-party domain is needed. (ENH-12, BUG-15)
|
|
17
23
|
- 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,
|
|
18
24
|
tail and clear logs, and stop the proxy. (FEAT-05)
|
|
19
25
|
- 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
|
@@ -29,7 +29,8 @@ npx locadot open # opens the dashboard at htt
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
> **Linux:** ports 80/443 need root. Either run locadot with `sudo`, or allow unprivileged binding once:
|
|
32
|
-
> `sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80` (persist it in `/etc/sysctl.d/`).
|
|
32
|
+
> `sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80` (persist it in `/etc/sysctl.d/`). Or use other ports:
|
|
33
|
+
> `locadot start --port 8080 --https-port 8443` (URLs then include the port). `locadot doctor` checks this for you.
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -54,20 +55,34 @@ Options for `add` / `update`:
|
|
|
54
55
|
| `-p, --port <port>` | Shorthand for `--target http://localhost:<port>`. |
|
|
55
56
|
| `-t, --target <url>` | Any http(s) upstream. A bare `host:port` means `http://host:port`. |
|
|
56
57
|
| `-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.
|
|
58
|
+
| `--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. The page's calls to other domains go through locadot automatically, with no extra mapping (see [Sites that call other domains](#sites-that-call-other-domains---cors)). |
|
|
58
59
|
| `--no-start` | Save the mapping without starting the proxy. |
|
|
59
60
|
|
|
60
61
|
### Proxy
|
|
61
62
|
|
|
62
63
|
| Command | What it does |
|
|
63
64
|
| --- | --- |
|
|
64
|
-
| `locadot start` | Start the central proxy. Fails loudly with the reason if it can't bind its ports. |
|
|
65
|
+
| `locadot start [--port 8080] [--https-port 8443]` | Start the central proxy. Default ports are 80 and 443. Ports you pass are remembered for later commands and start-at-boot, and a running proxy is moved to them. Fails loudly with the reason if it can't bind its ports. |
|
|
65
66
|
| `locadot stop` | Stop the proxy. Mappings and logs are kept. |
|
|
66
|
-
| `locadot restart` | Stop, then start. |
|
|
67
|
+
| `locadot restart [--port …] [--https-port …]` | Stop, then start. |
|
|
67
68
|
| `locadot kill` | Stop the proxy, remove all mappings and clear the logs. |
|
|
68
69
|
| `locadot status [--json]` | Is it running, its PID, ports, number of hosts, CA trust, start-at-boot, dashboard URL. |
|
|
69
70
|
| `locadot doctor [--host h]` | Check the proxy, ports, permissions, CA trust, the registry and every target. Exits 1 if anything fails. |
|
|
70
71
|
|
|
72
|
+
### Sharing (Cloudflare Tunnel)
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `locadot tunnel --host app.localhost` | Share a mapping on a public `https://<random>.trycloudflare.com` URL through a Cloudflare quick tunnel. No Cloudflare account is needed. `cloudflared` is downloaded on first use. |
|
|
77
|
+
| `locadot tunnel --host app.localhost --off` | Stop sharing it. |
|
|
78
|
+
| `locadot tunnel` | List the shared mappings and their public URLs. |
|
|
79
|
+
| `locadot tunnel:install` | Download `cloudflared` into the state dir (`<state>/bin`). Set `LOCADOT_CLOUDFLARED` to use your own binary; one on `PATH` is also picked up. |
|
|
80
|
+
|
|
81
|
+
The tunnel runs inside the proxy, so a shared mapping keeps working after `restart` and comes back at boot. Redirects to the
|
|
82
|
+
target are rewritten to the public URL. **Anyone with the link can reach the mapping**, so only share what you'd put online.
|
|
83
|
+
For safety, the `--cors` pass-through (`/__locadot/x/…`) is turned off for visitors coming through the tunnel, so the tunnel
|
|
84
|
+
can't be used to reach other hosts from your machine. The dashboard can also do this: use **Share** / **Unshare** on a row.
|
|
85
|
+
|
|
71
86
|
### Certificates
|
|
72
87
|
|
|
73
88
|
| Command | What it does |
|
|
@@ -90,12 +105,13 @@ Options for `add` / `update`:
|
|
|
90
105
|
## 📋 Dashboard & control panel
|
|
91
106
|
|
|
92
107
|
Open `https://localhost` or `http://localhost` (or run `locadot open`). Requests for bare `localhost` / `127.0.0.1` / `::1`
|
|
93
|
-
are answered by locadot itself, not proxied. The page is a
|
|
108
|
+
are answered by locadot itself, not proxied. The page is a control panel that follows the system light/dark theme:
|
|
94
109
|
|
|
95
110
|
- **System:** whether the proxy runs as root/admin, whether ports below 1024 can be bound (with the Linux sysctl fix when they
|
|
96
111
|
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.
|
|
97
112
|
- **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
|
|
98
|
-
each source → destination with up/down, status, latency, hits, errors,
|
|
113
|
+
each source → destination with up/down, status, latency, its public tunnel URL and traffic (hits, errors, average ms). It refreshes every 5 s.
|
|
114
|
+
- **Cloudflare Tunnel:** whether `cloudflared` is installed (with an **Install** button), and a **Share** button per row that puts the mapping on a public URL.
|
|
99
115
|
- **Logs:** a live tail with refresh and clear.
|
|
100
116
|
- **CLI / API snippets** you can copy.
|
|
101
117
|
|
|
@@ -148,8 +164,8 @@ All optional, via environment variables. Set them for the CLI; the proxy it star
|
|
|
148
164
|
| Variable | Default | Purpose |
|
|
149
165
|
| --- | --- | --- |
|
|
150
166
|
| `LOCADOT_HOME` | OS app-data dir (`~/.config/locadot` on Linux) | Where the registry, logs, lock file and certs live. |
|
|
151
|
-
| `LOCADOT_HTTP_PORT` | `80` | HTTP port of the proxy. |
|
|
152
|
-
| `LOCADOT_HTTPS_PORT` | `443` | HTTPS port of the proxy. |
|
|
167
|
+
| `LOCADOT_HTTP_PORT` | `80` | HTTP port of the proxy. Overrides the port saved by `locadot start --port`. |
|
|
168
|
+
| `LOCADOT_HTTPS_PORT` | `443` | HTTPS port of the proxy. Overrides the port saved by `locadot start --https-port`. |
|
|
153
169
|
| `LOCADOT_BIND` | `127.0.0.1,::1` | Comma-separated addresses to listen on. Use `0.0.0.0` to expose your mappings to your LAN (not recommended). |
|
|
154
170
|
| `LOCADOT_LOG_LEVEL` | `info` | winston log level (`debug` logs every WebSocket upgrade). |
|
|
155
171
|
|
|
@@ -165,6 +181,26 @@ redirect to a *different* domain (for example `google.com` → `www.google.com`)
|
|
|
165
181
|
locadot, map the final host instead (`--target https://www.google.com`). Some sites refuse to be framed or proxied; that's up to
|
|
166
182
|
the site.
|
|
167
183
|
|
|
184
|
+
### Sites that call other domains (`--cors`)
|
|
185
|
+
|
|
186
|
+
A frontend that calls its API or third-party services by absolute URL works with just the site mapped:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
locadot add --host signalsant.localhost --target https://signalsant.com --cors
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- **Pass-through.** HTML pages get a small script (`/__locadot/shim.js`) as the first thing in `<head>`. It sends `fetch`,
|
|
193
|
+
`XMLHttpRequest`, `EventSource`, `WebSocket` and `sendBeacon` calls to other origins through the page's own origin
|
|
194
|
+
(`/__locadot/x/https/api.signalsant.com/…`). The browser sees a same-origin request, so CORS never applies, and locadot
|
|
195
|
+
forwards it with `Origin: https://signalsant.com`. Only the page itself can use it: requests from other sites get a 403.
|
|
196
|
+
- **Cookies.** The page's cookies are forwarded only to the same site (`api.signalsant.com`), never to third parties.
|
|
197
|
+
Cookies set by third parties are dropped.
|
|
198
|
+
- **Mapped domains.** If you also map a domain (`api.signalsant.localhost` → `https://api.signalsant.com --cors`), its URLs in
|
|
199
|
+
HTML/JS/CSS/JSON are rewritten to the `.localhost` name and it's called there directly.
|
|
200
|
+
|
|
201
|
+
Rewritten responses are buffered and sent uncompressed. Event streams and binary files pass through untouched. Not covered:
|
|
202
|
+
requests made by web workers or service workers, and `<form>` posts or `<img>`/`<script>` tags (these don't need CORS).
|
|
203
|
+
|
|
168
204
|
---
|
|
169
205
|
|
|
170
206
|
## 🛠️ Contributing
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doctor = doctor;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const localhost_1 = __importDefault(require("../../lib/localhost"));
|
|
10
|
+
const proxy_control_1 = __importDefault(require("../../lib/proxy-control"));
|
|
11
|
+
const registry_1 = __importDefault(require("../../lib/registry"));
|
|
12
|
+
const constants_1 = __importDefault(require("../../constants"));
|
|
13
|
+
const trust_1 = require("../../utils/trust");
|
|
14
|
+
const shared_1 = require("../shared");
|
|
15
|
+
async function doctor(options) {
|
|
16
|
+
let failures = 0;
|
|
17
|
+
const check = (ok, label, hint) => {
|
|
18
|
+
if (ok === undefined)
|
|
19
|
+
(0, shared_1.print)(`➖ ${label}`);
|
|
20
|
+
else
|
|
21
|
+
(0, shared_1.print)(`${ok ? "✅" : "❌"} ${label}${!ok && hint ? `\n → ${hint}` : ""}`);
|
|
22
|
+
if (ok === false)
|
|
23
|
+
failures++;
|
|
24
|
+
};
|
|
25
|
+
const info = proxy_control_1.default.running();
|
|
26
|
+
check(Boolean(info), `Proxy process ${info ? `running (pid ${info.pid})` : "not running"}`, "Run `locadot start` and read the error it prints.");
|
|
27
|
+
for (const port of [constants_1.default.server.httpPort, constants_1.default.server.httpsPort]) {
|
|
28
|
+
const listening = await localhost_1.default.isPortListening(port);
|
|
29
|
+
if (info) {
|
|
30
|
+
check(listening, `Port ${port} answering`, "The proxy is running but not listening; check `locadot logs`.");
|
|
31
|
+
}
|
|
32
|
+
else if (listening) {
|
|
33
|
+
check(false, `Port ${port} is free`, `Something else holds port ${port}. Find it with \`sudo lsof -i :${port}\`, or pick another with \`locadot start ${port === constants_1.default.server.httpPort ? "--port" : "--https-port"} <port>\`.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (info && (await localhost_1.default.isPortListening(constants_1.default.server.httpPort))) {
|
|
37
|
+
const probe = await localhost_1.default.probe(`http://127.0.0.1:${constants_1.default.server.httpPort}/healthz`);
|
|
38
|
+
check(probe.up && probe.status === 200, "Dashboard responds", "Another program may be answering on the proxy port.");
|
|
39
|
+
}
|
|
40
|
+
if (os_1.default.platform() === "linux" && Math.min(constants_1.default.server.httpPort, constants_1.default.server.httpsPort) < 1024) {
|
|
41
|
+
let unprivileged = 1024;
|
|
42
|
+
try {
|
|
43
|
+
unprivileged = Number(fs_1.default.readFileSync("/proc/sys/net/ipv4/ip_unprivileged_port_start", "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
const canBind = process.getuid?.() === 0 || unprivileged <= Math.min(constants_1.default.server.httpPort, constants_1.default.server.httpsPort);
|
|
47
|
+
check(canBind || Boolean(info), "Allowed to bind ports 80/443", "Run with sudo, or allow it once: `sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80` (persist in /etc/sysctl.d/).");
|
|
48
|
+
}
|
|
49
|
+
const trusted = await (0, trust_1.isCATrusted)().catch(() => false);
|
|
50
|
+
check(trusted, "Locadot CA trusted by the system", `Run \`locadot trust\`. Manual steps:\n${(0, trust_1.trustInstructions)()}`);
|
|
51
|
+
try {
|
|
52
|
+
const registry = registry_1.default.read();
|
|
53
|
+
check(true, `Registry readable (${Object.keys(registry.hosts).length} host(s))`);
|
|
54
|
+
const hosts = options.host ? [localhost_1.default.requireHost(options.host)] : Object.keys(registry.hosts);
|
|
55
|
+
for (const host of hosts) {
|
|
56
|
+
const entry = registry.hosts[host];
|
|
57
|
+
if (!entry) {
|
|
58
|
+
check(false, `${host} is mapped`, `locadot add --host ${host} --port <port>`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const probe = await localhost_1.default.probe(entry.target, 2000, entry.insecure);
|
|
62
|
+
check(probe.up, `${host} → ${entry.target} ${probe.up ? `answers (${probe.status}, ${probe.ms}ms)` : `unreachable (${probe.error})`}`, "Start the app, or fix the target with `locadot update`.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
check(false, "Registry readable", error.message);
|
|
67
|
+
}
|
|
68
|
+
(0, shared_1.print)(failures ? `\n${failures} problem(s) found.` : "\nAll checks passed.");
|
|
69
|
+
if (failures)
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.logs = logs;
|
|
40
|
+
exports.clearLogs = clearLogs;
|
|
41
|
+
exports.logPath = logPath;
|
|
42
|
+
exports.hostPath = hostPath;
|
|
43
|
+
exports.configPath = configPath;
|
|
44
|
+
exports.token = token;
|
|
45
|
+
const localhost_1 = __importStar(require("../../lib/localhost"));
|
|
46
|
+
const locadot_file_1 = __importDefault(require("../../lib/locadot-file"));
|
|
47
|
+
const proxy_control_1 = __importDefault(require("../../lib/proxy-control"));
|
|
48
|
+
const constants_1 = __importDefault(require("../../constants"));
|
|
49
|
+
const file_1 = __importDefault(require("../../utils/file"));
|
|
50
|
+
const certs_1 = require("../../utils/certs");
|
|
51
|
+
const shared_1 = require("../shared");
|
|
52
|
+
function logs(options) {
|
|
53
|
+
const lines = options.lines ? localhost_1.default.parsePort(options.lines) : 50;
|
|
54
|
+
if (options.follow === false) {
|
|
55
|
+
console.log(file_1.default.lastLines("LOGS", lines).join("\n") || "(log is empty)");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
locadot_file_1.default.watchLogs(lines);
|
|
59
|
+
}
|
|
60
|
+
function clearLogs() {
|
|
61
|
+
locadot_file_1.default.clearLogs();
|
|
62
|
+
(0, shared_1.print)("☑️ Logs successfully cleared.");
|
|
63
|
+
}
|
|
64
|
+
function logPath() {
|
|
65
|
+
console.log(constants_1.default.paths.LOGS);
|
|
66
|
+
}
|
|
67
|
+
function hostPath() {
|
|
68
|
+
console.log(constants_1.default.paths.REGISTRY_FILE);
|
|
69
|
+
}
|
|
70
|
+
function configPath() {
|
|
71
|
+
const rows = [
|
|
72
|
+
["State dir", constants_1.default.paths.HOME],
|
|
73
|
+
["Registry", constants_1.default.paths.REGISTRY_FILE],
|
|
74
|
+
["Lock file", constants_1.default.paths.LOCK_FILE],
|
|
75
|
+
["Logs", constants_1.default.paths.LOGS],
|
|
76
|
+
["Certs", constants_1.default.paths.CERT_DIR],
|
|
77
|
+
["CA cert", (0, certs_1.caCertPath)()],
|
|
78
|
+
["API token", constants_1.default.paths.API_TOKEN],
|
|
79
|
+
["Config", constants_1.default.paths.CONFIG_FILE],
|
|
80
|
+
];
|
|
81
|
+
rows.forEach(([label, value]) => console.log(`${label.padEnd(10)} ${value}`));
|
|
82
|
+
}
|
|
83
|
+
/** For scripts and AI agents driving the dashboard API. */
|
|
84
|
+
function token() {
|
|
85
|
+
const token = proxy_control_1.default.running() ? file_1.default.read("API_TOKEN")?.trim() : undefined;
|
|
86
|
+
if (!token)
|
|
87
|
+
throw new localhost_1.InputError("❌ The proxy isn't running, so there's no API token. Run `locadot start`.");
|
|
88
|
+
console.log(token);
|
|
89
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.add = add;
|
|
40
|
+
exports.update = update;
|
|
41
|
+
exports.remove = remove;
|
|
42
|
+
exports.list = list;
|
|
43
|
+
exports.clearHosts = clearHosts;
|
|
44
|
+
const localhost_1 = __importStar(require("../../lib/localhost"));
|
|
45
|
+
const registry_1 = __importDefault(require("../../lib/registry"));
|
|
46
|
+
const hosts_1 = __importDefault(require("../../lib/hosts"));
|
|
47
|
+
const urls_1 = require("../../lib/urls");
|
|
48
|
+
const shared_1 = require("../shared");
|
|
49
|
+
const resolveTarget = (options) => {
|
|
50
|
+
if (options.port !== undefined && options.target !== undefined) {
|
|
51
|
+
throw new localhost_1.InputError("❌ Use either --port or --target, not both.");
|
|
52
|
+
}
|
|
53
|
+
if (options.port !== undefined)
|
|
54
|
+
return `http://localhost:${localhost_1.default.parsePort(options.port)}`;
|
|
55
|
+
if (options.target !== undefined)
|
|
56
|
+
return localhost_1.default.parseTarget(options.target);
|
|
57
|
+
throw new localhost_1.InputError("❌ Missing destination. Pass --port <port> or --target <url>.");
|
|
58
|
+
};
|
|
59
|
+
const mapping = (options) => ({
|
|
60
|
+
host: options.host,
|
|
61
|
+
target: resolveTarget(options),
|
|
62
|
+
insecure: options.insecure,
|
|
63
|
+
cors: options.cors,
|
|
64
|
+
});
|
|
65
|
+
const warnIfDown = async (entry) => {
|
|
66
|
+
const probe = await localhost_1.default.probe(entry.target, 1500, entry.insecure);
|
|
67
|
+
if (!probe.up)
|
|
68
|
+
(0, shared_1.print)(`⚠️ Nothing answered at ${entry.target} yet (${probe.error}). The mapping is saved anyway.`);
|
|
69
|
+
};
|
|
70
|
+
async function add(options) {
|
|
71
|
+
const { host, entry } = await hosts_1.default.add(mapping(options));
|
|
72
|
+
await (0, shared_1.ensureRunning)(options);
|
|
73
|
+
(0, shared_1.print)(`✅ ${(0, urls_1.urlFor)(host)} → ${entry.target}`);
|
|
74
|
+
if (!localhost_1.default.isLocalTarget(entry.target)) {
|
|
75
|
+
(0, shared_1.print)("ℹ️ Remote target: redirects to other domains will leave the .localhost name.");
|
|
76
|
+
}
|
|
77
|
+
await warnIfDown(entry);
|
|
78
|
+
}
|
|
79
|
+
async function update(options) {
|
|
80
|
+
const { host, entry } = await hosts_1.default.update(mapping(options));
|
|
81
|
+
await (0, shared_1.ensureRunning)(options);
|
|
82
|
+
(0, shared_1.print)(`✅ Updated ${(0, urls_1.urlFor)(host)} → ${entry.target}`);
|
|
83
|
+
await warnIfDown(entry);
|
|
84
|
+
}
|
|
85
|
+
async function remove(options) {
|
|
86
|
+
const { host } = await hosts_1.default.remove(options);
|
|
87
|
+
(0, shared_1.print)(`🗑️ Removed ${host}.`);
|
|
88
|
+
}
|
|
89
|
+
async function list(options) {
|
|
90
|
+
const hosts = Object.entries(registry_1.default.read().hosts).sort(([a], [b]) => a.localeCompare(b));
|
|
91
|
+
if (options.json) {
|
|
92
|
+
console.log(JSON.stringify(hosts.map(([host, entry]) => ({ host, url: (0, urls_1.urlFor)(host), ...entry })), null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!hosts.length) {
|
|
96
|
+
(0, shared_1.print)("No hosts mapped. Add one: npx locadot add --host app.localhost --port 3000");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const width = Math.max(...hosts.map(([host]) => (0, urls_1.urlFor)(host).length));
|
|
100
|
+
for (const [host, entry] of hosts) {
|
|
101
|
+
(0, shared_1.print)(`${(0, urls_1.urlFor)(host).padEnd(width)} → ${entry.target}${entry.insecure ? " (insecure)" : ""}${entry.cors ? " (cors)" : ""}`);
|
|
102
|
+
}
|
|
103
|
+
(0, shared_1.print)(`☑️ Total: ${hosts.length}.`);
|
|
104
|
+
}
|
|
105
|
+
async function clearHosts() {
|
|
106
|
+
await registry_1.default.mutate((registry) => (registry.hosts = {}));
|
|
107
|
+
(0, shared_1.print)("☑️ All hosts removed.");
|
|
108
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const hosts = __importStar(require("./hosts"));
|
|
37
|
+
const proxy = __importStar(require("./proxy"));
|
|
38
|
+
const files = __importStar(require("./files"));
|
|
39
|
+
const system = __importStar(require("./system"));
|
|
40
|
+
const tunnel = __importStar(require("./tunnel"));
|
|
41
|
+
const doctor_1 = require("./doctor");
|
|
42
|
+
const Commands = { ...hosts, ...proxy, ...files, ...system, ...tunnel, doctor: doctor_1.doctor };
|
|
43
|
+
exports.default = Commands;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.status = status;
|
|
7
|
+
exports.start = start;
|
|
8
|
+
exports.stop = stop;
|
|
9
|
+
exports.restart = restart;
|
|
10
|
+
exports.kill = kill;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const localhost_1 = require("../../lib/localhost");
|
|
13
|
+
const proxy_control_1 = __importDefault(require("../../lib/proxy-control"));
|
|
14
|
+
const registry_1 = __importDefault(require("../../lib/registry"));
|
|
15
|
+
const urls_1 = require("../../lib/urls");
|
|
16
|
+
const constants_1 = __importDefault(require("../../constants"));
|
|
17
|
+
const file_1 = __importDefault(require("../../utils/file"));
|
|
18
|
+
const startup_1 = __importDefault(require("../../utils/startup"));
|
|
19
|
+
const trust_1 = require("../../utils/trust");
|
|
20
|
+
const shared_1 = require("../shared");
|
|
21
|
+
/** Applies --port/--https-port and saves them, so `add`, `status`, `open` and boot startup use the same ports. */
|
|
22
|
+
const usePorts = (options) => {
|
|
23
|
+
if (options.port === undefined && options.httpsPort === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
const parse = (value, flag) => {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const port = constants_1.default.validPort(value);
|
|
29
|
+
if (!port)
|
|
30
|
+
throw new localhost_1.InputError(`❌ ${flag} must be a port between 1 and 65535, got "${value}".`);
|
|
31
|
+
return port;
|
|
32
|
+
};
|
|
33
|
+
const httpPort = parse(options.port, "--port") ?? constants_1.default.server.httpPort;
|
|
34
|
+
const httpsPort = parse(options.httpsPort, "--https-port") ?? constants_1.default.server.httpsPort;
|
|
35
|
+
if (httpPort === httpsPort)
|
|
36
|
+
throw new localhost_1.InputError(`❌ HTTP and HTTPS can't share port ${httpPort}.`);
|
|
37
|
+
Object.assign(constants_1.default.server, { httpPort, httpsPort });
|
|
38
|
+
file_1.default.ensureDir();
|
|
39
|
+
fs_1.default.writeFileSync(constants_1.default.paths.CONFIG_FILE, JSON.stringify({ httpPort, httpsPort }, null, 2) + "\n");
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
async function status(options) {
|
|
43
|
+
const info = proxy_control_1.default.running();
|
|
44
|
+
const hosts = Object.keys(registry_1.default.read().hosts).length;
|
|
45
|
+
const [trusted, startup] = await Promise.all([(0, trust_1.isCATrusted)().catch(() => undefined), startup_1.default.isEnabled()]);
|
|
46
|
+
const report = {
|
|
47
|
+
running: Boolean(info),
|
|
48
|
+
pid: info?.pid,
|
|
49
|
+
version: info?.version,
|
|
50
|
+
startedAt: info?.startedAt,
|
|
51
|
+
httpPort: info?.httpPort ?? constants_1.default.server.httpPort,
|
|
52
|
+
httpsPort: info?.httpsPort ?? constants_1.default.server.httpsPort,
|
|
53
|
+
bind: info?.bind ?? constants_1.default.server.bind,
|
|
54
|
+
hosts,
|
|
55
|
+
caTrusted: trusted,
|
|
56
|
+
startupEnabled: startup,
|
|
57
|
+
dashboard: (0, urls_1.urlFor)("localhost"),
|
|
58
|
+
stateDir: constants_1.default.paths.HOME,
|
|
59
|
+
};
|
|
60
|
+
if (options.json) {
|
|
61
|
+
console.log(JSON.stringify(report, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
(0, shared_1.print)(info ? `🟢 Proxy running (pid ${info.pid}${info.startedAt ? `, since ${info.startedAt}` : ""})` : "🔴 Proxy not running");
|
|
65
|
+
(0, shared_1.print)(` Ports: http ${report.httpPort}, https ${report.httpsPort} on ${report.bind.join(", ")}`);
|
|
66
|
+
(0, shared_1.print)(` Hosts: ${hosts}`);
|
|
67
|
+
(0, shared_1.print)(` CA: ${trusted === undefined ? "unknown" : trusted ? "trusted" : "not trusted (run `locadot trust`)"}`);
|
|
68
|
+
(0, shared_1.print)(` Startup: ${startup ? "enabled" : "disabled"}`);
|
|
69
|
+
(0, shared_1.print)(` Dashboard: ${report.dashboard}`);
|
|
70
|
+
(0, shared_1.print)(` State dir: ${report.stateDir}`);
|
|
71
|
+
}
|
|
72
|
+
async function start(options = {}) {
|
|
73
|
+
const moved = usePorts(options);
|
|
74
|
+
const running = proxy_control_1.default.running();
|
|
75
|
+
// Already up on other ports: the new ones only take effect after a restart.
|
|
76
|
+
if (running && (running.httpPort !== constants_1.default.server.httpPort || running.httpsPort !== constants_1.default.server.httpsPort)) {
|
|
77
|
+
const info = await proxy_control_1.default.restart();
|
|
78
|
+
(0, shared_1.print)(`🔁 Proxy restarted on http ${info.httpPort}, https ${info.httpsPort} (pid ${info.pid}).`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const info = await proxy_control_1.default.start();
|
|
82
|
+
(0, shared_1.print)(running ? `☑️ Proxy already running (pid ${info.pid}).` : `🚀 Central proxy started (pid ${info.pid}).`);
|
|
83
|
+
if (moved || !running)
|
|
84
|
+
(0, shared_1.print)(` Ports: http ${info.httpPort}, https ${info.httpsPort}`);
|
|
85
|
+
}
|
|
86
|
+
(0, shared_1.print)(` Dashboard: ${(0, urls_1.urlFor)("localhost")}`);
|
|
87
|
+
}
|
|
88
|
+
async function stop() {
|
|
89
|
+
const stopped = await proxy_control_1.default.stop();
|
|
90
|
+
(0, shared_1.print)(stopped ? constants_1.default.proxyInfo.softClose : "☑️ Proxy was not running.");
|
|
91
|
+
}
|
|
92
|
+
async function restart(options = {}) {
|
|
93
|
+
usePorts(options);
|
|
94
|
+
const info = await proxy_control_1.default.restart();
|
|
95
|
+
(0, shared_1.print)(`☑️ Proxy restarted on http ${info.httpPort}, https ${info.httpsPort} (pid ${info.pid}).`);
|
|
96
|
+
}
|
|
97
|
+
async function kill() {
|
|
98
|
+
await proxy_control_1.default.kill();
|
|
99
|
+
(0, shared_1.print)(constants_1.default.proxyInfo.proxyClose);
|
|
100
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.open = open;
|
|
7
|
+
exports.trust = trust;
|
|
8
|
+
exports.untrust = untrust;
|
|
9
|
+
exports.enableStartup = enableStartup;
|
|
10
|
+
exports.disableStartup = disableStartup;
|
|
11
|
+
exports.statusStartup = statusStartup;
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const localhost_1 = __importDefault(require("../../lib/localhost"));
|
|
15
|
+
const urls_1 = require("../../lib/urls");
|
|
16
|
+
const startup_1 = __importDefault(require("../../utils/startup"));
|
|
17
|
+
const certs_1 = require("../../utils/certs");
|
|
18
|
+
const trust_1 = require("../../utils/trust");
|
|
19
|
+
const shared_1 = require("../shared");
|
|
20
|
+
async function open(host) {
|
|
21
|
+
const url = (0, urls_1.urlFor)(host ? localhost_1.default.requireHost(host) : "localhost");
|
|
22
|
+
const [command, args] = os_1.default.platform() === "win32"
|
|
23
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
24
|
+
: os_1.default.platform() === "darwin"
|
|
25
|
+
? ["open", [url]]
|
|
26
|
+
: ["xdg-open", [url]];
|
|
27
|
+
try {
|
|
28
|
+
// windows-hide-exempt: run from the user's terminal (no popup), and hiding `cmd /c start` can hide the browser too.
|
|
29
|
+
(0, child_process_1.spawn)(command, args, { detached: true, stdio: "ignore" }).on("error", () => { }).unref();
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
(0, shared_1.print)(url);
|
|
33
|
+
}
|
|
34
|
+
async function trust() {
|
|
35
|
+
await (0, trust_1.trustCA)();
|
|
36
|
+
(0, shared_1.print)(`✅ Locadot CA trusted (${(0, certs_1.caCertPath)()}). Restart your browser if it was open.`);
|
|
37
|
+
}
|
|
38
|
+
async function untrust() {
|
|
39
|
+
await (0, trust_1.untrustCA)();
|
|
40
|
+
(0, shared_1.print)("✅ Locadot CA removed from the trust store.");
|
|
41
|
+
}
|
|
42
|
+
async function enableStartup() {
|
|
43
|
+
await startup_1.default.enable();
|
|
44
|
+
}
|
|
45
|
+
async function disableStartup() {
|
|
46
|
+
await startup_1.default.disable();
|
|
47
|
+
}
|
|
48
|
+
async function statusStartup() {
|
|
49
|
+
(0, shared_1.print)((await startup_1.default.isEnabled()) ? "enabled" : "disabled");
|
|
50
|
+
}
|