nixamp 0.5.3 → 0.5.4
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/dist/daemon.d.ts +5 -0
- package/dist/daemon.js +4 -0
- package/dist/main.js +2 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +23 -2
- package/dist/share.d.ts +14 -0
- package/dist/share.js +35 -0
- package/package.json +1 -1
- package/src/daemon.ts +13 -0
- package/src/main.ts +2 -0
- package/src/server.ts +30 -1
- package/src/share.ts +35 -0
- package/web/dist/sw.js +1 -1
package/dist/daemon.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface DaemonState {
|
|
|
22
22
|
}[];
|
|
23
23
|
/** The firewall standing between this port and the rest of the network. */
|
|
24
24
|
firewall?: string | null;
|
|
25
|
+
/**
|
|
26
|
+
* The public address was asked of an outside service rather than found on an
|
|
27
|
+
* interface, so it names the router and not this port.
|
|
28
|
+
*/
|
|
29
|
+
guessedPublic?: boolean;
|
|
25
30
|
}
|
|
26
31
|
/** XDG, with the usual fallback. One daemon per user, which is one too few for nobody. */
|
|
27
32
|
export declare function stateDir(): string;
|
package/dist/daemon.js
CHANGED
|
@@ -84,6 +84,9 @@ export function daemonLines(state) {
|
|
|
84
84
|
for (const { label, url } of addresses)
|
|
85
85
|
lines.push(` ${label.padEnd(width)} ${link(url)}`);
|
|
86
86
|
lines.push(` ${"source".padEnd(width)} ${state.source}`);
|
|
87
|
+
if (state.guessedPublic) {
|
|
88
|
+
lines.push("", " That internet address is this machine's router, not this port.", ` Nothing outside reaches it until ${state.port} is forwarded here.`);
|
|
89
|
+
}
|
|
87
90
|
if (state.firewall) {
|
|
88
91
|
const { open } = portCommands(state.firewall, state.port);
|
|
89
92
|
lines.push("", ` ${state.firewall} is running, so nothing else can reach port ${state.port} yet:`, ` sudo ${open.join(" ")}`, " or `nixamp daemon stop` and start again with --open-port.");
|
|
@@ -158,6 +161,7 @@ async function waitForAnnounce(log, timeoutMs) {
|
|
|
158
161
|
source: String(parsed["source"]),
|
|
159
162
|
...(Array.isArray(urls) ? { urls: urls } : {}),
|
|
160
163
|
...(typeof parsed["firewall"] === "string" ? { firewall: parsed["firewall"] } : {}),
|
|
164
|
+
...(parsed["guessedPublic"] === true ? { guessedPublic: true } : {}),
|
|
161
165
|
};
|
|
162
166
|
}
|
|
163
167
|
}
|
package/dist/main.js
CHANGED
|
@@ -70,6 +70,8 @@ Options for serve:
|
|
|
70
70
|
--public-url URL the address this server is reachable at from outside,
|
|
71
71
|
when that is a tunnel or a forwarded port rather than one of
|
|
72
72
|
its own interfaces. Also NIXAMP_PUBLIC_URL
|
|
73
|
+
--no-lookup do not ask ipinfo.io what this machine's public address is
|
|
74
|
+
when nothing local looks public
|
|
73
75
|
--ingest accept a live stream in at POST /api/ingest
|
|
74
76
|
--rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
|
|
75
77
|
--rtmp-streams N how many may publish at once (default 3, a port each)
|
package/dist/server.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface ServeOptions {
|
|
|
56
56
|
* listing is skipped and the printed links only work inside the house.
|
|
57
57
|
*/
|
|
58
58
|
publicUrl: string;
|
|
59
|
+
/**
|
|
60
|
+
* Ask an outside service what this machine's public address is, when no
|
|
61
|
+
* interface holds one and none was given. Behind NAT that is the only way to
|
|
62
|
+
* learn it, and it is one short request at startup.
|
|
63
|
+
*/
|
|
64
|
+
lookup: boolean;
|
|
59
65
|
/**
|
|
60
66
|
* Charge for listening once the stream is busy. Off unless asked for, and
|
|
61
67
|
* useless without somewhere to pay: see NIXAMP_PAY_TO.
|
package/dist/server.js
CHANGED
|
@@ -34,7 +34,7 @@ import { notifyAll, resendEmail, webPush } from "./notify.js";
|
|
|
34
34
|
import { confirm, DEFAULT_DIRECTORY, Publisher } from "./publish.js";
|
|
35
35
|
import { applyRemoteConfig, createPaywall, FREE_LISTENERS, paywallFromEnv, } from "./paywall.js";
|
|
36
36
|
import { isRemote, playsInBrowser } from "./sources.js";
|
|
37
|
-
import { allowedForListening, elevate, firewallInUse, keyCookie, keyFrom, newKey, portCommands, reachableAddresses, scopeOf, shareLink, audioLink, } from "./share.js";
|
|
37
|
+
import { allowedForListening, elevate, firewallInUse, keyCookie, keyFrom, lookupPublicIp, newKey, portCommands, reachableAddresses, scopeOf, shareLink, audioLink, } from "./share.js";
|
|
38
38
|
import { extname, join, normalize, resolve, sep } from "node:path";
|
|
39
39
|
import { fileURLToPath } from "node:url";
|
|
40
40
|
import { detectTools, peaks, RATE, Stream, toMono, } from "./audio.js";
|
|
@@ -67,6 +67,7 @@ export function parseServeArgs(argv) {
|
|
|
67
67
|
publish: "ask",
|
|
68
68
|
name: "",
|
|
69
69
|
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
70
|
+
lookup: true,
|
|
70
71
|
x402: false,
|
|
71
72
|
owner: "",
|
|
72
73
|
ingest: false,
|
|
@@ -127,6 +128,9 @@ export function parseServeArgs(argv) {
|
|
|
127
128
|
}
|
|
128
129
|
options.publicUrl = given.replace(/\/+$/, "");
|
|
129
130
|
}
|
|
131
|
+
else if (arg === "--no-lookup") {
|
|
132
|
+
options.lookup = false;
|
|
133
|
+
}
|
|
130
134
|
else if (arg === "--name") {
|
|
131
135
|
options.name = value();
|
|
132
136
|
}
|
|
@@ -2087,7 +2091,18 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2087
2091
|
};
|
|
2088
2092
|
// The link, not the address. Without the key the address is a 401, so
|
|
2089
2093
|
// printing a bare host:port would be printing something that does not work.
|
|
2090
|
-
|
|
2094
|
+
//
|
|
2095
|
+
// Behind NAT no interface holds the public address, so if nobody said what it
|
|
2096
|
+
// is and nothing local looks public, ask. What comes back is a fact about the
|
|
2097
|
+
// router and not about this port -- the port still has to be forwarded -- so
|
|
2098
|
+
// it is marked as a guess and everything that prints it says so.
|
|
2099
|
+
const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2100
|
+
const guessedPublic = options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
|
|
2101
|
+
? await lookupPublicIp()
|
|
2102
|
+
: "";
|
|
2103
|
+
const addresses = guessedPublic
|
|
2104
|
+
? reachableAddresses(options.host, port, `http://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`)
|
|
2105
|
+
: localAddresses;
|
|
2091
2106
|
// Listening on every interface proves the socket is open here and nothing
|
|
2092
2107
|
// about the path between here and the phone.
|
|
2093
2108
|
const listening = options.host === "0.0.0.0" || options.host === "::";
|
|
@@ -2108,6 +2123,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2108
2123
|
source: root,
|
|
2109
2124
|
urls: addresses,
|
|
2110
2125
|
firewall,
|
|
2126
|
+
guessedPublic: guessedPublic !== "",
|
|
2111
2127
|
}));
|
|
2112
2128
|
}
|
|
2113
2129
|
// The other half of "names now, tags later". It runs while the banner is
|
|
@@ -2128,6 +2144,11 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2128
2144
|
for (const { label, url } of addresses) {
|
|
2129
2145
|
console.log(` ${label.padEnd(width)} ${shareLink(url, key)}`);
|
|
2130
2146
|
}
|
|
2147
|
+
if (guessedPublic) {
|
|
2148
|
+
console.log("");
|
|
2149
|
+
console.log(` That internet address is this machine's router, not this port.`);
|
|
2150
|
+
console.log(` Nothing outside reaches it until ${port} is forwarded here.`);
|
|
2151
|
+
}
|
|
2131
2152
|
console.log("");
|
|
2132
2153
|
if (key === null) {
|
|
2133
2154
|
console.log(" No key: anyone who can reach this port can drive it and hear it.");
|
package/dist/share.d.ts
CHANGED
|
@@ -25,6 +25,20 @@ export declare function keyFrom(request: IncomingMessage, url: URL): string | nu
|
|
|
25
25
|
export declare function keyCookie(key: string): string;
|
|
26
26
|
/** Where an address actually goes, which is not always where you would like. */
|
|
27
27
|
export declare function classify(address: string): "private" | "cgnat" | "public";
|
|
28
|
+
/** Looks like an address, rather than an error page or a rate-limit notice. */
|
|
29
|
+
export declare function isIpAddress(value: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The address the rest of the world sees this machine as, by asking.
|
|
32
|
+
*
|
|
33
|
+
* Behind NAT no interface holds it, so there is nobody to ask but somebody
|
|
34
|
+
* outside. It is a claim about the router, not about this port: an address
|
|
35
|
+
* discovered this way is only reachable once the router forwards the port to
|
|
36
|
+
* this machine, which is why what prints it says so.
|
|
37
|
+
*
|
|
38
|
+
* Empty string for every failure. A player must not spend its startup waiting
|
|
39
|
+
* on somebody else's web service.
|
|
40
|
+
*/
|
|
41
|
+
export declare function lookupPublicIp(send?: typeof fetch, timeoutMs?: number): Promise<string>;
|
|
28
42
|
/**
|
|
29
43
|
* The addresses another device could actually reach this machine on, nearest
|
|
30
44
|
* first. On a server the public one is the point: it is the address a phone
|
package/dist/share.js
CHANGED
|
@@ -74,6 +74,41 @@ export function classify(address) {
|
|
|
74
74
|
return "cgnat";
|
|
75
75
|
return "public";
|
|
76
76
|
}
|
|
77
|
+
/** Looks like an address, rather than an error page or a rate-limit notice. */
|
|
78
|
+
export function isIpAddress(value) {
|
|
79
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) {
|
|
80
|
+
return value.split(".").every((part) => Number(part) <= 255);
|
|
81
|
+
}
|
|
82
|
+
// Enough of v6 to reject prose: hex groups and colons, nothing else. This is
|
|
83
|
+
// not a validator, it is a guard against printing an error page as an address.
|
|
84
|
+
return value.includes(":") && /^[0-9a-f:]+$/i.test(value);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The address the rest of the world sees this machine as, by asking.
|
|
88
|
+
*
|
|
89
|
+
* Behind NAT no interface holds it, so there is nobody to ask but somebody
|
|
90
|
+
* outside. It is a claim about the router, not about this port: an address
|
|
91
|
+
* discovered this way is only reachable once the router forwards the port to
|
|
92
|
+
* this machine, which is why what prints it says so.
|
|
93
|
+
*
|
|
94
|
+
* Empty string for every failure. A player must not spend its startup waiting
|
|
95
|
+
* on somebody else's web service.
|
|
96
|
+
*/
|
|
97
|
+
export async function lookupPublicIp(send = fetch, timeoutMs = 2500) {
|
|
98
|
+
try {
|
|
99
|
+
const answer = await send("https://ipinfo.io/ip", {
|
|
100
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
101
|
+
headers: { accept: "text/plain", "user-agent": "nixamp" },
|
|
102
|
+
});
|
|
103
|
+
if (!answer.ok)
|
|
104
|
+
return "";
|
|
105
|
+
const found = (await answer.text()).trim();
|
|
106
|
+
return isIpAddress(found) ? found : "";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
77
112
|
/**
|
|
78
113
|
* The addresses another device could actually reach this machine on, nearest
|
|
79
114
|
* first. On a server the public one is the point: it is the address a phone
|
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface DaemonState {
|
|
|
33
33
|
urls?: { label: string; url: string }[];
|
|
34
34
|
/** The firewall standing between this port and the rest of the network. */
|
|
35
35
|
firewall?: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* The public address was asked of an outside service rather than found on an
|
|
38
|
+
* interface, so it names the router and not this port.
|
|
39
|
+
*/
|
|
40
|
+
guessedPublic?: boolean;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/** XDG, with the usual fallback. One daemon per user, which is one too few for nobody. */
|
|
@@ -115,6 +120,13 @@ export function daemonLines(state: DaemonState): string[] {
|
|
|
115
120
|
for (const { label, url } of addresses) lines.push(` ${label.padEnd(width)} ${link(url)}`);
|
|
116
121
|
lines.push(` ${"source".padEnd(width)} ${state.source}`);
|
|
117
122
|
|
|
123
|
+
if (state.guessedPublic) {
|
|
124
|
+
lines.push(
|
|
125
|
+
"",
|
|
126
|
+
" That internet address is this machine's router, not this port.",
|
|
127
|
+
` Nothing outside reaches it until ${state.port} is forwarded here.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
118
130
|
if (state.firewall) {
|
|
119
131
|
const { open } = portCommands(state.firewall as Firewall, state.port);
|
|
120
132
|
lines.push(
|
|
@@ -209,6 +221,7 @@ async function waitForAnnounce(
|
|
|
209
221
|
source: String(parsed["source"]),
|
|
210
222
|
...(Array.isArray(urls) ? { urls: urls as { label: string; url: string }[] } : {}),
|
|
211
223
|
...(typeof parsed["firewall"] === "string" ? { firewall: parsed["firewall"] } : {}),
|
|
224
|
+
...(parsed["guessedPublic"] === true ? { guessedPublic: true } : {}),
|
|
212
225
|
};
|
|
213
226
|
}
|
|
214
227
|
} catch {
|
package/src/main.ts
CHANGED
|
@@ -94,6 +94,8 @@ Options for serve:
|
|
|
94
94
|
--public-url URL the address this server is reachable at from outside,
|
|
95
95
|
when that is a tunnel or a forwarded port rather than one of
|
|
96
96
|
its own interfaces. Also NIXAMP_PUBLIC_URL
|
|
97
|
+
--no-lookup do not ask ipinfo.io what this machine's public address is
|
|
98
|
+
when nothing local looks public
|
|
97
99
|
--ingest accept a live stream in at POST /api/ingest
|
|
98
100
|
--rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
|
|
99
101
|
--rtmp-streams N how many may publish at once (default 3, a port each)
|
package/src/server.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
keyCookie,
|
|
62
62
|
keyFrom,
|
|
63
63
|
keysMatch,
|
|
64
|
+
lookupPublicIp,
|
|
64
65
|
newKey,
|
|
65
66
|
portCommands,
|
|
66
67
|
reachableAddresses,
|
|
@@ -128,6 +129,12 @@ export interface ServeOptions {
|
|
|
128
129
|
* listing is skipped and the printed links only work inside the house.
|
|
129
130
|
*/
|
|
130
131
|
publicUrl: string;
|
|
132
|
+
/**
|
|
133
|
+
* Ask an outside service what this machine's public address is, when no
|
|
134
|
+
* interface holds one and none was given. Behind NAT that is the only way to
|
|
135
|
+
* learn it, and it is one short request at startup.
|
|
136
|
+
*/
|
|
137
|
+
lookup: boolean;
|
|
131
138
|
/**
|
|
132
139
|
* Charge for listening once the stream is busy. Off unless asked for, and
|
|
133
140
|
* useless without somewhere to pay: see NIXAMP_PAY_TO.
|
|
@@ -178,6 +185,7 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
178
185
|
publish: "ask",
|
|
179
186
|
name: "",
|
|
180
187
|
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
188
|
+
lookup: true,
|
|
181
189
|
x402: false,
|
|
182
190
|
owner: "",
|
|
183
191
|
ingest: false,
|
|
@@ -226,6 +234,8 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
226
234
|
throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
|
|
227
235
|
}
|
|
228
236
|
options.publicUrl = given.replace(/\/+$/, "");
|
|
237
|
+
} else if (arg === "--no-lookup") {
|
|
238
|
+
options.lookup = false;
|
|
229
239
|
} else if (arg === "--name") {
|
|
230
240
|
options.name = value();
|
|
231
241
|
} else if (arg === "--owner") {
|
|
@@ -2392,7 +2402,19 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2392
2402
|
|
|
2393
2403
|
// The link, not the address. Without the key the address is a 401, so
|
|
2394
2404
|
// printing a bare host:port would be printing something that does not work.
|
|
2395
|
-
|
|
2405
|
+
//
|
|
2406
|
+
// Behind NAT no interface holds the public address, so if nobody said what it
|
|
2407
|
+
// is and nothing local looks public, ask. What comes back is a fact about the
|
|
2408
|
+
// router and not about this port -- the port still has to be forwarded -- so
|
|
2409
|
+
// it is marked as a guess and everything that prints it says so.
|
|
2410
|
+
const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2411
|
+
const guessedPublic =
|
|
2412
|
+
options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
|
|
2413
|
+
? await lookupPublicIp()
|
|
2414
|
+
: "";
|
|
2415
|
+
const addresses = guessedPublic
|
|
2416
|
+
? reachableAddresses(options.host, port, `http://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`)
|
|
2417
|
+
: localAddresses;
|
|
2396
2418
|
|
|
2397
2419
|
// Listening on every interface proves the socket is open here and nothing
|
|
2398
2420
|
// about the path between here and the phone.
|
|
@@ -2416,6 +2438,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2416
2438
|
source: root,
|
|
2417
2439
|
urls: addresses,
|
|
2418
2440
|
firewall,
|
|
2441
|
+
guessedPublic: guessedPublic !== "",
|
|
2419
2442
|
}),
|
|
2420
2443
|
);
|
|
2421
2444
|
}
|
|
@@ -2441,6 +2464,12 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2441
2464
|
console.log(` ${label.padEnd(width)} ${shareLink(url, key)}`);
|
|
2442
2465
|
}
|
|
2443
2466
|
|
|
2467
|
+
if (guessedPublic) {
|
|
2468
|
+
console.log("");
|
|
2469
|
+
console.log(` That internet address is this machine's router, not this port.`);
|
|
2470
|
+
console.log(` Nothing outside reaches it until ${port} is forwarded here.`);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2444
2473
|
console.log("");
|
|
2445
2474
|
if (key === null) {
|
|
2446
2475
|
console.log(" No key: anyone who can reach this port can drive it and hear it.");
|
package/src/share.ts
CHANGED
|
@@ -87,6 +87,41 @@ export function classify(address: string): "private" | "cgnat" | "public" {
|
|
|
87
87
|
return "public";
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Looks like an address, rather than an error page or a rate-limit notice. */
|
|
91
|
+
export function isIpAddress(value: string): boolean {
|
|
92
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) {
|
|
93
|
+
return value.split(".").every((part) => Number(part) <= 255);
|
|
94
|
+
}
|
|
95
|
+
// Enough of v6 to reject prose: hex groups and colons, nothing else. This is
|
|
96
|
+
// not a validator, it is a guard against printing an error page as an address.
|
|
97
|
+
return value.includes(":") && /^[0-9a-f:]+$/i.test(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The address the rest of the world sees this machine as, by asking.
|
|
102
|
+
*
|
|
103
|
+
* Behind NAT no interface holds it, so there is nobody to ask but somebody
|
|
104
|
+
* outside. It is a claim about the router, not about this port: an address
|
|
105
|
+
* discovered this way is only reachable once the router forwards the port to
|
|
106
|
+
* this machine, which is why what prints it says so.
|
|
107
|
+
*
|
|
108
|
+
* Empty string for every failure. A player must not spend its startup waiting
|
|
109
|
+
* on somebody else's web service.
|
|
110
|
+
*/
|
|
111
|
+
export async function lookupPublicIp(send: typeof fetch = fetch, timeoutMs = 2500): Promise<string> {
|
|
112
|
+
try {
|
|
113
|
+
const answer = await send("https://ipinfo.io/ip", {
|
|
114
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
115
|
+
headers: { accept: "text/plain", "user-agent": "nixamp" },
|
|
116
|
+
});
|
|
117
|
+
if (!answer.ok) return "";
|
|
118
|
+
const found = (await answer.text()).trim();
|
|
119
|
+
return isIpAddress(found) ? found : "";
|
|
120
|
+
} catch {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
/**
|
|
91
126
|
* The addresses another device could actually reach this machine on, nearest
|
|
92
127
|
* first. On a server the public one is the point: it is the address a phone
|
package/web/dist/sw.js
CHANGED