|
| 1 | +const { TextEncoder, TextDecoder } = require('bare-encoding') |
| 2 | + |
| 3 | +const CAPABILITY_LEN = 32 |
| 4 | +const HEADER_LEN = CAPABILITY_LEN + 1 |
| 5 | + |
| 6 | +const MODE_TUNNEL = 0 |
| 7 | +const MODE_PROBE = 1 |
| 8 | + |
| 9 | +const PROBE_FIXED_LEN = 4 |
| 10 | +const MAX_HOST_LEN = 255 |
| 11 | + |
| 12 | +const utf8Encoder = new TextEncoder() |
| 13 | +const utf8Decoder = new TextDecoder('utf-8') |
| 14 | + |
| 15 | +function encodeHeader(capability, mode) { |
| 16 | + if (capability.length !== CAPABILITY_LEN) { |
| 17 | + throw new Error(`capability must be ${CAPABILITY_LEN} bytes`) |
| 18 | + } |
| 19 | + if (!Number.isInteger(mode) || mode < 0 || mode > 255) { |
| 20 | + throw new Error('mode must be a byte (0-255)') |
| 21 | + } |
| 22 | + const out = new Uint8Array(HEADER_LEN) |
| 23 | + out.set(capability, 0) |
| 24 | + out[CAPABILITY_LEN] = mode |
| 25 | + return out |
| 26 | +} |
| 27 | + |
| 28 | +function decodeHeader(bytes) { |
| 29 | + if (bytes.length < HEADER_LEN) return null |
| 30 | + return { |
| 31 | + capability: bytes.subarray(0, CAPABILITY_LEN), |
| 32 | + mode: bytes[CAPABILITY_LEN], |
| 33 | + leftover: bytes.subarray(HEADER_LEN) |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +function encodeProbeResponse({ port = 0, host = '', udp = false } = {}) { |
| 38 | + const hostBytes = utf8Encoder.encode(String(host)) |
| 39 | + if (hostBytes.length > MAX_HOST_LEN) { |
| 40 | + throw new Error(`host exceeds ${MAX_HOST_LEN} bytes`) |
| 41 | + } |
| 42 | + const portInt = +port | 0 |
| 43 | + if (portInt < 0 || portInt > 0xffff) { |
| 44 | + throw new Error('port must fit in uint16') |
| 45 | + } |
| 46 | + const out = new Uint8Array(PROBE_FIXED_LEN + hostBytes.length) |
| 47 | + out[0] = (portInt >>> 8) & 0xff |
| 48 | + out[1] = portInt & 0xff |
| 49 | + out[2] = udp ? 1 : 0 |
| 50 | + out[3] = hostBytes.length |
| 51 | + out.set(hostBytes, PROBE_FIXED_LEN) |
| 52 | + return out |
| 53 | +} |
| 54 | + |
| 55 | +function decodeProbeResponse(bytes) { |
| 56 | + if (bytes.length < PROBE_FIXED_LEN) return null |
| 57 | + const hostLen = bytes[3] |
| 58 | + const total = PROBE_FIXED_LEN + hostLen |
| 59 | + if (bytes.length < total) return null |
| 60 | + return { |
| 61 | + port: (bytes[0] << 8) | bytes[1], |
| 62 | + udp: bytes[2] === 1, |
| 63 | + host: utf8Decoder.decode(bytes.subarray(PROBE_FIXED_LEN, total)), |
| 64 | + leftover: bytes.subarray(total) |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +module.exports = { |
| 69 | + CAPABILITY_LEN, |
| 70 | + HEADER_LEN, |
| 71 | + MODE_TUNNEL, |
| 72 | + MODE_PROBE, |
| 73 | + encodeHeader, |
| 74 | + decodeHeader, |
| 75 | + encodeProbeResponse, |
| 76 | + decodeProbeResponse |
| 77 | +} |
0 commit comments