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.
@@ -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: "nginx",
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) routesByPort.set(route.port, [...routesByPort.get(route.port) ?? [], route]);
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 CONTROL_SOCKET_DIRECTORY = "/tmp/yaport-ssh";
322
- var NGINX_EXTERNAL_ROUTE_CACHE_MS = 300 * 1e3;
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), readCachedNginxExternalRoutes(machine, 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 readCachedNginxExternalRoutes(machine, runner, sshOptions, sshArgsOptions) {
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 = nginxExternalRouteCache.get(cacheKey);
481
+ const cached = proxyExternalRouteCache.get(cacheKey);
363
482
  if (cached && cached.expiresAt > Date.now()) return cached.routes;
364
- try {
365
- const routes = parseNginxExternalRoutes((await runner(buildSshArgs(machine, ["nginx", "-T"], sshArgsOptions), sshOptions)).stdout);
366
- nginxExternalRouteCache.set(cacheKey, {
367
- expiresAt: Date.now() + NGINX_EXTERNAL_ROUTE_CACHE_MS,
368
- routes
369
- });
370
- return routes;
371
- } catch {
372
- nginxExternalRouteCache.set(cacheKey, {
373
- expiresAt: Date.now() + NGINX_EXTERNAL_ROUTE_CACHE_MS,
374
- routes: []
375
- });
376
- return [];
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 mkdir(CONTROL_SOCKET_DIRECTORY, {
399
- recursive: true,
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
- await runCommand(options.spawnCommand ?? spawn, "osascript", [
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaport",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@11.5.0",
6
6
  "bin": {