import { jspack } from "jspack"; import * as nodeIp from "ip"; import { range } from "lodash"; import { IPV6_PROTOCOL, IPV4_PROTOCOL, COOKIE } from "./const"; import { Int64BE } from "int64-buffer"; function packAddress(value: [string, number]) { const [address] = value; let protocol = IPV6_PROTOCOL; if (nodeIp.isV4Format(address)) { protocol = IPV4_PROTOCOL; } return Buffer.concat([ Buffer.from(jspack.Pack("!BBH", [0, protocol, value[1]])), nodeIp.toBuffer(address), ]); } export function unpackErrorCode(data: Buffer): [number, string] { if (data.length < 4) throw new Error("STUN error code is less than 4 bytes"); const [, codeHigh, codeLow] = jspack.Unpack("!HBB", data.slice(0, 4)); const reason = data.slice(4).toString("utf8"); return [codeHigh * 100 + codeLow, reason]; } function unpackAddress(data: Buffer): [string, number] { if (data.length < 4) throw new Error("STUN address length is less than 4 bytes"); const [, protocol, port] = jspack.Unpack("!BBH", data.slice(0, 4)); const address = data.slice(4); switch (protocol) { case IPV4_PROTOCOL: if (address.length != 4) throw new Error(`STUN address has invalid length for IPv4`); return [nodeIp.toString(address), port]; case IPV6_PROTOCOL: if (address.length != 16) throw new Error("STUN address has invalid length for IPv6"); return [nodeIp.toString(address), port]; default: throw new Error("STUN address has unknown protocol"); } } function xorAddress(data: Buffer, transactionId: Buffer) { const xPad = [ ...jspack.Pack("!HI", [COOKIE >> 16, COOKIE]), ...transactionId, ]; let xData = data.slice(0, 2); for (let i of range(2, data.length)) { const num = data[i] ^ xPad[i - 2]; const buf = Buffer.alloc(1); buf.writeUIntBE(num, 0, 1); xData = Buffer.concat([xData, buf]); } return xData; } export function unpackXorAddress(data: Buffer, transactionId: Buffer) { return unpackAddress(xorAddress(data, transactionId)); } export function packErrorCode(value: [number, string]) { const pack = Buffer.from( jspack.Pack("!HBB", [0, Math.floor(value[0] / 100), value[0] % 100]) ); const encode = Buffer.from(value[1], "utf8"); return Buffer.concat([pack, encode]); } export function packXorAddress(value: [string, number], transactionId: Buffer) { return xorAddress(packAddress(value), transactionId); } const packUnsigned = (value: number) => Buffer.from(jspack.Pack("!I", [value])); const unpackUnsigned = (data: Buffer) => jspack.Unpack("!I", data)[0]; const packUnsignedShort = (value: number) => Buffer.concat([ Buffer.from(jspack.Pack("!H", [value])), Buffer.from("\x00\x00"), ]); const unpackUnsignedShort = (data: Buffer) => jspack.Unpack("!H", data.slice(0, 2))[0]; const packUnsigned64 = (value: BigInt) => { return new Int64BE(value.toString()).toBuffer(); }; const unpackUnsigned64 = (data: Buffer) => { const int = new Int64BE(data); return BigInt(int.toString()); }; const packString = (value: string) => Buffer.from(value, "utf8"); const unpackString = (data: Buffer) => data.toString("utf8"); const packBytes = (value: Buffer) => value; const unpackBytes = (data: Buffer) => data; const packNone = (value: Buffer) => Buffer.from([]); const unpackNone = (data: Buffer) => null; export type ATTRIBUTE = [ number, string, (...args: any) => Buffer, (...args: any) => any ]; const ATTRIBUTES: ATTRIBUTE[] = [ [0x0001, "MAPPED-ADDRESS", packAddress, unpackAddress], [0x0003, "CHANGE-REQUEST", packUnsigned, unpackUnsigned], [0x0004, "SOURCE-ADDRESS", packAddress, unpackAddress], [0x0005, "CHANGED-ADDRESS", packAddress, unpackAddress], [0x0006, "USERNAME", packString, unpackString], [0x0008, "MESSAGE-INTEGRITY", packBytes, unpackBytes], [0x0009, "ERROR-CODE", packErrorCode, unpackErrorCode], [0x000c, "CHANNEL-NUMBER", packUnsignedShort, unpackUnsignedShort], [0x000d, "LIFETIME", packUnsigned, unpackUnsigned], [0x0012, "XOR-PEER-ADDRESS", packXorAddress, unpackXorAddress], [0x0014, "REALM", packString, unpackString], [0x0015, "NONCE", packBytes, unpackBytes], [0x0016, "XOR-RELAYED-ADDRESS", packXorAddress, unpackXorAddress], [0x0019, "REQUESTED-TRANSPORT", packUnsigned, unpackUnsigned], [0x0020, "XOR-MAPPED-ADDRESS", packXorAddress, unpackXorAddress], [0x0024, "PRIORITY", packUnsigned, unpackUnsigned], [0x0025, "USE-CANDIDATE", packNone, unpackNone], [0x8022, "SOFTWARE", packString, unpackString], [0x8028, "FINGERPRINT", packUnsigned, unpackUnsigned], [0x8029, "ICE-CONTROLLED", packUnsigned64, unpackUnsigned64], [0x802a, "ICE-CONTROLLING", packUnsigned64, unpackUnsigned64], [0x802b, "RESPONSE-ORIGIN", packAddress, unpackAddress], [0x802c, "OTHER-ADDRESS", packAddress, unpackAddress], ]; export const ATTRIBUTES_BY_TYPE = ATTRIBUTES.reduce((acc, cur) => { acc[cur[0]] = cur; return acc; }, {} as { [key: string]: ATTRIBUTE }); export const ATTRIBUTES_BY_NAME = ATTRIBUTES.reduce((acc, cur) => { acc[cur[1]] = cur; return acc; }, {} as { [key: string]: ATTRIBUTE });