From 23d64921de3a3cf9acf839a73734f4ed56d69ff6 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Tue, 26 May 2026 11:30:21 -0700 Subject: [PATCH 1/2] install prettier, remove @stylistic removes 5 transitive dependencies --- eslint.config.mjs | 44 ++++++------------ package-lock.json | 116 ++++++++-------------------------------------- package.json | 9 +++- 3 files changed, 40 insertions(+), 129 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index e5c75ed..b632c2b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,44 +1,26 @@ -import js from '@eslint/js'; -import stylistic from '@stylistic/eslint-plugin'; -import globals from 'globals'; +import js from "@eslint/js"; +import globals from "globals"; export default [ js.configs.recommended, { - files : [ '**/*.js' ], + files: ["**/*.js"], languageOptions: { - ecmaVersion: 'latest', - sourceType : 'commonjs', - globals : { + ecmaVersion: "latest", + sourceType: "commonjs", + globals: { ...globals.node, }, }, - plugins: { - '@stylistic': stylistic, - }, rules: { - 'no-unused-vars': [ 'error', { - args : 'none', - caughtErrors : 'none', - ignoreRestSiblings: true, - } ], - '@stylistic/semi' : [ 'error', 'always' ], - '@stylistic/space-before-function-paren': [ 'error', { - anonymous : 'never', - named : 'never', - asyncArrow: 'never', - catch : 'always', - } ], - '@stylistic/no-multi-spaces' : 'error', - '@stylistic/array-bracket-spacing': [ 'error', 'always' ], - '@stylistic/key-spacing' : [ 'error', { - align: { - beforeColon: true, - afterColon : true, - on : 'colon', + "no-unused-vars": [ + "error", + { + args: "none", + caughtErrors: "none", + ignoreRestSiblings: true, }, - } ], - '@stylistic/comma-dangle': [ 'error', 'always-multiline' ], + ], }, }, ]; diff --git a/package-lock.json b/package-lock.json index c2526b1..3ad2a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "dns2", - "version": "2.1.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dns2", - "version": "2.1.0", + "version": "2.3.0", "license": "MIT", "devDependencies": { "@eslint/js": "^10.0.1", - "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.4.0", - "globals": "^17.6.0" + "globals": "^17.6.0", + "prettier": "^3.8.3" } }, "node_modules/@eslint-community/eslint-utils": { @@ -235,40 +235,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@stylistic/eslint-plugin": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", - "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/types": "^8.56.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "estraverse": "^5.3.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^9.0.0 || ^10.0.0" - } - }, - "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -290,20 +256,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", - "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -554,37 +506,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -920,19 +841,6 @@ "node": ">=8" } }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -943,6 +851,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index ff89ac5..c1a87a4 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,13 @@ "homepage": "https://github.com/lsongdev/node-dns#readme", "devDependencies": { "@eslint/js": "^10.0.1", - "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.4.0", - "globals": "^17.6.0" + "globals": "^17.6.0", + "prettier": "^3.8.3" + }, + "prettier": { + "printWidth": 80, + "semi": false, + "singleQuote": true } } From b3cda3935bd65f93aba180b25052ee88a26710d5 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Tue, 26 May 2026 11:48:15 -0700 Subject: [PATCH 2/2] first pass, nearly matching config --- .github/workflows/release.yml | 4 +- .prettierignore | 3 + README.md | 118 ++-- benchmark/README.md | 7 +- benchmark/udp.js | 18 +- client/doh.js | 97 +-- client/google.js | 13 +- client/tcp.js | 30 +- client/udp.js | 34 +- eslint.config.mjs | 18 +- example/client/google.js | 2 +- example/client/tcp-custom-dns.js | 2 +- example/client/tcp.js | 2 +- example/client/udp-custom-dns.js | 2 +- example/client/udp-default.js | 2 +- example/client/udp-subnet.js | 4 +- example/client/udp.js | 2 +- example/server/dns.js | 31 +- example/server/doh.js | 18 +- example/server/tcp.js | 8 +- example/server/udp.js | 10 +- index.js | 58 +- lib/proxy-protocol.js | 63 +- lib/reader.js | 10 +- lib/writer.js | 16 +- package.json | 11 +- packet.js | 490 +++++++------- server/dns.js | 25 +- server/doh.js | 80 ++- server/tcp.js | 60 +- server/udp.js | 14 +- test/client.js | 211 +++--- test/packet.js | 1043 ++++++++++++++++++------------ test/proxy-protocol.js | 63 +- test/server.js | 760 +++++++++++++--------- test/test.js | 2 +- 36 files changed, 1994 insertions(+), 1337 deletions(-) create mode 100644 .prettierignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6123f18..92f4737 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/setup-node@v6 with: - node-version: "24" + node-version: '24' - uses: actions/checkout@v6 - run: npm ci - run: npm test @@ -88,7 +88,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: - node-version: "24" + node-version: '24' registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish --provenance diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6e0b22a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +coverage +package-lock.json +ts diff --git a/README.md b/README.md index bec33b2..3e3166b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dns2 +# dns2 ![NPM version][npm-img] [![Build Status][ci-img]][ci-url] @@ -6,10 +6,10 @@ ### Features -+ Server and Client -+ Lot of Type Supported -+ Extremely lightweight -+ DNS over UDP, TCP, HTTPS Supported +- Server and Client +- Lot of Type Supported +- Extremely lightweight +- DNS over UDP, TCP, HTTPS Supported ### Installation @@ -19,7 +19,7 @@ $ npm install dns2 ### DNS Client (default UDP) -Lookup any records available for the domain `lsong.org`. +Lookup any records available for the domain `lsong.org`. DNS client will use UDP by default. ```js @@ -40,16 +40,16 @@ const dns = new dns2(options); The high-level `DNS` class exposes convenience methods for common record types: -| Method | Record type | Key answer fields | -|---|---|---| -| `resolveA(domain)` | A | `address` | -| `resolveAAAA(domain)` | AAAA | `address` | -| `resolveMX(domain)` | MX | `exchange`, `priority` | -| `resolveCNAME(domain)` | CNAME | `domain` | -| `resolveSOA(domain)` | SOA | `primary`, `admin`, `serial`, `refresh`, `retry`, `expiration`, `minimum` | -| `resolvePTR(domain)` | PTR | `domain` | -| `resolveDNSKEY(domain)` | DNSKEY | `publicKey`, `algorithm` | -| `resolveRRSIG(domain)` | RRSIG | varies | +| Method | Record type | Key answer fields | +| ----------------------- | ----------- | ------------------------------------------------------------------------- | +| `resolveA(domain)` | A | `address` | +| `resolveAAAA(domain)` | AAAA | `address` | +| `resolveMX(domain)` | MX | `exchange`, `priority` | +| `resolveCNAME(domain)` | CNAME | `domain` | +| `resolveSOA(domain)` | SOA | `primary`, `admin`, `serial`, `refresh`, `retry`, `expiration`, `minimum` | +| `resolvePTR(domain)` | PTR | `domain` | +| `resolveDNSKEY(domain)` | DNSKEY | `publicKey`, `algorithm` | +| `resolveRRSIG(domain)` | RRSIG | varies | For any record type not listed above, use `dns.resolve(domain, 'TYPE')` directly. @@ -68,13 +68,13 @@ const dns = new dns2({ nameServers: ['8.8.8.8'] }); const result = await dns.resolveSOA('google.com'); const soa = result.answers[0] || result.authorities[0]; if (soa) { - console.log(soa.primary); // ns1.google.com - console.log(soa.admin); // dns-admin.google.com - console.log(soa.serial); // zone serial number - console.log(soa.refresh); // refresh interval (seconds) - console.log(soa.retry); // retry interval (seconds) - console.log(soa.expiration); // expiry (seconds) - console.log(soa.minimum); // minimum TTL (seconds) + console.log(soa.primary); // ns1.google.com + console.log(soa.admin); // dns-admin.google.com + console.log(soa.serial); // zone serial number + console.log(soa.refresh); // refresh interval (seconds) + console.log(soa.retry); // retry interval (seconds) + console.log(soa.expiration); // expiry (seconds) + console.log(soa.minimum); // minimum TTL (seconds) } })(); ``` @@ -87,7 +87,7 @@ const { UDPClient } = require('dns2'); const resolve = UDPClient(); (async () => { - const response = await resolve('google.com') + const response = await resolve('google.com'); console.log(response.answers); })(); ``` @@ -103,10 +103,10 @@ const resolve = TCPClient(); (async () => { try { - const response = await resolve('lsong.org') + const response = await resolve('lsong.org'); console.log(response.answers); - } catch(error) { - // some DNS servers (i.e cloudflare 1.1.1.1, 1.0.0.1) + } catch (error) { + // some DNS servers (i.e cloudflare 1.1.1.1, 1.0.0.1) // may send an empty response when using TCP console.log(error); } @@ -121,14 +121,14 @@ You can pass your own DNS Server. const { TCPClient } = require('dns2'); const resolve = TCPClient({ - dns: '1.1.1.1' + dns: '1.1.1.1', }); (async () => { try { const result = await resolve('google.com'); console.log(result.answers); - } catch(error) { + } catch (error) { console.log(error); } })(); @@ -143,14 +143,14 @@ const dns = require('dns'); const { TCPClient } = require('dns2'); const resolve = TCPClient({ - dns: dns.getServers()[0] + dns: dns.getServers()[0], }); (async () => { try { const result = await resolve('google.com'); console.log(result.answers); - } catch(error) { + } catch (error) { console.log(error); } })(); @@ -167,24 +167,24 @@ const server = dns2.createServer({ udp: true, handle: (request, send, rinfo) => { const response = Packet.createResponseFromRequest(request); - const [ question ] = request.questions; + const [question] = request.questions; const { name } = question; response.answers.push({ name, type: Packet.TYPE.A, class: Packet.CLASS.IN, ttl: 300, - address: '8.8.8.8' + address: '8.8.8.8', }); send(response); - } + }, }); server.on('request', (request, response, rinfo) => { console.log(request.header.id, request.questions[0]); }); -server.on('requestError', (error) => { +server.on('requestError', error => { console.log('Client sent an invalid request', error); }); @@ -198,15 +198,15 @@ server.on('close', () => { server.listen({ // Optionally specify port, address and/or the family of socket() for udp server: - udp: { + udp: { port: 5333, - address: "127.0.0.1", + address: '127.0.0.1', }, - + // Optionally specify port and/or address for tcp server: - tcp: { + tcp: { port: 5333, - address: "127.0.0.1", + address: '127.0.0.1', }, }); @@ -227,14 +227,14 @@ will be found in `request.questions[0].name`. Use `Packet.RCODE` to send standard DNS error responses from your handler: -| Constant | Value | Meaning | -|---|---|---| -| `Packet.RCODE.NOERROR` | 0 | No error | -| `Packet.RCODE.FORMERR` | 1 | Format error | -| `Packet.RCODE.SERVFAIL` | 2 | Server failure | -| `Packet.RCODE.NXDOMAIN` | 3 | Non-existent domain | -| `Packet.RCODE.NOTIMP` | 4 | Not implemented | -| `Packet.RCODE.REFUSED` | 5 | Query refused | +| Constant | Value | Meaning | +| ----------------------- | ----- | ------------------- | +| `Packet.RCODE.NOERROR` | 0 | No error | +| `Packet.RCODE.FORMERR` | 1 | Format error | +| `Packet.RCODE.SERVFAIL` | 2 | Server failure | +| `Packet.RCODE.NXDOMAIN` | 3 | Non-existent domain | +| `Packet.RCODE.NOTIMP` | 4 | Not implemented | +| `Packet.RCODE.REFUSED` | 5 | Query refused | ```js const dns2 = require('dns2'); @@ -244,7 +244,7 @@ const server = dns2.createServer({ udp: true, handle: (request, send) => { const response = Packet.createResponseFromRequest(request); - const [ question ] = request.questions; + const [question] = request.questions; if (question.name.endsWith('.internal')) { // Refuse queries for internal names @@ -259,9 +259,15 @@ const server = dns2.createServer({ } // Normal answer ... - response.answers.push({ name: question.name, type: Packet.TYPE.A, class: Packet.CLASS.IN, ttl: 300, address: '1.2.3.4' }); + response.answers.push({ + name: question.name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '1.2.3.4', + }); send(response); - } + }, }); ``` @@ -271,11 +277,11 @@ see [benchmark/README.md](benchmark/README.md) ### Relevant Specifications -+ [RFC-1034 - Domain Names - Concepts and Facilities](https://tools.ietf.org/html/rfc1034) -+ [RFC-1035 - Domain Names - Implementation and Specification](https://tools.ietf.org/html/rfc1035) -+ [RFC-2782 - A DNS RR for specifying the location of services (DNS SRV)](https://tools.ietf.org/html/rfc2782) -+ [RFC-7766 - DNS Transport over TCP - Implementation Requirements](https://tools.ietf.org/html/rfc7766) -+ [RFC-8484 - DNS Queries over HTTPS (DoH)](https://tools.ietf.org/html/rfc8484) +- [RFC-1034 - Domain Names - Concepts and Facilities](https://tools.ietf.org/html/rfc1034) +- [RFC-1035 - Domain Names - Implementation and Specification](https://tools.ietf.org/html/rfc1035) +- [RFC-2782 - A DNS RR for specifying the location of services (DNS SRV)](https://tools.ietf.org/html/rfc2782) +- [RFC-7766 - DNS Transport over TCP - Implementation Requirements](https://tools.ietf.org/html/rfc7766) +- [RFC-8484 - DNS Queries over HTTPS (DoH)](https://tools.ietf.org/html/rfc8484) ### Contributing diff --git a/benchmark/README.md b/benchmark/README.md index 79e05d6..c2c22a8 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,4 +1,4 @@ -# dns2 +# dns2 ### Performance and Benchmarking @@ -47,8 +47,8 @@ unboundedly. ```js const server = dns2.createServer({ - udp : true, - maxConcurrent: 500, // at most 500 handler calls in flight at once + udp: true, + maxConcurrent: 500, // at most 500 handler calls in flight at once handle(request, send) { // async work here... }, @@ -67,4 +67,3 @@ const server = dns2.createServer({ `node --max-semi-space-size=64 server.js` - For production, consider a process manager (PM2, systemd) that auto-restarts on failure and enables multi-instance clustering. - diff --git a/benchmark/udp.js b/benchmark/udp.js index 20b8451..6ae0964 100644 --- a/benchmark/udp.js +++ b/benchmark/udp.js @@ -20,7 +20,9 @@ const CONCURRENCY = parseInt(process.env.CONCURRENCY || '100', 10); const DNS_TARGET = process.env.DNS || null; function percentile(sorted, p) { - return sorted[Math.max(0, Math.floor(sorted.length * p / 100) - 1 + (p === 100 ? 1 : 0))]; + return sorted[ + Math.max(0, Math.floor((sorted.length * p) / 100) - 1 + (p === 100 ? 1 : 0)) + ]; } async function run() { @@ -38,13 +40,13 @@ async function run() { udp: true, handle(request, send) { const response = Packet.createResponseFromRequest(request); - const [ q ] = request.questions; + const [q] = request.questions; response.answers.push({ - name : q.name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '127.0.0.1', + name: q.name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '127.0.0.1', }); send(response); }, @@ -64,7 +66,7 @@ async function run() { const wallStart = Date.now(); - await new Promise((resolve) => { + await new Promise(resolve => { let sent = 0; let inFlight = 0; diff --git a/client/doh.js b/client/doh.js index 41346c8..d338876 100644 --- a/client/doh.js +++ b/client/doh.js @@ -4,14 +4,14 @@ const http2 = require('node:http2'); const Packet = require('../packet'); const protocols = { - 'http:' : http.get, - 'https:' : https.get, - 'h2:' : (url, options, done) => { + 'http:': http.get, + 'https:': https.get, + 'h2:': (url, options, done) => { const urlObj = new URL(url); const client = http2.connect(url.replace('h2:', 'https:')); const req = client.request({ - ':path' : `${urlObj.pathname}${urlObj.search}`, - ':method' : 'GET', + ':path': `${urlObj.pathname}${urlObj.search}`, + ':method': 'GET', ...options.headers, }); @@ -19,8 +19,8 @@ const protocols = { client.close(); done({ headers, - statusCode : headers[':status'], - on : req.on.bind(req), + statusCode: headers[':status'], + on: req.on.bind(req), }); }); @@ -33,45 +33,60 @@ const protocols = { }, }; -const makeRequest = (url, query) => new Promise((resolve, reject) => { - const index = url.indexOf('://'); - if (index === -1) url = `https://${url}`; - const u = new URL(url); - // The DNS query is included in a single variable named “dns” in the - // query component of the request URI. The value of the “dns” variable - // is the content of the DNS request message, encoded with base64url - // [RFC4648](https://datatracker.ietf.org/doc/html/rfc8484#section-4.1). - const searchParams = u.searchParams; - searchParams.set('dns', query); - u.search = searchParams.toString(); - const get = protocols[u.protocol]; - if (!get) throw new Error(`Unsupported protocol: ${u.protocol}, must be specified (http://, https:// or h2://)`); - const req = get(u.toString(), { headers: { accept: 'application/dns-message' } }, resolve); - if (req) req.on('error', reject); -}); +const makeRequest = (url, query) => + new Promise((resolve, reject) => { + const index = url.indexOf('://'); + if (index === -1) url = `https://${url}`; + const u = new URL(url); + // The DNS query is included in a single variable named “dns” in the + // query component of the request URI. The value of the “dns” variable + // is the content of the DNS request message, encoded with base64url + // [RFC4648](https://datatracker.ietf.org/doc/html/rfc8484#section-4.1). + const searchParams = u.searchParams; + searchParams.set('dns', query); + u.search = searchParams.toString(); + const get = protocols[u.protocol]; + if (!get) + throw new Error( + `Unsupported protocol: ${u.protocol}, must be specified (http://, https:// or h2://)`, + ); + const req = get( + u.toString(), + { headers: { accept: 'application/dns-message' } }, + resolve, + ); + if (req) req.on('error', reject); + }); -const readStream = res => new Promise((resolve, reject) => { - const chunks = []; - res - .on('error', reject) - .on('data', chunk => chunks.push(chunk)) - .on('end', () => { - const data = Buffer.concat(chunks); - if (res.statusCode !== 200) { - reject(new Error(`HTTP ${res.statusCode}: ${data.toString()}`)); - } - resolve(data); - }); -}); +const readStream = res => + new Promise((resolve, reject) => { + const chunks = []; + res + .on('error', reject) + .on('data', chunk => chunks.push(chunk)) + .on('end', () => { + const data = Buffer.concat(chunks); + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode}: ${data.toString()}`)); + } + resolve(data); + }); + }); -const buildQuery = ({ name, type = 'A', cls = Packet.CLASS.IN, clientIp, recursive = true }) => { +const buildQuery = ({ + name, + type = 'A', + cls = Packet.CLASS.IN, + clientIp, + recursive = true, +}) => { const packet = new Packet(); packet.header.rd = recursive ? 1 : 0; if (clientIp) { - packet.additionals.push(Packet.Resource.EDNS([ - Packet.Resource.EDNS.ECS(clientIp), - ])); + packet.additionals.push( + Packet.Resource.EDNS([Packet.Resource.EDNS.ECS(clientIp)]), + ); } packet.questions.push({ name, class: cls, type: Packet.TYPE[type] }); @@ -79,7 +94,7 @@ const buildQuery = ({ name, type = 'A', cls = Packet.CLASS.IN, clientIp, recursi }; const DOHClient = ({ dns }) => { - return async(name, type, cls, options = {}) => { + return async (name, type, cls, options = {}) => { const query = buildQuery({ name, type, cls, ...options }); const response = await makeRequest(dns, query); const data = await readStream(response); diff --git a/client/google.js b/client/google.js index cac0e72..4a351e0 100644 --- a/client/google.js +++ b/client/google.js @@ -1,7 +1,6 @@ const https = require('node:https'); -const get = url => new Promise(resolve => - https.get(url, resolve)); +const get = url => new Promise(resolve => https.get(url, resolve)); const readStream = stream => { const buffer = []; @@ -15,11 +14,13 @@ const readStream = stream => { }); }; -const GoogleClient = () => +const GoogleClient = + () => (name, type = 'ANY') => { - return Promise - .resolve() - .then(() => get(`https://dns.google.com/resolve?name=${name}&type=${type}`)) + return Promise.resolve() + .then(() => + get(`https://dns.google.com/resolve?name=${name}&type=${type}`), + ) .then(readStream) .then(JSON.parse); }; diff --git a/client/tcp.js b/client/tcp.js index 275406b..132d57f 100644 --- a/client/tcp.js +++ b/client/tcp.js @@ -2,14 +2,20 @@ const tls = require('node:tls'); const tcp = require('node:net'); const Packet = require('../packet'); -const makeQuery = ({ name, type = 'A', cls = Packet.CLASS.IN, clientIp, recursive = true }) => { +const makeQuery = ({ + name, + type = 'A', + cls = Packet.CLASS.IN, + clientIp, + recursive = true, +}) => { const packet = new Packet(); packet.header.rd = recursive ? 1 : 0; if (clientIp) { - packet.additionals.push(Packet.Resource.EDNS([ - Packet.Resource.EDNS.ECS(clientIp), - ])); + packet.additionals.push( + Packet.Resource.EDNS([Packet.Resource.EDNS.ECS(clientIp)]), + ); } packet.questions.push({ name, class: cls, type: Packet.TYPE[type] }); @@ -19,22 +25,26 @@ const makeQuery = ({ name, type = 'A', cls = Packet.CLASS.IN, clientIp, recursiv const sendQuery = (client, message) => { const len = Buffer.alloc(2); len.writeUInt16BE(message.length); - client.write(Buffer.concat([ len, message ])); + client.write(Buffer.concat([len, message])); }; const protocols = { - 'tcp:' : (host, port) => tcp.connect({ host, port }), - 'tls:' : (host, port) => tls.connect({ host, port, servername: host }), + 'tcp:': (host, port) => tcp.connect({ host, port }), + 'tls:': (host, port) => tls.connect({ host, port, servername: host }), }; -const TCPClient = ({ dns, protocol = 'tcp:', port = protocol === 'tls:' ? 853 : 53 } = {}) => { +const TCPClient = ({ + dns, + protocol = 'tcp:', + port = protocol === 'tls:' ? 853 : 53, +} = {}) => { if (!protocols[protocol]) { throw new Error('Protocol must be tcp: or tls:'); } - return async(name, type, cls, options = {}) => { + return async (name, type, cls, options = {}) => { const message = makeQuery({ name, type, cls, ...options }); - const [ host ] = dns.split(':'); + const [host] = dns.split(':'); const client = protocols[protocol](host, port); sendQuery(client, message); diff --git a/client/udp.js b/client/udp.js index 844c432..323f4d7 100644 --- a/client/udp.js +++ b/client/udp.js @@ -22,14 +22,14 @@ module.exports = ({ query.header.rd = 1; } if (clientIp) { - query.additionals.push(Packet.Resource.EDNS([ - Packet.Resource.EDNS.ECS(clientIp), - ])); + query.additionals.push( + Packet.Resource.EDNS([Packet.Resource.EDNS.ECS(clientIp)]), + ); } query.questions.push({ name, - class : cls, - type : Packet.TYPE[type], + class: cls, + type: Packet.TYPE[type], }); const client = new udp.Socket(socketType); // Only enforce a strict source-address check when `dns` is an IP literal; @@ -44,12 +44,23 @@ module.exports = ({ if (timer) clearTimeout(timer); client.removeListener('message', onMessage); client.removeListener('error', onError); - try { client.close(); } catch (_) { /* already closed */ } + try { + client.close(); + } catch (_) { + /* already closed */ + } }; function onMessage(message, rinfo) { // Drop packets that didn't come from the configured resolver. - if (rinfo.port !== port || (expectedAddress && rinfo.address !== expectedAddress)) { - debug('udp: dropping packet from unexpected sender %s:%d', rinfo.address, rinfo.port); + if ( + rinfo.port !== port || + (expectedAddress && rinfo.address !== expectedAddress) + ) { + debug( + 'udp: dropping packet from unexpected sender %s:%d', + rinfo.address, + rinfo.port, + ); return; } let response; @@ -61,8 +72,11 @@ module.exports = ({ } // Stray / late reply from a reused ephemeral port — keep listening. if (response.header.id !== query.header.id) { - debug('udp: dropping response with mismatched id %d (expected %d)', - response.header.id, query.header.id); + debug( + 'udp: dropping response with mismatched id %d (expected %d)', + response.header.id, + query.header.id, + ); return; } // RFC 1035 §4.2.1: if the TC (truncated) bit is set the upstream had diff --git a/eslint.config.mjs b/eslint.config.mjs index b632c2b..f3e4b70 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,23 +1,23 @@ -import js from "@eslint/js"; -import globals from "globals"; +import js from '@eslint/js'; +import globals from 'globals'; export default [ js.configs.recommended, { - files: ["**/*.js"], + files: ['**/*.js'], languageOptions: { - ecmaVersion: "latest", - sourceType: "commonjs", + ecmaVersion: 'latest', + sourceType: 'commonjs', globals: { ...globals.node, }, }, rules: { - "no-unused-vars": [ - "error", + 'no-unused-vars': [ + 'error', { - args: "none", - caughtErrors: "none", + args: 'none', + caughtErrors: 'none', ignoreRestSiblings: true, }, ], diff --git a/example/client/google.js b/example/client/google.js index b3b8ba2..d562500 100644 --- a/example/client/google.js +++ b/example/client/google.js @@ -1,6 +1,6 @@ const { GoogleClient } = require('../..'); -(async() => { +(async () => { const resolve = GoogleClient(); const response = await resolve('google.com'); console.log(response); diff --git a/example/client/tcp-custom-dns.js b/example/client/tcp-custom-dns.js index 6505c37..ed0d3ae 100644 --- a/example/client/tcp-custom-dns.js +++ b/example/client/tcp-custom-dns.js @@ -4,7 +4,7 @@ const resolve = TCPClient({ dns: '1.1.1.1', }); -(async() => { +(async () => { const response = await resolve('google.com'); console.log(response.answers); })(); diff --git a/example/client/tcp.js b/example/client/tcp.js index 94c3275..ae53582 100644 --- a/example/client/tcp.js +++ b/example/client/tcp.js @@ -2,7 +2,7 @@ const { TCPClient } = require('../..'); const resolve = TCPClient(); -(async() => { +(async () => { try { const response = await resolve('google.com'); console.log(response.answers); diff --git a/example/client/udp-custom-dns.js b/example/client/udp-custom-dns.js index 35fc050..fc53d46 100644 --- a/example/client/udp-custom-dns.js +++ b/example/client/udp-custom-dns.js @@ -5,7 +5,7 @@ const resolve = UDPClient({ dns: dns.getServers()[0], }); -(async() => { +(async () => { const response = await resolve('google.com'); console.log(response.answers); })(); diff --git a/example/client/udp-default.js b/example/client/udp-default.js index 302dab7..8a3aa3e 100644 --- a/example/client/udp-default.js +++ b/example/client/udp-default.js @@ -2,7 +2,7 @@ const DNS = require('../..'); const dns = new DNS(); -(async() => { +(async () => { const result = await dns.resolveA('google.com'); console.log(result.answers); })(); diff --git a/example/client/udp-subnet.js b/example/client/udp-subnet.js index fd983b8..b294eef 100644 --- a/example/client/udp-subnet.js +++ b/example/client/udp-subnet.js @@ -1,9 +1,9 @@ const DNS = require('../..'); // Lookup directly from ns1.google.com -const dns = new DNS({ nameServers: [ '216.239.32.10' ] }); +const dns = new DNS({ nameServers: ['216.239.32.10'] }); -(async() => { +(async () => { // What is the IP address for google.com if a client in the subnet // '178.67.222.0/24' asks for it? const result = await dns.resolveA('google.com', '178.67.222.0/24'); diff --git a/example/client/udp.js b/example/client/udp.js index 6c6fe4d..64c3878 100644 --- a/example/client/udp.js +++ b/example/client/udp.js @@ -2,7 +2,7 @@ const { UDPClient } = require('../..'); const resolve = UDPClient(); -(async() => { +(async () => { const response = await resolve('google.com'); console.log(response.answers); })(); diff --git a/example/server/dns.js b/example/server/dns.js index b31713f..a861b79 100644 --- a/example/server/dns.js +++ b/example/server/dns.js @@ -6,36 +6,35 @@ const { Packet } = dns; // Create a SSL enabled server const server = dns.createServer({ - udp : true, - tcp : true, - doh : { - ssl : true, - cert : fs.readFileSync(path.join(__dirname, 'server.crt')), - key : fs.readFileSync(path.join(__dirname, 'secret.key')), + udp: true, + tcp: true, + doh: { + ssl: true, + cert: fs.readFileSync(path.join(__dirname, 'server.crt')), + key: fs.readFileSync(path.join(__dirname, 'secret.key')), }, }); // Handle the incomming request (same style as both UDP and TCP server) server.on('request', (request, send, client) => { const response = Packet.createResponseFromRequest(request); - const [ question ] = request.questions; + const [question] = request.questions; const { name } = question; response.answers.push({ name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '1.1.1.1', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '1.1.1.1', }); send(response); }); - -(async() => { +(async () => { const closed = new Promise(resolve => process.on('SIGINT', resolve)); await server.listen({ - doh : 8443, - udp : 5333, - tcp : 5334, + doh: 8443, + udp: 5333, + tcp: 5334, }); console.log('Listening.'); console.log(server.addresses()); diff --git a/example/server/doh.js b/example/server/doh.js index 2f5a89b..3d0ea6d 100644 --- a/example/server/doh.js +++ b/example/server/doh.js @@ -6,23 +6,23 @@ const { Packet } = dns; // Create a SSL enabled server const server = dns.createDOHServer({ - port : 8080, - ssl : true, - cert : fs.readFileSync(path.join(__dirname, 'server.crt')), - key : fs.readFileSync(path.join(__dirname, 'secret.key')), + port: 8080, + ssl: true, + cert: fs.readFileSync(path.join(__dirname, 'server.crt')), + key: fs.readFileSync(path.join(__dirname, 'secret.key')), }); // Handle the incomming request (same style as both UDP and TCP server) server.on('request', (request, send, client) => { const response = Packet.createResponseFromRequest(request); - const [ question ] = request.questions; + const [question] = request.questions; const { name } = question; response.answers.push({ name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '1.1.1.1', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '1.1.1.1', }); send(response); }); diff --git a/example/server/tcp.js b/example/server/tcp.js index e86a311..108608b 100644 --- a/example/server/tcp.js +++ b/example/server/tcp.js @@ -7,10 +7,10 @@ const server = dns.createTCPServer(); server.on('request', (request, send, client) => { const response = Packet.createResponseFromRequest(request); const answer = Packet.createResourceFromQuestion(request.questions[0], { - target : 'hermes2.jabber.org', - port : 8080, - weight : 30, - priority : 30, + target: 'hermes2.jabber.org', + port: 8080, + weight: 30, + priority: 30, }); response.answers.push(answer); send(response); diff --git a/example/server/udp.js b/example/server/udp.js index 83feaab..fd7f075 100644 --- a/example/server/udp.js +++ b/example/server/udp.js @@ -4,14 +4,14 @@ const { Packet } = dns; const server = dns.createUDPServer((request, send, rinfo) => { const response = Packet.createResponseFromRequest(request); - const [ question ] = request.questions; + const [question] = request.questions; const { name } = question; response.answers.push({ name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '8.8.8.8', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '8.8.8.8', }); send(response); }); diff --git a/index.js b/index.js index 60251bd..5d0dc80 100644 --- a/index.js +++ b/index.js @@ -20,24 +20,38 @@ class DNS extends EventEmitter { // Accept `dns` as a shorthand alias for `nameServers` so that // `new DNS({ dns: '8.8.8.8' })` works as documented and intuited. if (options.dns != null && options.nameServers == null) { - options = Object.assign({}, options, { nameServers: [].concat(options.dns) }); + options = Object.assign({}, options, { + nameServers: [].concat(options.dns), + }); } - Object.assign(this, { - port : 53, - retries : 3, - timeout : 3, - recursive : true, - retryOverTCP : true, - resolverProtocol : 'UDP', - nameServers : [ - '8.8.8.8', - '114.114.114.114', - ], - rootServers: [ - 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', - ].map(x => `${x}.root-servers.net`), - }, options); + Object.assign( + this, + { + port: 53, + retries: 3, + timeout: 3, + recursive: true, + retryOverTCP: true, + resolverProtocol: 'UDP', + nameServers: ['8.8.8.8', '114.114.114.114'], + rootServers: [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + ].map(x => `${x}.root-servers.net`), + }, + options, + ); } /** @@ -49,10 +63,12 @@ class DNS extends EventEmitter { resolve(domain, type = 'ANY', cls = DNS.Packet.CLASS.IN, options = {}) { const { port, nameServers, resolverProtocol = 'UDP', retryOverTCP } = this; const createResolver = DNS[resolverProtocol + 'Client']; - return Promise.race(nameServers.map(address => { - const resolve = createResolver({ dns: address, port, retryOverTCP }); - return resolve(domain, type, cls, options); - })); + return Promise.race( + nameServers.map(address => { + const resolve = createResolver({ dns: address, port, retryOverTCP }); + return resolve(domain, type, cls, options); + }), + ); } resolveA(domain, clientIp) { diff --git a/lib/proxy-protocol.js b/lib/proxy-protocol.js index 594f73f..f2097ac 100644 --- a/lib/proxy-protocol.js +++ b/lib/proxy-protocol.js @@ -9,8 +9,7 @@ // and throws when the bytes are not a valid PROXY header. const V2_SIGNATURE = Buffer.from([ - 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, - 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, + 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, ]); const V1_PREFIX = Buffer.from('PROXY '); const V1_MAX_LEN = 108; @@ -52,20 +51,20 @@ function parseV1(buffer) { return { header: { version: 1, command: 'UNKNOWN' }, headerLength }; } if (parts.length !== 6) throw new Error('PROXY v1: malformed header'); - const [ , proto, sourceAddress, destinationAddress, srcPort, dstPort ] = parts; + const [, proto, sourceAddress, destinationAddress, srcPort, dstPort] = parts; if (proto !== 'TCP4' && proto !== 'TCP6') { throw new Error(`PROXY v1: unsupported protocol ${proto}`); } return { header: { - version : 1, - command : 'PROXY', - family : proto === 'TCP4' ? 'IPv4' : 'IPv6', - transport : 'STREAM', + version: 1, + command: 'PROXY', + family: proto === 'TCP4' ? 'IPv4' : 'IPv6', + transport: 'STREAM', sourceAddress, - sourcePort : parseInt(srcPort, 10), + sourcePort: parseInt(srcPort, 10), destinationAddress, - destinationPort : parseInt(dstPort, 10), + destinationPort: parseInt(dstPort, 10), }, headerLength, }; @@ -75,8 +74,9 @@ function parseV2(buffer) { if (buffer.length < 16) return null; const verCmd = buffer[12]; const version = verCmd >> 4; - const command = verCmd & 0x0F; - if (version !== 2) throw new Error(`PROXY v2: unsupported version ${version}`); + const command = verCmd & 0x0f; + if (version !== 2) + throw new Error(`PROXY v2: unsupported version ${version}`); if (command !== 0 && command !== 1) { throw new Error(`PROXY v2: unknown command ${command}`); } @@ -91,8 +91,8 @@ function parseV2(buffer) { return { header: { version: 2, command: 'LOCAL' }, headerLength }; } - const family = FAMILY[famProto & 0xF0]; - const transport = TRANSPORT[famProto & 0x0F]; + const family = FAMILY[famProto & 0xf0]; + const transport = TRANSPORT[famProto & 0x0f]; let sourceAddress, destinationAddress, sourcePort, destinationPort; if (family === 'IPv4' && addressLength >= 12) { @@ -106,13 +106,15 @@ function parseV2(buffer) { sourcePort = buffer.readUInt16BE(48); destinationPort = buffer.readUInt16BE(50); } else { - throw new Error(`PROXY v2: unsupported address family/protocol 0x${famProto.toString(16)}`); + throw new Error( + `PROXY v2: unsupported address family/protocol 0x${famProto.toString(16)}`, + ); } return { header: { - version : 2, - command : 'PROXY', + version: 2, + command: 'PROXY', family, transport, sourceAddress, @@ -133,18 +135,37 @@ function ipv6FromBytes(bytes) { } // Test helpers — build wire-format headers used by tests and example code. -function buildV1({ family = 'TCP4', sourceAddress, destinationAddress, sourcePort, destinationPort }) { - return Buffer.from(`PROXY ${family} ${sourceAddress} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`, 'ascii'); +function buildV1({ + family = 'TCP4', + sourceAddress, + destinationAddress, + sourcePort, + destinationPort, +}) { + return Buffer.from( + `PROXY ${family} ${sourceAddress} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`, + 'ascii', + ); } -function buildV2Ipv4({ sourceAddress, destinationAddress, sourcePort, destinationPort, transport = 'STREAM' }) { +function buildV2Ipv4({ + sourceAddress, + destinationAddress, + sourcePort, + destinationPort, + transport = 'STREAM', +}) { const buf = Buffer.alloc(16 + 12); V2_SIGNATURE.copy(buf, 0); buf[12] = 0x21; // version 2 | PROXY command buf[13] = 0x10 | (transport === 'DGRAM' ? 0x02 : 0x01); // IPv4 | STREAM/DGRAM buf.writeUInt16BE(12, 14); - sourceAddress.split('.').forEach((o, i) => { buf[16 + i] = parseInt(o, 10); }); - destinationAddress.split('.').forEach((o, i) => { buf[20 + i] = parseInt(o, 10); }); + sourceAddress.split('.').forEach((o, i) => { + buf[16 + i] = parseInt(o, 10); + }); + destinationAddress.split('.').forEach((o, i) => { + buf[20 + i] = parseInt(o, 10); + }); buf.writeUInt16BE(sourcePort, 24); buf.writeUInt16BE(destinationPort, 26); return buf; diff --git a/lib/reader.js b/lib/reader.js index a67667b..8da0ecb 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -16,7 +16,7 @@ function BufferReader(buffer, offset) { * @param {[type]} length [description] * @return {[type]} [description] */ -BufferReader.read = function(buffer, offset, length) { +BufferReader.read = function (buffer, offset, length) { let a = []; let l = Math.floor(offset / 8); const m = offset % 8; @@ -25,7 +25,7 @@ BufferReader.read = function(buffer, offset, length) { // ceil(length/8) alone accounts for. let c = Math.ceil((length + m) / 8); function t(n) { - const r = [ 0, 0, 0, 0, 0, 0, 0, 0 ]; + const r = [0, 0, 0, 0, 0, 0, 0, 0]; for (let i = 7; i >= 0; i--) { r[7 - i] = n & Math.pow(2, i) ? 1 : 0; } @@ -34,7 +34,9 @@ BufferReader.read = function(buffer, offset, length) { function p(a) { let n = 0; const f = a.length - 1; - for (let i = f; i >= 0; i--) { if (a[f - i]) n += Math.pow(2, i); } + for (let i = f; i >= 0; i--) { + if (a[f - i]) n += Math.pow(2, i); + } return n; } while (c--) t(buffer.readUInt8(l++)); @@ -46,7 +48,7 @@ BufferReader.read = function(buffer, offset, length) { * @param {[type]} size [description] * @return {[type]} [description] */ -BufferReader.prototype.read = function(size) { +BufferReader.prototype.read = function (size) { const val = BufferReader.read(this.buffer, this.offset, size); this.offset += size; return val; diff --git a/lib/writer.js b/lib/writer.js index 66d0f32..60840a9 100644 --- a/lib/writer.js +++ b/lib/writer.js @@ -11,9 +11,9 @@ function BufferWriter() { * @param {[type]} size [description] * @return {[type]} [description] */ -BufferWriter.prototype.write = function(d, size) { +BufferWriter.prototype.write = function (d, size) { for (let i = 0; i < size; i++) { - this.buffer.push((d & Math.pow(2, size - i - 1)) ? 1 : 0); + this.buffer.push(d & Math.pow(2, size - i - 1) ? 1 : 0); } }; @@ -21,26 +21,26 @@ BufferWriter.prototype.write = function(d, size) { * [writeBuffer description] * @param {[type]} b [description] */ -BufferWriter.prototype.writeBuffer = function(b) { +BufferWriter.prototype.writeBuffer = function (b) { this.buffer = this.buffer.concat(b.buffer); }; // Current write position, in bits. -BufferWriter.prototype.bitLength = function() { +BufferWriter.prototype.bitLength = function () { return this.buffer.length; }; // Current write position, in bytes. Defined when DNS encoding has stayed on // byte boundaries (it always does at the points we expose this). -BufferWriter.prototype.byteLength = function() { +BufferWriter.prototype.byteLength = function () { return this.buffer.length / 8; }; // Overwrite `size` bits at `bitOffset` with `value`. Used to back-fill // placeholders (e.g. RDLENGTH) once the field's contents have been written. -BufferWriter.prototype.patch = function(bitOffset, value, size) { +BufferWriter.prototype.patch = function (bitOffset, value, size) { for (let i = 0; i < size; i++) { - this.buffer[bitOffset + i] = (value & Math.pow(2, size - i - 1)) ? 1 : 0; + this.buffer[bitOffset + i] = value & Math.pow(2, size - i - 1) ? 1 : 0; } }; @@ -48,7 +48,7 @@ BufferWriter.prototype.patch = function(bitOffset, value, size) { * [toBuffer description] * @return {[type]} [description] */ -BufferWriter.prototype.toBuffer = function() { +BufferWriter.prototype.toBuffer = function () { const arr = []; for (let i = 0; i < this.buffer.length; i += 8) { const chunk = this.buffer.slice(i, i + 8); diff --git a/package.json b/package.json index c1a87a4..4c90163 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "ts" ], "scripts": { + "lint": "npx eslint .", + "lint:fix": "npx eslint . --fix", + "prettier": "npx prettier . --check", + "prettier:fix": "npx prettier . --write --log-level=warn", "test": "node --test", "test:coverage": "node --test --experimental-test-coverage", "test:coverage:lcov": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info", - "lint": "npx eslint .", - "lint:fix": "npx eslint . --fix", "example-server-udp": "node example/server/udp.js", "example-server-tcp": "node example/server/tcp.js", "example-server-doh": "node example/server/doh.js", @@ -55,7 +57,8 @@ }, "prettier": { "printWidth": 80, - "semi": false, - "singleQuote": true + "semi": true, + "singleQuote": true, + "arrowParens": "avoid" } } diff --git a/packet.js b/packet.js index 90f7b25..ff6b3d0 100644 --- a/packet.js +++ b/packet.js @@ -12,13 +12,18 @@ const debug = debuglog('dns2'); // - a single zero group is NOT compressed const toIPv6 = buffer => { const segments = buffer.map(part => (part > 0 ? part.toString(16) : '0')); - let bestStart = -1; let bestLen = 0; - let curStart = -1; let curLen = 0; + let bestStart = -1; + let bestLen = 0; + let curStart = -1; + let curLen = 0; for (let i = 0; i < segments.length; i++) { if (segments[i] === '0') { if (curLen === 0) curStart = i; curLen++; - if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; } + if (curLen > bestLen) { + bestLen = curLen; + bestStart = curStart; + } } else { curLen = 0; } @@ -29,7 +34,7 @@ const toIPv6 = buffer => { return `${before}::${after}`; }; -const fromIPv6 = (address) => { +const fromIPv6 = address => { const digits = address.split(':'); // CAVEAT edge case for :: and IPs starting // or ending by :: @@ -41,7 +46,7 @@ const fromIPv6 = (address) => { } // node js 10 does not support Array.prototype.flatMap if (!Array.prototype.flatMap) { - Array.prototype.flatMap = function(f, ctx) { + Array.prototype.flatMap = function (f, ctx) { return this.reduce((r, x, i, a) => r.concat(f.call(ctx, x, i, a)), []); }; } @@ -49,7 +54,7 @@ const fromIPv6 = (address) => { // CAVEAT we have to take into account // the extra space used by the empty string const missingFields = 8 - digits.length + 1; - return digits.flatMap((digit) => { + return digits.flatMap(digit => { if (digit === '') { return Array(missingFields).fill('0'); } @@ -86,7 +91,7 @@ function Packet(data) { } else if (typeof data === 'string') { this.questions.push(data); } else if (typeof data === 'object') { - const type = ({}).toString.call(data).match(/\[object (\w+)\]/)[1]; + const type = {}.toString.call(data).match(/\[object (\w+)\]/)[1]; if (type === 'Array') { this.questions = data; } @@ -103,32 +108,32 @@ function Packet(data) { * @docs https://tools.ietf.org/html/rfc1035#section-3.2.2 */ Packet.TYPE = { - A : 0x01, - NS : 0x02, - MD : 0x03, - MF : 0x04, - CNAME : 0x05, - SOA : 0x06, - MB : 0x07, - MG : 0x08, - MR : 0x09, - NULL : 0x0A, - WKS : 0x0B, - PTR : 0x0C, - HINFO : 0x0D, - MINFO : 0x0E, - MX : 0x0F, - TXT : 0x10, - AAAA : 0x1C, - SRV : 0x21, - EDNS : 0x29, - SPF : 0x63, - AXFR : 0xFC, - MAILB : 0xFD, - MAILA : 0xFE, - ANY : 0xFF, - CAA : 0x101, - DNSKEY : 0x30, + A: 0x01, + NS: 0x02, + MD: 0x03, + MF: 0x04, + CNAME: 0x05, + SOA: 0x06, + MB: 0x07, + MG: 0x08, + MR: 0x09, + NULL: 0x0a, + WKS: 0x0b, + PTR: 0x0c, + HINFO: 0x0d, + MINFO: 0x0e, + MX: 0x0f, + TXT: 0x10, + AAAA: 0x1c, + SRV: 0x21, + EDNS: 0x29, + SPF: 0x63, + AXFR: 0xfc, + MAILB: 0xfd, + MAILA: 0xfe, + ANY: 0xff, + CAA: 0x101, + DNSKEY: 0x30, }; /** * [QUERY_CLASS description] @@ -136,11 +141,11 @@ Packet.TYPE = { * @docs https://tools.ietf.org/html/rfc1035#section-3.2.4 */ Packet.CLASS = { - IN : 0x01, - CS : 0x02, - CH : 0x03, - HS : 0x04, - ANY : 0xFF, + IN: 0x01, + CS: 0x02, + CH: 0x03, + HS: 0x04, + ANY: 0xff, }; /** * DNS response codes @@ -148,12 +153,12 @@ Packet.CLASS = { * @docs https://tools.ietf.org/html/rfc1035#section-4.1.1 */ Packet.RCODE = { - NOERROR : 0, - FORMERR : 1, - SERVFAIL : 2, - NXDOMAIN : 3, - NOTIMP : 4, - REFUSED : 5, + NOERROR: 0, + FORMERR: 1, + SERVFAIL: 2, + NXDOMAIN: 3, + NOTIMP: 4, + REFUSED: 5, }; /** * [EDNS_OPTION_CODE description] @@ -170,7 +175,7 @@ Packet.EDNS_OPTION_CODE = { * response forgery / cache poisoning impractical. * @return {number} integer in [0, 0xFFFF] */ -Packet.uuid = function() { +Packet.uuid = function () { return randomInt(0x10000); }; @@ -179,16 +184,17 @@ Packet.uuid = function() { * @param {[type]} buffer [description] * @return {[type]} [description] */ -Packet.parse = function(buffer) { +Packet.parse = function (buffer) { const packet = new Packet(); const reader = new Packet.Reader(buffer); packet.header = Packet.Header.parse(reader); - ([ // props parser count - [ 'questions', Packet.Question, packet.header.qdcount ], - [ 'answers', Packet.Resource, packet.header.ancount ], - [ 'authorities', Packet.Resource, packet.header.nscount ], - [ 'additionals', Packet.Resource, packet.header.arcount ], - ]).forEach(function(def) { + [ + // props parser count + ['questions', Packet.Question, packet.header.qdcount], + ['answers', Packet.Resource, packet.header.ancount], + ['authorities', Packet.Resource, packet.header.nscount], + ['additionals', Packet.Resource, packet.header.arcount], + ].forEach(function (def) { const section = def[0]; const decoder = def[1]; let count = def[2]; @@ -206,7 +212,8 @@ Packet.parse = function(buffer) { // record's TTL high byte. Merge them so callers see the full 12-bit value. const opt = packet.additionals.find(r => r && r.type === Packet.TYPE.EDNS); if (opt && opt.extendedRcode) { - packet.header.rcode = (opt.extendedRcode << 4) | (packet.header.rcode & 0xF); + packet.header.rcode = + (opt.extendedRcode << 4) | (packet.header.rcode & 0xf); } return packet; }; @@ -215,8 +222,8 @@ Packet.parse = function(buffer) { * recursive */ Object.defineProperty(Packet.prototype, 'recursive', { - enumerable : true, - configurable : true, + enumerable: true, + configurable: true, get() { return !!this.header.rd; }, @@ -229,7 +236,7 @@ Object.defineProperty(Packet.prototype, 'recursive', { * [toBuffer description] * @return {[type]} [description] */ -Packet.prototype.toBuffer = function(writer) { +Packet.prototype.toBuffer = function (writer) { writer = writer || new Packet.Writer(); // RFC 1035 §4.1.4 — record the byte offset of each name we encode so later // occurrences can be replaced by a compression pointer. The map is owned by @@ -240,33 +247,40 @@ Packet.prototype.toBuffer = function(writer) { this.header.ancount = this.answers.length; this.header.nscount = this.authorities.length; this.header.arcount = this.additionals.length; - if (!(this instanceof Packet.Header)) { this.header = new Packet.Header(this.header); } + if (!(this instanceof Packet.Header)) { + this.header = new Packet.Header(this.header); + } // RFC 6891 §6.1.3: if the caller set a header.rcode >= 16 the high byte must // be carried in the OPT record's TTL. Propagate it before the header is // serialized so the low nibble alone goes into the header. - if (this.header.rcode > 0xF) { + if (this.header.rcode > 0xf) { const opt = this.additionals.find(r => r && r.type === Packet.TYPE.EDNS); if (opt) { - opt.extendedRcode = (this.header.rcode >>> 4) & 0xFF; + opt.extendedRcode = (this.header.rcode >>> 4) & 0xff; opt.ttl = ednsTtl(opt.extendedRcode, opt.version || 0, opt.doFlag); } else { - debug('node-dns > rcode %d > 15 but no OPT record; truncating to low nibble', - this.header.rcode); + debug( + 'node-dns > rcode %d > 15 but no OPT record; truncating to low nibble', + this.header.rcode, + ); } } this.header.toBuffer(writer); - ([ // section encoder - [ 'questions', Packet.Question ], - [ 'answers', Packet.Resource ], - [ 'authorities', Packet.Resource ], - [ 'additionals', Packet.Resource ], - ]).forEach(function(def) { - const section = def[0]; - const Encoder = def[1]; - (this[section] || []).forEach(function(resource) { - Encoder.encode(resource, writer); - }); - }.bind(this)); + [ + // section encoder + ['questions', Packet.Question], + ['answers', Packet.Resource], + ['authorities', Packet.Resource], + ['additionals', Packet.Resource], + ].forEach( + function (def) { + const section = def[0]; + const Encoder = def[1]; + (this[section] || []).forEach(function (resource) { + Encoder.encode(resource, writer); + }); + }.bind(this), + ); return writer.toBuffer(); }; @@ -275,7 +289,7 @@ Packet.prototype.toBuffer = function(writer) { * @param {[type]} options [description] * @docs https://tools.ietf.org/html/rfc1035#section-4.1.1 */ -Packet.Header = function(header) { +Packet.Header = function (header) { this.id = 0; this.qr = 0; this.opcode = 0; @@ -302,7 +316,7 @@ Packet.Header = function(header) { * @return {[type]} [description] * @docs https://tools.ietf.org/html/rfc1035#section-4.1.1 */ -Packet.Header.parse = function(reader) { +Packet.Header.parse = function (reader) { const header = new Packet.Header(); if (reader instanceof Buffer) { reader = new Packet.Reader(reader); @@ -330,7 +344,7 @@ Packet.Header.parse = function(reader) { * [toBuffer description] * @return {[type]} [description] */ -Packet.Header.prototype.toBuffer = function(writer) { +Packet.Header.prototype.toBuffer = function (writer) { writer = writer || new Packet.Writer(); writer.write(this.id, 16); writer.write(this.qr, 1); @@ -344,7 +358,7 @@ Packet.Header.prototype.toBuffer = function(writer) { writer.write(0, 1); writer.write(this.ad, 1); writer.write(this.cd, 1); - writer.write(this.rcode & 0xF, 4); + writer.write(this.rcode & 0xf, 4); writer.write(this.qdcount, 16); writer.write(this.ancount, 16); writer.write(this.nscount, 16); @@ -356,10 +370,10 @@ Packet.Header.prototype.toBuffer = function(writer) { * Question section format * @docs https://tools.ietf.org/html/rfc1035#section-4.1.2 */ -Packet.Question = function(name, type, cls) { +Packet.Question = function (name, type, cls) { const defaults = { - type : Packet.TYPE.ANY, - class : Packet.CLASS.ANY, + type: Packet.TYPE.ANY, + class: Packet.CLASS.ANY, }; if (typeof name === 'object') { for (const k in name) { @@ -378,7 +392,7 @@ Packet.Question = function(name, type, cls) { * @param {[type]} writer [description] * @return {[type]} [description] */ -Packet.Question.prototype.toBuffer = function(writer) { +Packet.Question.prototype.toBuffer = function (writer) { return Packet.Question.encode(this, writer); }; @@ -387,8 +401,7 @@ Packet.Question.prototype.toBuffer = function(writer) { * @param {[type]} reader [description] * @return {[type]} [description] */ -Packet.Question.parse = -Packet.Question.decode = function(reader) { +Packet.Question.parse = Packet.Question.decode = function (reader) { const question = new Packet.Question(); if (reader instanceof Buffer) { reader = new Packet.Reader(reader); @@ -399,7 +412,7 @@ Packet.Question.decode = function(reader) { return question; }; -Packet.Question.encode = function(question, writer) { +Packet.Question.encode = function (question, writer) { const ownsWriter = !writer; writer = writer || new Packet.Writer(); Packet.Name.encode(question.name, writer); @@ -412,19 +425,22 @@ Packet.Question.encode = function(question, writer) { * Resource record format * @docs https://tools.ietf.org/html/rfc1035#section-4.1.3 */ -Packet.Resource = function(name, type, cls, ttl) { +Packet.Resource = function (name, type, cls, ttl) { const defaults = { - name : '', - ttl : 300, - type : Packet.TYPE.ANY, - class : Packet.CLASS.ANY, + name: '', + ttl: 300, + type: Packet.TYPE.ANY, + class: Packet.CLASS.ANY, }; let input; if (typeof name === 'object') { input = name; } else { input = { - name, type, class: cls, ttl, + name, + type, + class: cls, + ttl, }; } Object.assign(this, defaults, input); @@ -436,7 +452,7 @@ Packet.Resource = function(name, type, cls, ttl) { * @param {[type]} writer [description] * @return {[type]} [description] */ -Packet.Resource.prototype.toBuffer = function(writer) { +Packet.Resource.prototype.toBuffer = function (writer) { return Packet.Resource.encode(this, writer); }; @@ -446,15 +462,15 @@ Packet.Resource.prototype.toBuffer = function(writer) { * @param {[type]} writer [description] * @return {[type]} [description] */ -Packet.Resource.encode = function(resource, writer) { +Packet.Resource.encode = function (resource, writer) { writer = writer || new Packet.Writer(); Packet.Name.encode(resource.name, writer); writer.write(resource.type, 16); writer.write(resource.class, 16); // RFC 2181 §8: TTL is an unsigned 32-bit value but high-bit values are // historically unsafe; clamp to 2^31 - 1 on the wire. - writer.write(Math.min(resource.ttl >>> 0, 0x7FFFFFFF), 32); - const encoder = Object.keys(Packet.TYPE).filter(function(type) { + writer.write(Math.min(resource.ttl >>> 0, 0x7fffffff), 32); + const encoder = Object.keys(Packet.TYPE).filter(function (type) { return resource.type === Packet.TYPE[type]; })[0]; // RDLENGTH is owned here, not by each rdata encoder. We write a 16-bit @@ -472,7 +488,9 @@ Packet.Resource.encode = function(resource, writer) { // decoder preserved as `resource.data`. Without this, RDATA would be // omitted entirely, truncating the wire format and corrupting any // records that follow. - const data = Buffer.isBuffer(resource.data) ? resource.data : Buffer.alloc(0); + const data = Buffer.isBuffer(resource.data) + ? resource.data + : Buffer.alloc(0); for (const byte of data) { writer.write(byte, 8); } @@ -486,8 +504,7 @@ Packet.Resource.encode = function(resource, writer) { * @param {[type]} reader [description] * @return {[type]} [description] */ -Packet.Resource.parse = -Packet.Resource.decode = function(reader) { +Packet.Resource.parse = Packet.Resource.decode = function (reader) { if (reader instanceof Buffer) { reader = new Packet.Reader(reader); } @@ -499,9 +516,9 @@ Packet.Resource.decode = function(reader) { // RFC 2181 §8: TTLs are an unsigned 32-bit field but legacy implementations // treated them as signed. Anything with the high bit set is clamped to // 2^31 - 1 so it cannot be misinterpreted as a negative value. - if (resource.ttl > 0x7FFFFFFF) resource.ttl = 0x7FFFFFFF; + if (resource.ttl > 0x7fffffff) resource.ttl = 0x7fffffff; let length = reader.read(16); - const parser = Object.keys(Packet.TYPE).filter(function(type) { + const parser = Object.keys(Packet.TYPE).filter(function (type) { return resource.type === Packet.TYPE[type]; })[0]; if (parser in Packet.Resource) { @@ -522,14 +539,16 @@ Packet.Resource.decode = function(reader) { */ // RFC 1035 §2.3.4 — wire-format limits. Packet.Name = { - COPY : 0xc0, - MAX_LABEL : 63, - MAX_NAME : 255, - decode : function(reader) { + COPY: 0xc0, + MAX_LABEL: 63, + MAX_NAME: 255, + decode: function (reader) { if (reader instanceof Buffer) { reader = new Packet.Reader(reader); } - const name = []; let o; let len = reader.read(8); + const name = []; + let o; + let len = reader.read(8); // Track each pointer target we follow. A crafted packet can chain // pointers in a cycle; without this guard, decode would loop forever. const visited = new Set(); @@ -554,15 +573,21 @@ Packet.Name = { } // RFC 1035: a label length byte has its top two bits clear (00). // The 01/10 combinations are reserved and indicate a malformed name. - if (len & 0xC0) { - throw new Error(`Name decode: invalid label length byte 0x${len.toString(16)}`); + if (len & 0xc0) { + throw new Error( + `Name decode: invalid label length byte 0x${len.toString(16)}`, + ); } if (len > Packet.Name.MAX_LABEL) { - throw new Error(`Name decode: label exceeds ${Packet.Name.MAX_LABEL} octets`); + throw new Error( + `Name decode: label exceeds ${Packet.Name.MAX_LABEL} octets`, + ); } totalOctets += len + 1; if (totalOctets > Packet.Name.MAX_NAME) { - throw new Error(`Name decode: name exceeds ${Packet.Name.MAX_NAME} octets`); + throw new Error( + `Name decode: name exceeds ${Packet.Name.MAX_NAME} octets`, + ); } let part = ''; while (len--) part += String.fromCharCode(reader.read(8)); @@ -572,7 +597,7 @@ Packet.Name = { if (o) reader.offset = o; return name.join('.'); }, - encode: function(domain, writer) { + encode: function (domain, writer) { // Only materialize a Buffer when we created the writer; if the caller // passed one, they own the final toBuffer() and we avoid an O(buffer) // materialization per name (a big deal once many records share a suffix). @@ -584,14 +609,16 @@ Packet.Name = { if (part.length > Packet.Name.MAX_LABEL) { throw new Error( `Name encode: label "${part}" is ${part.length} octets ` + - `(max ${Packet.Name.MAX_LABEL})`); + `(max ${Packet.Name.MAX_LABEL})`, + ); } totalOctets += part.length + 1; } if (totalOctets > Packet.Name.MAX_NAME) { throw new Error( `Name encode: name "${domain}" encodes to ${totalOctets} octets ` + - `(max ${Packet.Name.MAX_NAME})`); + `(max ${Packet.Name.MAX_NAME})`, + ); } // RFC 1035 §4.1.4 — if the writer carries a name-offset table, emit a // compression pointer for any suffix we've already serialized; otherwise @@ -602,7 +629,7 @@ Packet.Name = { for (let i = 0; i < parts.length; i++) { const suffix = parts.slice(i).join('.').toLowerCase(); if (compress && writer.names.has(suffix)) { - writer.write(0xC000 | writer.names.get(suffix), 16); + writer.write(0xc000 | writer.names.get(suffix), 16); return ownsWriter ? writer.toBuffer() : undefined; } if (compress) { @@ -624,24 +651,24 @@ Packet.Name = { * @type {Object} * @docs https://tools.ietf.org/html/rfc1035#section-3.4.1 */ -Packet.Resource.A = function(address) { +Packet.Resource.A = function (address) { this.type = Packet.TYPE.A; this.class = Packet.CLASS.IN; this.address = address; return this; }; -Packet.Resource.A.encode = function(record, writer) { +Packet.Resource.A.encode = function (record, writer) { writer = writer || new Packet.Writer(); // RDLENGTH is written by Packet.Resource.encode; only emit the rdata here. // No toBuffer() — the caller owns materialization (avoids O(N) re-walks of // the message bit-array per record). - record.address.split('.').forEach(function(part) { + record.address.split('.').forEach(function (part) { writer.write(parseInt(part, 10), 8); }); }; -Packet.Resource.A.decode = function(reader, length) { +Packet.Resource.A.decode = function (reader, length) { const parts = []; while (length--) parts.push(reader.read(8)); this.address = parts.join('.'); @@ -654,7 +681,7 @@ Packet.Resource.A.decode = function(reader, length) { * @param {[type]} priority [description] * @docs https://tools.ietf.org/html/rfc1035#section-3.3.9 */ -Packet.Resource.MX = function(exchange, priority) { +Packet.Resource.MX = function (exchange, priority) { this.type = Packet.TYPE.MX; this.class = Packet.CLASS.IN; this.exchange = exchange; @@ -667,7 +694,7 @@ Packet.Resource.MX = function(exchange, priority) { * @param {[type]} writer [description] * @return {[type]} [description] */ -Packet.Resource.MX.encode = function(record, writer) { +Packet.Resource.MX.encode = function (record, writer) { writer = writer || new Packet.Writer(); writer.write(record.priority, 16); Packet.Name.encode(record.exchange, writer); @@ -678,7 +705,7 @@ Packet.Resource.MX.encode = function(record, writer) { * @param {[type]} length [description] * @return {[type]} [description] */ -Packet.Resource.MX.decode = function(reader, length) { +Packet.Resource.MX.decode = function (reader, length) { this.priority = reader.read(16); this.exchange = Packet.Name.decode(reader); return this; @@ -689,7 +716,7 @@ Packet.Resource.MX.decode = function(reader, length) { * @docs https://en.wikipedia.org/wiki/IPv6 */ Packet.Resource.AAAA = { - decode: function(reader, length) { + decode: function (reader, length) { const parts = []; while (length) { length -= 2; @@ -698,9 +725,9 @@ Packet.Resource.AAAA = { this.address = toIPv6(parts); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); - fromIPv6(record.address).forEach(function(part) { + fromIPv6(record.address).forEach(function (part) { writer.write(parseInt(part, 16), 16); }); }, @@ -711,11 +738,11 @@ Packet.Resource.AAAA = { * @docs https://tools.ietf.org/html/rfc1035#section-3.3.11 */ Packet.Resource.NS = { - decode: function(reader, length) { + decode: function (reader, length) { this.ns = Packet.Name.decode(reader); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); Packet.Name.encode(record.ns, writer); }, @@ -725,13 +752,12 @@ Packet.Resource.NS = { * @type {Object} * @docs https://tools.ietf.org/html/rfc1035#section-3.3.1 */ -Packet.Resource.PTR = -Packet.Resource.CNAME = { - decode: function(reader, length) { +Packet.Resource.PTR = Packet.Resource.CNAME = { + decode: function (reader, length) { this.domain = Packet.Name.decode(reader); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); Packet.Name.encode(record.domain, writer); }, @@ -741,11 +767,11 @@ Packet.Resource.CNAME = { * @type {[type]} * @docs https://tools.ietf.org/html/rfc1035#section-3.3.14 */ -Packet.Resource.SPF = -Packet.Resource.TXT = { - decode: function(reader, length) { +Packet.Resource.SPF = Packet.Resource.TXT = { + decode: function (reader, length) { const parts = []; - let bytesRead = 0; let chunkLength; + let bytesRead = 0; + let chunkLength; while (bytesRead < length) { chunkLength = reader.read(8); // text length @@ -760,29 +786,33 @@ Packet.Resource.TXT = { this.data = Buffer.from(parts).toString('utf8'); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); // make sure that resource data is a an array of strings - const characterStrings = Array.isArray(record.data) ? record.data : [ record.data ]; + const characterStrings = Array.isArray(record.data) + ? record.data + : [record.data]; // convert array of strings to array of buffers - const characterStringBuffers = characterStrings.map(function(characterString) { - if (Buffer.isBuffer(characterString)) { + const characterStringBuffers = characterStrings + .map(function (characterString) { + if (Buffer.isBuffer(characterString)) { + return characterString; + } + if (typeof characterString === 'string') { + return Buffer.from(characterString, 'utf8'); + } + return false; + }) + .filter(function (characterString) { + // remove invalid values from the array return characterString; - } - if (typeof characterString === 'string') { - return Buffer.from(characterString, 'utf8'); - } - return false; - }).filter(function(characterString) { - // remove invalid values from the array - return characterString; - }); + }); // write each string to output (RDLENGTH is back-filled by Resource.encode) - characterStringBuffers.forEach(function(buffer) { + characterStringBuffers.forEach(function (buffer) { writer.write(buffer.length, 8); // text length - buffer.forEach(function(c) { + buffer.forEach(function (c) { writer.write(c, 8); }); }); @@ -794,7 +824,7 @@ Packet.Resource.TXT = { * @docs https://tools.ietf.org/html/rfc1035#section-3.3.13 */ Packet.Resource.SOA = { - decode: function(reader, length) { + decode: function (reader, length) { this.primary = Packet.Name.decode(reader); this.admin = Packet.Name.decode(reader); this.serial = reader.read(32); @@ -804,7 +834,7 @@ Packet.Resource.SOA = { this.minimum = reader.read(32); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); Packet.Name.encode(record.primary, writer); Packet.Name.encode(record.admin, writer); @@ -813,7 +843,7 @@ Packet.Resource.SOA = { writer.write(record.retry, 32); writer.write(record.expiration, 32); // RFC 2308 §4: the SOA minimum field is also a TTL; same 31-bit ceiling. - writer.write(Math.min(record.minimum >>> 0, 0x7FFFFFFF), 32); + writer.write(Math.min(record.minimum >>> 0, 0x7fffffff), 32); }, }; /** @@ -822,14 +852,14 @@ Packet.Resource.SOA = { * @docs https://tools.ietf.org/html/rfc2782 */ Packet.Resource.SRV = { - decode: function(reader, length) { + decode: function (reader, length) { this.priority = reader.read(16); this.weight = reader.read(16); this.port = reader.read(16); this.target = Packet.Name.decode(reader); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); writer.write(record.priority, 16); writer.write(record.weight, 16); @@ -844,24 +874,25 @@ Packet.Resource.SRV = { // bit 16: DO (DNSSEC OK) // bits 17-31: reserved Z, must be zero const ednsTtl = (extendedRcode, version, doFlag) => - (((extendedRcode & 0xff) << 24) >>> 0) - | ((version & 0xff) << 16) - | (doFlag ? 0x8000 : 0); + (((extendedRcode & 0xff) << 24) >>> 0) | + ((version & 0xff) << 16) | + (doFlag ? 0x8000 : 0); // RFC 6891 §6.2.5 — a reasonable default for the requestor's UDP payload size. // The pre-EDNS 512-byte limit is conservative; modern resolvers advertise // 4096 so upstreams need not truncate responses that fit in a typical MTU. Packet.EDNS_DEFAULT_UDP_PAYLOAD_SIZE = 4096; -Packet.Resource.EDNS = function(rdata, opts = {}) { +Packet.Resource.EDNS = function (rdata, opts = {}) { const extendedRcode = opts.extendedRcode || 0; const version = opts.version || 0; const doFlag = !!opts.doFlag; - const udpPayloadSize = opts.udpPayloadSize || Packet.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; + const udpPayloadSize = + opts.udpPayloadSize || Packet.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; return { - type : Packet.TYPE.EDNS, - class : udpPayloadSize, - ttl : ednsTtl(extendedRcode, version, doFlag), + type: Packet.TYPE.EDNS, + class: udpPayloadSize, + ttl: ednsTtl(extendedRcode, version, doFlag), extendedRcode, version, doFlag, @@ -869,7 +900,7 @@ Packet.Resource.EDNS = function(rdata, opts = {}) { }; }; -Packet.Resource.EDNS.decode = function(reader, length) { +Packet.Resource.EDNS.decode = function (reader, length) { // When invoked through Resource.parse, this.type/class/ttl are already set // from the wire. Direct callers (e.g. unit tests) hit defaults instead. this.type = this.type ?? Packet.TYPE.EDNS; @@ -885,15 +916,24 @@ Packet.Resource.EDNS.decode = function(reader, length) { const optionCode = reader.read(16); const optionLength = reader.read(16); // In octet (https://tools.ietf.org/html/rfc6891#page-8) - const decoder = Object.keys(Packet.EDNS_OPTION_CODE).filter(function(type) { - return optionCode === Packet.EDNS_OPTION_CODE[type]; - })[0]; - if (decoder in Packet.Resource.EDNS && Packet.Resource.EDNS[decoder].decode) { + const decoder = Object.keys(Packet.EDNS_OPTION_CODE).filter( + function (type) { + return optionCode === Packet.EDNS_OPTION_CODE[type]; + }, + )[0]; + if ( + decoder in Packet.Resource.EDNS && + Packet.Resource.EDNS[decoder].decode + ) { const rdata = Packet.Resource.EDNS[decoder].decode(reader, optionLength); this.rdata.push(rdata); } else { reader.read(optionLength); // Ignore data that doesn't understand - debug('node-dns > unknown EDNS rdata decoder %s(%j)', decoder, optionCode); + debug( + 'node-dns > unknown EDNS rdata decoder %s(%j)', + decoder, + optionCode, + ); } length = length - 4 - optionLength; @@ -901,39 +941,48 @@ Packet.Resource.EDNS.decode = function(reader, length) { return this; }; -Packet.Resource.EDNS.encode = function(record, writer) { +Packet.Resource.EDNS.encode = function (record, writer) { writer = writer || new Packet.Writer(); // RDLENGTH is owned by Packet.Resource.encode; emit option records back to // back into the main writer. for (const rdata of record.rdata) { - const encoder = Object.keys(Packet.EDNS_OPTION_CODE).filter(function(type) { - return rdata.ednsCode === Packet.EDNS_OPTION_CODE[type]; - })[0]; - if (encoder in Packet.Resource.EDNS && Packet.Resource.EDNS[encoder].encode) { + const encoder = Object.keys(Packet.EDNS_OPTION_CODE).filter( + function (type) { + return rdata.ednsCode === Packet.EDNS_OPTION_CODE[type]; + }, + )[0]; + if ( + encoder in Packet.Resource.EDNS && + Packet.Resource.EDNS[encoder].encode + ) { const w = new Packet.Writer(); Packet.Resource.EDNS[encoder].encode(rdata, w); writer.write(rdata.ednsCode, 16); writer.write(w.buffer.length / 8, 16); writer.writeBuffer(w); } else { - debug('node-dns > unknown EDNS rdata encoder %s(%j)', encoder, rdata.ednsCode); + debug( + 'node-dns > unknown EDNS rdata encoder %s(%j)', + encoder, + rdata.ednsCode, + ); } } }; -Packet.Resource.EDNS.ECS = function(clientIp) { - const [ ip, prefixLength ] = clientIp.split('/'); +Packet.Resource.EDNS.ECS = function (clientIp) { + const [ip, prefixLength] = clientIp.split('/'); const numPrefixLength = parseInt(prefixLength) || 32; return { - ednsCode : Packet.EDNS_OPTION_CODE.ECS, - family : 1, - sourcePrefixLength : numPrefixLength, - scopePrefixLength : 0, + ednsCode: Packet.EDNS_OPTION_CODE.ECS, + family: 1, + sourcePrefixLength: numPrefixLength, + scopePrefixLength: 0, ip, }; }; -Packet.Resource.EDNS.ECS.decode = function(reader, length) { +Packet.Resource.EDNS.ECS.decode = function (reader, length) { const rdata = {}; rdata.ednsCode = Packet.EDNS_OPTION_CODE.ECS; rdata.family = reader.read(16); @@ -968,7 +1017,7 @@ Packet.Resource.EDNS.ECS.decode = function(reader, length) { return rdata; }; -Packet.Resource.EDNS.ECS.encode = function(record, writer) { +Packet.Resource.EDNS.ECS.encode = function (record, writer) { // RFC 7871 §6: the ADDRESS field carries only the leftmost // ceil(sourcePrefixLength / 8) octets. const octets = Math.ceil(record.sourcePrefixLength / 8); @@ -997,10 +1046,13 @@ function expandIPv6ToBytes(address) { tail = []; } else { head = address.slice(0, idx).split(':').filter(Boolean); - tail = address.slice(idx + 2).split(':').filter(Boolean); + tail = address + .slice(idx + 2) + .split(':') + .filter(Boolean); } const missing = 8 - head.length - tail.length; - const groups = [ ...head, ...new Array(missing).fill('0'), ...tail ]; + const groups = [...head, ...new Array(missing).fill('0'), ...tail]; const out = new Array(16).fill(0); for (let g = 0; g < 8; g++) { const n = parseInt(groups[g], 16) || 0; @@ -1011,18 +1063,18 @@ function expandIPv6ToBytes(address) { } Packet.Resource.CAA = { - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); // RDLENGTH is written by Packet.Resource.encode. const buffer = Buffer.from(record.tag + record.value, 'utf8'); writer.write(record.flags, 8); writer.write(record.tag.length, 8); - buffer.forEach(function(c) { + buffer.forEach(function (c) { writer.write(c, 8); }); }, - decode: function(reader, length) { + decode: function (reader, length) { this.flags = reader.read(8); const tagLength = reader.read(8); const bytes = []; @@ -1041,21 +1093,21 @@ Packet.Resource.CAA = { * @link https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml#table-dns-sec-alg-numbers-1 */ Packet.Resource.DNSKEY = { - decode: function(reader, length) { + decode: function (reader, length) { const RData = []; while (RData.length < length) { RData.push(reader.read(8)); } - this.flags = RData[0] << 8 | RData[1]; + this.flags = (RData[0] << 8) | RData[1]; this.protocol = RData[2]; this.algorithm = RData[3]; // for key tag let ac = 0; for (let i = 0; i < length; ++i) { - ac += (i & 1) ? RData[i] : RData[i] << 8; + ac += i & 1 ? RData[i] : RData[i] << 8; } - ac += (ac >> 16) & 0xFFFF; - this.keyTag = ac & 0XFFFF; + ac += (ac >> 16) & 0xffff; + this.keyTag = ac & 0xffff; // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 = 16 // convert binary flags @@ -1069,14 +1121,14 @@ Packet.Resource.DNSKEY = { this.key = Buffer.from(RData.slice(4)).toString('base64'); return this; }, - encode: function(record, writer) { + encode: function (record, writer) { writer = writer || new Packet.Writer(); // RDLENGTH is written by Packet.Resource.encode. const buffer = Buffer.from(record.key, 'base64'); writer.write(record.flags, 16); writer.write(record.protocol, 8); writer.write(record.algorithm, 8); - buffer.forEach(function(c) { + buffer.forEach(function (c) { writer.write(c, 8); }); }, @@ -1089,16 +1141,16 @@ Packet.Resource.DNSKEY = { * @type {{decode: (function(*, *): Packet.Resource.RRSIG)}} */ Packet.Resource.RRSIG = { - decode: function(reader, length) { + decode: function (reader, length) { function dateForSig(date) { // javascript date is from millisecond date = new Date(date * 1000); const definitions = { - month : (date.getUTCMonth() + 1), - date : date.getUTCDate(), - hour : date.getUTCHours(), - minutes : date.getUTCMinutes(), - seconds : date.getUTCSeconds(), + month: date.getUTCMonth() + 1, + date: date.getUTCDate(), + hour: date.getUTCHours(), + minutes: date.getUTCMinutes(), + seconds: date.getUTCSeconds(), }; let i; for (i in definitions) { @@ -1107,16 +1159,23 @@ Packet.Resource.RRSIG = { definitions[i] = '0' + '' + definitions[i]; } } - return date.getFullYear() + '' + - definitions.month + '' + - definitions.date + '' + - definitions.hour + '' + - definitions.minutes + '' + - definitions.seconds; + return ( + date.getFullYear() + + '' + + definitions.month + + '' + + definitions.date + + '' + + definitions.hour + + '' + + definitions.minutes + + '' + + definitions.seconds + ); } // calculate max-offset uint8 - const maxOffset = reader.offset + (length * 8); + const maxOffset = reader.offset + length * 8; /* * Stuff sign contains 18 octets */ @@ -1141,19 +1200,19 @@ Packet.Resource.RRSIG = { Packet.Reader = BufferReader; Packet.Writer = BufferWriter; -Packet.createResponseFromRequest = function(request) { +Packet.createResponseFromRequest = function (request) { const response = new Packet(); response.header = new Packet.Header({ - id : request.header.id, - opcode : request.header.opcode, - rd : request.header.rd, - qr : 1, + id: request.header.id, + opcode: request.header.opcode, + rd: request.header.rd, + qr: 1, }); response.questions = request.questions.slice(); return response; }; -Packet.createResourceFromQuestion = function(base, record) { +Packet.createResourceFromQuestion = function (base, record) { const resource = new Packet.Resource(base); Object.assign(resource, record); return resource; @@ -1181,7 +1240,7 @@ Packet.readStream = socket => { } if (!expected && chunklen >= 2) { if (chunks.length > 1) { - chunks = [ Buffer.concat(chunks, chunklen) ]; + chunks = [Buffer.concat(chunks, chunklen)]; } expected = chunks[0].readUInt16BE(0); } @@ -1197,13 +1256,10 @@ Packet.readStream = socket => { * DoH * @docs https://tools.ietf.org/html/rfc8484 */ -Packet.prototype.toBase64URL = function() { +Packet.prototype.toBase64URL = function () { const buffer = this.toBuffer(); const base64 = buffer.toString('base64'); - return base64 - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); }; module.exports = Packet; diff --git a/server/dns.js b/server/dns.js index 00bf6b7..a809495 100644 --- a/server/dns.js +++ b/server/dns.js @@ -9,26 +9,33 @@ class DNSServer extends EventEmitter { super(); this.servers = {}; if (options.doh) { - this.servers.doh = (new DOHServer(options.doh)) - .on('error', error => this.emit('error', error, 'doh')); + this.servers.doh = new DOHServer(options.doh).on('error', error => + this.emit('error', error, 'doh'), + ); } if (options.tcp) { - this.servers.tcp = (new TCPServer()) - .on('error', error => this.emit('error', error, 'tcp')); + this.servers.tcp = new TCPServer().on('error', error => + this.emit('error', error, 'tcp'), + ); } if (options.udp) { - this.servers.udp = (new UDPServer(typeof options.udp === 'object' ? options.udp : undefined)) - .on('error', error => this.emit('error', error, 'udp')); + this.servers.udp = new UDPServer( + typeof options.udp === 'object' ? options.udp : undefined, + ).on('error', error => this.emit('error', error, 'udp')); } const servers = Object.values(this.servers); this.closed = Promise.all( - servers.map(server => new Promise(resolve => server.once('close', resolve))), + servers.map( + server => new Promise(resolve => server.once('close', resolve)), + ), ).then(() => { this.emit('close'); }); this.listening = Promise.all( - servers.map(server => new Promise(resolve => server.once('listening', resolve))), + servers.map( + server => new Promise(resolve => server.once('listening', resolve)), + ), ).then(() => { const addresses = this.addresses(); this.emit('listening', addresses); @@ -52,7 +59,7 @@ class DNSServer extends EventEmitter { }; this.emit('request', request, wrappedSend, client); }; - const emitRequestError = (error) => this.emit('requestError', error); + const emitRequestError = error => this.emit('requestError', error); for (const server of servers) { server.on('request', emitRequest); server.on('requestError', emitRequestError); diff --git a/server/doh.js b/server/doh.js index 6f054e5..44e3763 100644 --- a/server/doh.js +++ b/server/doh.js @@ -8,9 +8,7 @@ const { debuglog } = require('node:util'); const debug = debuglog('dns2-server'); const decodeBase64URL = str => { - let queryData = str - .replace(/-/g, '+') - .replace(/_/g, '/'); + let queryData = str.replace(/-/g, '+').replace(/_/g, '/'); const pad = queryData.length % 4; if (pad === 1) return; if (pad) { @@ -19,13 +17,14 @@ const decodeBase64URL = str => { return queryData; }; -const readStream = stream => new Promise((resolve, reject) => { - const chunks = []; - stream - .on('error', reject) - .on('data', chunk => chunks.push(chunk)) - .on('end', () => resolve(Buffer.concat(chunks))); -}); +const readStream = stream => + new Promise((resolve, reject) => { + const chunks = []; + stream + .on('error', reject) + .on('data', chunk => chunks.push(chunk)) + .on('end', () => resolve(Buffer.concat(chunks))); + }); // RFC 8484 §4.1 — accept the request unless the client explicitly asked for // media types that exclude application/dns-message. @@ -34,28 +33,29 @@ const readStream = stream => new Promise((resolve, reject) => { // like "application/dns-message;q=0" is an explicit rejection even though // the media range matches. Parse q and treat q=0 as absent for matching; // only if no surviving entry matches do we reject the request. -const parseAccept = accept => accept.split(',').map(entry => { - const parts = entry.split(';').map(s => s.trim()); - const type = (parts.shift() || '').toLowerCase(); - let q = 1; - for (const param of parts) { - const [ name, value ] = param.split('=').map(s => s.trim()); - if (name && name.toLowerCase() === 'q') { - const parsed = parseFloat(value); - if (Number.isFinite(parsed)) q = parsed; +const parseAccept = accept => + accept.split(',').map(entry => { + const parts = entry.split(';').map(s => s.trim()); + const type = (parts.shift() || '').toLowerCase(); + let q = 1; + for (const param of parts) { + const [name, value] = param.split('=').map(s => s.trim()); + if (name && name.toLowerCase() === 'q') { + const parsed = parseFloat(value); + if (Number.isFinite(parsed)) q = parsed; + } } - } - return { type, q }; -}); + return { type, q }; + }); const isAcceptable = accept => { if (!accept) return true; - return parseAccept(accept).some(({ type, q }) => - q > 0 && ( - type === '*/*' || - type === 'application/*' || - type === 'application/dns-message' - ), + return parseAccept(accept).some( + ({ type, q }) => + q > 0 && + (type === '*/*' || + type === 'application/*' || + type === 'application/dns-message'), ); }; @@ -69,7 +69,11 @@ const isAcceptable = accept => { // whose TTL field is not a real TTL). const minResponseTtl = packet => { let min = Infinity; - for (const section of [ packet.answers, packet.authorities, packet.additionals ]) { + for (const section of [ + packet.answers, + packet.authorities, + packet.additionals, + ]) { if (!section) continue; for (const rr of section) { if (!rr || typeof rr.ttl !== 'number') continue; @@ -78,7 +82,7 @@ const minResponseTtl = packet => { } } if (!Number.isFinite(min) || min < 0) return 0; - return Math.min(min >>> 0, 0x7FFFFFFF); + return Math.min(min >>> 0, 0x7fffffff); }; class Server extends EventEmitter { @@ -108,13 +112,16 @@ class Server extends EventEmitter { res.setHeader('Vary', 'Origin'); } else if (typeof cors === 'function') { const isAllowed = cors(headers.origin); - res.setHeader('Access-Control-Allow-Origin', isAllowed ? headers.origin : 'false'); + res.setHeader( + 'Access-Control-Allow-Origin', + isAllowed ? headers.origin : 'false', + ); res.setHeader('Vary', 'Origin'); } // debug debug('request', method, url); // We are only handling get and post as reqired by rfc - if ((method !== 'GET' && method !== 'POST')) { + if (method !== 'GET' && method !== 'POST') { res.writeHead(405, { 'Content-Type': 'text/plain' }); res.write('405 Method not allowed\n'); res.end(); @@ -162,10 +169,15 @@ class Server extends EventEmitter { } else if (method === 'POST') { // RFC 8484 §4.1: POST request bodies have Content-Type: // application/dns-message. Anything else is unsupported. - const ct = (headers['content-type'] || '').split(';')[0].trim().toLowerCase(); + const ct = (headers['content-type'] || '') + .split(';')[0] + .trim() + .toLowerCase(); if (ct !== 'application/dns-message') { res.writeHead(415, { 'Content-Type': 'text/plain' }); - res.write('415 Unsupported Media Type: expected application/dns-message\n'); + res.write( + '415 Unsupported Media Type: expected application/dns-message\n', + ); res.end(); return; } diff --git a/server/tcp.js b/server/tcp.js index 3fec086..4faf29f 100644 --- a/server/tcp.js +++ b/server/tcp.js @@ -18,7 +18,8 @@ class Server extends tcp.Server { let idleTimeout = DEFAULT_IDLE_TIMEOUT_MS; if (typeof options === 'object' && options !== null) { proxyProtocolEnabled = options.proxyProtocol ?? false; - if (typeof options.idleTimeout === 'number') idleTimeout = options.idleTimeout; + if (typeof options.idleTimeout === 'number') + idleTimeout = options.idleTimeout; } if (typeof options === 'function') { this.on('request', options); @@ -49,23 +50,33 @@ class Server extends tcp.Server { // stays open until the client sends FIN AND every outstanding response // has been written, an error occurs, or the idle timeout fires. if (this.idleTimeout > 0) client.setTimeout(this.idleTimeout); - readPipelinedMessages(client, state, data => { - let message; - try { - message = Packet.parse(data); - } catch (e) { - this.emit('requestError', e); + readPipelinedMessages( + client, + state, + data => { + let message; + try { + message = Packet.parse(data); + } catch (e) { + this.emit('requestError', e); + client.destroy(); + return; + } + // Increment before emitting so a synchronous handler that calls + // send() inside the listener sees inFlight === 1 → 0, not 0 → -1. + state.inFlight++; + this.emit( + 'request', + message, + this.response.bind(this, client), + client, + ); + }, + err => { + this.emit('requestError', err); client.destroy(); - return; - } - // Increment before emitting so a synchronous handler that calls - // send() inside the listener sees inFlight === 1 → 0, not 0 → -1. - state.inFlight++; - this.emit('request', message, this.response.bind(this, client), client); - }, err => { - this.emit('requestError', err); - client.destroy(); - }); + }, + ); } catch (e) { this.emit('requestError', e); client.destroy(); @@ -83,7 +94,7 @@ class Server extends tcp.Server { const len = Buffer.alloc(2); len.writeUInt16BE(message.length); if (!client.destroyed && client.writable) { - client.write(Buffer.concat([ len, message ])); + client.write(Buffer.concat([len, message])); } // Decrement in-flight and, if the peer has already half-closed and this // was the last outstanding response, half-close our side too. Without @@ -126,9 +137,14 @@ function readPipelinedMessages(socket, state, onMessage, onError) { const onReadable = () => { let chunk; while ((chunk = socket.read()) !== null) { - buffered = buffered.length === 0 ? chunk : Buffer.concat([ buffered, chunk ]); + buffered = + buffered.length === 0 ? chunk : Buffer.concat([buffered, chunk]); + } + try { + drain(); + } catch (e) { + onError(e); } - try { drain(); } catch (e) { onError(e); } }; socket.on('readable', onReadable); @@ -136,7 +152,9 @@ function readPipelinedMessages(socket, state, onMessage, onError) { // Half-close from the peer ends the message stream. If there's a partial // message still in the buffer, that's a framing error. if (expected !== null || buffered.length > 0) { - onError(new Error('TCP message truncated: connection closed mid-message')); + onError( + new Error('TCP message truncated: connection closed mid-message'), + ); return; } state.peerEnded = true; diff --git a/server/udp.js b/server/udp.js index d7a4c5f..26dab05 100644 --- a/server/udp.js +++ b/server/udp.js @@ -83,9 +83,9 @@ class Server extends udp.Socket { if (parsed.header.command === 'PROXY') { clientInfo = { ...rinfo, - address : parsed.header.sourceAddress, - port : parsed.header.sourcePort, - proxy : parsed.header, + address: parsed.header.sourceAddress, + port: parsed.header.sourcePort, + proxy: parsed.header, }; } else { clientInfo = { ...rinfo, proxy: parsed.header }; @@ -93,7 +93,10 @@ class Server extends udp.Socket { data = data.slice(parsed.headerLength); } const message = Packet.parse(data); - const ctx = { rinfo: responder, maxPayload: negotiatedPayloadSize(message) }; + const ctx = { + rinfo: responder, + maxPayload: negotiatedPayloadSize(message), + }; this.emit('request', message, this.response.bind(this, ctx), clientInfo); } catch (e) { this.emit('requestError', e); @@ -113,8 +116,7 @@ class Server extends udp.Socket { } listen(port, address) { - return new Promise(resolve => - this.bind(port, address, resolve)); + return new Promise(resolve => this.bind(port, address, resolve)); } } diff --git a/test/client.js b/test/client.js index 6426259..135f15b 100644 --- a/test/client.js +++ b/test/client.js @@ -12,7 +12,7 @@ const { } = DNS; const udp = require('node:dgram'); -test('client/udp ignores stray response and resolves on matching id', async() => { +test('client/udp ignores stray response and resolves on matching id', async () => { // Simulate the scenario from upstream issue #100: a stray UDP packet (e.g. // late reply on a reused ephemeral port) arrives before the real response. // The client must drop it and keep listening rather than asserting/crashing. @@ -32,23 +32,30 @@ test('client/udp ignores stray response and resolves on matching id', async() => // Real reply, slightly delayed so the stray definitely lands first. const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '1.2.3.4', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '1.2.3.4', }); - setTimeout(() => server.send(response.toBuffer(), rinfo.port, rinfo.address), 5); + setTimeout( + () => server.send(response.toBuffer(), rinfo.port, rinfo.address), + 5, + ); }); - const query = UDPClient({ dns: '127.0.0.1', port: serverPort, timeout: 2000 }); + const query = UDPClient({ + dns: '127.0.0.1', + port: serverPort, + timeout: 2000, + }); const reply = await query('stray.test'); assert.equal(reply.answers.length, 1); assert.equal(reply.answers[0].address, '1.2.3.4'); await new Promise(resolve => server.close(resolve)); }); -test('client/udp times out when no matching response arrives', async() => { +test('client/udp times out when no matching response arrives', async () => { // Server replies with only stray packets; client must time out, not hang. const server = udp.createSocket('udp4'); await new Promise(resolve => server.bind(0, '127.0.0.1', resolve)); @@ -67,7 +74,7 @@ test('client/udp times out when no matching response arrives', async() => { await new Promise(resolve => server.close(resolve)); }); -test('client/udp sends ECS additional when clientIp is set', async() => { +test('client/udp sends ECS additional when clientIp is set', async () => { const server = createUDPServer(); let ednsRecord; let rd; @@ -91,7 +98,7 @@ test('client/udp sends ECS additional when clientIp is set', async() => { await new Promise(resolve => server.close(resolve)); }); -test('client/udp honors recursive=false option', async() => { +test('client/udp honors recursive=false option', async () => { const server = createUDPServer(); let rd; server.on('request', (request, send) => { @@ -108,7 +115,7 @@ test('client/udp honors recursive=false option', async() => { await new Promise(resolve => server.close(resolve)); }); -test('client/udp passes type through to query', async() => { +test('client/udp passes type through to query', async () => { const server = createUDPServer(); let questionType; server.on('request', (request, send) => { @@ -124,7 +131,7 @@ test('client/udp passes type through to query', async() => { await new Promise(resolve => server.close(resolve)); }); -test('client/udp drops packets from unexpected source port', async() => { +test('client/udp drops packets from unexpected source port', async () => { // A response coming from a port other than the configured one must be ignored. const realServer = udp.createSocket('udp4'); const decoy = udp.createSocket('udp4'); @@ -138,7 +145,11 @@ test('client/udp drops packets from unexpected source port', async() => { p.header.qr = 1; p.questions.push({ name, type: Packet.TYPE.A, class: Packet.CLASS.IN }); p.answers.push({ - name, type: Packet.TYPE.A, class: Packet.CLASS.IN, ttl: 60, address, + name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address, }); return p.toBuffer(); }; @@ -150,7 +161,15 @@ test('client/udp drops packets from unexpected source port', async() => { // Decoy reply (correct id, wrong sender) — must be dropped. decoy.send(makeReply(id, name, '9.9.9.9'), rinfo.port, rinfo.address); // Real reply, delayed so the decoy lands first. - setTimeout(() => realServer.send(makeReply(id, name, '1.1.1.1'), rinfo.port, rinfo.address), 10); + setTimeout( + () => + realServer.send( + makeReply(id, name, '1.1.1.1'), + rinfo.port, + rinfo.address, + ), + 10, + ); }); const query = UDPClient({ dns: '127.0.0.1', port: realPort, timeout: 2000 }); @@ -161,16 +180,16 @@ test('client/udp drops packets from unexpected source port', async() => { await new Promise(resolve => decoy.close(resolve)); }); -test('client/tcp end-to-end against local server', async() => { +test('client/tcp end-to-end against local server', async () => { const server = createTCPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '10.20.30.40', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '10.20.30.40', }); send(response); }); @@ -183,7 +202,7 @@ test('client/tcp end-to-end against local server', async() => { await new Promise(resolve => server.close(resolve)); }); -test('client/tcp sends ECS additional when clientIp is set', async() => { +test('client/tcp sends ECS additional when clientIp is set', async () => { const server = createTCPServer(); let ednsRecord; server.on('request', (request, send) => { @@ -194,27 +213,31 @@ test('client/tcp sends ECS additional when clientIp is set', async() => { const { port } = server.address(); const query = TCPClient({ dns: '127.0.0.1', port }); - await query('tcp-ecs.test', 'A', Packet.CLASS.IN, { clientIp: '198.51.100.0/24' }); + await query('tcp-ecs.test', 'A', Packet.CLASS.IN, { + clientIp: '198.51.100.0/24', + }); assert.ok(ednsRecord); assert.equal(ednsRecord.rdata[0].ip, '198.51.100.0'); await new Promise(resolve => server.close(resolve)); }); -test('client/tcp rejects unknown protocol', function() { - assert.throws(() => TCPClient({ dns: '127.0.0.1', protocol: 'udp:' }), - /Protocol must be tcp: or tls:/); +test('client/tcp rejects unknown protocol', function () { + assert.throws( + () => TCPClient({ dns: '127.0.0.1', protocol: 'udp:' }), + /Protocol must be tcp: or tls:/, + ); }); -test('client/doh local http end-to-end', async() => { +test('client/doh local http end-to-end', async () => { const server = createDOHServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '172.16.0.1', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '172.16.0.1', }); send(response); }); @@ -229,9 +252,12 @@ test('client/doh local http end-to-end', async() => { server.close(); }); -test('client/doh', async() => { +test('client/doh', async () => { const timeout = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('DOH client timed out after 10s')), 10000).unref(), + setTimeout( + () => reject(new Error('DOH client timed out after 10s')), + 10000, + ).unref(), ); const res = await Promise.race([ DOHClient({ dns: 'https://1.0.0.1/dns-query' })('cdnjs.com', 'NS'), @@ -253,29 +279,29 @@ test('client/doh', async() => { assert.equal(res.header.rcode, 0); }); -test('dns#resolveSOA returns SOA record via high-level DNS class', async() => { +test('dns#resolveSOA returns SOA record via high-level DNS class', async () => { const server = createUDPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.SOA, - class : Packet.CLASS.IN, - ttl : 3600, - primary : 'ns1.example.com', - admin : 'hostmaster.example.com', - serial : 2024010101, - refresh : 7200, - retry : 3600, - expiration : 1209600, - minimum : 300, + name: request.questions[0].name, + type: Packet.TYPE.SOA, + class: Packet.CLASS.IN, + ttl: 3600, + primary: 'ns1.example.com', + admin: 'hostmaster.example.com', + serial: 2024010101, + refresh: 7200, + retry: 3600, + expiration: 1209600, + minimum: 300, }); send(response); }); await server.listen(0, '127.0.0.1'); const { port } = server.address(); - const dns = new DNS({ nameServers: [ '127.0.0.1' ], port }); + const dns = new DNS({ nameServers: ['127.0.0.1'], port }); const result = await dns.resolveSOA('example.com'); const soa = result.answers[0]; @@ -292,16 +318,16 @@ test('dns#resolveSOA returns SOA record via high-level DNS class', async() => { await new Promise(resolve => server.close(resolve)); }); -test('dns#constructor accepts dns shorthand alias for nameServers', async() => { +test('dns#constructor accepts dns shorthand alias for nameServers', async () => { const server = createUDPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '1.2.3.4', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '1.2.3.4', }); send(response); }); @@ -310,31 +336,43 @@ test('dns#constructor accepts dns shorthand alias for nameServers', async() => { // Use `dns` string shorthand — this is what the docs showed and what broke. const dns = new DNS({ dns: '127.0.0.1', port }); - assert.deepEqual(dns.nameServers, [ '127.0.0.1' ], - '`dns` string is normalised to nameServers array'); + assert.deepEqual( + dns.nameServers, + ['127.0.0.1'], + '`dns` string is normalised to nameServers array', + ); const result = await dns.resolveA('alias.test'); - assert.equal(result.answers[0].address, '1.2.3.4', - 'query reaches the intended server, promise settles'); + assert.equal( + result.answers[0].address, + '1.2.3.4', + 'query reaches the intended server, promise settles', + ); await new Promise(resolve => server.close(resolve)); }); test('dns#constructor accepts dns array shorthand for nameServers', () => { - const dns = new DNS({ dns: [ '1.1.1.1', '8.8.8.8' ] }); - assert.deepEqual(dns.nameServers, [ '1.1.1.1', '8.8.8.8' ], - '`dns` array is normalised to nameServers'); + const dns = new DNS({ dns: ['1.1.1.1', '8.8.8.8'] }); + assert.deepEqual( + dns.nameServers, + ['1.1.1.1', '8.8.8.8'], + '`dns` array is normalised to nameServers', + ); }); test('dns#constructor dns alias does not override explicit nameServers', () => { - const dns = new DNS({ dns: '1.1.1.1', nameServers: [ '9.9.9.9' ] }); - assert.deepEqual(dns.nameServers, [ '9.9.9.9' ], - 'explicit nameServers takes precedence over dns alias'); + const dns = new DNS({ dns: '1.1.1.1', nameServers: ['9.9.9.9'] }); + assert.deepEqual( + dns.nameServers, + ['9.9.9.9'], + 'explicit nameServers takes precedence over dns alias', + ); }); // ── Issue #45 — TC-bit fallback ─────────────────────────────────────────────── -test('client/udp falls back to TCP when TC bit is set', async() => { +test('client/udp falls back to TCP when TC bit is set', async () => { // DNS allows UDP and TCP to share a port. createServer binds both transports // to the same port, letting the UDPClient's TC fallback reach the TCP server. const net = require('node:net'); @@ -360,30 +398,34 @@ test('client/udp falls back to TCP when TC bit is set', async() => { } for (let i = 1; i <= 5; i++) { response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : `10.0.0.${i}`, + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: `10.0.0.${i}`, }); } send(response); }); await server.listen({ - udp : { port, address: '127.0.0.1' }, - tcp : { port, address: '127.0.0.1' }, + udp: { port, address: '127.0.0.1' }, + tcp: { port, address: '127.0.0.1' }, }); const query = UDPClient({ dns: '127.0.0.1', port, timeout: 3000 }); const reply = await query('tc.fallback'); assert.equal(reply.header.tc, 0, 'TCP response does not have TC set'); - assert.equal(reply.answers.length, 5, 'all 5 answers received after TCP fallback'); + assert.equal( + reply.answers.length, + 5, + 'all 5 answers received after TCP fallback', + ); await server.close(); }); -test('client/udp respects retryOverTCP:false and returns truncated packet', async() => { +test('client/udp respects retryOverTCP:false and returns truncated packet', async () => { const udpServer = udp.createSocket('udp4'); await new Promise(resolve => udpServer.bind(0, '127.0.0.1', resolve)); const { port: serverPort } = udpServer.address(); @@ -393,18 +435,27 @@ test('client/udp respects retryOverTCP:false and returns truncated packet', asyn const response = Packet.createResponseFromRequest(request); response.header.tc = 1; response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '1.2.3.4', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '1.2.3.4', }); udpServer.send(response.toBuffer(), rinfo.port, rinfo.address); }); - const query = UDPClient({ dns: '127.0.0.1', port: serverPort, timeout: 2000, retryOverTCP: false }); + const query = UDPClient({ + dns: '127.0.0.1', + port: serverPort, + timeout: 2000, + retryOverTCP: false, + }); const reply = await query('truncated.test'); assert.equal(reply.header.tc, 1, 'TC bit is set (no fallback)'); - assert.equal(reply.answers.length, 1, 'only the truncated single answer returned'); + assert.equal( + reply.answers.length, + 1, + 'only the truncated single answer returned', + ); await new Promise(resolve => udpServer.close(resolve)); }); diff --git a/test/packet.js b/test/packet.js index cc67f48..a60bbca 100644 --- a/test/packet.js +++ b/test/packet.js @@ -3,20 +3,36 @@ const test = require('./test'); const { Packet } = require('..'); const response = Buffer.from([ - 0x29, 0x64, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, - 0x01, 0x7a, 0x02, 0x63, 0x6e, 0x00, 0x00, 0x01, - 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x01, 0x90, 0x00, 0x04, 0x36, 0xde, - 0x3c, 0xfc ]); - -test('Name#encode', function() { + 0x29, 0x64, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x77, 0x77, 0x77, 0x01, 0x7a, 0x02, 0x63, 0x6e, 0x00, 0x00, 0x01, 0x00, 0x01, + 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x90, 0x00, 0x04, 0x36, + 0xde, 0x3c, 0xfc, +]); + +test('Name#encode', function () { const name = Packet.Name.encode('www.google.com'); - const pattern = [ 3, 'w', 'w', 'w', 5, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm', '0' ]; + const pattern = [ + 3, + 'w', + 'w', + 'w', + 5, + 'g', + 'o', + 'o', + 'g', + 'l', + 'e', + 3, + 'c', + 'o', + 'm', + '0', + ]; assert.equal(name.length, pattern.length); }); -test('Name#decode', function() { +test('Name#decode', function () { const reader = new Packet.Reader(response, 8 * 12); let name = Packet.Name.decode(reader); assert.equal(name, 'www.z.cn'); @@ -27,15 +43,19 @@ test('Name#decode', function() { assert.equal(name, 'www.z.cn'); }); -test('Header#encode', function() { +test('Header#encode', function () { const header = new Packet.Header({ id: 0x2964, qr: 1 }); header.qdcount = 1; header.ancount = 2; - assert.deepEqual(header.toBuffer(), Buffer.from([ - 0x29, 0x64, 0x80, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00 ])); + assert.deepEqual( + header.toBuffer(), + Buffer.from([ + 0x29, 0x64, 0x80, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + ]), + ); }); -test('Header#parse', function() { +test('Header#parse', function () { const header = Packet.Header.parse(response); assert.equal(header.id, 0x2964); assert.equal(header.qr, 1); @@ -51,70 +71,156 @@ test('Header#parse', function() { assert.equal(header.arcount, 0); }); -test('Question#encode', function() { +test('Question#encode', function () { const question = new Packet.Question({ - name : 'google.com', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, + name: 'google.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, }); // - assert.deepEqual(question.toBuffer(), Buffer.from([ - 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, - 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, - ])); -}); - -test('Question#decode', function() { - const question = new Packet.Question('google.com', - Packet.TYPE.A, Packet.CLASS.IN); - assert.deepEqual(question.toBuffer(), Buffer.from([ - 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, - 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, - ])); + assert.deepEqual( + question.toBuffer(), + Buffer.from([ + 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + ]), + ); +}); + +test('Question#decode', function () { + const question = new Packet.Question( + 'google.com', + Packet.TYPE.A, + Packet.CLASS.IN, + ); + assert.deepEqual( + question.toBuffer(), + Buffer.from([ + 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + 0x00, 0x01, 0x00, 0x01, + ]), + ); }); // -test('Package#toIPv6', function() { - assert.equal(Packet.toIPv6([ 10756, 20034, 512, 0, 0, 0, 0, 803 ]), '2a04:4e42:200::323'); - assert.equal(Packet.toIPv6([ 10755, 45248, 3, 208, 0, 0, 5057, 61441 ]), '2a03:b0c0:3:d0::13c1:f001'); - assert.equal(Packet.toIPv6([ 10752, 5200, 16387, 2055, 0, 0, 0, 8206 ]), '2a00:1450:4003:807::200e'); - assert.equal(Packet.toIPv6([ 9734, 18176, 12552, 0, 0, 0, 44098, 10984 ]), '2606:4700:3108::ac42:2ae8'); -}); - -test('Package#toIPv6 RFC 5952 — leading-zero addresses', function() { - assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0, 0, 1 ]), '::1'); - assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0, 0, 0 ]), '::'); - assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0xffff, 0xc000, 0x0201 ]), '::ffff:c000:201'); -}); - -test('Package#toIPv6 RFC 5952 — trailing-zero addresses', function() { - assert.equal(Packet.toIPv6([ 1, 0, 0, 0, 0, 0, 0, 0 ]), '1::'); - assert.equal(Packet.toIPv6([ 0x2001, 0xdb8, 0, 0, 0, 0, 0, 0 ]), '2001:db8::'); -}); - -test('Package#toIPv6 RFC 5952 — single zero group is not compressed', function() { +test('Package#toIPv6', function () { + assert.equal( + Packet.toIPv6([10756, 20034, 512, 0, 0, 0, 0, 803]), + '2a04:4e42:200::323', + ); + assert.equal( + Packet.toIPv6([10755, 45248, 3, 208, 0, 0, 5057, 61441]), + '2a03:b0c0:3:d0::13c1:f001', + ); + assert.equal( + Packet.toIPv6([10752, 5200, 16387, 2055, 0, 0, 0, 8206]), + '2a00:1450:4003:807::200e', + ); + assert.equal( + Packet.toIPv6([9734, 18176, 12552, 0, 0, 0, 44098, 10984]), + '2606:4700:3108::ac42:2ae8', + ); +}); + +test('Package#toIPv6 RFC 5952 — leading-zero addresses', function () { + assert.equal(Packet.toIPv6([0, 0, 0, 0, 0, 0, 0, 1]), '::1'); + assert.equal(Packet.toIPv6([0, 0, 0, 0, 0, 0, 0, 0]), '::'); + assert.equal( + Packet.toIPv6([0, 0, 0, 0, 0, 0xffff, 0xc000, 0x0201]), + '::ffff:c000:201', + ); +}); + +test('Package#toIPv6 RFC 5952 — trailing-zero addresses', function () { + assert.equal(Packet.toIPv6([1, 0, 0, 0, 0, 0, 0, 0]), '1::'); + assert.equal(Packet.toIPv6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]), '2001:db8::'); +}); + +test('Package#toIPv6 RFC 5952 — single zero group is not compressed', function () { // §4.2.2: "::" MUST NOT be used to shorten just one 16-bit 0 field. - assert.equal(Packet.toIPv6([ 1, 0, 1, 1, 1, 1, 1, 1 ]), '1:0:1:1:1:1:1:1'); + assert.equal(Packet.toIPv6([1, 0, 1, 1, 1, 1, 1, 1]), '1:0:1:1:1:1:1:1'); }); -test('Package#toIPv6 RFC 5952 — first run wins on tie', function() { +test('Package#toIPv6 RFC 5952 — first run wins on tie', function () { // §4.2.3: when there is more than one run of equal maximum length, // the first is shortened. - assert.equal(Packet.toIPv6([ 1, 0, 0, 1, 0, 0, 1, 1 ]), '1::1:0:0:1:1'); + assert.equal(Packet.toIPv6([1, 0, 0, 1, 0, 0, 1, 1]), '1::1:0:0:1:1'); }); -test('Package#fromIPv6', function() { +test('Package#fromIPv6', function () { assert.deepEqual(Packet.fromIPv6('2a04:4e42:200::323'), [ - '2a04', '4e42', '0200', '0', '0', '0', '0', '0323' ]); - assert.deepEqual(Packet.fromIPv6('2a03:b0c0:3:d0::13c1:f001'), [ '2a03', 'b0c0', '0003', '00d0', '0', '0', '13c1', 'f001' ]); - assert.deepEqual(Packet.fromIPv6('2a00:1450:4003:807::200e'), [ '2a00', '1450', '4003', '0807', '0', '0', '0', '200e' ]); - assert.deepEqual(Packet.fromIPv6('2606:4700:3108::ac42:2ae8'), [ '2606', '4700', '3108', '0', '0', '0', 'ac42', '2ae8' ]); - assert.deepEqual(Packet.fromIPv6('::'), [ '0', '0', '0', '0', '0', '0', '0', '0' ]); - assert.deepEqual(Packet.fromIPv6('::2606:4700:3108'), [ '0', '0', '0', '0', '0', '2606', '4700', '3108' ]); - assert.deepEqual(Packet.fromIPv6('606:4700:3108::'), [ '0606', '4700', '3108', '0', '0', '0', '0', '0' ]); + '2a04', + '4e42', + '0200', + '0', + '0', + '0', + '0', + '0323', + ]); + assert.deepEqual(Packet.fromIPv6('2a03:b0c0:3:d0::13c1:f001'), [ + '2a03', + 'b0c0', + '0003', + '00d0', + '0', + '0', + '13c1', + 'f001', + ]); + assert.deepEqual(Packet.fromIPv6('2a00:1450:4003:807::200e'), [ + '2a00', + '1450', + '4003', + '0807', + '0', + '0', + '0', + '200e', + ]); + assert.deepEqual(Packet.fromIPv6('2606:4700:3108::ac42:2ae8'), [ + '2606', + '4700', + '3108', + '0', + '0', + '0', + 'ac42', + '2ae8', + ]); + assert.deepEqual(Packet.fromIPv6('::'), [ + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + ]); + assert.deepEqual(Packet.fromIPv6('::2606:4700:3108'), [ + '0', + '0', + '0', + '0', + '0', + '2606', + '4700', + '3108', + ]); + assert.deepEqual(Packet.fromIPv6('606:4700:3108::'), [ + '0606', + '4700', + '3108', + '0', + '0', + '0', + '0', + '0', + ]); }); -test('Packet#parse', function() { +test('Packet#parse', function () { const packet = Packet.parse(response); assert.equal(packet.questions[0].name, 'www.z.cn'); assert.equal(packet.questions[0].type, Packet.TYPE.A); @@ -124,121 +230,126 @@ test('Packet#parse', function() { assert.equal(packet.answers[0].address, '54.222.60.252'); }); -test('Packet#encode', function() { +test('Packet#encode', function () { const response = new Packet(); // response.header.qr = 1; response.answers.push({ - name : 'lsong.org', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '127.0.0.1', + name: 'lsong.org', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '127.0.0.1', }); response.answers.push({ - name : 'lsong.org', - type : Packet.TYPE.AAAA, - class : Packet.CLASS.IN, - ttl : 300, - address : '2001:db8::ff00:42:8329', + name: 'lsong.org', + type: Packet.TYPE.AAAA, + class: Packet.CLASS.IN, + ttl: 300, + address: '2001:db8::ff00:42:8329', }); response.answers.push({ - name : 'lsong.org', - type : Packet.TYPE.CNAME, - class : Packet.CLASS.IN, - ttl : 300, - domain : 'sfo1.lsong.org', + name: 'lsong.org', + type: Packet.TYPE.CNAME, + class: Packet.CLASS.IN, + ttl: 300, + domain: 'sfo1.lsong.org', }); response.answers.push({ - name : 'lsong.org', - type : Packet.TYPE.PTR, - class : Packet.CLASS.IN, - ttl : 300, - domain : 'sfo1.lsong.org', + name: 'lsong.org', + type: Packet.TYPE.PTR, + class: Packet.CLASS.IN, + ttl: 300, + domain: 'sfo1.lsong.org', }); // DNS KEY response.answers.push({ - name : 'lsong.org', - ttl : 300, - type : 48, - class : 1, - flags : 256, - protocol : 3, - algorithm : 13, - keyTag : 1721, - zoneKey : true, - zoneSep : false, - key : 'PM8S6PI0Gf8d3HK9gHSVpW3X3zeieMEa+PLCijFuaFgiIANdUQen5xNn0/9+eo3E4VIJGU27lk6q4xXqMuQl7A==', + name: 'lsong.org', + ttl: 300, + type: 48, + class: 1, + flags: 256, + protocol: 3, + algorithm: 13, + keyTag: 1721, + zoneKey: true, + zoneSep: false, + key: 'PM8S6PI0Gf8d3HK9gHSVpW3X3zeieMEa+PLCijFuaFgiIANdUQen5xNn0/9+eo3E4VIJGU27lk6q4xXqMuQl7A==', }); response.authorities.push({ - name : 'lsong.org', - type : Packet.TYPE.MX, - class : Packet.CLASS.IN, - ttl : 300, - exchange : 'mail.lsong.org', - priority : 5, + name: 'lsong.org', + type: Packet.TYPE.MX, + class: Packet.CLASS.IN, + ttl: 300, + exchange: 'mail.lsong.org', + priority: 5, }); response.authorities.push({ - name : 'lsong.org', - type : Packet.TYPE.NS, - class : Packet.CLASS.IN, - ttl : 300, - ns : 'ns1.lsong.org', + name: 'lsong.org', + type: Packet.TYPE.NS, + class: Packet.CLASS.IN, + ttl: 300, + ns: 'ns1.lsong.org', }); response.additionals.push({ - name : 'lsong.org', - type : Packet.TYPE.SOA, - class : Packet.CLASS.IN, - ttl : 300, - primary : 'lsong.org', - admin : 'admin@lsong.org', - serial : 2016121301, - refresh : 300, - retry : 3, - expiration : 10, - minimum : 10, + name: 'lsong.org', + type: Packet.TYPE.SOA, + class: Packet.CLASS.IN, + ttl: 300, + primary: 'lsong.org', + admin: 'admin@lsong.org', + serial: 2016121301, + refresh: 300, + retry: 3, + expiration: 10, + minimum: 10, }); // response.additionals.push({ - name : 'lsong.org', - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 300, - data : '#v=spf1 include:_spf.google.com ~all', + name: 'lsong.org', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 300, + data: '#v=spf1 include:_spf.google.com ~all', }); assert.deepEqual(Packet.parse(response.toBuffer()), response); }); -test('Packet#encode array of character strings', function() { +test('Packet#encode array of character strings', function () { const response = new Packet(); // - const dkim = [ 'v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsD6Th73ZDKkFAntNZDbx', + const dkim = [ + 'v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsD6Th73ZDKkFAntNZDbx', 'Eh8VV2DSMs3re6v9/gXoT3dGcbSsuUMpfLzP5MWp4sW5cPyZxEGSiC03ZVIcCca0GRAuX9b1M0Qy25wLmPq', '8eT129mhwbeX50xTaXqq63A/oDM0QOPe1IeBMfPnR9tWXxvEzZKvVbmTlMY5bf+3QHLqmaEihnGlXh2LRVZ', 'be2EMlYo18YM4LU/LkZKe06rxlq38W22TL7964tr7jmOZ+huXf2iLSg4nc4UzLwb2aOdOA+w4c87h+HW/L8', - '0548pFguF46TKc0C0egZ+oll3Y8zySYrbkVrWFrcpnrw5qDiRVHEjxqZSubSYX+16TjNcJg9QIDAQAB' ]; + '0548pFguF46TKc0C0egZ+oll3Y8zySYrbkVrWFrcpnrw5qDiRVHEjxqZSubSYX+16TjNcJg9QIDAQAB', + ]; response.header.qr = 1; response.answers.push({ - name : 'lsong.org', - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 300, - data : dkim, + name: 'lsong.org', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 300, + data: dkim, }); - assert.equal(Packet.parse(response.toBuffer()).answers[0].data, dkim.join('')); + assert.equal( + Packet.parse(response.toBuffer()).answers[0].data, + dkim.join(''), + ); }); -test('EDNS.ECS#encode', function() { +test('EDNS.ECS#encode', function () { const query = new Packet.Resource.EDNS([ new Packet.Resource.EDNS.ECS('10.11.12.13/24'), ]); @@ -247,14 +358,19 @@ test('EDNS.ECS#encode', function() { // so /24 writes 3 address bytes (10.11.12), not 4. // class=0x1000=4096 is the RFC 6891 §6.2.5 default UDP payload size. const b = Packet.Resource.encode(query); - assert.deepEqual(b, Buffer.from([ - 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x07, 0x00, - 0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c ])); + assert.deepEqual( + b, + Buffer.from([ + 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, + 0x08, 0x00, 0x07, 0x00, 0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c, + ]), + ); }); -test('EDNS#decode', function() { - const buffer = Buffer.from([ 0x00, 0x08, 0x00, 0x08, 0x00, 0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c, 0x0d ]); +test('EDNS#decode', function () { + const buffer = Buffer.from([ + 0x00, 0x08, 0x00, 0x08, 0x00, 0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c, 0x0d, + ]); const reader = new Packet.Reader(buffer); const record = Packet.Resource.EDNS.decode(reader, buffer.length); @@ -275,7 +391,7 @@ test('EDNS#decode', function() { assert.deepEqual(decoded, query); }); -test('EDNS#decode multiple', function() { +test('EDNS#decode multiple', function () { const query = new Packet.Resource.EDNS([ new Packet.Resource.EDNS.ECS('10.0.0.0/8'), new Packet.Resource.EDNS.ECS('10.9.0.0/16'), @@ -298,13 +414,13 @@ function roundTripAnswer(answer) { return parsed.answers[0]; } -test('Resource#A round-trip', function() { +test('Resource#A round-trip', function () { const out = roundTripAnswer({ - name : 'a.example.com', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '203.0.113.42', + name: 'a.example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '203.0.113.42', }); assert.equal(out.name, 'a.example.com'); assert.equal(out.type, Packet.TYPE.A); @@ -313,75 +429,75 @@ test('Resource#A round-trip', function() { assert.equal(out.address, '203.0.113.42'); }); -test('Resource#AAAA round-trip preserves compressed form', function() { +test('Resource#AAAA round-trip preserves compressed form', function () { const out = roundTripAnswer({ - name : 'v6.example.com', - type : Packet.TYPE.AAAA, - class : Packet.CLASS.IN, - ttl : 300, - address : '2001:db8::1', + name: 'v6.example.com', + type: Packet.TYPE.AAAA, + class: Packet.CLASS.IN, + ttl: 300, + address: '2001:db8::1', }); assert.equal(out.type, Packet.TYPE.AAAA); // toIPv6 normalizes, so we compare against the normalized form assert.equal(out.address, '2001:db8::1'); }); -test('Resource#CNAME round-trip', function() { +test('Resource#CNAME round-trip', function () { const out = roundTripAnswer({ - name : 'alias.example.com', - type : Packet.TYPE.CNAME, - class : Packet.CLASS.IN, - ttl : 3600, - domain : 'canonical.example.com', + name: 'alias.example.com', + type: Packet.TYPE.CNAME, + class: Packet.CLASS.IN, + ttl: 3600, + domain: 'canonical.example.com', }); assert.equal(out.domain, 'canonical.example.com'); }); -test('Resource#PTR round-trip', function() { +test('Resource#PTR round-trip', function () { const out = roundTripAnswer({ - name : '1.0.0.127.in-addr.arpa', - type : Packet.TYPE.PTR, - class : Packet.CLASS.IN, - ttl : 86400, - domain : 'localhost', + name: '1.0.0.127.in-addr.arpa', + type: Packet.TYPE.PTR, + class: Packet.CLASS.IN, + ttl: 86400, + domain: 'localhost', }); assert.equal(out.domain, 'localhost'); }); -test('Resource#NS round-trip', function() { +test('Resource#NS round-trip', function () { const out = roundTripAnswer({ - name : 'example.com', - type : Packet.TYPE.NS, - class : Packet.CLASS.IN, - ttl : 172800, - ns : 'ns1.example.com', + name: 'example.com', + type: Packet.TYPE.NS, + class: Packet.CLASS.IN, + ttl: 172800, + ns: 'ns1.example.com', }); assert.equal(out.ns, 'ns1.example.com'); }); -test('Resource#MX round-trip', function() { +test('Resource#MX round-trip', function () { const out = roundTripAnswer({ - name : 'example.com', - type : Packet.TYPE.MX, - class : Packet.CLASS.IN, - ttl : 300, - exchange : 'mail.example.com', - priority : 10, + name: 'example.com', + type: Packet.TYPE.MX, + class: Packet.CLASS.IN, + ttl: 300, + exchange: 'mail.example.com', + priority: 10, }); assert.equal(out.exchange, 'mail.example.com'); assert.equal(out.priority, 10); }); -test('Resource#SRV round-trip', function() { +test('Resource#SRV round-trip', function () { const out = roundTripAnswer({ - name : '_sip._tcp.example.com', - type : Packet.TYPE.SRV, - class : Packet.CLASS.IN, - ttl : 300, - priority : 10, - weight : 60, - port : 5060, - target : 'sipserver.example.com', + name: '_sip._tcp.example.com', + type: Packet.TYPE.SRV, + class: Packet.CLASS.IN, + ttl: 300, + priority: 10, + weight: 60, + port: 5060, + target: 'sipserver.example.com', }); assert.equal(out.priority, 10); assert.equal(out.weight, 60); @@ -389,41 +505,41 @@ test('Resource#SRV round-trip', function() { assert.equal(out.target, 'sipserver.example.com'); }); -test('Resource#TXT round-trip single string', function() { +test('Resource#TXT round-trip single string', function () { const out = roundTripAnswer({ - name : 'example.com', - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 300, - data : 'hello world', + name: 'example.com', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 300, + data: 'hello world', }); assert.equal(out.data, 'hello world'); }); -test('Resource#TXT round-trip with utf-8', function() { +test('Resource#TXT round-trip with utf-8', function () { const out = roundTripAnswer({ - name : 'example.com', - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 300, - data : 'café résumé 日本', + name: 'example.com', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 300, + data: 'café résumé 日本', }); assert.equal(out.data, 'café résumé 日本'); }); -test('Resource#SOA round-trip', function() { +test('Resource#SOA round-trip', function () { const out = roundTripAnswer({ - name : 'example.com', - type : Packet.TYPE.SOA, - class : Packet.CLASS.IN, - ttl : 3600, - primary : 'ns1.example.com', - admin : 'hostmaster.example.com', - serial : 2024010101, - refresh : 7200, - retry : 3600, - expiration : 1209600, - minimum : 86400, + name: 'example.com', + type: Packet.TYPE.SOA, + class: Packet.CLASS.IN, + ttl: 3600, + primary: 'ns1.example.com', + admin: 'hostmaster.example.com', + serial: 2024010101, + refresh: 7200, + retry: 3600, + expiration: 1209600, + minimum: 86400, }); assert.equal(out.primary, 'ns1.example.com'); assert.equal(out.admin, 'hostmaster.example.com'); @@ -434,16 +550,16 @@ test('Resource#SOA round-trip', function() { assert.equal(out.minimum, 86400); }); -test('Resource#DNSKEY round-trip preserves keyTag and flags', function() { +test('Resource#DNSKEY round-trip preserves keyTag and flags', function () { const out = roundTripAnswer({ - name : 'example.com', - type : Packet.TYPE.DNSKEY, - class : Packet.CLASS.IN, - ttl : 3600, - flags : 257, - protocol : 3, - algorithm : 8, - key : 'AwEAAdHoNTOW+et86KuJOWRD3iY/HsZ6dQ4FFNS1Z+0DxiAk7BWv', + name: 'example.com', + type: Packet.TYPE.DNSKEY, + class: Packet.CLASS.IN, + ttl: 3600, + flags: 257, + protocol: 3, + algorithm: 8, + key: 'AwEAAdHoNTOW+et86KuJOWRD3iY/HsZ6dQ4FFNS1Z+0DxiAk7BWv', }); assert.equal(out.flags, 257); assert.equal(out.protocol, 3); @@ -454,16 +570,19 @@ test('Resource#DNSKEY round-trip preserves keyTag and flags', function() { assert.ok(typeof out.keyTag === 'number'); }); -test('Resource#CAA encode produces correct wire bytes', function() { +test('Resource#CAA encode produces correct wire bytes', function () { // CAA only has an encoder in this library; verify the rdata layout directly. // RDLENGTH is owned by Packet.Resource.encode now (so it can back-fill the // value after compression), and is not emitted by per-type encoders. const writer = new Packet.Writer(); - Packet.Resource.CAA.encode({ - flags : 0, - tag : 'issue', - value : 'letsencrypt.org', - }, writer); + Packet.Resource.CAA.encode( + { + flags: 0, + tag: 'issue', + value: 'letsencrypt.org', + }, + writer, + ); const buffer = writer.toBuffer(); // Layout (rdata only): [ flags, tagLen, tag..., value... ] assert.equal(buffer[0], 0); // flags @@ -472,13 +591,11 @@ test('Resource#CAA encode produces correct wire bytes', function() { assert.equal(buffer.slice(2 + 5).toString(), 'letsencrypt.org'); }); -test('EDNS.ECS#decode family=2 (IPv6)', function() { +test('EDNS.ECS#decode family=2 (IPv6)', function () { // Hand-built rdata for ECS with IPv6 family covering "2001:db8::/32" // (4 bytes of address: 0x20 0x01 0x0d 0xb8). Format: // family(16=0x0002) | srcPrefix(8=32) | scopePrefix(8=0) | addr bytes - const buffer = Buffer.from([ - 0x00, 0x02, 0x20, 0x00, 0x20, 0x01, 0x0d, 0xb8, - ]); + const buffer = Buffer.from([0x00, 0x02, 0x20, 0x00, 0x20, 0x01, 0x0d, 0xb8]); const reader = new Packet.Reader(buffer); const rdata = Packet.Resource.EDNS.ECS.decode(reader, buffer.length); assert.equal(rdata.family, 2); @@ -487,15 +604,22 @@ test('EDNS.ECS#decode family=2 (IPv6)', function() { assert.equal(rdata.ip, '2001:db8:0:0:0:0:0:0'); }); -test('Packet#toBase64URL is reversible', function() { +test('Packet#toBase64URL is reversible', function () { const packet = new Packet(); packet.header.id = 0x1234; packet.header.rd = 1; - packet.questions.push({ name: 'example.com', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + packet.questions.push({ + name: 'example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const url = packet.toBase64URL(); // No padding, no '+' or '/' assert.ok(!/[+/=]/.test(url)); - const restored = Buffer.from(url.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); + const restored = Buffer.from( + url.replace(/-/g, '+').replace(/_/g, '/'), + 'base64', + ); const parsed = Packet.parse(restored); assert.equal(parsed.header.id, 0x1234); assert.equal(parsed.header.rd, 1); @@ -503,12 +627,22 @@ test('Packet#toBase64URL is reversible', function() { assert.equal(parsed.questions[0].type, Packet.TYPE.A); }); -test('Packet.createResponseFromRequest sets qr=1 and clears additionals', function() { +test('Packet.createResponseFromRequest sets qr=1 and clears additionals', function () { const request = new Packet(); request.header.id = 0xabcd; request.header.rd = 1; - request.questions.push({ name: 'foo.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); - request.additionals.push({ name: 'foo.test', type: Packet.TYPE.A, class: 1, ttl: 1, address: '1.1.1.1' }); + request.questions.push({ + name: 'foo.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); + request.additionals.push({ + name: 'foo.test', + type: Packet.TYPE.A, + class: 1, + ttl: 1, + address: '1.1.1.1', + }); const response = Packet.createResponseFromRequest(request); assert.equal(response.header.qr, 1); @@ -517,11 +651,15 @@ test('Packet.createResponseFromRequest sets qr=1 and clears additionals', functi assert.deepEqual(response.additionals, []); }); -test('Packet.createResourceFromQuestion copies name and applies record fields', function() { - const question = { name: 'svc.example.com', type: Packet.TYPE.A, class: Packet.CLASS.IN }; +test('Packet.createResourceFromQuestion copies name and applies record fields', function () { + const question = { + name: 'svc.example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }; const resource = Packet.createResourceFromQuestion(question, { - ttl : 120, - address : '198.51.100.7', + ttl: 120, + address: '198.51.100.7', }); assert.equal(resource.name, 'svc.example.com'); assert.equal(resource.type, Packet.TYPE.A); @@ -530,7 +668,7 @@ test('Packet.createResourceFromQuestion copies name and applies record fields', assert.equal(resource.address, '198.51.100.7'); }); -test('Packet#recursive getter/setter mirrors header.rd', function() { +test('Packet#recursive getter/setter mirrors header.rd', function () { const packet = new Packet(); assert.equal(packet.recursive, false); packet.recursive = true; @@ -541,13 +679,13 @@ test('Packet#recursive getter/setter mirrors header.rd', function() { assert.equal(packet.recursive, false); }); -test('Packet constructor accepts string as question name', function() { +test('Packet constructor accepts string as question name', function () { const packet = new Packet('lookup.example'); assert.equal(packet.questions.length, 1); assert.equal(packet.questions[0], 'lookup.example'); }); -test('Packet constructor accepts array of questions', function() { +test('Packet constructor accepts array of questions', function () { const questions = [ { name: 'a.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }, { name: 'b.test', type: Packet.TYPE.AAAA, class: Packet.CLASS.IN }, @@ -556,43 +694,43 @@ test('Packet constructor accepts array of questions', function() { assert.deepEqual(packet.questions, questions); }); -test('Packet constructor accepts Header instance', function() { +test('Packet constructor accepts Header instance', function () { const header = new Packet.Header({ id: 0x5555, qr: 1 }); const packet = new Packet(header); assert.equal(packet.header.id, 0x5555); assert.equal(packet.header.qr, 1); }); -test('Reader.read at non-byte-aligned offsets', function() { +test('Reader.read at non-byte-aligned offsets', function () { // Buffer: 0b10110010 0b01101100 = 0xB2 0x6C // Read 3 bits → 101 = 5 // Read 5 bits → 10010 = 18 // Read 4 bits → 0110 = 6 // Read 4 bits → 1100 = 12 - const reader = new Packet.Reader(Buffer.from([ 0xB2, 0x6C ])); + const reader = new Packet.Reader(Buffer.from([0xb2, 0x6c])); assert.equal(reader.read(3), 5); assert.equal(reader.read(5), 18); assert.equal(reader.read(4), 6); assert.equal(reader.read(4), 12); }); -test('Writer→Reader round-trip at byte-aligned widths', function() { +test('Writer→Reader round-trip at byte-aligned widths', function () { const writer = new Packet.Writer(); writer.write(0x12, 8); - writer.write(0xABCD, 16); - writer.write(0xDEADBEEF, 32); + writer.write(0xabcd, 16); + writer.write(0xdeadbeef, 32); const buffer = writer.toBuffer(); assert.equal(buffer.length, 7); const reader = new Packet.Reader(buffer); assert.equal(reader.read(8), 0x12); - assert.equal(reader.read(16), 0xABCD); - assert.equal(reader.read(32), 0xDEADBEEF); + assert.equal(reader.read(16), 0xabcd); + assert.equal(reader.read(32), 0xdeadbeef); }); -test('Writer→Reader header-shape bitfield round-trip', function() { +test('Writer→Reader header-shape bitfield round-trip', function () { // Mirrors Packet.Header layout: 16+1+4+1+1+1+1+3+4 = 32 bits const writer = new Packet.Writer(); - writer.write(0xCAFE, 16); + writer.write(0xcafe, 16); writer.write(1, 1); // qr writer.write(0, 4); // opcode writer.write(1, 1); // aa @@ -604,7 +742,7 @@ test('Writer→Reader header-shape bitfield round-trip', function() { const buffer = writer.toBuffer(); assert.equal(buffer.length, 4); const reader = new Packet.Reader(buffer); - assert.equal(reader.read(16), 0xCAFE); + assert.equal(reader.read(16), 0xcafe); assert.equal(reader.read(1), 1); assert.equal(reader.read(4), 0); assert.equal(reader.read(1), 1); @@ -615,7 +753,7 @@ test('Writer→Reader header-shape bitfield round-trip', function() { assert.equal(reader.read(4), 2); }); -test('Packet.Name encode/decode round-trip handles long labels', function() { +test('Packet.Name encode/decode round-trip handles long labels', function () { // 63 chars is the max single-label length per RFC 1035 const label = 'a'.repeat(63); const name = `${label}.example.com`; @@ -625,24 +763,24 @@ test('Packet.Name encode/decode round-trip handles long labels', function() { assert.equal(Packet.Name.decode(reader), name); }); -test('Packet.Name encode filters empty labels (trailing dot)', function() { +test('Packet.Name encode filters empty labels (trailing dot)', function () { // Trailing dot is canonical in DNS but the encoder drops empty parts. const a = Packet.Name.encode('example.com.'); const b = Packet.Name.encode('example.com'); assert.deepEqual(a, b); }); -test('Resource#CAA round-trip via Packet.parse', function() { +test('Resource#CAA round-trip via Packet.parse', function () { const packet = new Packet(); packet.header.qr = 1; packet.answers.push({ - name : 'example.com', - type : Packet.TYPE.CAA, - class : Packet.CLASS.IN, - ttl : 300, - flags : 0, - tag : 'issue', - value : 'letsencrypt.org', + name: 'example.com', + type: Packet.TYPE.CAA, + class: Packet.CLASS.IN, + ttl: 300, + flags: 0, + tag: 'issue', + value: 'letsencrypt.org', }); const parsed = Packet.parse(packet.toBuffer()); assert.equal(parsed.answers.length, 1); @@ -651,31 +789,50 @@ test('Resource#CAA round-trip via Packet.parse', function() { assert.equal(parsed.answers[0].value, 'letsencrypt.org'); }); -test('Packet.createResponseFromRequest does not mutate request', function() { +test('Packet.createResponseFromRequest does not mutate request', function () { const request = new Packet(); request.header.id = 0x1234; - request.questions.push({ name: 'x.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + request.questions.push({ + name: 'x.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); request.additionals.push({ - name: 'opt', type: Packet.TYPE.EDNS, class: 512, ttl: 0, rdata: [], + name: 'opt', + type: Packet.TYPE.EDNS, + class: 512, + ttl: 0, + rdata: [], }); const originalAdditionalsLength = request.additionals.length; const originalQr = request.header.qr; const response = Packet.createResponseFromRequest(request); - assert.notStrictEqual(response, request, 'response should be a distinct object'); - assert.equal(request.header.qr, originalQr, 'request.header.qr should not be mutated'); - assert.equal(request.additionals.length, originalAdditionalsLength, - 'request.additionals should not be cleared'); -}); - -test('Reader.read across non-aligned multi-byte offsets', function() { + assert.notStrictEqual( + response, + request, + 'response should be a distinct object', + ); + assert.equal( + request.header.qr, + originalQr, + 'request.header.qr should not be mutated', + ); + assert.equal( + request.additionals.length, + originalAdditionalsLength, + 'request.additionals should not be cleared', + ); +}); + +test('Reader.read across non-aligned multi-byte offsets', function () { // 0xAB=10101011, 0xCD=11001101, 0xEF=11101111 // After consuming 4 bits, bits 4-19 are: 1011 11001101 1110 = 0xBCDE - const reader = new Packet.Reader(Buffer.from([ 0xAB, 0xCD, 0xEF ])); - assert.equal(reader.read(4), 0xA); - assert.equal(reader.read(16), 0xBCDE); + const reader = new Packet.Reader(Buffer.from([0xab, 0xcd, 0xef])); + assert.equal(reader.read(4), 0xa); + assert.equal(reader.read(16), 0xbcde); }); -test('Packet.RCODE contains all standard error codes', function() { +test('Packet.RCODE contains all standard error codes', function () { assert.equal(Packet.RCODE.NOERROR, 0); assert.equal(Packet.RCODE.FORMERR, 1); assert.equal(Packet.RCODE.SERVFAIL, 2); @@ -684,157 +841,176 @@ test('Packet.RCODE contains all standard error codes', function() { assert.equal(Packet.RCODE.REFUSED, 5); }); -test('Packet.RCODE is preserved through encode/parse round-trip', function() { - for (const [ name, code ] of Object.entries(Packet.RCODE)) { +test('Packet.RCODE is preserved through encode/parse round-trip', function () { + for (const [name, code] of Object.entries(Packet.RCODE)) { const pkt = new Packet(); pkt.header.id = 0x1234; pkt.header.qr = 1; pkt.header.rcode = code; const parsed = Packet.parse(pkt.toBuffer()); - assert.equal(parsed.header.rcode, code, - `RCODE.${name} (${code}) did not survive encode→parse`); + assert.equal( + parsed.header.rcode, + code, + `RCODE.${name} (${code}) did not survive encode→parse`, + ); } }); -test('Resource encode round-trips unknown type via raw data fallback', function() { +test('Resource encode round-trips unknown type via raw data fallback', function () { // the encoder must write RDLENGTH+RDATA for types it doesn't know how // to serialize, else the wire format is truncated. // 0xABCD is intentionally not in Packet.TYPE. - const rdata = Buffer.from([ 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01 ]); + const rdata = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0x00, 0x01]); const packet = new Packet(); packet.header.qr = 1; packet.answers.push({ - name : 'unknown.example', - type : 0xABCD, - class : Packet.CLASS.IN, - ttl : 60, - data : rdata, + name: 'unknown.example', + type: 0xabcd, + class: Packet.CLASS.IN, + ttl: 60, + data: rdata, }); const parsed = Packet.parse(packet.toBuffer()); assert.equal(parsed.answers.length, 1); - assert.equal(parsed.answers[0].type, 0xABCD); + assert.equal(parsed.answers[0].type, 0xabcd); assert.equal(parsed.answers[0].class, Packet.CLASS.IN); assert.equal(parsed.answers[0].ttl, 60); assert.ok(Buffer.isBuffer(parsed.answers[0].data)); assert.deepEqual(parsed.answers[0].data, rdata); }); -test('Resource encode of unknown type does not corrupt following records', function() { +test('Resource encode of unknown type does not corrupt following records', function () { // without the fix, the missing RDLENGTH would make the parser interpret the // next record's bytes as RDATA, and the A record would never appear in `answers`. const packet = new Packet(); packet.header.qr = 1; packet.answers.push({ - name : 'unknown.example', - type : 0xABCD, - class : Packet.CLASS.IN, - ttl : 60, - data : Buffer.from([ 0x01, 0x02, 0x03 ]), + name: 'unknown.example', + type: 0xabcd, + class: Packet.CLASS.IN, + ttl: 60, + data: Buffer.from([0x01, 0x02, 0x03]), }); packet.answers.push({ - name : 'after.example', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 30, - address : '203.0.113.9', + name: 'after.example', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 30, + address: '203.0.113.9', }); const parsed = Packet.parse(packet.toBuffer()); assert.equal(parsed.answers.length, 2); - assert.equal(parsed.answers[0].type, 0xABCD); + assert.equal(parsed.answers[0].type, 0xabcd); assert.equal(parsed.answers[1].type, Packet.TYPE.A); assert.equal(parsed.answers[1].name, 'after.example'); assert.equal(parsed.answers[1].address, '203.0.113.9'); }); -test('Resource encode of unknown type with no data writes empty RDATA', function() { +test('Resource encode of unknown type with no data writes empty RDATA', function () { // When an unknown-type record has no `data`, encode should still emit a // valid RDLENGTH=0 block so the packet remains parseable. const packet = new Packet(); packet.header.qr = 1; packet.answers.push({ - name : 'bare.example', - type : 0xABCD, - class : Packet.CLASS.IN, - ttl : 0, + name: 'bare.example', + type: 0xabcd, + class: Packet.CLASS.IN, + ttl: 0, }); packet.answers.push({ - name : 'follow.example', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 30, - address : '198.51.100.1', + name: 'follow.example', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 30, + address: '198.51.100.1', }); const parsed = Packet.parse(packet.toBuffer()); assert.equal(parsed.answers.length, 2); - assert.equal(parsed.answers[0].type, 0xABCD); + assert.equal(parsed.answers[0].type, 0xabcd); assert.equal(parsed.answers[0].data.length, 0); assert.equal(parsed.answers[1].address, '198.51.100.1'); }); -test('Packet.uuid returns a 16-bit integer', function() { +test('Packet.uuid returns a 16-bit integer', function () { // must use the full 16-bit space, not Math.random()*1e5. for (let i = 0; i < 1000; i++) { const id = Packet.uuid(); assert.ok(Number.isInteger(id), `not an integer: ${id}`); - assert.ok(id >= 0 && id <= 0xFFFF, `out of range: ${id}`); + assert.ok(id >= 0 && id <= 0xffff, `out of range: ${id}`); } }); -test('Packet.uuid exercises the full 16-bit range with high diversity', function() { +test('Packet.uuid exercises the full 16-bit range with high diversity', function () { // Sample size large enough that a CSPRNG over [0, 0xFFFF] almost certainly // produces values in every quartile of the range. Catches regressions to a // constant, low-entropy, or capped implementation. const samples = new Set(); - const quartile = [ 0, 0, 0, 0 ]; + const quartile = [0, 0, 0, 0]; for (let i = 0; i < 5000; i++) { const id = Packet.uuid(); samples.add(id); quartile[Math.floor((id / 0x10000) * 4)]++; } - assert.ok(samples.size > 4000, `expected high diversity, got ${samples.size}`); + assert.ok( + samples.size > 4000, + `expected high diversity, got ${samples.size}`, + ); for (let q = 0; q < 4; q++) { - assert.ok(quartile[q] > 200, `quartile ${q} underrepresented (${quartile[q]}/5000)`); + assert.ok( + quartile[q] > 200, + `quartile ${q} underrepresented (${quartile[q]}/5000)`, + ); } }); -test('Name decode rejects a pointer cycle (no infinite loop)', function() { +test('Name decode rejects a pointer cycle (no infinite loop)', function () { // Hand-built packet header (12 bytes) followed by a name that points to // itself: byte 12 = 0xC0 (pointer high), byte 13 = 0x0C (offset = 12). // Without cycle detection this would loop forever. const buf = Buffer.alloc(14); - buf[12] = 0xC0; - buf[13] = 0x0C; + buf[12] = 0xc0; + buf[13] = 0x0c; const reader = new Packet.Reader(buf); reader.offset = 8 * 12; assert.throws(() => Packet.Name.decode(reader), /pointer cycle/); }); -test('Name decode rejects a two-step pointer cycle', function() { +test('Name decode rejects a two-step pointer cycle', function () { // Two pointers pointing at each other: bytes 12-13 = C0 0E, bytes 14-15 = C0 0C. const buf = Buffer.alloc(16); - buf[12] = 0xC0; buf[13] = 0x0E; - buf[14] = 0xC0; buf[15] = 0x0C; + buf[12] = 0xc0; + buf[13] = 0x0e; + buf[14] = 0xc0; + buf[15] = 0x0c; const reader = new Packet.Reader(buf); reader.offset = 8 * 12; assert.throws(() => Packet.Name.decode(reader), /pointer cycle/); }); -test('Header default constructor initializes ancount/ad/cd', function() { +test('Header default constructor initializes ancount/ad/cd', function () { const header = new Packet.Header(); assert.equal(header.ancount, 0); assert.equal(header.ad, 0); assert.equal(header.cd, 0); }); -test('Header#parse exposes AD and CD bits (RFC 4035)', function() { +test('Header#parse exposes AD and CD bits (RFC 4035)', function () { // Second header word with AD=1, CD=1, all other flags zero. // Layout: qr(1) opcode(4) aa(1) tc(1) rd(1) ra(1) z(1) ad(1) cd(1) rcode(4) // bits : 0 0000 0 0 0 0 0 1 1 0000 => 0000 0000 0011 0000 = 0x0030 const buf = Buffer.from([ - 0x00, 0x01, // id - 0x00, 0x30, // flags: AD=1, CD=1 - 0x00, 0x00, 0x00, 0x00, // counts - 0x00, 0x00, 0x00, 0x00, + 0x00, + 0x01, // id + 0x00, + 0x30, // flags: AD=1, CD=1 + 0x00, + 0x00, + 0x00, + 0x00, // counts + 0x00, + 0x00, + 0x00, + 0x00, ]); const header = Packet.Header.parse(buf); assert.equal(header.z, 0); @@ -842,7 +1018,7 @@ test('Header#parse exposes AD and CD bits (RFC 4035)', function() { assert.equal(header.cd, 1); }); -test('Header#toBuffer round-trips AD and CD bits', function() { +test('Header#toBuffer round-trips AD and CD bits', function () { const header = new Packet.Header({ id: 0x4242, ad: 1, cd: 1 }); const parsed = Packet.Header.parse(header.toBuffer()); assert.equal(parsed.id, 0x4242); @@ -851,8 +1027,12 @@ test('Header#toBuffer round-trips AD and CD bits', function() { assert.equal(parsed.z, 0); }); -test('EDNS exposes extendedRcode / version / doFlag', function() { - const opt = new Packet.Resource.EDNS([], { extendedRcode: 16, version: 0, doFlag: true }); +test('EDNS exposes extendedRcode / version / doFlag', function () { + const opt = new Packet.Resource.EDNS([], { + extendedRcode: 16, + version: 0, + doFlag: true, + }); assert.equal(opt.extendedRcode, 16); assert.equal(opt.version, 0); assert.equal(opt.doFlag, true); @@ -860,30 +1040,36 @@ test('EDNS exposes extendedRcode / version / doFlag', function() { assert.equal(opt.ttl, (16 << 24) | 0x8000); }); -test('EDNS round-trip preserves DO bit and extended RCODE', function() { - const opt = new Packet.Resource.EDNS([], { extendedRcode: 23, version: 0, doFlag: true }); +test('EDNS round-trip preserves DO bit and extended RCODE', function () { + const opt = new Packet.Resource.EDNS([], { + extendedRcode: 23, + version: 0, + doFlag: true, + }); const parsed = Packet.Resource.decode(Packet.Resource.encode(opt)); assert.equal(parsed.extendedRcode, 23); assert.equal(parsed.doFlag, true); }); -test('EDNS udpPayloadSize is configurable (RFC 6891 §6.2.3)', function() { +test('EDNS udpPayloadSize is configurable (RFC 6891 §6.2.3)', function () { const opt = new Packet.Resource.EDNS([], { udpPayloadSize: 4096 }); assert.equal(opt.class, 4096); const parsed = Packet.Resource.decode(Packet.Resource.encode(opt)); assert.equal(parsed.class, 4096); }); -test('EDNS.ECS#encode truncates IPv4 address to prefix length (RFC 7871)', function() { +test('EDNS.ECS#encode truncates IPv4 address to prefix length (RFC 7871)', function () { // /8 → 1 octet, /17 → 3 octets (ceil) - for (const [ cidr, expectedOctets ] of [ - [ '10.0.0.0/8', 1 ], - [ '10.20.0.0/16', 2 ], - [ '10.20.30.0/24', 3 ], - [ '10.20.30.0/17', 3 ], - [ '10.20.30.40/32', 4 ], + for (const [cidr, expectedOctets] of [ + ['10.0.0.0/8', 1], + ['10.20.0.0/16', 2], + ['10.20.30.0/24', 3], + ['10.20.30.0/17', 3], + ['10.20.30.40/32', 4], ]) { - const query = new Packet.Resource.EDNS([ new Packet.Resource.EDNS.ECS(cidr) ]); + const query = new Packet.Resource.EDNS([ + new Packet.Resource.EDNS.ECS(cidr), + ]); const buf = Packet.Resource.encode(query); // Layout: name(1) type(2) class(2) ttl(4) rdlength(2) optionCode(2) // optionLength(2) → optionLength sits at offset 13. Address byte count = @@ -893,11 +1079,11 @@ test('EDNS.ECS#encode truncates IPv4 address to prefix length (RFC 7871)', funct } }); -test('EDNS.ECS#encode supports IPv6 family', function() { +test('EDNS.ECS#encode supports IPv6 family', function () { // family=2 (IPv6), /32 prefix → 4 leading octets of the address. const ecs = Packet.Resource.EDNS.ECS('2001:db8::/32'); ecs.family = 2; // factory currently hard-codes family 1; opt into IPv6 - const opt = new Packet.Resource.EDNS([ ecs ]); + const opt = new Packet.Resource.EDNS([ecs]); const buf = Packet.Resource.encode(opt); const parsed = Packet.Resource.decode(buf); assert.equal(parsed.rdata[0].family, 2); @@ -906,7 +1092,7 @@ test('EDNS.ECS#encode supports IPv6 family', function() { assert.equal(parsed.rdata[0].ip, '2001:db8:0:0:0:0:0:0'); }); -test('Packet#encode compresses repeated names (RFC 1035 §4.1.4)', function() { +test('Packet#encode compresses repeated names (RFC 1035 §4.1.4)', function () { // Same name in the question and answer should be encoded as a 2-byte // pointer (0xC0 0x0C → offset 12, immediately after the header). Without // compression the answer name alone would take 1 + 7 + 1 + 7 + 1 + 3 + 1 @@ -914,13 +1100,17 @@ test('Packet#encode compresses repeated names (RFC 1035 §4.1.4)', function() { const pkt = new Packet(); pkt.header.id = 1; pkt.header.qr = 1; - pkt.questions.push({ name: 'example.com', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + pkt.questions.push({ + name: 'example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); pkt.answers.push({ - name : 'example.com', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '192.0.2.1', + name: 'example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '192.0.2.1', }); const buf = pkt.toBuffer(); // Question name 'example.com' starts at byte 12. The answer name should be @@ -928,27 +1118,35 @@ test('Packet#encode compresses repeated names (RFC 1035 §4.1.4)', function() { const headerLen = 12; const questionNameLen = 1 + 7 + 1 + 3 + 1; // 'example' label + 'com' label + root const ansNameStart = headerLen + questionNameLen + 4; // + qtype + qclass - assert.equal(buf[ansNameStart], 0xC0, 'answer name should start with pointer high byte'); - assert.equal(buf[ansNameStart + 1], 0x0C, 'pointer should target byte 12'); + assert.equal( + buf[ansNameStart], + 0xc0, + 'answer name should start with pointer high byte', + ); + assert.equal(buf[ansNameStart + 1], 0x0c, 'pointer should target byte 12'); // Round-trip back to verify the pointer resolves to the original name. const parsed = Packet.parse(buf); assert.equal(parsed.questions[0].name, 'example.com'); assert.equal(parsed.answers[0].name, 'example.com'); }); -test('Packet#encode compresses common suffixes (RFC 1035 §4.1.4)', function() { +test('Packet#encode compresses common suffixes (RFC 1035 §4.1.4)', function () { // a.example.com and b.example.com share the 'example.com' suffix; the // second name should be 'b' label + pointer to 'example.com'. const pkt = new Packet(); pkt.header.id = 2; pkt.header.qr = 1; - pkt.questions.push({ name: 'a.example.com', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + pkt.questions.push({ + name: 'a.example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); pkt.answers.push({ - name : 'b.example.com', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '192.0.2.2', + name: 'b.example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '192.0.2.2', }); const buf = pkt.toBuffer(); // Without compression the answer name uses 15 bytes; with shared-suffix @@ -962,22 +1160,30 @@ test('Packet#encode compresses common suffixes (RFC 1035 §4.1.4)', function() { const ansNameStart = headerLen + questionNameLen + 4; assert.equal(buf[ansNameStart], 1, "first byte is label length for 'b'"); assert.equal(buf[ansNameStart + 1], 0x62, "second byte is 'b'"); - assert.equal(buf[ansNameStart + 2] & 0xC0, 0xC0, 'third byte starts a compression pointer'); + assert.equal( + buf[ansNameStart + 2] & 0xc0, + 0xc0, + 'third byte starts a compression pointer', + ); }); -test('Packet#encode compresses names inside rdata (CNAME)', function() { +test('Packet#encode compresses names inside rdata (CNAME)', function () { // The CNAME's target is example.com — same as the question name, so the // rdata should be just a 2-byte pointer. const pkt = new Packet(); pkt.header.id = 3; pkt.header.qr = 1; - pkt.questions.push({ name: 'alias.example.com', type: Packet.TYPE.CNAME, class: Packet.CLASS.IN }); + pkt.questions.push({ + name: 'alias.example.com', + type: Packet.TYPE.CNAME, + class: Packet.CLASS.IN, + }); pkt.answers.push({ - name : 'alias.example.com', - type : Packet.TYPE.CNAME, - class : Packet.CLASS.IN, - ttl : 60, - domain : 'example.com', + name: 'alias.example.com', + type: Packet.TYPE.CNAME, + class: Packet.CLASS.IN, + ttl: 60, + domain: 'example.com', }); const buf = pkt.toBuffer(); const parsed = Packet.parse(buf); @@ -987,79 +1193,122 @@ test('Packet#encode compresses names inside rdata (CNAME)', function() { // class(2) + ttl(4) + rdlength(2). const rdlengthAt = 12 + (1 + 5 + 1 + 7 + 1 + 3 + 1) + 4 + 2 + 2 + 2 + 4; const rdlength = buf.readUInt16BE(rdlengthAt); - assert.equal(rdlength, 2, 'CNAME pointing to existing name should compress to 2 bytes'); + assert.equal( + rdlength, + 2, + 'CNAME pointing to existing name should compress to 2 bytes', + ); }); -test('Packet#encode rejects oversized labels (RFC 1035 §2.3.4)', function() { +test('Packet#encode rejects oversized labels (RFC 1035 §2.3.4)', function () { const pkt = new Packet(); pkt.questions.push({ - name : 'x'.repeat(64) + '.example.com', - type : Packet.TYPE.A, - class : Packet.CLASS.IN, + name: 'x'.repeat(64) + '.example.com', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, }); assert.throws(() => pkt.toBuffer(), /label/); }); -test('Packet#encode rejects oversized names (RFC 1035 §2.3.4)', function() { +test('Packet#encode rejects oversized names (RFC 1035 §2.3.4)', function () { // 6 labels of 41 chars + 5 dots + root = 41*6 + 6 length bytes + 1 root = 253? // Build a name guaranteed to exceed 255 octets: 4 labels of 63 + dots. - const labels = [ 'a'.repeat(63), 'b'.repeat(63), 'c'.repeat(63), 'd'.repeat(63) ]; + const labels = [ + 'a'.repeat(63), + 'b'.repeat(63), + 'c'.repeat(63), + 'd'.repeat(63), + ]; const pkt = new Packet(); pkt.questions.push({ - name : labels.join('.'), - type : Packet.TYPE.A, - class : Packet.CLASS.IN, + name: labels.join('.'), + type: Packet.TYPE.A, + class: Packet.CLASS.IN, }); assert.throws(() => pkt.toBuffer(), /name/); }); -test('Name#decode rejects an oversized label byte', function() { +test('Name#decode rejects an oversized label byte', function () { // Length byte 64 (0x40) has the second-highest bit set and is reserved. - const buf = Buffer.from([ 0x40, 0x61, 0x00 ]); + const buf = Buffer.from([0x40, 0x61, 0x00]); const reader = new Packet.Reader(buf); assert.throws(() => Packet.Name.decode(reader), /invalid label length/); }); -test('Resource#decode clamps TTL with the sign bit set (RFC 2181 §8)', function() { +test('Resource#decode clamps TTL with the sign bit set (RFC 2181 §8)', function () { // Hand-build a minimal packet: header + 0 questions + 1 answer with TTL = // 0xFFFFFFFF and 4-byte A rdata. const pkt = Buffer.from([ - 0x00, 0x01, 0x00, 0x00, // id, flags - 0x00, 0x00, 0x00, 0x01, // qdcount, ancount - 0x00, 0x00, 0x00, 0x00, // nscount, arcount + 0x00, + 0x01, + 0x00, + 0x00, // id, flags + 0x00, + 0x00, + 0x00, + 0x01, // qdcount, ancount + 0x00, + 0x00, + 0x00, + 0x00, // nscount, arcount // answer - 0x03, 0x66, 0x6f, 0x6f, 0x00, // name "foo" - 0x00, 0x01, // type A - 0x00, 0x01, // class IN - 0xFF, 0xFF, 0xFF, 0xFF, // TTL = 2^32 - 1 (high bit set) - 0x00, 0x04, // rdlength - 0xC0, 0x00, 0x02, 0x01, // 192.0.2.1 + 0x03, + 0x66, + 0x6f, + 0x6f, + 0x00, // name "foo" + 0x00, + 0x01, // type A + 0x00, + 0x01, // class IN + 0xff, + 0xff, + 0xff, + 0xff, // TTL = 2^32 - 1 (high bit set) + 0x00, + 0x04, // rdlength + 0xc0, + 0x00, + 0x02, + 0x01, // 192.0.2.1 ]); const parsed = Packet.parse(pkt); - assert.equal(parsed.answers[0].ttl, 0x7FFFFFFF, 'high-bit TTL must be clamped to 2^31 - 1'); + assert.equal( + parsed.answers[0].ttl, + 0x7fffffff, + 'high-bit TTL must be clamped to 2^31 - 1', + ); }); -test('Packet#encode merges extended RCODE into OPT (RFC 6891 §6.1.3)', function() { +test('Packet#encode merges extended RCODE into OPT (RFC 6891 §6.1.3)', function () { // header.rcode = 16 (BADVERS) should propagate the high byte into the OPT // record's TTL and only the low nibble (0) into the header. const pkt = new Packet(); - pkt.header.id = 0xAAAA; + pkt.header.id = 0xaaaa; pkt.header.qr = 1; pkt.header.rcode = 16; pkt.additionals.push(Packet.Resource.EDNS([])); const buf = pkt.toBuffer(); // Header byte 3 holds Z|AD|CD|RCODE(low4). For rcode=16 → low nibble is 0. - assert.equal(buf[3] & 0x0F, 0); + assert.equal(buf[3] & 0x0f, 0); // Parse it back: the merge should restore rcode=16. const parsed = Packet.parse(buf); assert.equal(parsed.header.rcode, 16); }); -test('Packet.parse tolerates multiple questions', function() { +test('Packet.parse tolerates multiple questions', function () { const request = new Packet(); request.header.id = 0x9999; - request.questions.push({ name: 'one.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); - request.questions.push({ name: 'two.test', type: Packet.TYPE.AAAA, class: Packet.CLASS.IN }); + request.questions.push({ + name: 'one.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); + request.questions.push({ + name: 'two.test', + type: Packet.TYPE.AAAA, + class: Packet.CLASS.IN, + }); const parsed = Packet.parse(request.toBuffer()); assert.equal(parsed.header.qdcount, 2); assert.equal(parsed.questions.length, 2); diff --git a/test/proxy-protocol.js b/test/proxy-protocol.js index 7c2844d..9fd82fd 100644 --- a/test/proxy-protocol.js +++ b/test/proxy-protocol.js @@ -2,8 +2,11 @@ const assert = require('node:assert'); const test = require('./test'); const proxy = require('../lib/proxy-protocol'); -test('proxy v1: TCP4 header parses', function() { - const buf = Buffer.from('PROXY TCP4 203.0.113.5 198.51.100.1 56324 53\r\n', 'ascii'); +test('proxy v1: TCP4 header parses', function () { + const buf = Buffer.from( + 'PROXY TCP4 203.0.113.5 198.51.100.1 56324 53\r\n', + 'ascii', + ); const { header, headerLength } = proxy.parse(buf); assert.equal(header.version, 1); assert.equal(header.command, 'PROXY'); @@ -15,9 +18,11 @@ test('proxy v1: TCP4 header parses', function() { assert.equal(headerLength, buf.length); }); -test('proxy v1: TCP6 header parses', function() { +test('proxy v1: TCP6 header parses', function () { const buf = Buffer.from( - 'PROXY TCP6 2001:db8::1 2001:db8::2 49152 53\r\n', 'ascii'); + 'PROXY TCP6 2001:db8::1 2001:db8::2 49152 53\r\n', + 'ascii', + ); const { header } = proxy.parse(buf); assert.equal(header.family, 'IPv6'); assert.equal(header.sourceAddress, '2001:db8::1'); @@ -25,7 +30,7 @@ test('proxy v1: TCP6 header parses', function() { assert.equal(header.sourcePort, 49152); }); -test('proxy v1: UNKNOWN protocol parses without address info', function() { +test('proxy v1: UNKNOWN protocol parses without address info', function () { const buf = Buffer.from('PROXY UNKNOWN\r\n', 'ascii'); const { header, headerLength } = proxy.parse(buf); assert.equal(header.version, 1); @@ -34,31 +39,31 @@ test('proxy v1: UNKNOWN protocol parses without address info', function() { assert.equal(headerLength, buf.length); }); -test('proxy v1: payload after header is preserved via headerLength', function() { +test('proxy v1: payload after header is preserved via headerLength', function () { const header = 'PROXY TCP4 1.2.3.4 5.6.7.8 1024 53\r\n'; - const payload = Buffer.from([ 0x00, 0x01, 0x02, 0x03 ]); - const buf = Buffer.concat([ Buffer.from(header, 'ascii'), payload ]); + const payload = Buffer.from([0x00, 0x01, 0x02, 0x03]); + const buf = Buffer.concat([Buffer.from(header, 'ascii'), payload]); const { headerLength } = proxy.parse(buf); assert.deepEqual(buf.slice(headerLength), payload); }); -test('proxy v1: incomplete header (no \\r\\n yet) returns null', function() { +test('proxy v1: incomplete header (no \\r\\n yet) returns null', function () { const buf = Buffer.from('PROXY TCP4 1.2.3.4', 'ascii'); assert.equal(proxy.parse(buf), null); }); -test('proxy v1: oversized header without terminator throws', function() { +test('proxy v1: oversized header without terminator throws', function () { // V1 max line length is 108; build something past that with no \r\n. const buf = Buffer.from('PROXY ' + 'x'.repeat(200), 'ascii'); assert.throws(() => proxy.parse(buf), /exceeds maximum length/); }); -test('proxy v2: IPv4 PROXY header parses', function() { +test('proxy v2: IPv4 PROXY header parses', function () { const buf = proxy.buildV2Ipv4({ - sourceAddress : '203.0.113.99', - destinationAddress : '198.51.100.1', - sourcePort : 51000, - destinationPort : 53, + sourceAddress: '203.0.113.99', + destinationAddress: '198.51.100.1', + sourcePort: 51000, + destinationPort: 53, }); const { header, headerLength } = proxy.parse(buf); assert.equal(header.version, 2); @@ -72,32 +77,30 @@ test('proxy v2: IPv4 PROXY header parses', function() { assert.equal(headerLength, 28); }); -test('proxy v2: incomplete header (signature only) returns null', function() { +test('proxy v2: incomplete header (signature only) returns null', function () { const sig = Buffer.from([ - 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, - 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, + 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, ]); assert.equal(proxy.parse(sig), null); }); -test('proxy v2: payload after header is preserved via headerLength', function() { +test('proxy v2: payload after header is preserved via headerLength', function () { const header = proxy.buildV2Ipv4({ - sourceAddress : '10.0.0.1', - destinationAddress : '10.0.0.2', - sourcePort : 12345, - destinationPort : 53, + sourceAddress: '10.0.0.1', + destinationAddress: '10.0.0.2', + sourcePort: 12345, + destinationPort: 53, }); - const payload = Buffer.from([ 0xAB, 0xCD, 0xEF ]); - const buf = Buffer.concat([ header, payload ]); + const payload = Buffer.from([0xab, 0xcd, 0xef]); + const buf = Buffer.concat([header, payload]); const parsed = proxy.parse(buf); assert.deepEqual(buf.slice(parsed.headerLength), payload); }); -test('proxy v2: LOCAL command is recognized without address info', function() { +test('proxy v2: LOCAL command is recognized without address info', function () { const buf = Buffer.alloc(16); Buffer.from([ - 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, - 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, + 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, ]).copy(buf, 0); buf[12] = 0x20; // version 2 | LOCAL command (0) buf[13] = 0x00; // AF_UNSPEC @@ -107,12 +110,12 @@ test('proxy v2: LOCAL command is recognized without address info', function() { assert.equal(header.command, 'LOCAL'); }); -test('proxy: unrelated bytes throw "header missing or malformed"', function() { +test('proxy: unrelated bytes throw "header missing or malformed"', function () { const buf = Buffer.from('GET / HTTP/1.1\r\n', 'ascii'); assert.throws(() => proxy.parse(buf), /header missing or malformed/); }); -test('proxy: empty buffer needs more (returns null via prefix match)', function() { +test('proxy: empty buffer needs more (returns null via prefix match)', function () { // V1_PREFIX.slice(0,0).equals(Buffer.alloc(0)) is true, so empty bytes // are treated as "incomplete v1 header" rather than malformed. assert.equal(proxy.parse(Buffer.alloc(0)), null); diff --git a/test/server.js b/test/server.js index 9b89d3a..96bc60e 100644 --- a/test/server.js +++ b/test/server.js @@ -21,10 +21,12 @@ function get(url, options) { const result = []; res.on('data', data => result.push(data)); res.once('error', reject); - res.once('end', () => resolve({ - body : Buffer.concat(result), - headers : res.headers, - })); + res.once('end', () => + resolve({ + body: Buffer.concat(result), + headers: res.headers, + }), + ); }); req.on('error', reject); } catch (err) { @@ -42,7 +44,7 @@ function readOneTcpReply(port, payload) { const sock = tcp.connect(port, '127.0.0.1', () => sock.write(payload)); let buffered = Buffer.alloc(0); sock.on('data', chunk => { - buffered = Buffer.concat([ buffered, chunk ]); + buffered = Buffer.concat([buffered, chunk]); if (buffered.length < 2) return; const len = buffered.readUInt16BE(0); if (buffered.length < 2 + len) return; @@ -54,7 +56,7 @@ function readOneTcpReply(port, payload) { }); } -test('server/doh#cors - default', async function() { +test('server/doh#cors - default', async function () { const server = createDOHServer(); const { port } = await new Promise(resolve => { server.on('listening', resolve); @@ -65,7 +67,7 @@ test('server/doh#cors - default', async function() { server.close(); }); -test('server/doh#cors - no cors', async function() { +test('server/doh#cors - no cors', async function () { const server = createDOHServer({ cors: false, }); @@ -78,7 +80,7 @@ test('server/doh#cors - no cors', async function() { server.close(); }); -test('server/doh#cors - cors origin', async function() { +test('server/doh#cors - cors origin', async function () { const server = createDOHServer({ cors: 'some.domain', }); @@ -92,7 +94,7 @@ test('server/doh#cors - cors origin', async function() { server.close(); }); -test('server/doh#cors - cors function', async function() { +test('server/doh#cors - cors function', async function () { const server = createDOHServer({ cors(domain) { if (domain === 'a.domain') { @@ -107,32 +109,40 @@ test('server/doh#cors - cors function', async function() { server.on('listening', resolve); server.listen(); }); - let headers = (await get(`http://localhost:${port}`, { headers: { origin: 'a.domain' } })).headers; + let headers = ( + await get(`http://localhost:${port}`, { headers: { origin: 'a.domain' } }) + ).headers; assert.equal(headers['access-control-allow-origin'], 'a.domain'); assert.equal(headers.vary, 'Origin'); - headers = (await get(`http://localhost:${port}`, { headers: { origin: 'b.domain' } })).headers; + headers = ( + await get(`http://localhost:${port}`, { headers: { origin: 'b.domain' } }) + ).headers; assert.equal(headers['access-control-allow-origin'], 'false'); assert.equal(headers.vary, 'Origin'); server.close(); }); -test('server/udp-tcp#simple-request-async-response', async() => { +test('server/udp-tcp#simple-request-async-response', async () => { const server = createServer({ - tcp : true, - udp : true, + tcp: true, + udp: true, handle(request, send, _info) { - const [ question ] = request.questions; - assert.deepEqual(request.questions, [ { name: 'test.com', type: 1, class: 1 } ]); + const [question] = request.questions; + assert.deepEqual(request.questions, [ + { name: 'test.com', type: 1, class: 1 }, + ]); const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : question.name, - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 300, - data : [ 'Hello World' ], + name: question.name, + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 300, + data: ['Hello World'], }); - (new Promise((resolve) => setTimeout(() => resolve(), 1))).then(() => send(response)); + new Promise(resolve => setTimeout(() => resolve(), 1)).then(() => + send(response), + ); }, }); const servers = await server.listen(); @@ -140,24 +150,26 @@ test('server/udp-tcp#simple-request-async-response', async() => { assert.ok(servers.tcp.port > 1000); const tcpClient = TCPClient({ dns: '127.0.0.1', port: servers.tcp.port }); const udpClient = UDPClient({ dns: '127.0.0.1', port: servers.udp.port }); - const expected = [ { name: 'test.com', ttl: 300, type: 16, class: 1, data: 'Hello World' } ]; + const expected = [ + { name: 'test.com', ttl: 300, type: 16, class: 1, data: 'Hello World' }, + ]; assert.deepEqual((await tcpClient('test.com')).answers, expected); assert.deepEqual((await udpClient('test.com')).answers, expected); await server.close(); }); -test('server/udp#oversized response sets TC=1 and truncates (RFC 1035 §4.2.1)', async() => { +test('server/udp#oversized response sets TC=1 and truncates (RFC 1035 §4.2.1)', async () => { const server = createUDPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); // 60 TXT answers with long strings → far past the 512 byte UDP limit. for (let i = 0; i < 60; i++) { response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 60, - data : 'x'.repeat(200), + name: request.questions[0].name, + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 60, + data: 'x'.repeat(200), }); } send(response); @@ -167,9 +179,13 @@ test('server/udp#oversized response sets TC=1 and truncates (RFC 1035 §4.2.1)', // Send a non-EDNS query so the server applies the 512-byte ceiling. const query = new Packet(); - query.header.id = 0xABCD; + query.header.id = 0xabcd; query.header.rd = 1; - query.questions.push({ name: 'big.test', type: Packet.TYPE.TXT, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'big.test', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + }); const client = udp.createSocket('udp4'); const reply = await new Promise((resolve, reject) => { client.on('message', msg => resolve(msg)); @@ -180,12 +196,12 @@ test('server/udp#oversized response sets TC=1 and truncates (RFC 1035 §4.2.1)', assert.ok(reply.length <= 512, `reply ${reply.length} bytes must be ≤ 512`); const parsed = Packet.parse(reply); assert.equal(parsed.header.tc, 1, 'TC bit must be set on truncated reply'); - assert.equal(parsed.header.id, 0xABCD); + assert.equal(parsed.header.id, 0xabcd); assert.equal(parsed.questions[0].name, 'big.test'); await new Promise(resolve => server.close(resolve)); }); -test('server/udp#EDNS-advertised payload raises UDP ceiling (RFC 6891 §6.2.3)', async() => { +test('server/udp#EDNS-advertised payload raises UDP ceiling (RFC 6891 §6.2.3)', async () => { // When the request carries an OPT record with class=4096, the server may // send a response up to 4096 bytes without truncating. const server = createUDPServer(); @@ -193,11 +209,11 @@ test('server/udp#EDNS-advertised payload raises UDP ceiling (RFC 6891 §6.2.3)', const response = Packet.createResponseFromRequest(request); for (let i = 0; i < 5; i++) { response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 60, - data : 'y'.repeat(200), + name: request.questions[0].name, + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 60, + data: 'y'.repeat(200), }); } send(response); @@ -208,7 +224,11 @@ test('server/udp#EDNS-advertised payload raises UDP ceiling (RFC 6891 §6.2.3)', const query = new Packet(); query.header.id = 0x1234; query.header.rd = 1; - query.questions.push({ name: 'edns.test', type: Packet.TYPE.TXT, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'edns.test', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + }); query.additionals.push(Packet.Resource.EDNS([], { udpPayloadSize: 4096 })); const client = udp.createSocket('udp4'); @@ -218,23 +238,30 @@ test('server/udp#EDNS-advertised payload raises UDP ceiling (RFC 6891 §6.2.3)', client.send(query.toBuffer(), port, '127.0.0.1'); }); await new Promise(resolve => client.close(resolve)); - assert.ok(reply.length > 512, `EDNS-advertised payload should permit > 512 bytes; got ${reply.length}`); + assert.ok( + reply.length > 512, + `EDNS-advertised payload should permit > 512 bytes; got ${reply.length}`, + ); const parsed = Packet.parse(reply); - assert.equal(parsed.header.tc, 0, 'no truncation expected within EDNS budget'); + assert.equal( + parsed.header.tc, + 0, + 'no truncation expected within EDNS budget', + ); assert.equal(parsed.answers.length, 5); await new Promise(resolve => server.close(resolve)); }); -test('server/udp#standalone end-to-end query', async() => { +test('server/udp#standalone end-to-end query', async () => { const server = createUDPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '198.51.100.10', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '198.51.100.10', }); send(response); }); @@ -249,16 +276,16 @@ test('server/udp#standalone end-to-end query', async() => { await new Promise(resolve => server.close(resolve)); }); -test('server/tcp#standalone end-to-end query', async() => { +test('server/tcp#standalone end-to-end query', async () => { const server = createTCPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '198.51.100.20', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '198.51.100.20', }); send(response); }); @@ -273,7 +300,7 @@ test('server/tcp#standalone end-to-end query', async() => { await new Promise(resolve => server.close(resolve)); }); -test('server/tcp#async handler still responds when client sends socket.end(frame)', async() => { +test('server/tcp#async handler still responds when client sends socket.end(frame)', async () => { // Regression: a client that bundles query + FIN in a single socket.end() // would trigger 'end' on the server before an async handler had a chance // to call send(). The server must not half-close its write side while any @@ -283,11 +310,11 @@ test('server/tcp#async handler still responds when client sends socket.end(frame setTimeout(() => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '203.0.113.99', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '203.0.113.99', }); send(response); }, 25); @@ -298,7 +325,11 @@ test('server/tcp#async handler still responds when client sends socket.end(frame const query = new Packet(); query.header.id = 0x9001; query.header.rd = 1; - query.questions.push({ name: 'end-with-frame.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'end-with-frame.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const body = query.toBuffer(); const len = Buffer.alloc(2); len.writeUInt16BE(body.length); @@ -307,11 +338,11 @@ test('server/tcp#async handler still responds when client sends socket.end(frame const sock = tcp.connect(port, '127.0.0.1', () => { // socket.end(frame) sends both the query data and FIN. The server's // 'end' event will fire before the async handler calls send(). - sock.end(Buffer.concat([ len, body ])); + sock.end(Buffer.concat([len, body])); }); let buffered = Buffer.alloc(0); sock.on('data', chunk => { - buffered = Buffer.concat([ buffered, chunk ]); + buffered = Buffer.concat([buffered, chunk]); if (buffered.length < 2) return; const replyLen = buffered.readUInt16BE(0); if (buffered.length < 2 + replyLen) return; @@ -319,7 +350,8 @@ test('server/tcp#async handler still responds when client sends socket.end(frame }); sock.on('error', reject); sock.on('close', () => { - if (buffered.length === 0) reject(new Error('connection closed before any response')); + if (buffered.length === 0) + reject(new Error('connection closed before any response')); }); }); @@ -328,16 +360,16 @@ test('server/tcp#async handler still responds when client sends socket.end(frame await new Promise(resolve => server.close(resolve)); }); -test('server/tcp#pipelined queries share a connection (RFC 7766 §6.2.1.1)', async() => { +test('server/tcp#pipelined queries share a connection (RFC 7766 §6.2.1.1)', async () => { const server = createTCPServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '192.0.2.42', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '192.0.2.42', }); send(response); }); @@ -347,7 +379,7 @@ test('server/tcp#pipelined queries share a connection (RFC 7766 §6.2.1.1)', asy // Build two distinct queries and send them back-to-back on one connection // without waiting for each reply. The server must process both and write // length-prefixed replies on the same socket. - const queries = [ 'pipe1.test', 'pipe2.test', 'pipe3.test' ].map((name, i) => { + const queries = ['pipe1.test', 'pipe2.test', 'pipe3.test'].map((name, i) => { const q = new Packet(); q.header.id = 0x1000 + i; q.header.rd = 1; @@ -355,7 +387,7 @@ test('server/tcp#pipelined queries share a connection (RFC 7766 §6.2.1.1)', asy const body = q.toBuffer(); const len = Buffer.alloc(2); len.writeUInt16BE(body.length); - return { id: q.header.id, frame: Buffer.concat([ len, body ]) }; + return { id: q.header.id, frame: Buffer.concat([len, body]) }; }); const replies = await new Promise((resolve, reject) => { @@ -365,7 +397,7 @@ test('server/tcp#pipelined queries share a connection (RFC 7766 §6.2.1.1)', asy const out = []; let buffered = Buffer.alloc(0); sock.on('data', chunk => { - buffered = Buffer.concat([ buffered, chunk ]); + buffered = Buffer.concat([buffered, chunk]); while (buffered.length >= 2) { const len = buffered.readUInt16BE(0); if (buffered.length < 2 + len) break; @@ -390,16 +422,16 @@ test('server/tcp#pipelined queries share a connection (RFC 7766 §6.2.1.1)', asy await new Promise(resolve => server.close(resolve)); }); -test('server/doh#GET via DOHClient end-to-end', async() => { +test('server/doh#GET via DOHClient end-to-end', async () => { const server = createDOHServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '198.51.100.30', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '198.51.100.30', }); send(response); }); @@ -415,16 +447,16 @@ test('server/doh#GET via DOHClient end-to-end', async() => { server.close(); }); -test('server/doh#POST end-to-end', async() => { +test('server/doh#POST end-to-end', async () => { const server = createDOHServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.TXT, - class : Packet.CLASS.IN, - ttl : 60, - data : 'post-ok', + name: request.questions[0].name, + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + ttl: 60, + data: 'post-ok', }); send(response); }); @@ -435,26 +467,33 @@ test('server/doh#POST end-to-end', async() => { const packet = new Packet(); packet.header.rd = 1; - packet.questions.push({ name: 'doh-post.test', type: Packet.TYPE.TXT, class: Packet.CLASS.IN }); + packet.questions.push({ + name: 'doh-post.test', + type: Packet.TYPE.TXT, + class: Packet.CLASS.IN, + }); const body = await new Promise((resolve, reject) => { - const req = http.request({ - host : '127.0.0.1', - port, - path : '/dns-query', - method : 'POST', - headers : { - accept : 'application/dns-message', - 'content-type' : 'application/dns-message', + const req = http.request( + { + host: '127.0.0.1', + port, + path: '/dns-query', + method: 'POST', + headers: { + accept: 'application/dns-message', + 'content-type': 'application/dns-message', + }, }, - }, res => { - assert.equal(res.statusCode, 200); - assert.equal(res.headers['content-type'], 'application/dns-message'); - const chunks = []; - res.on('data', c => chunks.push(c)); - res.on('end', () => resolve(Buffer.concat(chunks))); - res.on('error', reject); - }); + res => { + assert.equal(res.statusCode, 200); + assert.equal(res.headers['content-type'], 'application/dns-message'); + const chunks = []; + res.on('data', c => chunks.push(c)); + res.on('end', () => resolve(Buffer.concat(chunks))); + res.on('error', reject); + }, + ); req.on('error', reject); req.end(packet.toBuffer()); }); @@ -464,20 +503,23 @@ test('server/doh#POST end-to-end', async() => { server.close(); }); -test('server/doh#405 on unsupported method', async() => { +test('server/doh#405 on unsupported method', async () => { const server = createDOHServer(); const { port } = await new Promise(resolve => { server.on('listening', resolve); server.listen(); }); const { statusCode } = await new Promise((resolve, reject) => { - const req = http.request({ - host : '127.0.0.1', - port, - path : '/dns-query', - method : 'PUT', - headers : { accept: 'application/dns-message' }, - }, resolve); + const req = http.request( + { + host: '127.0.0.1', + port, + path: '/dns-query', + method: 'PUT', + headers: { accept: 'application/dns-message' }, + }, + resolve, + ); req.on('error', reject); req.end(); }); @@ -485,25 +527,30 @@ test('server/doh#405 on unsupported method', async() => { server.close(); }); -test('server/doh#404 on unknown path', async() => { +test('server/doh#404 on unknown path', async () => { const server = createDOHServer(); const { port } = await new Promise(resolve => { server.on('listening', resolve); server.listen(); }); const statusCode = await new Promise((resolve, reject) => { - http.get({ - host : '127.0.0.1', - port, - path : '/something-else', - headers : { accept: 'application/dns-message' }, - }, res => resolve(res.statusCode)).on('error', reject); + http + .get( + { + host: '127.0.0.1', + port, + path: '/something-else', + headers: { accept: 'application/dns-message' }, + }, + res => resolve(res.statusCode), + ) + .on('error', reject); }); assert.equal(statusCode, 404); server.close(); }); -test('server/doh#406 on incompatible Accept header', async() => { +test('server/doh#406 on incompatible Accept header', async () => { // RFC 8484 §4.1: the client SHOULD send Accept: application/dns-message but // the server is not required to reject other values. Only reject when the // client explicitly asked for media types that exclude @@ -514,47 +561,57 @@ test('server/doh#406 on incompatible Accept header', async() => { server.listen(); }); const statusCode = await new Promise((resolve, reject) => { - http.get({ - host : '127.0.0.1', - port, - path : '/dns-query?dns=abc', - headers : { accept: 'text/html' }, - }, res => resolve(res.statusCode)).on('error', reject); + http + .get( + { + host: '127.0.0.1', + port, + path: '/dns-query?dns=abc', + headers: { accept: 'text/html' }, + }, + res => resolve(res.statusCode), + ) + .on('error', reject); }); assert.equal(statusCode, 406); server.close(); }); -test('server/doh#400 on missing dns query param', async() => { +test('server/doh#400 on missing dns query param', async () => { const server = createDOHServer(); const { port } = await new Promise(resolve => { server.on('listening', resolve); server.listen(); }); const statusCode = await new Promise((resolve, reject) => { - http.get({ - host : '127.0.0.1', - port, - path : '/dns-query', - headers : { accept: 'application/dns-message' }, - }, res => resolve(res.statusCode)).on('error', reject); + http + .get( + { + host: '127.0.0.1', + port, + path: '/dns-query', + headers: { accept: 'application/dns-message' }, + }, + res => resolve(res.statusCode), + ) + .on('error', reject); }); assert.equal(statusCode, 400); server.close(); }); -test('server/doh#GET with Accept: */* is accepted (RFC 8484 §4.1)', async() => { +test('server/doh#GET with Accept: */* is accepted (RFC 8484 §4.1)', async () => { // curl-style clients send */*; the server must not reject them — it always // replies with application/dns-message anyway. const server = createDOHServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '198.51.100.40', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '198.51.100.40', }); send(response); }); @@ -570,54 +627,79 @@ test('server/doh#GET with Accept: */* is accepted (RFC 8484 §4.1)', async() => const packet = new Packet(); packet.header.rd = 1; - packet.questions.push({ name: 'star.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + packet.questions.push({ + name: 'star.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const dns = packet.toBase64URL(); const status = await new Promise((resolve, reject) => { - http.get({ - host : '127.0.0.1', - port, - path : `/dns-query?dns=${dns}`, - headers : { accept: '*/*' }, - }, res => resolve(res.statusCode)).on('error', reject); + http + .get( + { + host: '127.0.0.1', + port, + path: `/dns-query?dns=${dns}`, + headers: { accept: '*/*' }, + }, + res => resolve(res.statusCode), + ) + .on('error', reject); }); assert.equal(status, 200); server.close(); }); -test('server/doh#POST 415 on missing or wrong Content-Type (RFC 8484 §4.1)', async() => { +test('server/doh#POST 415 on missing or wrong Content-Type (RFC 8484 §4.1)', async () => { const server = createDOHServer(); const requestErrors = []; - server.on('request', (request, send) => send(Packet.createResponseFromRequest(request))); + server.on('request', (request, send) => + send(Packet.createResponseFromRequest(request)), + ); server.on('requestError', e => requestErrors.push(e)); const { port } = await new Promise(resolve => { server.on('listening', resolve); server.listen(); }); - const sendPost = (contentType, body = Buffer.alloc(12)) => new Promise((resolve, reject) => { - const headers = { accept: 'application/dns-message' }; - if (contentType) headers['content-type'] = contentType; - const req = http.request({ - host : '127.0.0.1', - port, - path : '/dns-query', - method : 'POST', - headers, - }, res => resolve({ status: res.statusCode })); - req.on('error', err => resolve({ error: err })); - req.end(body); - }); + const sendPost = (contentType, body = Buffer.alloc(12)) => + new Promise((resolve, reject) => { + const headers = { accept: 'application/dns-message' }; + if (contentType) headers['content-type'] = contentType; + const req = http.request( + { + host: '127.0.0.1', + port, + path: '/dns-query', + method: 'POST', + headers, + }, + res => resolve({ status: res.statusCode }), + ); + req.on('error', err => resolve({ error: err })); + req.end(body); + }); assert.equal((await sendPost(undefined)).status, 415, 'missing Content-Type'); - assert.equal((await sendPost('application/json')).status, 415, 'wrong Content-Type'); + assert.equal( + (await sendPost('application/json')).status, + 415, + 'wrong Content-Type', + ); // With the correct Content-Type the 415 check passes, so a body too short // for a DNS header surfaces as a server-side parse error (handler destroys // the connection); the client sees a socket-level error, not a 415. const malformed = await sendPost('application/dns-message', Buffer.alloc(0)); - assert.ok(malformed.error, 'malformed body should fail at the socket, not return 415'); - assert.ok(requestErrors.length >= 1, 'requestError should have fired for the malformed body'); + assert.ok( + malformed.error, + 'malformed body should fail at the socket, not return 415', + ); + assert.ok( + requestErrors.length >= 1, + 'requestError should have fired for the malformed body', + ); server.close(); }); -test('server/doh#406 when Accept lists application/dns-message with q=0', async() => { +test('server/doh#406 when Accept lists application/dns-message with q=0', async () => { // Per RFC 7231 §5.3.1 q=0 means "not acceptable". An entry like // application/dns-message;q=0 is an explicit rejection, even though the // media range matches. @@ -627,18 +709,23 @@ test('server/doh#406 when Accept lists application/dns-message with q=0', async( server.listen(); }); const status = await new Promise((resolve, reject) => { - http.get({ - host : '127.0.0.1', - port, - path : '/dns-query?dns=abc', - headers : { accept: 'application/dns-message;q=0, text/html' }, - }, res => resolve(res.statusCode)).on('error', reject); + http + .get( + { + host: '127.0.0.1', + port, + path: '/dns-query?dns=abc', + headers: { accept: 'application/dns-message;q=0, text/html' }, + }, + res => resolve(res.statusCode), + ) + .on('error', reject); }); assert.equal(status, 406); server.close(); }); -test('server/doh#Cache-Control ignores OPT pseudo-RR TTL (RFC 6891)', async() => { +test('server/doh#Cache-Control ignores OPT pseudo-RR TTL (RFC 6891)', async () => { // The OPT record's "TTL" field carries flags/extended RCODE; it is almost // always 0 and must not drive the min TTL down to zero on cacheable // responses. @@ -646,11 +733,11 @@ test('server/doh#Cache-Control ignores OPT pseudo-RR TTL (RFC 6891)', async() => server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 120, - address : '203.0.113.20', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 120, + address: '203.0.113.20', }); // OPT in additionals with the conventional TTL=0. Without the fix, the // Cache-Control max-age would collapse to 0. @@ -663,32 +750,39 @@ test('server/doh#Cache-Control ignores OPT pseudo-RR TTL (RFC 6891)', async() => }); const packet = new Packet(); packet.header.rd = 1; - packet.questions.push({ name: 'opt-ttl.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); - const dns = packet.toBase64URL(); - const { headers } = await get(`http://127.0.0.1:${port}/dns-query?dns=${dns}`, { - headers: { accept: 'application/dns-message' }, + packet.questions.push({ + name: 'opt-ttl.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, }); + const dns = packet.toBase64URL(); + const { headers } = await get( + `http://127.0.0.1:${port}/dns-query?dns=${dns}`, + { + headers: { accept: 'application/dns-message' }, + }, + ); assert.equal(headers['cache-control'], 'max-age=120'); server.close(); }); -test('server/doh#response carries Cache-Control: max-age= (RFC 8484 §5.1)', async() => { +test('server/doh#response carries Cache-Control: max-age= (RFC 8484 §5.1)', async () => { const server = createDOHServer(); server.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 300, - address : '203.0.113.10', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 300, + address: '203.0.113.10', }); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 30, // minimum across the two answers - address : '203.0.113.11', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 30, // minimum across the two answers + address: '203.0.113.11', }); send(response); }); @@ -698,27 +792,34 @@ test('server/doh#response carries Cache-Control: max-age= (RFC 8484 §5 }); const packet = new Packet(); packet.header.rd = 1; - packet.questions.push({ name: 'cc.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); - const dns = packet.toBase64URL(); - const { headers } = await get(`http://127.0.0.1:${port}/dns-query?dns=${dns}`, { - headers: { accept: 'application/dns-message' }, + packet.questions.push({ + name: 'cc.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, }); + const dns = packet.toBase64URL(); + const { headers } = await get( + `http://127.0.0.1:${port}/dns-query?dns=${dns}`, + { + headers: { accept: 'application/dns-message' }, + }, + ); assert.equal(headers['cache-control'], 'max-age=30'); server.close(); }); -test('server/all#multi-question request is preserved through handle', async() => { +test('server/all#multi-question request is preserved through handle', async () => { const server = createServer({ - udp : true, - handle : (request, send) => { + udp: true, + handle: (request, send) => { const response = Packet.createResponseFromRequest(request); for (const q of request.questions) { response.answers.push({ - name : q.name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '127.0.0.1', + name: q.name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '127.0.0.1', }); } send(response); @@ -730,8 +831,16 @@ test('server/all#multi-question request is preserved through handle', async() => const request = new Packet(); request.header.id = 0x4242; request.header.rd = 1; - request.questions.push({ name: 'first.multi', type: Packet.TYPE.A, class: Packet.CLASS.IN }); - request.questions.push({ name: 'second.multi', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + request.questions.push({ + name: 'first.multi', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); + request.questions.push({ + name: 'second.multi', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const client = udp.createSocket('udp4'); const reply = await new Promise((resolve, reject) => { client.on('message', msg => resolve(Packet.parse(msg))); @@ -745,20 +854,25 @@ test('server/all#multi-question request is preserved through handle', async() => await server.close(); }); -test('server/all#close event fires once all sub-servers close', async() => { - const server = createServer({ doh: true, tcp: true, udp: true, handle: () => {} }); +test('server/all#close event fires once all sub-servers close', async () => { + const server = createServer({ + doh: true, + tcp: true, + udp: true, + handle: () => {}, + }); await server.listen(); const closed = new Promise(resolve => server.on('close', resolve)); await server.close(); await closed; }); -test('server/all#invalid-request', async() => { +test('server/all#invalid-request', async () => { const server = createServer({ - doh : true, - tcp : true, - udp : true, - handle : () => {}, + doh: true, + tcp: true, + udp: true, + handle: () => {}, }); const servers = await server.listen(); assert.ok(servers.udp.port > 1000); @@ -766,7 +880,7 @@ test('server/all#invalid-request', async() => { assert.ok(servers.doh.port > 1000); const errors = []; - server.on('requestError', (e) => { + server.on('requestError', e => { errors.push(e); }); @@ -774,16 +888,20 @@ test('server/all#invalid-request', async() => { tcpSocket.on('connect', () => tcpSocket.end('INVALID')); const udpSocket = udp.createSocket('udp4'); - udpSocket.send('INVALID', servers.udp.port, '127.0.0.1', () => udpSocket.close()); + udpSocket.send('INVALID', servers.udp.port, '127.0.0.1', () => + udpSocket.close(), + ); - const dohConn = http.get(`http://127.0.0.1:${servers.doh.port}/dns-query?dns=INVALID`, { - headers: { accept: 'application/dns-message' }, - }).on('error', () => {}); + const dohConn = http + .get(`http://127.0.0.1:${servers.doh.port}/dns-query?dns=INVALID`, { + headers: { accept: 'application/dns-message' }, + }) + .on('error', () => {}); await Promise.all([ - new Promise((resolve) => tcpSocket.on('close', resolve)), - new Promise((resolve) => udpSocket.on('close', resolve)), - new Promise((resolve) => dohConn.on('close', resolve)), + new Promise(resolve => tcpSocket.on('close', resolve)), + new Promise(resolve => udpSocket.on('close', resolve)), + new Promise(resolve => dohConn.on('close', resolve)), ]); assert.equal(errors.length, 3); @@ -791,13 +909,13 @@ test('server/all#invalid-request', async() => { await server.close(); }); -test('server/all#handler can respond with RCODE error codes', async() => { +test('server/all#handler can respond with RCODE error codes', async () => { const server = createServer({ - udp : true, - tcp : true, + udp: true, + tcp: true, handle(request, send) { const response = Packet.createResponseFromRequest(request); - const [ question ] = request.questions; + const [question] = request.questions; if (question.name === 'refused.test') { response.header.rcode = Packet.RCODE.REFUSED; } else if (question.name === 'nxdomain.test') { @@ -836,23 +954,25 @@ test('server/all#handler can respond with RCODE error codes', async() => { await server.close(); }); -test('server/all#maxConcurrent - requests within limit are served normally', async() => { +test('server/all#maxConcurrent - requests within limit are served normally', async () => { const server = createServer({ - udp : true, - maxConcurrent : 10, + udp: true, + maxConcurrent: 10, handle(request, send) { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '1.2.3.4', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '1.2.3.4', }); send(response); }, }); - const { udp: { port } } = await server.listen(); + const { + udp: { port }, + } = await server.listen(); const client = UDPClient({ dns: '127.0.0.1', port }); const reply = await client('within-limit.test'); @@ -862,18 +982,20 @@ test('server/all#maxConcurrent - requests within limit are served normally', asy await server.close(); }); -test('server/all#maxConcurrent - excess requests receive SERVFAIL', async() => { +test('server/all#maxConcurrent - excess requests receive SERVFAIL', async () => { // Use a handler that holds requests open until we release them, so we can // saturate the concurrency limit predictably. const pending = []; const server = createServer({ - udp : true, - maxConcurrent : 2, + udp: true, + maxConcurrent: 2, handle(request, send) { pending.push({ request, send }); }, }); - const { udp: { port } } = await server.listen(); + const { + udp: { port }, + } = await server.listen(); const client = UDPClient({ dns: '127.0.0.1', port }); // Fire q1 and q2 but don't await — they stay in the handler holding 2 slots. @@ -885,7 +1007,11 @@ test('server/all#maxConcurrent - excess requests receive SERVFAIL', async() => { // q3 arrives when the limit is already full — should be shed immediately. const r3 = await client('q3.test'); - assert.equal(r3.header.rcode, Packet.RCODE.SERVFAIL, 'shed request gets SERVFAIL'); + assert.equal( + r3.header.rcode, + Packet.RCODE.SERVFAIL, + 'shed request gets SERVFAIL', + ); // Drain the two held requests so the server can close cleanly. for (const { request, send } of pending) { @@ -893,7 +1019,7 @@ test('server/all#maxConcurrent - excess requests receive SERVFAIL', async() => { response.header.rcode = Packet.RCODE.NOERROR; send(response); } - await Promise.all([ p1, p2 ]); + await Promise.all([p1, p2]); await server.close(); }); @@ -905,18 +1031,18 @@ test('server/all#maxConcurrent - excess requests receive SERVFAIL', async() => { const proxyProtocol = require('../lib/proxy-protocol'); -test('server/udp#proxyProtocol exposes real client address (v2 IPv4)', async() => { +test('server/udp#proxyProtocol exposes real client address (v2 IPv4)', async () => { const server = createUDPServer({ proxyProtocol: true }); let observedClient; server.on('request', (request, send, info) => { observedClient = info; const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '127.0.0.1', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '127.0.0.1', }); send(response); }); @@ -927,15 +1053,19 @@ test('server/udp#proxyProtocol exposes real client address (v2 IPv4)', async() = const query = new Packet(); query.header.id = 0x4321; query.header.rd = 1; - query.questions.push({ name: 'proxied.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'proxied.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const header = proxyProtocol.buildV2Ipv4({ - sourceAddress : '203.0.113.77', - destinationAddress : '127.0.0.1', - sourcePort : 50001, - destinationPort : serverPort, - transport : 'DGRAM', + sourceAddress: '203.0.113.77', + destinationAddress: '127.0.0.1', + sourcePort: 50001, + destinationPort: serverPort, + transport: 'DGRAM', }); - const datagram = Buffer.concat([ header, query.toBuffer() ]); + const datagram = Buffer.concat([header, query.toBuffer()]); const sender = udp.createSocket('udp4'); const reply = await new Promise((resolve, reject) => { @@ -954,19 +1084,27 @@ test('server/udp#proxyProtocol exposes real client address (v2 IPv4)', async() = await new Promise(resolve => server.close(resolve)); }); -test('server/udp#proxyProtocol with missing header emits requestError', async() => { +test('server/udp#proxyProtocol with missing header emits requestError', async () => { const server = createUDPServer({ proxyProtocol: true }); let captured; - server.on('requestError', e => { captured = e; }); + server.on('requestError', e => { + captured = e; + }); await server.listen(0, '127.0.0.1'); const { port: serverPort } = server.address(); const query = new Packet(); query.header.id = 1; - query.questions.push({ name: 'noheader.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'noheader.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const sender = udp.createSocket('udp4'); - await new Promise(resolve => sender.send(query.toBuffer(), serverPort, '127.0.0.1', resolve)); + await new Promise(resolve => + sender.send(query.toBuffer(), serverPort, '127.0.0.1', resolve), + ); // Give the server a moment to handle the datagram. await new Promise(resolve => setTimeout(resolve, 20)); await new Promise(resolve => sender.close(resolve)); @@ -976,18 +1114,22 @@ test('server/udp#proxyProtocol with missing header emits requestError', async() await new Promise(resolve => server.close(resolve)); }); -test('server/tcp#proxyProtocol v1 exposes real client address', async() => { +test('server/tcp#proxyProtocol v1 exposes real client address', async () => { const server = createTCPServer({ proxyProtocol: true }); let observed; server.on('request', (request, send, client) => { - observed = { address: client.proxyAddress, port: client.proxyPort, proxy: client.proxy }; + observed = { + address: client.proxyAddress, + port: client.proxyPort, + proxy: client.proxy, + }; const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '127.0.0.1', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '127.0.0.1', }); send(response); }); @@ -996,19 +1138,26 @@ test('server/tcp#proxyProtocol v1 exposes real client address', async() => { const query = new Packet(); query.header.id = 0x1111; - query.questions.push({ name: 'proxied-tcp.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'proxied-tcp.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const dnsMessage = query.toBuffer(); const length = Buffer.alloc(2); length.writeUInt16BE(dnsMessage.length); const proxyHeader = proxyProtocol.buildV1({ - family : 'TCP4', - sourceAddress : '198.51.100.42', - destinationAddress : '127.0.0.1', - sourcePort : 51515, - destinationPort : serverPort, + family: 'TCP4', + sourceAddress: '198.51.100.42', + destinationAddress: '127.0.0.1', + sourcePort: 51515, + destinationPort: serverPort, }); - const reply = await readOneTcpReply(serverPort, Buffer.concat([ proxyHeader, length, dnsMessage ])); + const reply = await readOneTcpReply( + serverPort, + Buffer.concat([proxyHeader, length, dnsMessage]), + ); assert.equal(reply.header.id, 0x1111); assert.equal(reply.answers[0].address, '127.0.0.1'); @@ -1018,18 +1167,22 @@ test('server/tcp#proxyProtocol v1 exposes real client address', async() => { await new Promise(resolve => server.close(resolve)); }); -test('server/tcp#proxyProtocol v2 exposes real client address', async() => { +test('server/tcp#proxyProtocol v2 exposes real client address', async () => { const server = createTCPServer({ proxyProtocol: true }); let observed; server.on('request', (request, send, client) => { - observed = { address: client.proxyAddress, port: client.proxyPort, version: client.proxy.version }; + observed = { + address: client.proxyAddress, + port: client.proxyPort, + version: client.proxy.version, + }; const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '127.0.0.1', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '127.0.0.1', }); send(response); }); @@ -1038,18 +1191,25 @@ test('server/tcp#proxyProtocol v2 exposes real client address', async() => { const query = new Packet(); query.header.id = 0x2222; - query.questions.push({ name: 'proxied-tcp-v2.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + query.questions.push({ + name: 'proxied-tcp-v2.test', + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + }); const dnsMessage = query.toBuffer(); const length = Buffer.alloc(2); length.writeUInt16BE(dnsMessage.length); const proxyHeader = proxyProtocol.buildV2Ipv4({ - sourceAddress : '198.51.100.99', - destinationAddress : '127.0.0.1', - sourcePort : 52525, - destinationPort : serverPort, + sourceAddress: '198.51.100.99', + destinationAddress: '127.0.0.1', + sourcePort: 52525, + destinationPort: serverPort, }); - const reply = await readOneTcpReply(serverPort, Buffer.concat([ proxyHeader, length, dnsMessage ])); + const reply = await readOneTcpReply( + serverPort, + Buffer.concat([proxyHeader, length, dnsMessage]), + ); assert.equal(reply.header.id, 0x2222); assert.equal(observed.address, '198.51.100.99'); @@ -1058,10 +1218,12 @@ test('server/tcp#proxyProtocol v2 exposes real client address', async() => { await new Promise(resolve => server.close(resolve)); }); -test('server/tcp#proxyProtocol with garbage prefix emits requestError', async() => { +test('server/tcp#proxyProtocol with garbage prefix emits requestError', async () => { const server = createTCPServer({ proxyProtocol: true }); let captured; - server.on('requestError', e => { captured = e; }); + server.on('requestError', e => { + captured = e; + }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const { port: serverPort } = server.address(); @@ -1080,22 +1242,25 @@ test('server/tcp#proxyProtocol with garbage prefix emits requestError', async() await new Promise(resolve => server.close(resolve)); }); -test('server/udp/tcp without proxyProtocol still work normally', async() => { +test('server/udp/tcp without proxyProtocol still work normally', async () => { // Regression guard: enabling the option is opt-in; default behavior unchanged. const udpServer = createUDPServer(); udpServer.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '10.0.0.1', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '10.0.0.1', }); send(response); }); await udpServer.listen(0, '127.0.0.1'); - const udpQuery = UDPClient({ dns: '127.0.0.1', port: udpServer.address().port }); + const udpQuery = UDPClient({ + dns: '127.0.0.1', + port: udpServer.address().port, + }); const udpReply = await udpQuery('plain.test'); assert.equal(udpReply.answers[0].address, '10.0.0.1'); await new Promise(resolve => udpServer.close(resolve)); @@ -1104,16 +1269,19 @@ test('server/udp/tcp without proxyProtocol still work normally', async() => { tcpServer.on('request', (request, send) => { const response = Packet.createResponseFromRequest(request); response.answers.push({ - name : request.questions[0].name, - type : Packet.TYPE.A, - class : Packet.CLASS.IN, - ttl : 60, - address : '10.0.0.2', + name: request.questions[0].name, + type: Packet.TYPE.A, + class: Packet.CLASS.IN, + ttl: 60, + address: '10.0.0.2', }); send(response); }); await new Promise(resolve => tcpServer.listen(0, '127.0.0.1', resolve)); - const tcpQuery = TCPClient({ dns: '127.0.0.1', port: tcpServer.address().port }); + const tcpQuery = TCPClient({ + dns: '127.0.0.1', + port: tcpServer.address().port, + }); const tcpReply = await tcpQuery('plain.test'); assert.equal(tcpReply.answers[0].address, '10.0.0.2'); await new Promise(resolve => tcpServer.close(resolve)); diff --git a/test/test.js b/test/test.js index 45d5e59..eb7ad7f 100644 --- a/test/test.js +++ b/test/test.js @@ -9,7 +9,7 @@ let previous = Promise.resolve(); * @github https://github.com/song940 */ const test = (title, fn) => { - previous = previous.then(async() => { + previous = previous.then(async () => { try { await fn(); console.log(color(` ✔ ${title}`, 32));