socket-function 1.2.8 → 1.2.9

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/index.d.ts CHANGED
@@ -942,11 +942,31 @@ declare module "socket-function/src/formatting/logColors" {
942
942
  }
943
943
 
944
944
  declare module "socket-function/src/forwardPort" {
945
+ export interface PortMapping {
946
+ externalPort: number;
947
+ internalPort: number;
948
+ protocol: string;
949
+ /** The LAN client the mapping forwards to (NewInternalClient). */
950
+ internalClient: string;
951
+ /** Empty string means "any" remote host (the usual case). */
952
+ remoteHost: string;
953
+ enabled: boolean;
954
+ description: string;
955
+ /** Remaining lease in seconds; 0 means a permanent (static) mapping. */
956
+ leaseDuration: number;
957
+ }
958
+ /** Queries the router for every existing UPnP port mapping by walking
959
+ * GetGenericPortMappingEntry from index 0 until the gateway reports the index
960
+ * is out of range (SOAP error 713 / a non-200 response). */
961
+ export declare function listPortMappings(): Promise<PortMapping[]>;
945
962
  export declare function forwardPort(config: {
946
963
  externalPort: number;
947
964
  internalPort: number;
948
965
  duration?: number;
949
966
  }): Promise<void>;
967
+ /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
968
+ * mapping points at us or at a different machine on the network. */
969
+ export declare function getLocalInternalIP(): string | undefined;
950
970
 
951
971
  }
952
972
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -1,5 +1,25 @@
1
+ export interface PortMapping {
2
+ externalPort: number;
3
+ internalPort: number;
4
+ protocol: string;
5
+ /** The LAN client the mapping forwards to (NewInternalClient). */
6
+ internalClient: string;
7
+ /** Empty string means "any" remote host (the usual case). */
8
+ remoteHost: string;
9
+ enabled: boolean;
10
+ description: string;
11
+ /** Remaining lease in seconds; 0 means a permanent (static) mapping. */
12
+ leaseDuration: number;
13
+ }
14
+ /** Queries the router for every existing UPnP port mapping by walking
15
+ * GetGenericPortMappingEntry from index 0 until the gateway reports the index
16
+ * is out of range (SOAP error 713 / a non-200 response). */
17
+ export declare function listPortMappings(): Promise<PortMapping[]>;
1
18
  export declare function forwardPort(config: {
2
19
  externalPort: number;
3
20
  internalPort: number;
4
21
  duration?: number;
5
22
  }): Promise<void>;
23
+ /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
24
+ * mapping points at us or at a different machine on the network. */
25
+ export declare function getLocalInternalIP(): string | undefined;
@@ -6,6 +6,72 @@ import { timeInHour } from "./misc";
6
6
  const SSDP_DISCOVER_MX = 2;
7
7
  const SSDP_DISCOVER_MSG = `M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: "ssdp:discover"\r\nMX: ${SSDP_DISCOVER_MX}\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n`;
8
8
 
9
+ /** Resolves the UPnP Internet Gateway Device we can talk to, along with the local
10
+ * addressing needed to build SOAP requests against it. Shared by every operation
11
+ * that needs to reach the router's control endpoint. */
12
+ async function resolveGateway(): Promise<{
13
+ internalIP: string;
14
+ gatewayIP: string;
15
+ controlPort: number;
16
+ controlURLs: string[];
17
+ }> {
18
+ const localObj = getLocalInterfaceAddress();
19
+ if (!localObj) throw new Error("Could not find the local address / gateway");
20
+
21
+ const { internalIP, gatewayIP } = localObj;
22
+ console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
23
+ let gateway = await discoverGateway(internalIP);
24
+ let controlURLs = await getControlPaths(gateway);
25
+ let controlPort = Number(new URL(gateway).port);
26
+
27
+ return { internalIP, gatewayIP, controlPort, controlURLs };
28
+ }
29
+
30
+ export interface PortMapping {
31
+ externalPort: number;
32
+ internalPort: number;
33
+ protocol: string;
34
+ /** The LAN client the mapping forwards to (NewInternalClient). */
35
+ internalClient: string;
36
+ /** Empty string means "any" remote host (the usual case). */
37
+ remoteHost: string;
38
+ enabled: boolean;
39
+ description: string;
40
+ /** Remaining lease in seconds; 0 means a permanent (static) mapping. */
41
+ leaseDuration: number;
42
+ }
43
+
44
+ /** Queries the router for every existing UPnP port mapping by walking
45
+ * GetGenericPortMappingEntry from index 0 until the gateway reports the index
46
+ * is out of range (SOAP error 713 / a non-200 response). */
47
+ export async function listPortMappings(): Promise<PortMapping[]> {
48
+ if (os.platform() === "linux") return [];
49
+
50
+ const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
51
+
52
+ let lastError: unknown;
53
+ for (let controlURL of controlURLs) {
54
+ try {
55
+ const mappings: PortMapping[] = [];
56
+ for (let index = 0; ; index++) {
57
+ const entry = await getGenericPortMappingEntry({
58
+ gatewayIP,
59
+ controlPort,
60
+ controlPath: controlURL,
61
+ index,
62
+ });
63
+ if (!entry) break;
64
+ mappings.push(entry);
65
+ }
66
+ return mappings;
67
+ } catch (e) {
68
+ lastError = e;
69
+ console.error(`Failed to list port mappings using controlURL ${controlURL}`, e);
70
+ }
71
+ }
72
+ throw new Error(`Failed to list port mappings, could not find a working controlURL. Last error: ${(lastError as Error)?.stack ?? lastError}`);
73
+ }
74
+
9
75
  export async function forwardPort(config: {
10
76
  externalPort: number;
11
77
  internalPort: number;
@@ -18,14 +84,7 @@ export async function forwardPort(config: {
18
84
  const { externalPort, internalPort } = config;
19
85
  let duration = config.duration ?? timeInHour;
20
86
 
21
- const localObj = getLocalInterfaceAddress();
22
- if (!localObj) throw new Error("Could not find the local address / gateway");
23
-
24
- const { internalIP, gatewayIP } = localObj;
25
- console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
26
- let gateway = await discoverGateway(internalIP);
27
- let controlURLs = await getControlPaths(gateway);
28
- let controlPort = Number(new URL(gateway).port);
87
+ const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
29
88
 
30
89
  for (let controlURL of controlURLs) {
31
90
  try {
@@ -49,6 +108,12 @@ export async function forwardPort(config: {
49
108
  }
50
109
  }
51
110
 
111
+ /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
112
+ * mapping points at us or at a different machine on the network. */
113
+ export function getLocalInternalIP(): string | undefined {
114
+ return getLocalInterfaceAddress()?.internalIP;
115
+ }
116
+
52
117
  function getLocalInterfaceAddress(): { internalIP: string; gatewayIP: string; } | undefined {
53
118
  let looksLikeRouter = (ip: string) => ip.startsWith("10.0.0") || ip.startsWith("10.0.1") || ip.startsWith("192.168.0");
54
119
 
@@ -216,3 +281,61 @@ async function createPortMapping(config: {
216
281
  throw new Error(`Failed to create port mapping: ${data}`);
217
282
  }
218
283
  }
284
+
285
+ // UPnP returns this error code when we walk past the last mapping index, which is how
286
+ // we detect the end of the list rather than a real failure.
287
+ const UPNP_ARRAY_INDEX_INVALID = 713;
288
+
289
+ /** Fetches a single port mapping by its index. Returns undefined once the index is past
290
+ * the end of the table (the router's signal that there are no more entries). */
291
+ async function getGenericPortMappingEntry(config: {
292
+ gatewayIP: string;
293
+ controlPort: number;
294
+ controlPath: string;
295
+ index: number;
296
+ }): Promise<PortMapping | undefined> {
297
+ const { gatewayIP, controlPort, controlPath, index } = config;
298
+ const action = "\"urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry\"";
299
+
300
+ const soapBody = `
301
+ <?xml version="1.0"?>
302
+ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
303
+ <s:Body>
304
+ <u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
305
+ <NewPortMappingIndex>${index}</NewPortMappingIndex>
306
+ </u:GetGenericPortMappingEntry>
307
+ </s:Body>
308
+ </s:Envelope>
309
+ `;
310
+
311
+ const res = await fetch(`http://${gatewayIP}:${controlPort}${controlPath}`, {
312
+ method: "POST",
313
+ headers: {
314
+ "Content-Type": "text/xml; charset=\"utf-8\"",
315
+ "SOAPAction": action,
316
+ "Content-Length": Buffer.byteLength(soapBody) + "",
317
+ },
318
+ body: soapBody
319
+ });
320
+
321
+ const data = await res.text();
322
+ if (res.status !== 200) {
323
+ let errorCode = Number(data.match(/<errorCode>(\d+)<\/errorCode>/)?.[1]);
324
+ if (errorCode === UPNP_ARRAY_INDEX_INVALID) {
325
+ return undefined;
326
+ }
327
+ throw new Error(`Failed to get port mapping entry ${index}: ${res.status} ${data}`);
328
+ }
329
+
330
+ let getField = (name: string) => data.match(new RegExp(`<${name}>(.*?)</${name}>`, "s"))?.[1] ?? "";
331
+ return {
332
+ externalPort: Number(getField("NewExternalPort")),
333
+ internalPort: Number(getField("NewInternalPort")),
334
+ protocol: getField("NewProtocol"),
335
+ internalClient: getField("NewInternalClient"),
336
+ remoteHost: getField("NewRemoteHost"),
337
+ enabled: getField("NewEnabled") === "1",
338
+ description: getField("NewPortMappingDescription"),
339
+ leaseDuration: Number(getField("NewLeaseDuration")),
340
+ };
341
+ }
@@ -12,13 +12,14 @@ import debugbreak from "debugbreak";
12
12
  import { getNodeId } from "./nodeCache";
13
13
  import crypto from "crypto";
14
14
  import { Watchable, getRootDomain, timeInHour, timeInMinute } from "./misc";
15
- import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "./batching";
15
+ import { delay, runInfinitePoll } from "./batching";
16
16
  import { magenta, red } from "./formatting/logColors";
17
17
  import { yellow } from "./formatting/logColors";
18
18
  import { green } from "./formatting/logColors";
19
19
  import { formatTime } from "./formatting/format";
20
20
  import { getExternalIP, testTCPIsListening } from "./networking";
21
- import { forwardPort } from "./forwardPort";
21
+ import { forwardPort, listPortMappings, getLocalInternalIP, PortMapping } from "./forwardPort";
22
+ import os from "os";
22
23
 
23
24
  // When a requested port is taken and useAvailablePortIfPortInUse is set, we scan
24
25
  // upwards from this base instead of binding a random OS-assigned port, so restarts
@@ -348,49 +349,103 @@ export async function startSocketServer(
348
349
  });
349
350
  }
350
351
 
351
- let bound = false;
352
- // Honor an explicitly requested port first. A falsy port (0, the default) means
353
- // "no preference" skip straight to the scan so we land on a consistent port
354
- // instead of an OS-assigned random one.
355
- if (port) {
356
- bound = await tryListen(port);
357
- if (!bound && !config.useAvailablePortIfPortInUse) {
358
- throw new Error(`Port ${port} is already in use (set useAvailablePortIfPortInUse to fall back to another port)`);
352
+ // Frees the currently-bound listening socket so we can rebind on a different port
353
+ // (used when a locally-free port turns out to be unusable for forwarding).
354
+ async function releasePort(): Promise<void> {
355
+ await new Promise<void>((resolve, reject) => {
356
+ if (!realServer.listening) {
357
+ resolve();
358
+ return;
359
+ }
360
+ realServer.close(e => e ? reject(e) : resolve());
361
+ });
362
+ }
363
+
364
+ // Forwarding maps the external port to an equal internal port, so a candidate is only
365
+ // usable if we can also own its external mapping on the router. Skipped on linux,
366
+ // where forwardPort is a no-op anyway.
367
+ const doForward = !!(config.autoForwardPort && config.public && os.platform() !== "linux");
368
+
369
+ // Ensures the router's external mapping for `externalPort` belongs to us. Returns true
370
+ // if it's ours to keep (existing-and-ours → refresh the lease; free → create and
371
+ // confirm we won it), false if another machine owns it and we should try a new port.
372
+ async function claimPortForward(externalPort: number): Promise<boolean> {
373
+ const ourIP = getLocalInternalIP();
374
+ const matches = (m: PortMapping) => m.externalPort === externalPort && m.protocol.toUpperCase() === "TCP";
375
+
376
+ const existing = (await listPortMappings()).find(matches);
377
+ if (existing) {
378
+ if (ourIP && existing.internalClient === ourIP) {
379
+ await forwardPort({ externalPort, internalPort: externalPort });
380
+ return true;
381
+ }
382
+ console.log(magenta(`External port ${externalPort} is already forwarded to ${existing.internalClient}, trying another port`));
383
+ return false;
384
+ }
385
+
386
+ // Free right now — create the mapping, then re-read it to make sure another host
387
+ // didn't grab the same external port in the race between our list and our create.
388
+ await forwardPort({ externalPort, internalPort: externalPort });
389
+ const ours = (await listPortMappings()).find(matches);
390
+ if (!ours || (ourIP && ours.internalClient !== ourIP)) {
391
+ console.log(magenta(`Failed to claim external port ${externalPort} (now ${ours ? `forwarded to ${ours.internalClient}` : "still unmapped"}), trying another port`));
392
+ return false;
359
393
  }
394
+ return true;
360
395
  }
361
- // Scan upwards from PORT_SCAN_START for the first free port, so restarts land on
362
- // predictable ports.
363
- if (!bound) {
396
+
397
+ // Candidate ports: an explicitly requested port first (a falsy port, the default 0,
398
+ // means "no preference"), then a consistent upward scan so restarts are predictable.
399
+ function* candidatePorts(): Generator<number> {
400
+ if (port) yield port;
364
401
  for (let candidate = PORT_SCAN_START; candidate < PORT_SCAN_START + PORT_SCAN_COUNT; candidate++) {
365
402
  if (candidate === port) continue;
366
- if (await tryListen(candidate)) {
367
- bound = true;
368
- port = candidate;
369
- break;
403
+ yield candidate;
404
+ }
405
+ }
406
+
407
+ let bound = false;
408
+ for (const candidate of candidatePorts()) {
409
+ if (!await tryListen(candidate)) {
410
+ if (candidate === config.port && !config.useAvailablePortIfPortInUse) {
411
+ throw new Error(`Port ${candidate} is already in use (set useAvailablePortIfPortInUse to fall back to another port)`);
370
412
  }
413
+ continue;
371
414
  }
372
- if (!bound) {
373
- throw new Error(`Could not find an available port in range ${PORT_SCAN_START}-${PORT_SCAN_START + PORT_SCAN_COUNT - 1} (requested ${config.port})`);
415
+ // Locally bound. If we also forward, the external mapping must be ours too, or we
416
+ // release this port and keep scanning.
417
+ if (doForward) {
418
+ let claimed: boolean;
419
+ try {
420
+ claimed = await claimPortForward(candidate);
421
+ } catch (e) {
422
+ // UPnP unavailable (no gateway / discovery failed): fall back to best-effort
423
+ // forwarding rather than refusing to start.
424
+ console.error(red(`Could not verify forwarding for port ${candidate}, continuing best-effort: ${(e as Error).stack}`));
425
+ claimed = true;
426
+ }
427
+ if (!claimed) {
428
+ await releasePort();
429
+ continue;
430
+ }
374
431
  }
432
+ port = candidate;
433
+ bound = true;
434
+ break;
435
+ }
436
+ if (!bound) {
437
+ throw new Error(`Could not find an available port in range ${PORT_SCAN_START}-${PORT_SCAN_START + PORT_SCAN_COUNT - 1} (requested ${config.port})`);
375
438
  }
376
439
 
377
440
  port = (realServer.address() as net.AddressInfo).port;
378
441
 
379
- if (config.autoForwardPort && config.public) {
380
- // let externalIP = await getExternalIP();
381
- // let isListening = await testTCPIsListening(externalIP, port);
382
- // if (!isListening) {
383
- // console.log(magenta(`Port ${port} is not externally reachable, trying to forward it`));
384
- // await forwardPort({ externalPort: port, internalPort: port });
385
- // }
386
- // Even if they are listening, they might not stay listening. Forward every 8 hours
387
- // (including at the start, in case the forward is about to expire).
388
- async function forward() {
442
+ if (doForward) {
443
+ // The mapping is claimed above; keep refreshing the lease so it doesn't expire.
444
+ async function refreshForward() {
389
445
  await forwardPort({ externalPort: port, internalPort: port });
390
- console.log(magenta(`Forwarded port ${port} to our machine`));
446
+ console.log(magenta(`Refreshed port forward ${port} to our machine`));
391
447
  }
392
- // Every hour, in case our network configuration changes
393
- runInfinitePollCallAtStart(timeInMinute * 30, forward).catch(e => console.error(red(`Error in port forwarding ${e.stack}`)));
448
+ runInfinitePoll(timeInMinute * 30, refreshForward);
394
449
  }
395
450
 
396
451
  let nodeId = getNodeId(getCommonName(config.cert), port);
package/test.ts CHANGED
@@ -1,161 +1,17 @@
1
- import { computeTweenedOffset, getTimeComponentsDetailed, waitForFirstTimeSync } from "./time/trueTimeShim";
2
1
  import * as fs from "fs";
3
2
  import * as path from "path";
3
+ import { listPortMappings } from "./src/forwardPort";
4
4
 
5
- const SAMPLE_COUNT = 1000;
6
- const SAMPLE_INTERVAL_MS = 10;
7
-
8
- // Generate a simple numeric ID for this test run
9
- const TEST_RUN_ID = Math.floor(Math.random() * 10000);
10
-
11
- async function sampleMode() {
12
- await waitForFirstTimeSync();
13
-
14
- type Sample = {
15
- id: number;
16
- systemTime: number;
17
- offset: number;
18
- fromOffset: number;
19
- toOffset: number;
20
- fromTime: number;
21
- toTime: number;
22
- };
23
-
24
- const samples: Sample[] = [];
25
-
26
- console.log(`[Test ID: ${TEST_RUN_ID}] Sampling ${SAMPLE_COUNT} time components...`);
27
- for (let i = 0; i < SAMPLE_COUNT; i++) {
28
- const detailed = getTimeComponentsDetailed();
29
-
30
- // Calculate the tween using the same systemTime
31
- const offset = computeTweenedOffset(detailed);
32
-
33
- samples.push({
34
- id: TEST_RUN_ID,
35
- systemTime: detailed.systemTime,
36
- offset,
37
- fromOffset: detailed.fromOffset,
38
- toOffset: detailed.toOffset,
39
- fromTime: detailed.fromTime,
40
- toTime: detailed.toTime,
41
- });
42
-
43
- if (SAMPLE_INTERVAL_MS > 0) {
44
- await new Promise(resolve => setTimeout(resolve, SAMPLE_INTERVAL_MS));
45
- }
46
- }
47
-
48
- // Write to file with unique name
49
- const distDir = path.join(__dirname, "dist");
50
- if (!fs.existsSync(distDir)) {
51
- fs.mkdirSync(distDir, { recursive: true });
52
- }
53
-
54
- const filename = path.join(distDir, `time-samples-${Date.now()}-${process.pid}.json`);
55
- fs.writeFileSync(filename, JSON.stringify(samples, null, 2));
56
-
57
- console.log(`[Test ID: ${TEST_RUN_ID}] Wrote ${samples.length} samples to ${filename}`);
58
- }
59
-
60
- async function verifyMode() {
61
- const distDir = path.join(__dirname, "dist");
62
- if (!fs.existsSync(distDir)) {
63
- console.error("No dist directory found. Run sampling mode first.");
64
- process.exit(1);
65
- }
66
-
67
- const files = fs.readdirSync(distDir)
68
- .filter(f => f.startsWith("time-samples-") && f.endsWith(".json"))
69
- .map(f => path.join(distDir, f));
70
-
71
- if (files.length === 0) {
72
- console.error("No sample files found. Run sampling mode first.");
73
- process.exit(1);
74
- }
75
-
76
- console.log(`Found ${files.length} sample files`);
77
-
78
- type Sample = {
79
- id: number;
80
- systemTime: number;
81
- offset: number;
82
- fromOffset: number;
83
- toOffset: number;
84
- fromTime: number;
85
- toTime: number;
86
- file: string;
87
- };
88
-
89
- // Load all samples from all files
90
- const allSamples: Sample[] = [];
91
-
92
- for (const file of files) {
93
- const content = fs.readFileSync(file, "utf-8");
94
- const samples = JSON.parse(content);
95
- for (const sample of samples) {
96
- allSamples.push({ ...sample, file: path.basename(file) });
97
- }
98
- }
99
-
100
- console.log(`Loaded ${allSamples.length} total samples`);
101
-
102
- // Sort by systemTime
103
- allSamples.sort((a, b) => a.systemTime - b.systemTime);
104
-
105
- // Verify that systemTime + offset is monotonically increasing
106
- let errors = 0;
107
- let lastTrueTime = -Infinity;
108
-
109
- for (let i = 0; i < allSamples.length; i++) {
110
- const sample = allSamples[i];
111
- const trueTime = sample.systemTime + sample.offset;
112
-
113
- if (trueTime < lastTrueTime) {
114
- errors++;
115
- const prev = allSamples[i - 1];
116
- const prevTrueTime = prev.systemTime + prev.offset;
117
-
118
- console.error(`\nERROR at index ${i}: trueTime went backwards!`);
119
- console.error(` Previous [ID: ${prev.id}, file: ${prev.file}]:`);
120
- console.error(` systemTime: ${prev.systemTime}`);
121
- console.error(` offset: ${prev.offset}`);
122
- console.error(` trueTime: ${prevTrueTime}`);
123
- console.error(` tween: ${prev.fromOffset} -> ${prev.toOffset}`);
124
- console.error(` timeWindow: ${prev.fromTime} -> ${prev.toTime}`);
125
- console.error(` Current [ID: ${sample.id}, file: ${sample.file}]:`);
126
- console.error(` systemTime: ${sample.systemTime}`);
127
- console.error(` offset: ${sample.offset}`);
128
- console.error(` trueTime: ${trueTime}`);
129
- console.error(` tween: ${sample.fromOffset} -> ${sample.toOffset}`);
130
- console.error(` timeWindow: ${sample.fromTime} -> ${sample.toTime}`);
131
- console.error(` Difference: ${trueTime - lastTrueTime}ms`);
132
- }
133
-
134
- lastTrueTime = trueTime;
135
- }
136
-
137
- if (errors === 0) {
138
- console.log(`✓ SUCCESS: All ${allSamples.length} samples are correctly ordered!`);
139
- console.log(` Time range: ${allSamples[0].systemTime} to ${allSamples[allSamples.length - 1].systemTime}`);
140
- console.log(` Duration: ${allSamples[allSamples.length - 1].systemTime - allSamples[0].systemTime}ms`);
141
- } else {
142
- console.error(`✗ FAILED: Found ${errors} ordering violations`);
143
- process.exit(1);
144
- }
145
- }
146
5
 
147
6
  async function main() {
148
- const mode = process.argv[2];
149
-
150
- if (mode === "once") {
151
- await waitForFirstTimeSync();
152
- console.log(Date.now());
153
- }
154
- else if (mode === "verify") {
155
- await verifyMode();
156
- } else {
157
- await sampleMode();
158
- }
7
+ let mappings = await listPortMappings();
8
+ console.log(`Found ${mappings.length} port mapping(s):`);
9
+ for (let mapping of mappings) {
10
+ let host = mapping.remoteHost || "*";
11
+ let lease = mapping.leaseDuration === 0 ? "permanent" : `${mapping.leaseDuration}s left`;
12
+ console.log(` ${mapping.protocol} ${host}:${mapping.externalPort} -> ${mapping.internalClient}:${mapping.internalPort} (${mapping.enabled ? "enabled" : "disabled"}, ${lease})${mapping.description ? ` "${mapping.description}"` : ""}`);
13
+ }
14
+ console.table(mappings);
159
15
  }
160
16
 
161
- main().catch(e => console.error(e)).finally(() => process.exit(0));
17
+ main().catch(e => console.error(e)).finally(() => process.exit(0));