locadot 2.0.0 → 2.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
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.HubTunnel = void 0;
7
+ exports.setupNamedTunnel = setupNamedTunnel;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
12
+ const constants_1 = __importDefault(require("../constants"));
13
+ const logger_1 = __importDefault(require("../utils/logger"));
14
+ const hub_config_1 = __importDefault(require("../lib/hub-config"));
15
+ const tunnel_1 = require("./tunnel");
16
+ /**
17
+ * Runs the sender's own cloudflared, mirroring TunnelManager in tunnel.ts but for a single
18
+ * public hostname instead of one tunnel per mapping.
19
+ */
20
+ const QUICK_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
21
+ const REGISTERED_PATTERN = /Registered tunnel connection/i;
22
+ const HTTPS_URL_PATTERN = /https:\/\/\S+/;
23
+ const LOGIN_WAIT_MS = 10 * 60000;
24
+ const DEFAULT_TUNNEL = "locadot";
25
+ const isHostname = (value) => {
26
+ if (!value || value.length > 253)
27
+ return false;
28
+ const labels = value.toLowerCase().split(".");
29
+ return labels.length >= 2 && labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
30
+ };
31
+ const trimSlash = (value) => value.replace(/\/$/, "");
32
+ const configEqual = (a, b) => {
33
+ if (!a || !b)
34
+ return a === b;
35
+ return a.mode === b.mode && a.domain === b.domain && a.tunnel === b.tunnel && a.url === b.url;
36
+ };
37
+ class HubTunnel {
38
+ constructor(origin) {
39
+ this.origin = origin;
40
+ this.st = { enabled: false, status: "off" };
41
+ }
42
+ state() {
43
+ return { ...this.st };
44
+ }
45
+ /** hostname of state().url when up (manual mode too: the configured url). */
46
+ publicHost() {
47
+ if (this.st.status !== "up" || !this.st.url)
48
+ return undefined;
49
+ try {
50
+ return new URL(this.st.url).hostname;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ sync(config) {
57
+ const origin = this.origin();
58
+ if (this.applied && configEqual(this.applied.config, config) && this.applied.origin === origin)
59
+ return;
60
+ this.killChild();
61
+ this.applied = { config, origin };
62
+ if (!config) {
63
+ this.st = { enabled: false, status: "off" };
64
+ return;
65
+ }
66
+ if (config.mode === "manual") {
67
+ const url = config.url ? trimSlash(config.url) : undefined;
68
+ this.st = url
69
+ ? { enabled: true, mode: "manual", status: "up", url }
70
+ : { enabled: true, mode: "manual", status: "error", error: "No url configured." };
71
+ return;
72
+ }
73
+ const file = (0, tunnel_1.cloudflaredPath)();
74
+ if (!file) {
75
+ this.st = { enabled: true, mode: config.mode, status: "error", error: "cloudflared is not installed. Run `locadot tunnel:install`." };
76
+ return;
77
+ }
78
+ this.st = { enabled: true, mode: config.mode, status: "starting" };
79
+ if (config.mode === "quick")
80
+ this.startQuick(file, origin);
81
+ else
82
+ this.startNamed(file, config, origin);
83
+ }
84
+ startQuick(file, origin) {
85
+ const config = path_1.default.join(constants_1.default.paths.HOME, "cloudflared-hub-quick.yml");
86
+ fs_1.default.writeFileSync(config, "");
87
+ const child = (0, child_process_1.spawn)(file, ["tunnel", "--no-autoupdate", "--config", config, "--url", origin], {
88
+ stdio: ["ignore", "pipe", "pipe"],
89
+ windowsHide: true,
90
+ });
91
+ this.attach(child, (text) => {
92
+ const url = text.match(QUICK_URL_PATTERN)?.[0];
93
+ if (url && this.st.status !== "up") {
94
+ this.st = { ...this.st, status: "up", url };
95
+ logger_1.default.info(`🌍 hub is public at ${url}`);
96
+ }
97
+ });
98
+ }
99
+ startNamed(file, config, origin) {
100
+ const tunnel = config.tunnel || DEFAULT_TUNNEL;
101
+ const child = (0, child_process_1.spawn)(file, ["tunnel", "--no-autoupdate", "run", "--url", origin, tunnel], {
102
+ stdio: ["ignore", "pipe", "pipe"],
103
+ windowsHide: true,
104
+ });
105
+ this.attach(child, (text) => {
106
+ if (REGISTERED_PATTERN.test(text) && this.st.status !== "up") {
107
+ const url = `https://${config.domain}`;
108
+ this.st = { ...this.st, status: "up", url };
109
+ logger_1.default.info(`🌍 hub is public at ${url}`);
110
+ }
111
+ });
112
+ }
113
+ attach(child, onData) {
114
+ const running = { child, tail: "" };
115
+ this.running = running;
116
+ const read = (chunk) => {
117
+ const text = chunk.toString("utf8");
118
+ running.tail = (running.tail + text).slice(-2000);
119
+ onData(text);
120
+ };
121
+ child.stdout?.on("data", read);
122
+ child.stderr?.on("data", read);
123
+ child.on("error", (error) => {
124
+ if (this.running !== running)
125
+ return;
126
+ this.st = { ...this.st, status: "error", error: error.message };
127
+ this.running = undefined;
128
+ });
129
+ child.on("exit", (code) => {
130
+ if (this.running !== running)
131
+ return;
132
+ const reason = running.tail
133
+ .split(/\r?\n/)
134
+ .filter((line) => /ERR|error|failed/i.test(line))
135
+ .pop()
136
+ ?.trim();
137
+ this.st = { ...this.st, status: "error", error: reason || `cloudflared exited (${code})`, url: undefined };
138
+ this.running = undefined;
139
+ logger_1.default.warn(`🌍 hub tunnel stopped: ${this.st.error}`);
140
+ });
141
+ }
142
+ killChild() {
143
+ this.running?.child.kill();
144
+ this.running = undefined;
145
+ }
146
+ /** Background named-tunnel setup: reflects progress (incl. loginUrl) in state() rather than throwing. */
147
+ setupNamed(domain, tunnel) {
148
+ this.st = { enabled: true, mode: "named", status: "login" };
149
+ return setupNamedTunnel(domain, tunnel, (url) => {
150
+ this.st = { ...this.st, loginUrl: url };
151
+ })
152
+ .then((config) => {
153
+ hub_config_1.default.write(config);
154
+ this.applied = undefined; // force sync() to (re)apply the fresh config
155
+ this.sync(config);
156
+ })
157
+ .catch((error) => {
158
+ this.st = { enabled: true, mode: "named", status: "error", error: error?.message || String(error) };
159
+ });
160
+ }
161
+ stop() {
162
+ this.killChild();
163
+ this.applied = undefined;
164
+ this.st = { enabled: false, status: "off" };
165
+ }
166
+ }
167
+ exports.HubTunnel = HubTunnel;
168
+ const runCapture = (file, args, opts = {}) => new Promise((resolve, reject) => {
169
+ const child = (0, child_process_1.spawn)(file, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
170
+ let output = "";
171
+ let done = false;
172
+ const timer = opts.timeoutMs
173
+ ? setTimeout(() => {
174
+ if (done)
175
+ return;
176
+ done = true;
177
+ child.kill();
178
+ reject(new Error(`${args.join(" ")} timed out`));
179
+ }, opts.timeoutMs)
180
+ : undefined;
181
+ const read = (chunk) => {
182
+ const text = chunk.toString("utf8");
183
+ output += text;
184
+ opts.onData?.(text);
185
+ };
186
+ child.stdout?.on("data", read);
187
+ child.stderr?.on("data", read);
188
+ child.on("error", (error) => {
189
+ if (done)
190
+ return;
191
+ done = true;
192
+ if (timer)
193
+ clearTimeout(timer);
194
+ reject(error);
195
+ });
196
+ child.on("exit", (code) => {
197
+ if (done)
198
+ return;
199
+ done = true;
200
+ if (timer)
201
+ clearTimeout(timer);
202
+ resolve({ code, output });
203
+ });
204
+ });
205
+ /**
206
+ * Used by the CLI (outside the proxy). Logs in if ~/.cloudflared/cert.pem is missing, creates the
207
+ * tunnel (reusing an existing one), routes DNS with --overwrite-dns.
208
+ */
209
+ async function setupNamedTunnel(domain, tunnel = DEFAULT_TUNNEL, onLoginUrl) {
210
+ if (!isHostname(domain))
211
+ throw new Error(`Invalid domain "${domain}".`);
212
+ const file = (0, tunnel_1.cloudflaredPath)();
213
+ if (!file)
214
+ throw new Error("cloudflared is not installed. Run `locadot tunnel:install`.");
215
+ const certPath = path_1.default.join(os_1.default.homedir(), ".cloudflared", "cert.pem");
216
+ if (!fs_1.default.existsSync(certPath)) {
217
+ let announced = false;
218
+ const login = await runCapture(file, ["tunnel", "login"], {
219
+ timeoutMs: LOGIN_WAIT_MS,
220
+ onData: (text) => {
221
+ if (announced)
222
+ return;
223
+ const url = text.match(HTTPS_URL_PATTERN)?.[0];
224
+ if (url) {
225
+ announced = true;
226
+ onLoginUrl?.(url);
227
+ }
228
+ },
229
+ });
230
+ if (login.code !== 0)
231
+ throw new Error(`cloudflared login failed: ${login.output.trim().slice(-500)}`);
232
+ }
233
+ const create = await runCapture(file, ["tunnel", "create", tunnel]);
234
+ if (create.code !== 0 && !/already exists/i.test(create.output)) {
235
+ throw new Error(`cloudflared tunnel create failed: ${create.output.trim().slice(-500)}`);
236
+ }
237
+ const route = await runCapture(file, ["tunnel", "route", "dns", "--overwrite-dns", tunnel, domain]);
238
+ if (route.code !== 0)
239
+ throw new Error(`cloudflared tunnel route dns failed: ${route.output.trim().slice(-500)}`);
240
+ return { mode: "named", domain, tunnel };
241
+ }
@@ -0,0 +1,280 @@
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.resetRateLimiter = void 0;
40
+ exports.classify = classify;
41
+ exports.handleHubApi = handleHubApi;
42
+ const os_1 = __importDefault(require("os"));
43
+ const constants_1 = __importDefault(require("../constants"));
44
+ const logger_1 = __importDefault(require("../utils/logger"));
45
+ const links_1 = __importStar(require("../lib/links"));
46
+ const hosts_1 = __importStar(require("../lib/hosts"));
47
+ const localhost_1 = require("../lib/localhost");
48
+ const registry_1 = require("../lib/registry");
49
+ const request_1 = require("./request");
50
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
51
+ const { version } = require("../../package.json");
52
+ const MAX_BODY = 64 * 1024;
53
+ const RATE_LIMIT_WINDOW_MS = 10 * 60000;
54
+ const RATE_LIMIT_MAX = 20;
55
+ class HubApiError extends Error {
56
+ constructor(status, message) {
57
+ super(message);
58
+ this.status = status;
59
+ }
60
+ }
61
+ /* ---------- per-IP auth-failure rate limiting, shared by classify() and the /_locadot/v1 API ---------- */
62
+ const failures = new Map();
63
+ const recentFailures = (ip, now) => (failures.get(ip) || []).filter((at) => now - at < RATE_LIMIT_WINDOW_MS);
64
+ const isLimited = (ip) => {
65
+ const now = Date.now();
66
+ const recent = recentFailures(ip, now);
67
+ failures.set(ip, recent);
68
+ return recent.length >= RATE_LIMIT_MAX;
69
+ };
70
+ const recordFailure = (ip) => {
71
+ const now = Date.now();
72
+ failures.set(ip, [...recentFailures(ip, now), now]);
73
+ };
74
+ /** Test-only: forget every recorded failure. */
75
+ const resetRateLimiter = () => failures.clear();
76
+ exports.resetRateLimiter = resetRateLimiter;
77
+ const clientIp = (req) => {
78
+ const header = req.headers["cf-connecting-ip"];
79
+ const value = Array.isArray(header) ? header[0] : header;
80
+ return value || req.socket.remoteAddress || "unknown";
81
+ };
82
+ const bearerToken = (req) => {
83
+ const header = req.headers.authorization;
84
+ const match = typeof header === "string" ? /^Bearer\s+(\S+)$/i.exec(header) : null;
85
+ return match?.[1];
86
+ };
87
+ /* ---------- app traffic: classify every request that arrives on the hub's public hostname ---------- */
88
+ function classify(req, publicHost, hosts) {
89
+ if (!publicHost || (0, request_1.hostOf)(req) !== publicHost)
90
+ return { kind: "none" };
91
+ const pathname = (req.url || "/").split("?")[0];
92
+ if (pathname === "/_locadot" || pathname.startsWith("/_locadot/"))
93
+ return { kind: "api" };
94
+ const ip = clientIp(req);
95
+ if (isLimited(ip))
96
+ return { kind: "deny", status: 429, message: "Too many failed attempts. Try again later." };
97
+ // Not Authorization: the sender's own apps may need that header from the visitor.
98
+ const token = req.headers["x-locadot-peer"];
99
+ const peer = links_1.default.authenticate(typeof token === "string" ? token.trim() : undefined);
100
+ if (!peer) {
101
+ recordFailure(ip);
102
+ return { kind: "deny", status: 401, message: "Unauthorized" };
103
+ }
104
+ const hostHeader = req.headers["x-locadot-host"];
105
+ const host = typeof hostHeader === "string" ? hostHeader.trim().toLowerCase() : "";
106
+ const entry = host ? hosts[host] : undefined;
107
+ if (!host || constants_1.default.dashboardHosts.includes(host) || !entry || entry.remote) {
108
+ return { kind: "deny", status: 404, message: "Not found" };
109
+ }
110
+ const visible = links_1.default.visibleHosts(peer, Object.keys(hosts));
111
+ if (!visible.includes(host)) {
112
+ return { kind: "deny", status: 403, message: "Forbidden" };
113
+ }
114
+ for (const name of Object.keys(req.headers))
115
+ if (name.toLowerCase().startsWith("x-locadot-"))
116
+ delete req.headers[name];
117
+ return { kind: "app", host, peer };
118
+ }
119
+ /* ---------- /_locadot/v1 API ---------- */
120
+ const send = (res, status, body) => {
121
+ const json = JSON.stringify(body);
122
+ res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(json) });
123
+ res.end(json);
124
+ };
125
+ const readJson = (req) => new Promise((resolve, reject) => {
126
+ const type = String(req.headers["content-type"] || "");
127
+ const chunks = [];
128
+ let size = 0;
129
+ req.on("data", (chunk) => {
130
+ size += chunk.length;
131
+ if (size > MAX_BODY)
132
+ reject(new HubApiError(413, "Request body too large."));
133
+ else
134
+ chunks.push(chunk);
135
+ });
136
+ req.on("error", reject);
137
+ req.on("end", () => {
138
+ if (!/^application\/json\b/i.test(type))
139
+ return reject(new HubApiError(400, "Content-Type must be application/json."));
140
+ try {
141
+ const value = size ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
142
+ if (!value || typeof value !== "object" || Array.isArray(value))
143
+ throw new Error();
144
+ resolve(value);
145
+ }
146
+ catch {
147
+ reject(new HubApiError(400, "Body must be a JSON object."));
148
+ }
149
+ });
150
+ });
151
+ const requireString = (value, name) => {
152
+ if (typeof value !== "string" || !value.trim())
153
+ throw new HubApiError(400, `\`${name}\` is required.`);
154
+ return value;
155
+ };
156
+ const optionalBool = (value, name) => {
157
+ if (value === undefined)
158
+ return undefined;
159
+ if (typeof value !== "boolean")
160
+ throw new HubApiError(400, `\`${name}\` must be true or false.`);
161
+ return value;
162
+ };
163
+ /** connect name: printable characters only, capped so it can't be used to stuff the peers file. */
164
+ const sanitizeName = (value) => {
165
+ const raw = typeof value === "string" ? value : "";
166
+ const printable = raw.replace(/[^\x20-\x7E]/g, "").trim();
167
+ return (printable || "peer").slice(0, 40);
168
+ };
169
+ const requirePerm = (peer, permission) => {
170
+ if (!links_1.default.can(peer, permission))
171
+ throw new HubApiError(403, "Forbidden");
172
+ };
173
+ const peerView = (peer) => {
174
+ const { id, name, role, hosts } = peer;
175
+ return hosts ? { id, name, role, hosts } : { id, name, role };
176
+ };
177
+ const senderInfo = () => ({ hostname: os_1.default.hostname(), version: String(version) });
178
+ const hostView = (host, entry) => ({
179
+ host,
180
+ target: entry.target,
181
+ cors: Boolean(entry.cors),
182
+ insecure: Boolean(entry.insecure),
183
+ });
184
+ async function handleHubApi(req, res, ctx) {
185
+ const url = new URL(req.url || "/", "http://hub.invalid");
186
+ const path = url.pathname;
187
+ const method = req.method || "GET";
188
+ const ip = clientIp(req);
189
+ try {
190
+ if (path === "/_locadot/v1/connect" && method === "POST") {
191
+ const body = await readJson(req);
192
+ const code = requireString(body.code, "code");
193
+ const name = sanitizeName(body.name);
194
+ const { peer, token } = links_1.default.redeem(code, name);
195
+ logger_1.default.info(`🔗 hub: ${peer.name} connected as ${peer.role}`);
196
+ return send(res, 200, { token, peer: peerView(peer), sender: senderInfo() });
197
+ }
198
+ if (isLimited(ip))
199
+ return send(res, 429, { error: "Too many failed attempts. Try again later." });
200
+ const peer = links_1.default.authenticate(bearerToken(req));
201
+ if (!peer) {
202
+ recordFailure(ip);
203
+ return send(res, 401, { error: "Unauthorized" });
204
+ }
205
+ if (path === "/_locadot/v1/whoami" && method === "GET") {
206
+ requirePerm(peer, "read");
207
+ return send(res, 200, { peer: peerView(peer), sender: senderInfo() });
208
+ }
209
+ if (path === "/_locadot/v1/hosts" && method === "GET") {
210
+ requirePerm(peer, "read");
211
+ const registry = ctx.getRegistry();
212
+ const visible = links_1.default.visibleHosts(peer, Object.keys(registry.hosts));
213
+ const hosts = visible.filter((host) => !registry.hosts[host].remote).map((host) => hostView(host, registry.hosts[host]));
214
+ return send(res, 200, { hosts });
215
+ }
216
+ if (path === "/_locadot/v1/hosts" && method === "POST") {
217
+ requirePerm(peer, "write");
218
+ const body = await readJson(req);
219
+ const { host, entry } = await hosts_1.default.add({
220
+ host: body.host,
221
+ target: body.target,
222
+ insecure: optionalBool(body.insecure, "insecure"),
223
+ cors: optionalBool(body.cors, "cors"),
224
+ });
225
+ ctx.reload();
226
+ logger_1.default.info(`➕ hub: ${peer.name} added ${host} → ${entry.target}`);
227
+ return send(res, 201, { host: hostView(host, entry) });
228
+ }
229
+ const hostMatch = /^\/_locadot\/v1\/hosts\/([^/]+)$/.exec(path);
230
+ if (hostMatch && method === "PUT") {
231
+ requirePerm(peer, "write");
232
+ const host = decodeURIComponent(hostMatch[1]);
233
+ const body = await readJson(req);
234
+ const tunnel = optionalBool(body.tunnel, "tunnel");
235
+ if (tunnel !== undefined)
236
+ requirePerm(peer, "settings");
237
+ let updated;
238
+ if (body.target !== undefined || tunnel === undefined) {
239
+ updated = await hosts_1.default.update({
240
+ host,
241
+ target: body.target,
242
+ insecure: optionalBool(body.insecure, "insecure"),
243
+ cors: optionalBool(body.cors, "cors"),
244
+ });
245
+ }
246
+ if (tunnel !== undefined)
247
+ updated = await hosts_1.default.setTunnel({ host, tunnel });
248
+ ctx.reload();
249
+ if (tunnel)
250
+ ctx.retryTunnels();
251
+ logger_1.default.info(`✏️ hub: ${peer.name} updated ${host}`);
252
+ return send(res, 200, { host: hostView(updated.host, updated.entry) });
253
+ }
254
+ if (hostMatch && method === "DELETE") {
255
+ requirePerm(peer, "delete");
256
+ const host = decodeURIComponent(hostMatch[1]);
257
+ await hosts_1.default.remove({ host });
258
+ ctx.reload();
259
+ logger_1.default.info(`🗑️ hub: ${peer.name} removed ${host}`);
260
+ return send(res, 200, { ok: true });
261
+ }
262
+ send(res, 404, { error: "Not found" });
263
+ }
264
+ catch (error) {
265
+ if (error instanceof hosts_1.NotFoundError)
266
+ return send(res, 404, { error: error.message.replace(/^❌\s*/u, "") });
267
+ if (error instanceof hosts_1.ConflictError)
268
+ return send(res, 409, { error: error.message.replace(/^❌\s*/u, "") });
269
+ if (error instanceof localhost_1.InputError)
270
+ return send(res, 400, { error: error.message.replace(/^❌\s*/u, "") });
271
+ if (error instanceof registry_1.RegistryError)
272
+ return send(res, 500, { error: error.message });
273
+ if (error instanceof HubApiError)
274
+ return send(res, error.status, { error: error.message });
275
+ if (error instanceof links_1.LinkError)
276
+ return send(res, error.status, { error: error.message });
277
+ logger_1.default.error(error);
278
+ return send(res, 500, { error: "Internal error." });
279
+ }
280
+ }
@@ -0,0 +1,51 @@
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.remoteOptions = exports.remoteFor = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const constants_1 = __importDefault(require("../constants"));
9
+ const request_1 = require("./request");
10
+ let cache;
11
+ /** REMOTES_FILE, re-parsed only when its mtime changes so the proxy doesn't hit disk per request. */
12
+ const remoteFor = (name) => {
13
+ let stat;
14
+ try {
15
+ stat = fs_1.default.statSync(constants_1.default.paths.REMOTES_FILE);
16
+ }
17
+ catch {
18
+ cache = undefined;
19
+ return undefined;
20
+ }
21
+ if (!cache || cache.mtimeMs !== stat.mtimeMs) {
22
+ let data = { remotes: {} };
23
+ try {
24
+ const parsed = JSON.parse(fs_1.default.readFileSync(constants_1.default.paths.REMOTES_FILE, "utf8"));
25
+ if (parsed && typeof parsed.remotes === "object")
26
+ data = parsed;
27
+ }
28
+ catch { }
29
+ cache = { mtimeMs: stat.mtimeMs, data };
30
+ }
31
+ return cache.data.remotes[name];
32
+ };
33
+ exports.remoteFor = remoteFor;
34
+ const remoteOptions = (req, entry, remote) => ({
35
+ target: remote.url,
36
+ changeOrigin: true,
37
+ // Don't leak the receiver's LAN addresses to the sender.
38
+ xfwd: false,
39
+ ws: true,
40
+ secure: true,
41
+ autoRewrite: true,
42
+ hostRewrite: req.headers.host,
43
+ protocolRewrite: (0, request_1.isTls)(req) ? "https" : "http",
44
+ cookieDomainRewrite: { "*": "" },
45
+ headers: {
46
+ "X-Original-Host": req.headers.host || "",
47
+ "X-Locadot-Peer": remote.token,
48
+ "X-Locadot-Host": entry.remote.host,
49
+ },
50
+ });
51
+ exports.remoteOptions = remoteOptions;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isTls = exports.viaOf = exports.fromTunnel = exports.mappedHost = exports.hostOf = exports.tag = void 0;
3
+ exports.isTls = exports.viaOf = 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 });
@@ -19,8 +19,10 @@ const mappedHost = (req) => tags.get(req)?.host ?? (0, exports.hostOf)(req);
19
19
  exports.mappedHost = mappedHost;
20
20
  const fromTunnel = (req) => Boolean(tags.get(req)?.tunnel);
21
21
  exports.fromTunnel = fromTunnel;
22
+ const fromRemote = (req) => Boolean(tags.get(req)?.remote);
23
+ exports.fromRemote = fromRemote;
22
24
  const viaOf = (req) => tags.get(req)?.via;
23
25
  exports.viaOf = viaOf;
24
26
  /** Tunnel visitors are on https even though cloudflared talks plain http to us. */
25
- const isTls = (req) => Boolean(req.socket.encrypted) || (0, exports.fromTunnel)(req);
27
+ const isTls = (req) => Boolean(req.socket.encrypted) || (0, exports.fromTunnel)(req) || Boolean(tags.get(req)?.secure);
26
28
  exports.isTls = isTls;