@jsprismarine/nethernet 0.0.0 → 0.20.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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/HttpSignaling.d.ts +60 -0
- package/dist/HttpSignaling.d.ts.map +1 -0
- package/dist/HttpSignaling.js +177 -0
- package/dist/HttpSignaling.js.map +1 -0
- package/dist/LanAdvertisement.d.ts +36 -0
- package/dist/LanAdvertisement.d.ts.map +1 -0
- package/dist/LanAdvertisement.js +70 -0
- package/dist/LanAdvertisement.js.map +1 -0
- package/dist/NetherNetDefaults.d.ts +3 -0
- package/dist/NetherNetDefaults.d.ts.map +1 -0
- package/dist/NetherNetDefaults.js +6 -0
- package/dist/NetherNetDefaults.js.map +1 -0
- package/dist/NetherNetIdentity.d.ts +88 -0
- package/dist/NetherNetIdentity.d.ts.map +1 -0
- package/dist/NetherNetIdentity.js +164 -0
- package/dist/NetherNetIdentity.js.map +1 -0
- package/dist/NetherNetStatus.d.ts +27 -0
- package/dist/NetherNetStatus.d.ts.map +1 -0
- package/dist/NetherNetStatus.js +46 -0
- package/dist/NetherNetStatus.js.map +1 -0
- package/dist/constrainIcePorts.d.ts +14 -0
- package/dist/constrainIcePorts.d.ts.map +1 -0
- package/dist/constrainIcePorts.js +35 -0
- package/dist/constrainIcePorts.js.map +1 -0
- package/dist/es384.d.ts +26 -0
- package/dist/es384.d.ts.map +1 -0
- package/dist/es384.js +50 -0
- package/dist/es384.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/isRecord.d.ts +3 -0
- package/dist/isRecord.d.ts.map +1 -0
- package/dist/isRecord.js +6 -0
- package/dist/isRecord.js.map +1 -0
- package/dist/joinByAddress.d.ts +41 -0
- package/dist/joinByAddress.d.ts.map +1 -0
- package/dist/joinByAddress.js +52 -0
- package/dist/joinByAddress.js.map +1 -0
- package/dist/sdp.d.ts +26 -0
- package/dist/sdp.d.ts.map +1 -0
- package/dist/sdp.js +43 -0
- package/dist/sdp.js.map +1 -0
- package/dist/tracePeerConnection.d.ts +15 -0
- package/dist/tracePeerConnection.d.ts.map +1 -0
- package/dist/tracePeerConnection.js +44 -0
- package/dist/tracePeerConnection.js.map +1 -0
- package/dist/udpPorts.d.ts +37 -0
- package/dist/udpPorts.d.ts.map +1 -0
- package/dist/udpPorts.js +89 -0
- package/dist/udpPorts.js.map +1 -0
- package/package.json +57 -9
- package/index.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdp.d.ts","sources":["../src/sdp.ts"],"names":[],"mappings":"AAUA,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAS,MAAM,KAAG,cAAc,EAItD,CAAC;AAEP;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAS,MAAM,KAAG,MAC2B,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAS,MAAM,aAAa,MAAM,KAAG,MAKrE,CAAC"}
|
package/dist/sdp.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/sdp.ts
|
|
2
|
+
/** SDP separates lines with CRLF; a peer that sends bare LF is read the same way. */
|
|
3
|
+
var LINE_BREAK = /\r?\n/;
|
|
4
|
+
/** The lines of a session description, without a trailing empty line. */
|
|
5
|
+
var linesOf = (sdp) => sdp.split(LINE_BREAK).filter((line, index, lines) => line !== "" || index < lines.length - 1);
|
|
6
|
+
/** Joins lines back into SDP, CRLF-terminated as the format requires. */
|
|
7
|
+
var sdpOf = (lines) => `${lines.join("\r\n")}\r\n`;
|
|
8
|
+
/**
|
|
9
|
+
* Every `a=fingerprint` in a description, in the order they appear.
|
|
10
|
+
* @param sdp - The session description.
|
|
11
|
+
*/
|
|
12
|
+
var sdpFingerprints = (sdp) => linesOf(sdp).flatMap((line) => {
|
|
13
|
+
const match = /^a=fingerprint:(\S+) (\S+)$/.exec(line);
|
|
14
|
+
return match === null ? [] : [{
|
|
15
|
+
algorithm: match[1],
|
|
16
|
+
digest: match[2]
|
|
17
|
+
}];
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* The description with every `a=identity` line removed. The identity is NetherNet's, checked
|
|
21
|
+
* before the description reaches WebRTC, which does not know the attribute.
|
|
22
|
+
* @param sdp - The session description.
|
|
23
|
+
*/
|
|
24
|
+
var withoutIdentity = (sdp) => sdpOf(linesOf(sdp).filter((line) => !line.startsWith("a=identity:")));
|
|
25
|
+
/**
|
|
26
|
+
* The description with one session-level attribute added: after every other session-level line
|
|
27
|
+
* and before the first media section, which is where NetherNet expects `a=identity`.
|
|
28
|
+
* @param sdp - The session description.
|
|
29
|
+
* @param attribute - The attribute line, such as `a=identity:…`.
|
|
30
|
+
*/
|
|
31
|
+
var withSessionAttribute = (sdp, attribute) => {
|
|
32
|
+
const lines = linesOf(sdp);
|
|
33
|
+
const media = lines.findIndex((line) => line.startsWith("m="));
|
|
34
|
+
const at = media === -1 ? lines.length : media;
|
|
35
|
+
return sdpOf([
|
|
36
|
+
...lines.slice(0, at),
|
|
37
|
+
attribute,
|
|
38
|
+
...lines.slice(at)
|
|
39
|
+
]);
|
|
40
|
+
};
|
|
41
|
+
export { sdpFingerprints, withSessionAttribute, withoutIdentity };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=sdp.js.map
|
package/dist/sdp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdp.js","names":[],"sources":["../src/sdp.ts"],"sourcesContent":["/** SDP separates lines with CRLF; a peer that sends bare LF is read the same way. */\nconst LINE_BREAK = /\\r?\\n/;\n\n/** The lines of a session description, without a trailing empty line. */\nconst linesOf = (sdp: string): string[] =>\n sdp.split(LINE_BREAK).filter((line, index, lines) => line !== '' || index < lines.length - 1);\n\n/** Joins lines back into SDP, CRLF-terminated as the format requires. */\nconst sdpOf = (lines: readonly string[]): string => `${lines.join('\\r\\n')}\\r\\n`;\n\n/** One `a=fingerprint` attribute: the DTLS certificate hash the peer must present. */\nexport interface SdpFingerprint {\n /** The hash function, such as `sha-256`. */\n readonly algorithm: string;\n /** The hash, colon-separated hex, as the SDP spells it. */\n readonly digest: string;\n}\n\n/**\n * Every `a=fingerprint` in a description, in the order they appear.\n * @param sdp - The session description.\n */\nexport const sdpFingerprints = (sdp: string): SdpFingerprint[] =>\n linesOf(sdp).flatMap((line) => {\n const match = /^a=fingerprint:(\\S+) (\\S+)$/.exec(line);\n return match === null ? [] : [{ algorithm: match[1]!, digest: match[2]! }];\n });\n\n/**\n * The description with every `a=identity` line removed. The identity is NetherNet's, checked\n * before the description reaches WebRTC, which does not know the attribute.\n * @param sdp - The session description.\n */\nexport const withoutIdentity = (sdp: string): string =>\n sdpOf(linesOf(sdp).filter((line) => !line.startsWith('a=identity:')));\n\n/**\n * The description with one session-level attribute added: after every other session-level line\n * and before the first media section, which is where NetherNet expects `a=identity`.\n * @param sdp - The session description.\n * @param attribute - The attribute line, such as `a=identity:…`.\n */\nexport const withSessionAttribute = (sdp: string, attribute: string): string => {\n const lines = linesOf(sdp);\n const media = lines.findIndex((line) => line.startsWith('m='));\n const at = media === -1 ? lines.length : media;\n return sdpOf([...lines.slice(0, at), attribute, ...lines.slice(at)]);\n};\n"],"mappings":";;AACA,IAAM,aAAa;;AAGnB,IAAM,WAAW,QACb,IAAI,MAAM,UAAU,CAAC,CAAC,QAAQ,MAAM,OAAO,UAAU,SAAS,MAAM,QAAQ,MAAM,SAAS,CAAC;;AAGhG,IAAM,SAAS,UAAqC,GAAG,MAAM,KAAK,MAAM,EAAE;;;;;AAc1E,IAAa,mBAAmB,QAC5B,QAAQ,GAAG,CAAC,CAAC,SAAS,SAAS;CAC3B,MAAM,QAAQ,8BAA8B,KAAK,IAAI;CACrD,OAAO,UAAU,OAAO,CAAC,IAAI,CAAC;EAAE,WAAW,MAAM;EAAK,QAAQ,MAAM;CAAI,CAAC;AAC7E,CAAC;;;;;;AAOL,IAAa,mBAAmB,QAC5B,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,SAAS,CAAC,KAAK,WAAW,aAAa,CAAC,CAAC;;;;;;;AAQxE,IAAa,wBAAwB,KAAa,cAA8B;CAC5E,MAAM,QAAQ,QAAQ,GAAG;CACzB,MAAM,QAAQ,MAAM,WAAW,SAAS,KAAK,WAAW,IAAI,CAAC;CAC7D,MAAM,KAAK,UAAU,KAAK,MAAM,SAAS;CACzC,OAAO,MAAM;EAAC,GAAG,MAAM,MAAM,GAAG,EAAE;EAAG;EAAW,GAAG,MAAM,MAAM,EAAE;CAAC,CAAC;AACvE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The candidates a description lists, as `address:port type`, for a log line. */
|
|
2
|
+
export declare const describeCandidates: (description: string) => string[];
|
|
3
|
+
/**
|
|
4
|
+
* Reports a peer connection's ICE and connection states as they change, and the candidate pair
|
|
5
|
+
* ICE settles on: where a join that never finishes stopped. A pair that never connects usually
|
|
6
|
+
* means the server's UDP ports are not reachable from the client, as behind a firewall that opens
|
|
7
|
+
* only the game port.
|
|
8
|
+
*
|
|
9
|
+
* Werift, `nethernet`'s default backend, exposes these as events; with another backend this
|
|
10
|
+
* reports nothing.
|
|
11
|
+
* @param rtcConnection - The peer connection `nethernet` built.
|
|
12
|
+
* @param log - Where each change goes, as one line.
|
|
13
|
+
*/
|
|
14
|
+
export declare const tracePeerConnection: (rtcConnection: unknown, log: (line: string) => void) => void;
|
|
15
|
+
//# sourceMappingURL=tracePeerConnection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracePeerConnection.d.ts","sources":["../src/tracePeerConnection.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,gBAAiB,MAAM,KAAG,MAAM,EAI1D,CAAC;AAcN;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,kBAAmB,OAAO,OAAO,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAG,IAezF,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isRecord } from "./isRecord.js";
|
|
2
|
+
// src/tracePeerConnection.ts
|
|
3
|
+
/** The candidates a description lists, as `address:port type`, for a log line. */
|
|
4
|
+
var describeCandidates = (description) => [...description.matchAll(/^a=candidate:\S+ \d+ (\S+) \d+ (\S+) (\d+) typ (\S+)/gim)].map(([, protocol, address, port, type]) => `${address}:${port} ${type}${protocol?.toLowerCase() === "udp" ? "" : ` ${protocol}`}`);
|
|
5
|
+
/** A candidate as an SDP line; werift's own candidate strings leave out the `candidate:` prefix. */
|
|
6
|
+
var candidateLine = (candidate) => {
|
|
7
|
+
const text = String(candidate);
|
|
8
|
+
return text.startsWith("candidate:") ? `a=${text}` : `a=candidate:${text}`;
|
|
9
|
+
};
|
|
10
|
+
/** Subscribes to one of werift's events, if the object has it. */
|
|
11
|
+
var subscribe = (owner, name, listener) => {
|
|
12
|
+
const event = owner[name];
|
|
13
|
+
if (isRecord(event) && typeof event.subscribe === "function") event.subscribe(listener);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Reports a peer connection's ICE and connection states as they change, and the candidate pair
|
|
17
|
+
* ICE settles on: where a join that never finishes stopped. A pair that never connects usually
|
|
18
|
+
* means the server's UDP ports are not reachable from the client, as behind a firewall that opens
|
|
19
|
+
* only the game port.
|
|
20
|
+
*
|
|
21
|
+
* Werift, `nethernet`'s default backend, exposes these as events; with another backend this
|
|
22
|
+
* reports nothing.
|
|
23
|
+
* @param rtcConnection - The peer connection `nethernet` built.
|
|
24
|
+
* @param log - Where each change goes, as one line.
|
|
25
|
+
*/
|
|
26
|
+
var tracePeerConnection = (rtcConnection, log) => {
|
|
27
|
+
if (!isRecord(rtcConnection)) return;
|
|
28
|
+
subscribe(rtcConnection, "iceConnectionStateChange", (state) => {
|
|
29
|
+
log(`ICE ${String(state)}`);
|
|
30
|
+
if (state !== "connected" && state !== "completed") return;
|
|
31
|
+
const transports = rtcConnection.iceTransports;
|
|
32
|
+
const ice = Array.isArray(transports) ? transports[0] : void 0;
|
|
33
|
+
if (!isRecord(ice) || typeof ice.getSelectedCandidatePair !== "function") return;
|
|
34
|
+
const pair = ice.getSelectedCandidatePair();
|
|
35
|
+
if (!isRecord(pair) || !isRecord(pair.local) || !isRecord(pair.remote)) return;
|
|
36
|
+
const [local] = describeCandidates(candidateLine(pair.local.candidate));
|
|
37
|
+
const [remote] = describeCandidates(candidateLine(pair.remote.candidate));
|
|
38
|
+
log(`ICE pair ${local ?? "?"} <-> ${remote ?? "?"}`);
|
|
39
|
+
});
|
|
40
|
+
subscribe(rtcConnection, "connectionStateChange", (state) => log(`connection ${String(state)}`));
|
|
41
|
+
};
|
|
42
|
+
export { describeCandidates, tracePeerConnection };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=tracePeerConnection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracePeerConnection.js","names":[],"sources":["../src/tracePeerConnection.ts"],"sourcesContent":["import { isRecord } from './isRecord';\n\n/** The candidates a description lists, as `address:port type`, for a log line. */\nexport const describeCandidates = (description: string): string[] =>\n [...description.matchAll(/^a=candidate:\\S+ \\d+ (\\S+) \\d+ (\\S+) (\\d+) typ (\\S+)/gim)].map(\n ([, protocol, address, port, type]) =>\n `${address}:${port} ${type}${protocol?.toLowerCase() === 'udp' ? '' : ` ${protocol}`}`\n );\n\n/** A candidate as an SDP line; werift's own candidate strings leave out the `candidate:` prefix. */\nconst candidateLine = (candidate: unknown): string => {\n const text = String(candidate);\n return text.startsWith('candidate:') ? `a=${text}` : `a=candidate:${text}`;\n};\n\n/** Subscribes to one of werift's events, if the object has it. */\nconst subscribe = (owner: Record<string, unknown>, name: string, listener: (state: unknown) => void): void => {\n const event = owner[name];\n if (isRecord(event) && typeof event.subscribe === 'function') event.subscribe(listener);\n};\n\n/**\n * Reports a peer connection's ICE and connection states as they change, and the candidate pair\n * ICE settles on: where a join that never finishes stopped. A pair that never connects usually\n * means the server's UDP ports are not reachable from the client, as behind a firewall that opens\n * only the game port.\n *\n * Werift, `nethernet`'s default backend, exposes these as events; with another backend this\n * reports nothing.\n * @param rtcConnection - The peer connection `nethernet` built.\n * @param log - Where each change goes, as one line.\n */\nexport const tracePeerConnection = (rtcConnection: unknown, log: (line: string) => void): void => {\n if (!isRecord(rtcConnection)) return;\n subscribe(rtcConnection, 'iceConnectionStateChange', (state) => {\n log(`ICE ${String(state)}`);\n if (state !== 'connected' && state !== 'completed') return;\n const transports = rtcConnection.iceTransports;\n const ice: unknown = Array.isArray(transports) ? transports[0] : undefined;\n if (!isRecord(ice) || typeof ice.getSelectedCandidatePair !== 'function') return;\n const pair: unknown = ice.getSelectedCandidatePair();\n if (!isRecord(pair) || !isRecord(pair.local) || !isRecord(pair.remote)) return;\n const [local] = describeCandidates(candidateLine(pair.local.candidate));\n const [remote] = describeCandidates(candidateLine(pair.remote.candidate));\n log(`ICE pair ${local ?? '?'} <-> ${remote ?? '?'}`);\n });\n subscribe(rtcConnection, 'connectionStateChange', (state) => log(`connection ${String(state)}`));\n};\n"],"mappings":";;;AAGA,IAAa,sBAAsB,gBAC/B,CAAC,GAAG,YAAY,SAAS,yDAAyD,CAAC,CAAC,CAAC,KAChF,GAAG,UAAU,SAAS,MAAM,UACzB,GAAG,QAAQ,GAAG,KAAK,GAAG,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,IAAI,YAClF;;AAGJ,IAAM,iBAAiB,cAA+B;CAClD,MAAM,OAAO,OAAO,SAAS;CAC7B,OAAO,KAAK,WAAW,YAAY,IAAI,KAAK,SAAS,eAAe;AACxE;;AAGA,IAAM,aAAa,OAAgC,MAAc,aAA6C;CAC1G,MAAM,QAAQ,MAAM;CACpB,IAAI,SAAS,KAAK,KAAK,OAAO,MAAM,cAAc,YAAY,MAAM,UAAU,QAAQ;AAC1F;;;;;;;;;;;;AAaA,IAAa,uBAAuB,eAAwB,QAAsC;CAC9F,IAAI,CAAC,SAAS,aAAa,GAAG;CAC9B,UAAU,eAAe,6BAA6B,UAAU;EAC5D,IAAI,OAAO,OAAO,KAAK,GAAG;EAC1B,IAAI,UAAU,eAAe,UAAU,aAAa;EACpD,MAAM,aAAa,cAAc;EACjC,MAAM,MAAe,MAAM,QAAQ,UAAU,IAAI,WAAW,KAAK,KAAA;EACjE,IAAI,CAAC,SAAS,GAAG,KAAK,OAAO,IAAI,6BAA6B,YAAY;EAC1E,MAAM,OAAgB,IAAI,yBAAyB;EACnD,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,KAAK,KAAK,KAAK,CAAC,SAAS,KAAK,MAAM,GAAG;EACxE,MAAM,CAAC,SAAS,mBAAmB,cAAc,KAAK,MAAM,SAAS,CAAC;EACtE,MAAM,CAAC,UAAU,mBAAmB,cAAc,KAAK,OAAO,SAAS,CAAC;EACxE,IAAI,YAAY,SAAS,IAAI,OAAO,UAAU,KAAK;CACvD,CAAC;CACD,UAAU,eAAe,0BAA0B,UAAU,IAAI,cAAc,OAAO,KAAK,GAAG,CAAC;AACnG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** An inclusive range of UDP ports. */
|
|
2
|
+
export interface PortRange {
|
|
3
|
+
readonly start: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
}
|
|
6
|
+
/** A NAT or port-forwarding mapping: clients reach `internal` through `external`. */
|
|
7
|
+
export interface PortMapping {
|
|
8
|
+
/** The address clients connect to, or undefined to keep each candidate's own address. */
|
|
9
|
+
readonly address?: string;
|
|
10
|
+
readonly external: PortRange;
|
|
11
|
+
readonly internal: PortRange;
|
|
12
|
+
}
|
|
13
|
+
/** What a `server-udp-ports` setting says. */
|
|
14
|
+
export interface UdpPorts {
|
|
15
|
+
/** The local ports WebRTC may allocate, or undefined to leave it to the operating system. */
|
|
16
|
+
readonly local?: PortRange;
|
|
17
|
+
/** Mappings to advertise in answers, in the order they were written. */
|
|
18
|
+
readonly mappings: readonly PortMapping[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reads Bedrock Dedicated Server's `server-udp-ports` syntax: comma-separated entries, each either
|
|
22
|
+
* `internal` or `start-end` (a local port range for WebRTC), or `[ip:]external[-external]:internal[-internal]`
|
|
23
|
+
* (a mapping to advertise, its two sides the same length). An IPv6 address goes in brackets.
|
|
24
|
+
* @param setting - The setting, or an empty string for none.
|
|
25
|
+
* @throws {Error} For an entry in neither form, ports out of range, or mismatched mapping lengths;
|
|
26
|
+
* and for local ranges werift cannot allocate from: more than one, or a single port.
|
|
27
|
+
*/
|
|
28
|
+
export declare const parseUdpPorts: (setting: string) => UdpPorts;
|
|
29
|
+
/**
|
|
30
|
+
* The answer with a server-reflexive candidate added for every host candidate a mapping covers:
|
|
31
|
+
* the external address (or the candidate's own) and the external port its internal port maps to.
|
|
32
|
+
* That is how a client outside the network learns where to reach a server behind NAT.
|
|
33
|
+
* @param answer - The SDP answer, with its candidates.
|
|
34
|
+
* @param mappings - The mappings to advertise.
|
|
35
|
+
*/
|
|
36
|
+
export declare const withMappedCandidates: (answer: string, mappings: readonly PortMapping[]) => string;
|
|
37
|
+
//# sourceMappingURL=udpPorts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"udpPorts.d.ts","sources":["../src/udpPorts.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACxB;AAED,qFAAqF;AACrF,MAAM,WAAW,WAAW;IACxB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAChC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACrB,6FAA6F;IAC7F,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;CAC7C;AAiBD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,YAAa,MAAM,KAAG,QAqC/C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,WAAY,MAAM,YAAY,SAAS,WAAW,EAAE,KAAG,MAqBvF,CAAC"}
|
package/dist/udpPorts.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/udpPorts.ts
|
|
2
|
+
var PORT = "(\\d{1,5})";
|
|
3
|
+
var RANGE = `${PORT}(?:-${PORT})?`;
|
|
4
|
+
var LOCAL_ENTRY = new RegExp(`^${RANGE}$`);
|
|
5
|
+
var MAPPING_ENTRY = new RegExp(`^(?:(\\[[^\\]]+\\]|[^:\\[\\]]+):)?${RANGE}:${RANGE}$`);
|
|
6
|
+
var rangeOf = (entry, start, end) => {
|
|
7
|
+
const range = {
|
|
8
|
+
start: Number(start),
|
|
9
|
+
end: Number(end ?? start)
|
|
10
|
+
};
|
|
11
|
+
if (range.start < 1 || range.end > 65535 || range.start > range.end) throw new Error(`server-udp-ports: ${entry} is not a range of ports between 1 and 65535`);
|
|
12
|
+
return range;
|
|
13
|
+
};
|
|
14
|
+
var sizeOf = ({ start, end }) => end - start + 1;
|
|
15
|
+
/**
|
|
16
|
+
* Reads Bedrock Dedicated Server's `server-udp-ports` syntax: comma-separated entries, each either
|
|
17
|
+
* `internal` or `start-end` (a local port range for WebRTC), or `[ip:]external[-external]:internal[-internal]`
|
|
18
|
+
* (a mapping to advertise, its two sides the same length). An IPv6 address goes in brackets.
|
|
19
|
+
* @param setting - The setting, or an empty string for none.
|
|
20
|
+
* @throws {Error} For an entry in neither form, ports out of range, or mismatched mapping lengths;
|
|
21
|
+
* and for local ranges werift cannot allocate from: more than one, or a single port.
|
|
22
|
+
*/
|
|
23
|
+
var parseUdpPorts = (setting) => {
|
|
24
|
+
const ranges = [];
|
|
25
|
+
const mappings = [];
|
|
26
|
+
for (const entry of setting.split(",").map((part) => part.trim()).filter((part) => part !== "")) {
|
|
27
|
+
const local = LOCAL_ENTRY.exec(entry);
|
|
28
|
+
if (local !== null) {
|
|
29
|
+
ranges.push(rangeOf(entry, local[1], local[2]));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const mapping = MAPPING_ENTRY.exec(entry);
|
|
33
|
+
if (mapping === null) throw new Error(`server-udp-ports: ${entry} is neither a port range nor a mapping`);
|
|
34
|
+
const external = rangeOf(entry, mapping[2], mapping[3]);
|
|
35
|
+
const internal = rangeOf(entry, mapping[4], mapping[5]);
|
|
36
|
+
if (sizeOf(external) !== sizeOf(internal)) throw new Error(`server-udp-ports: ${entry} maps ${sizeOf(external)} ports onto ${sizeOf(internal)}`);
|
|
37
|
+
const address = mapping[1]?.replace(/^\[|\]$/g, "");
|
|
38
|
+
mappings.push(address === void 0 ? {
|
|
39
|
+
external,
|
|
40
|
+
internal
|
|
41
|
+
} : {
|
|
42
|
+
address,
|
|
43
|
+
external,
|
|
44
|
+
internal
|
|
45
|
+
});
|
|
46
|
+
ranges.push(internal);
|
|
47
|
+
}
|
|
48
|
+
if (ranges.length === 0) return { mappings };
|
|
49
|
+
const start = Math.min(...ranges.map((range) => range.start));
|
|
50
|
+
const end = Math.max(...ranges.map((range) => range.end));
|
|
51
|
+
if (!ranges.every((range) => range.start === start && range.end === end)) throw new Error("server-udp-ports: every local range must be the same one; werift allocates from one range");
|
|
52
|
+
if (start === end) throw new Error("server-udp-ports: a local range needs at least two ports; each player takes a port of its own");
|
|
53
|
+
return {
|
|
54
|
+
local: {
|
|
55
|
+
start,
|
|
56
|
+
end
|
|
57
|
+
},
|
|
58
|
+
mappings
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The answer with a server-reflexive candidate added for every host candidate a mapping covers:
|
|
63
|
+
* the external address (or the candidate's own) and the external port its internal port maps to.
|
|
64
|
+
* That is how a client outside the network learns where to reach a server behind NAT.
|
|
65
|
+
* @param answer - The SDP answer, with its candidates.
|
|
66
|
+
* @param mappings - The mappings to advertise.
|
|
67
|
+
*/
|
|
68
|
+
var withMappedCandidates = (answer, mappings) => {
|
|
69
|
+
if (mappings.length === 0) return answer;
|
|
70
|
+
const lines = answer.split(/\r?\n/);
|
|
71
|
+
const output = [];
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
output.push(line);
|
|
74
|
+
const candidate = /^a=candidate:(\S+) (\d+) udp (\d+) (\S+) (\d+) typ host/i.exec(line);
|
|
75
|
+
if (candidate === null) continue;
|
|
76
|
+
const [, foundation, component, priority, address, portText] = candidate;
|
|
77
|
+
const port = Number(portText);
|
|
78
|
+
for (const mapping of mappings) {
|
|
79
|
+
if (port < mapping.internal.start || port > mapping.internal.end) continue;
|
|
80
|
+
const externalPort = mapping.external.start + (port - mapping.internal.start);
|
|
81
|
+
const reflexivePriority = Math.max(0, Number(priority) - 1);
|
|
82
|
+
output.push(`a=candidate:${foundation}m ${component} udp ${reflexivePriority} ${mapping.address ?? address} ${externalPort} typ srflx raddr ${address} rport ${port}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return output.join("\r\n");
|
|
86
|
+
};
|
|
87
|
+
export { parseUdpPorts, withMappedCandidates };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=udpPorts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"udpPorts.js","names":[],"sources":["../src/udpPorts.ts"],"sourcesContent":["/** An inclusive range of UDP ports. */\nexport interface PortRange {\n readonly start: number;\n readonly end: number;\n}\n\n/** A NAT or port-forwarding mapping: clients reach `internal` through `external`. */\nexport interface PortMapping {\n /** The address clients connect to, or undefined to keep each candidate's own address. */\n readonly address?: string;\n readonly external: PortRange;\n readonly internal: PortRange;\n}\n\n/** What a `server-udp-ports` setting says. */\nexport interface UdpPorts {\n /** The local ports WebRTC may allocate, or undefined to leave it to the operating system. */\n readonly local?: PortRange;\n /** Mappings to advertise in answers, in the order they were written. */\n readonly mappings: readonly PortMapping[];\n}\n\nconst PORT = '(\\\\d{1,5})';\nconst RANGE = `${PORT}(?:-${PORT})?`;\nconst LOCAL_ENTRY = new RegExp(`^${RANGE}$`);\nconst MAPPING_ENTRY = new RegExp(`^(?:(\\\\[[^\\\\]]+\\\\]|[^:\\\\[\\\\]]+):)?${RANGE}:${RANGE}$`);\n\nconst rangeOf = (entry: string, start: string | undefined, end: string | undefined): PortRange => {\n const range = { start: Number(start), end: Number(end ?? start) };\n if (range.start < 1 || range.end > 65_535 || range.start > range.end) {\n throw new Error(`server-udp-ports: ${entry} is not a range of ports between 1 and 65535`);\n }\n return range;\n};\n\nconst sizeOf = ({ start, end }: PortRange): number => end - start + 1;\n\n/**\n * Reads Bedrock Dedicated Server's `server-udp-ports` syntax: comma-separated entries, each either\n * `internal` or `start-end` (a local port range for WebRTC), or `[ip:]external[-external]:internal[-internal]`\n * (a mapping to advertise, its two sides the same length). An IPv6 address goes in brackets.\n * @param setting - The setting, or an empty string for none.\n * @throws {Error} For an entry in neither form, ports out of range, or mismatched mapping lengths;\n * and for local ranges werift cannot allocate from: more than one, or a single port.\n */\nexport const parseUdpPorts = (setting: string): UdpPorts => {\n const ranges: PortRange[] = [];\n const mappings: PortMapping[] = [];\n for (const entry of setting\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part !== '')) {\n const local = LOCAL_ENTRY.exec(entry);\n if (local !== null) {\n ranges.push(rangeOf(entry, local[1], local[2]));\n continue;\n }\n const mapping = MAPPING_ENTRY.exec(entry);\n if (mapping === null) throw new Error(`server-udp-ports: ${entry} is neither a port range nor a mapping`);\n const external = rangeOf(entry, mapping[2], mapping[3]);\n const internal = rangeOf(entry, mapping[4], mapping[5]);\n if (sizeOf(external) !== sizeOf(internal)) {\n throw new Error(`server-udp-ports: ${entry} maps ${sizeOf(external)} ports onto ${sizeOf(internal)}`);\n }\n const address = mapping[1]?.replace(/^\\[|\\]$/g, '');\n mappings.push(address === undefined ? { external, internal } : { address, external, internal });\n ranges.push(internal);\n }\n\n if (ranges.length === 0) return { mappings };\n const start = Math.min(...ranges.map((range) => range.start));\n const end = Math.max(...ranges.map((range) => range.end));\n const covered = ranges.every((range) => range.start === start && range.end === end);\n if (!covered) {\n throw new Error('server-udp-ports: every local range must be the same one; werift allocates from one range');\n }\n if (start === end) {\n throw new Error(\n 'server-udp-ports: a local range needs at least two ports; each player takes a port of its own'\n );\n }\n return { local: { start, end }, mappings };\n};\n\n/**\n * The answer with a server-reflexive candidate added for every host candidate a mapping covers:\n * the external address (or the candidate's own) and the external port its internal port maps to.\n * That is how a client outside the network learns where to reach a server behind NAT.\n * @param answer - The SDP answer, with its candidates.\n * @param mappings - The mappings to advertise.\n */\nexport const withMappedCandidates = (answer: string, mappings: readonly PortMapping[]): string => {\n if (mappings.length === 0) return answer;\n const lines = answer.split(/\\r?\\n/);\n const output: string[] = [];\n for (const line of lines) {\n output.push(line);\n const candidate = /^a=candidate:(\\S+) (\\d+) udp (\\d+) (\\S+) (\\d+) typ host/i.exec(line);\n if (candidate === null) continue;\n const [, foundation, component, priority, address, portText] = candidate;\n const port = Number(portText);\n for (const mapping of mappings) {\n if (port < mapping.internal.start || port > mapping.internal.end) continue;\n const externalPort = mapping.external.start + (port - mapping.internal.start);\n // Below the host candidate's priority, so a client on the same network still prefers it.\n const reflexivePriority = Math.max(0, Number(priority) - 1);\n output.push(\n `a=candidate:${foundation}m ${component} udp ${reflexivePriority} ${mapping.address ?? address} ${externalPort} typ srflx raddr ${address} rport ${port}`\n );\n }\n }\n return output.join('\\r\\n');\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsprismarine/nethernet",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "NetherNet, the WebRTC transport of Minecraft: Bedrock Edition: signaling, identities and LAN discovery",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./*": {
|
|
24
|
+
"types": "./dist/*.d.ts",
|
|
25
|
+
"import": "./dist/*.js",
|
|
26
|
+
"module": "./dist/*.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
8
30
|
"repository": {
|
|
9
31
|
"type": "git",
|
|
10
32
|
"url": "git+https://github.com/JSPrismarine/JSPrismarine.git"
|
|
11
33
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
34
|
+
"keywords": [
|
|
35
|
+
"nodejs",
|
|
36
|
+
"minecraft",
|
|
37
|
+
"bedrock",
|
|
38
|
+
"nethernet",
|
|
39
|
+
"webrtc"
|
|
40
|
+
],
|
|
41
|
+
"author": "JSPrismarine",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://github.com/JSPrismarine/JSPrismarine/tree/master/packages/nethernet",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@jsprismarine/binaryutils": "0.20.0",
|
|
46
|
+
"@jsprismarine/logger": "0.20.0",
|
|
47
|
+
"jose": "6.2.17",
|
|
48
|
+
"nethernet": "1.1.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "26.6.6",
|
|
52
|
+
"glob": "14.0.4",
|
|
53
|
+
"rimraf": "6.1.4",
|
|
54
|
+
"typescript": "7.0.2",
|
|
55
|
+
"vite": "8.3.7",
|
|
56
|
+
"vitest": "5.1.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"clean": "rimraf dist jsp *.tsbuildinfo vite.config.ts.timestamp-* .tsup .turbo",
|
|
60
|
+
"build": "vite build && node ../../scripts/emit-dts.mts",
|
|
61
|
+
"build:watch": "vite build --watch",
|
|
62
|
+
"typecheck": "tsc-ts --noEmit && tsc-ts --noEmit -p tsconfig.test.json"
|
|
15
63
|
}
|
|
16
|
-
}
|
|
64
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
throw new Error('@jsprismarine/nethernet@0.0.0 is a placeholder package. Install a real release instead.');
|