yaport 0.1.1 → 0.1.2
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/README.md +40 -24
- package/dist/client/assets/{index-DQ_KWWqB.css → index-BMJd0Oii.css} +1 -1
- package/dist/client/assets/index-CgNFN5bT.js +9 -0
- package/dist/client/index.html +2 -2
- package/dist/server/cli.js +156 -45
- package/package.json +1 -1
- package/dist/client/assets/index-g0iLkETl.js +0 -9
package/dist/server/cli.js
CHANGED
|
@@ -168,21 +168,109 @@ function sameEndpoint(first, second) {
|
|
|
168
168
|
return first.host === second.host && first.port === second.port && first.user === second.user;
|
|
169
169
|
}
|
|
170
170
|
//#endregion
|
|
171
|
+
//#region src/server/caddyExternalRoutes.ts
|
|
172
|
+
function parseCaddyExternalRoutes(caddyfile) {
|
|
173
|
+
return extractSiteAddresses(stripComments$1(caddyfile)).flatMap((address) => ({
|
|
174
|
+
port: address.port,
|
|
175
|
+
serverName: address.serverName,
|
|
176
|
+
source: "caddy",
|
|
177
|
+
url: caddyRouteUrl(address)
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
function extractSiteAddresses(caddyfile) {
|
|
181
|
+
const addresses = [];
|
|
182
|
+
for (const header of extractTopLevelBlockHeaders(caddyfile)) for (const rawAddress of header.split(",")) {
|
|
183
|
+
const address = parseCaddyAddress(rawAddress.trim());
|
|
184
|
+
if (address) addresses.push(address);
|
|
185
|
+
}
|
|
186
|
+
return addresses;
|
|
187
|
+
}
|
|
188
|
+
function extractTopLevelBlockHeaders(caddyfile) {
|
|
189
|
+
const headers = [];
|
|
190
|
+
let depth = 0;
|
|
191
|
+
let lineStart = 0;
|
|
192
|
+
for (let index = 0; index < caddyfile.length; index += 1) {
|
|
193
|
+
const character = caddyfile[index];
|
|
194
|
+
if (character === "{") {
|
|
195
|
+
if (depth === 0) {
|
|
196
|
+
const header = caddyfile.slice(lineStart, index).trim();
|
|
197
|
+
if (header) headers.push(header);
|
|
198
|
+
}
|
|
199
|
+
depth += 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (character === "}") {
|
|
203
|
+
depth = Math.max(0, depth - 1);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (character === "\n" && depth === 0) lineStart = index + 1;
|
|
207
|
+
}
|
|
208
|
+
return headers;
|
|
209
|
+
}
|
|
210
|
+
function parseCaddyAddress(rawAddress) {
|
|
211
|
+
const candidate = rawAddress.trim();
|
|
212
|
+
if (candidate.startsWith("http://") || candidate.startsWith("https://")) return parseUrlAddress(candidate);
|
|
213
|
+
const address = candidate.replace(/\/.*$/, "").trim();
|
|
214
|
+
if (!address || address === "_" || address.includes("$") || address.startsWith("*.") || address.startsWith(":")) return;
|
|
215
|
+
const portMatch = address.match(/^(.+):(\d+)$/);
|
|
216
|
+
const serverName = portMatch ? portMatch[1] : address;
|
|
217
|
+
const port = portMatch ? Number(portMatch[2]) : 443;
|
|
218
|
+
if (!isDisplayableServerName$1(serverName) || !isValidPort(port)) return;
|
|
219
|
+
return {
|
|
220
|
+
port,
|
|
221
|
+
scheme: port === 443 ? "https" : "http",
|
|
222
|
+
serverName
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function parseUrlAddress(address) {
|
|
226
|
+
try {
|
|
227
|
+
const url = new URL(address);
|
|
228
|
+
const scheme = url.protocol === "https:" ? "https" : "http";
|
|
229
|
+
const serverName = url.hostname;
|
|
230
|
+
const port = url.port ? Number(url.port) : scheme === "https" ? 443 : 80;
|
|
231
|
+
if (!isDisplayableServerName$1(serverName) || !isValidPort(port)) return;
|
|
232
|
+
return {
|
|
233
|
+
port,
|
|
234
|
+
scheme,
|
|
235
|
+
serverName
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function stripComments$1(config) {
|
|
242
|
+
return config.split("\n").map((line) => line.replace(/(^|[^\\])#.*/, "$1")).join("\n");
|
|
243
|
+
}
|
|
244
|
+
function isDisplayableServerName$1(serverName) {
|
|
245
|
+
return Boolean(serverName) && !serverName.includes("*") && /^[A-Za-z0-9.-]+$/.test(serverName);
|
|
246
|
+
}
|
|
247
|
+
function isValidPort(port) {
|
|
248
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
249
|
+
}
|
|
250
|
+
function caddyRouteUrl(address) {
|
|
251
|
+
if (address.scheme === "https" && address.port === 443) return `https://${address.serverName}`;
|
|
252
|
+
return `${address.scheme}://${address.serverName}:${address.port}`;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
171
255
|
//#region src/server/nginxExternalRoutes.ts
|
|
172
|
-
function parseNginxExternalRoutes(config) {
|
|
256
|
+
function parseNginxExternalRoutes(config, source = "nginx") {
|
|
173
257
|
return extractServerBlocks(stripComments(config)).flatMap((block) => {
|
|
174
258
|
const validNames = block.serverNames.filter(isDisplayableServerName);
|
|
175
259
|
return block.listens.flatMap((listen) => validNames.map((serverName) => ({
|
|
176
260
|
port: listen.port,
|
|
177
261
|
serverName,
|
|
178
|
-
source
|
|
262
|
+
source,
|
|
179
263
|
url: routeUrl(serverName, listen)
|
|
180
264
|
})));
|
|
181
265
|
});
|
|
182
266
|
}
|
|
183
267
|
function attachExternalRoutes(ports, routes) {
|
|
184
268
|
const routesByPort = /* @__PURE__ */ new Map();
|
|
185
|
-
for (const route of routes)
|
|
269
|
+
for (const route of routes) {
|
|
270
|
+
const currentRoutes = routesByPort.get(route.port) ?? [];
|
|
271
|
+
if (currentRoutes.some((currentRoute) => sameExternalRoute(currentRoute, route))) continue;
|
|
272
|
+
routesByPort.set(route.port, [...currentRoutes, route]);
|
|
273
|
+
}
|
|
186
274
|
return ports.map((port) => {
|
|
187
275
|
const externalRoutes = routesByPort.get(port.port);
|
|
188
276
|
if (!externalRoutes?.length) return port;
|
|
@@ -192,6 +280,9 @@ function attachExternalRoutes(ports, routes) {
|
|
|
192
280
|
};
|
|
193
281
|
});
|
|
194
282
|
}
|
|
283
|
+
function sameExternalRoute(left, right) {
|
|
284
|
+
return left.port === right.port && left.serverName === right.serverName && left.source === right.source && left.url === right.url;
|
|
285
|
+
}
|
|
195
286
|
function stripComments(config) {
|
|
196
287
|
return config.split("\n").map((line) => line.replace(/(^|[^\\])#.*/, "$1")).join("\n");
|
|
197
288
|
}
|
|
@@ -309,6 +400,33 @@ function parseProcess(value) {
|
|
|
309
400
|
};
|
|
310
401
|
}
|
|
311
402
|
//#endregion
|
|
403
|
+
//#region src/server/sshConnection.ts
|
|
404
|
+
var SSH_CONTROL_SOCKET_DIRECTORY = "/tmp/yaport-ssh";
|
|
405
|
+
async function ensureSshControlSocketDirectory() {
|
|
406
|
+
await mkdir(SSH_CONTROL_SOCKET_DIRECTORY, {
|
|
407
|
+
recursive: true,
|
|
408
|
+
mode: 448
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function buildSshControlPath(machine) {
|
|
412
|
+
return join(SSH_CONTROL_SOCKET_DIRECTORY, machineConnectionHash(machine).slice(0, 40));
|
|
413
|
+
}
|
|
414
|
+
function machineConnectionHash(machine) {
|
|
415
|
+
return createHash("sha256").update(JSON.stringify(machineConnectionIdentity(machine))).digest("hex");
|
|
416
|
+
}
|
|
417
|
+
function machineConnectionIdentity(machine) {
|
|
418
|
+
return {
|
|
419
|
+
host: machine.host,
|
|
420
|
+
port: machine.port,
|
|
421
|
+
user: machine.user,
|
|
422
|
+
jumpHosts: machine.jumpHosts?.map((jumpHost) => ({
|
|
423
|
+
host: jumpHost.host,
|
|
424
|
+
port: jumpHost.port,
|
|
425
|
+
user: jumpHost.user
|
|
426
|
+
}))
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
312
430
|
//#region src/server/sshPortInventory.ts
|
|
313
431
|
var SshCommandError = class extends Error {
|
|
314
432
|
stderr;
|
|
@@ -318,9 +436,8 @@ var SshCommandError = class extends Error {
|
|
|
318
436
|
this.name = "SshCommandError";
|
|
319
437
|
}
|
|
320
438
|
};
|
|
321
|
-
var
|
|
322
|
-
var
|
|
323
|
-
var nginxExternalRouteCache = /* @__PURE__ */ new Map();
|
|
439
|
+
var PROXY_EXTERNAL_ROUTE_CACHE_MS = 300 * 1e3;
|
|
440
|
+
var proxyExternalRouteCache = /* @__PURE__ */ new Map();
|
|
324
441
|
async function collectPortInventory(machine, runner = runSsh, options = {}) {
|
|
325
442
|
const sshOptions = buildSshOptions(machine);
|
|
326
443
|
const sshArgsOptions = await buildSshArgsOptions(machine, options);
|
|
@@ -330,7 +447,7 @@ async function collectPortInventory(machine, runner = runSsh, options = {}) {
|
|
|
330
447
|
"-lntup"
|
|
331
448
|
], sshArgsOptions), sshOptions);
|
|
332
449
|
const ports = parseSsListeningPorts(stdout);
|
|
333
|
-
const [portsWithCommandsResult, externalRoutesResult] = await Promise.allSettled([attachProcessCommands(machine, ports, runner, sshOptions, sshArgsOptions),
|
|
450
|
+
const [portsWithCommandsResult, externalRoutesResult] = await Promise.allSettled([attachProcessCommands(machine, ports, runner, sshOptions, sshArgsOptions), readCachedProxyExternalRoutes(machine, ports, runner, sshOptions, sshArgsOptions)]);
|
|
334
451
|
return attachExternalRoutes(portsWithCommandsResult.status === "fulfilled" ? portsWithCommandsResult.value : ports, externalRoutesResult.status === "fulfilled" ? externalRoutesResult.value : []);
|
|
335
452
|
}
|
|
336
453
|
async function attachProcessCommands(machine, ports, runner, sshOptions, sshArgsOptions) {
|
|
@@ -357,24 +474,33 @@ async function attachProcessCommands(machine, ports, runner, sshOptions, sshArgs
|
|
|
357
474
|
return ports;
|
|
358
475
|
}
|
|
359
476
|
}
|
|
360
|
-
async function
|
|
477
|
+
async function readCachedProxyExternalRoutes(machine, ports, runner, sshOptions, sshArgsOptions) {
|
|
478
|
+
const proxySources = proxySourcesForPorts(ports);
|
|
479
|
+
if (proxySources.length === 0) return [];
|
|
361
480
|
const cacheKey = machineConnectionCacheKey(machine);
|
|
362
|
-
const cached =
|
|
481
|
+
const cached = proxyExternalRouteCache.get(cacheKey);
|
|
363
482
|
if (cached && cached.expiresAt > Date.now()) return cached.routes;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
483
|
+
const routes = (await Promise.allSettled(proxySources.map((source) => readProxyExternalRoutes(machine, source, runner, sshOptions, sshArgsOptions)))).flatMap((result) => result.status === "fulfilled" ? result.value : []);
|
|
484
|
+
proxyExternalRouteCache.set(cacheKey, {
|
|
485
|
+
expiresAt: Date.now() + PROXY_EXTERNAL_ROUTE_CACHE_MS,
|
|
486
|
+
routes
|
|
487
|
+
});
|
|
488
|
+
return routes;
|
|
489
|
+
}
|
|
490
|
+
function proxySourcesForPorts(ports) {
|
|
491
|
+
const sources = /* @__PURE__ */ new Set();
|
|
492
|
+
for (const port of ports) {
|
|
493
|
+
const processName = port.processName?.toLowerCase() ?? "";
|
|
494
|
+
if (processName.includes("nginx")) sources.add("nginx");
|
|
495
|
+
if (processName.includes("openresty")) sources.add("openresty");
|
|
496
|
+
if (processName.includes("tengine")) sources.add("tengine");
|
|
497
|
+
if (processName.includes("caddy")) sources.add("caddy");
|
|
377
498
|
}
|
|
499
|
+
return [...sources];
|
|
500
|
+
}
|
|
501
|
+
async function readProxyExternalRoutes(machine, source, runner, sshOptions, sshArgsOptions) {
|
|
502
|
+
if (source === "caddy") return parseCaddyExternalRoutes((await runner(buildSshArgs(machine, ["cat", "/etc/caddy/Caddyfile"], sshArgsOptions), sshOptions)).stdout);
|
|
503
|
+
return parseNginxExternalRoutes((await runner(buildSshArgs(machine, [source, "-T"], sshArgsOptions), sshOptions)).stdout, source);
|
|
378
504
|
}
|
|
379
505
|
function buildSshOptions(machine) {
|
|
380
506
|
const entries = buildAskPassEntries(machine);
|
|
@@ -395,11 +521,8 @@ function parsePsCommands(output) {
|
|
|
395
521
|
}
|
|
396
522
|
async function buildSshArgsOptions(machine, options) {
|
|
397
523
|
if (!options.reuseConnection) return {};
|
|
398
|
-
await
|
|
399
|
-
|
|
400
|
-
mode: 448
|
|
401
|
-
});
|
|
402
|
-
return { controlPath: join(CONTROL_SOCKET_DIRECTORY, machineConnectionHash(machine).slice(0, 40)) };
|
|
524
|
+
await ensureSshControlSocketDirectory();
|
|
525
|
+
return { controlPath: buildSshControlPath(machine) };
|
|
403
526
|
}
|
|
404
527
|
function buildSshArgs(machine, command, options = {}) {
|
|
405
528
|
const hasPasswords = buildAskPassEntries(machine).length > 0;
|
|
@@ -418,21 +541,6 @@ function buildSshArgs(machine, command, options = {}) {
|
|
|
418
541
|
function machineConnectionCacheKey(machine) {
|
|
419
542
|
return `${machine.id}:${machineConnectionHash(machine)}`;
|
|
420
543
|
}
|
|
421
|
-
function machineConnectionHash(machine) {
|
|
422
|
-
return createHash("sha256").update(JSON.stringify(machineConnectionIdentity(machine))).digest("hex");
|
|
423
|
-
}
|
|
424
|
-
function machineConnectionIdentity(machine) {
|
|
425
|
-
return {
|
|
426
|
-
host: machine.host,
|
|
427
|
-
port: machine.port,
|
|
428
|
-
user: machine.user,
|
|
429
|
-
jumpHosts: machine.jumpHosts?.map((jumpHost) => ({
|
|
430
|
-
host: jumpHost.host,
|
|
431
|
-
port: jumpHost.port,
|
|
432
|
-
user: jumpHost.user
|
|
433
|
-
}))
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
544
|
function buildAskPassEntries(machine) {
|
|
437
545
|
return [...machine.jumpHosts ?? [], machine].flatMap((endpoint) => endpoint.password ? [{
|
|
438
546
|
password: endpoint.password,
|
|
@@ -556,8 +664,9 @@ var UnsupportedTerminalError = class extends Error {
|
|
|
556
664
|
this.name = "UnsupportedTerminalError";
|
|
557
665
|
}
|
|
558
666
|
};
|
|
559
|
-
function buildInteractiveSshArgs(machine) {
|
|
667
|
+
function buildInteractiveSshArgs(machine, options = {}) {
|
|
560
668
|
const args = [];
|
|
669
|
+
if (options.controlPath) args.push("-o", "ControlMaster=auto", "-o", "ControlPersist=5m", "-o", `ControlPath=${options.controlPath}`);
|
|
561
670
|
if (machine.jumpHosts?.length) args.push("-J", machine.jumpHosts.map(formatJumpHost).join(","));
|
|
562
671
|
if (machine.port) args.push("-p", String(machine.port));
|
|
563
672
|
args.push(formatSshTarget(machine));
|
|
@@ -566,11 +675,13 @@ function buildInteractiveSshArgs(machine) {
|
|
|
566
675
|
async function openSshTerminal(machine, options = {}) {
|
|
567
676
|
const platform = options.platform ?? process.platform;
|
|
568
677
|
if (platform !== "darwin") throw new UnsupportedTerminalError(platform);
|
|
569
|
-
|
|
678
|
+
const spawnCommand = options.spawnCommand ?? spawn;
|
|
679
|
+
await ensureSshControlSocketDirectory();
|
|
680
|
+
await runCommand(spawnCommand, "osascript", [
|
|
570
681
|
"-e",
|
|
571
682
|
"tell application \"Terminal\" to activate",
|
|
572
683
|
"-e",
|
|
573
|
-
`tell application "Terminal" to do script ${appleScriptString(["ssh", ...buildInteractiveSshArgs(machine)].map(shellQuote).join(" "))}`
|
|
684
|
+
`tell application "Terminal" to do script ${appleScriptString(["ssh", ...buildInteractiveSshArgs(machine, { controlPath: buildSshControlPath(machine) })].map(shellQuote).join(" "))}`
|
|
574
685
|
]);
|
|
575
686
|
}
|
|
576
687
|
function runCommand(spawnCommand, command, args) {
|