UNPKG

dns-query

Version:

Node & Browser tested, Non-JSON DNS over HTTPS fetching with minimal dependencies.

400 lines (317 loc) 10 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.URL = exports.UDPEndpoint = exports.UDP6Endpoint = exports.UDP4Endpoint = exports.TimeoutError = exports.ResponseError = exports.InvalidProtocolError = exports.HTTPStatusError = exports.HTTPEndpoint = exports.BaseEndpoint = exports.AbortError = void 0; exports.parseEndpoint = parseEndpoint; exports.reduceError = reduceError; exports.supportedProtocols = void 0; exports.toEndpoint = toEndpoint; let AbortError = typeof global !== 'undefined' ? global.AbortError : typeof window !== 'undefined' ? window.AbortError : null; exports.AbortError = AbortError; if (!AbortError) { exports.AbortError = AbortError = class AbortError extends Error { constructor(message = 'Request aborted.') { super(message); } }; } AbortError.prototype.name = 'AbortError'; AbortError.prototype.code = 'ABORT_ERR'; const URL = typeof globalThis !== 'undefined' && globalThis.URL || require('url').URL; exports.URL = URL; class HTTPStatusError extends Error { constructor(uri, code, method) { super('status=' + code + ' while requesting ' + uri + ' [' + method + ']'); this.uri = uri; this.status = code; this.method = method; } toJSON() { return { code: this.code, uri: this.uri, status: this.status, method: this.method, endpoint: this.endpoint }; } } exports.HTTPStatusError = HTTPStatusError; HTTPStatusError.prototype.name = 'HTTPStatusError'; HTTPStatusError.prototype.code = 'HTTP_STATUS'; class ResponseError extends Error { constructor(message, cause) { super(message); this.cause = cause; } toJSON() { return { message: this.message, endpoint: this.endpoint, code: this.code, cause: reduceError(this.cause) }; } } exports.ResponseError = ResponseError; ResponseError.prototype.name = 'ResponseError'; ResponseError.prototype.code = 'RESPONSE_ERR'; class TimeoutError extends Error { constructor(timeout) { super('Timeout (t=' + timeout + ').'); this.timeout = timeout; } toJSON() { return { code: this.code, endpoint: this.endpoint, timeout: this.timeout }; } } exports.TimeoutError = TimeoutError; TimeoutError.prototype.name = 'TimeoutError'; TimeoutError.prototype.code = 'ETIMEOUT'; const v4Regex = /^((\d{1,3}\.){3,3}\d{1,3})(:(\d{2,5}))?$/; const v6Regex = /^((::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?)(:(\d{2,5}))?$/i; function reduceError(err) { if (typeof err === 'string') { return { message: err }; } try { const json = JSON.stringify(err); if (json !== '{}') { return JSON.parse(json); } } catch (e) {} const error = { message: String(err.message || err) }; if (err.code !== undefined) { error.code = String(err.code); } return error; } const baseParts = /^(([a-z0-9]+:)\/\/)?([^/[\s:]+|\[[^\]]+\])?(:([^/\s]+))?(\/[^\s]*)?(.*)$/; const httpFlags = /\[(post|get|((ipv4|ipv6|name)=([^\]]+)))\]/ig; const updFlags = /\[(((pk|name)=([^\]]+)))\]/ig; function parseEndpoint(endpoint) { const parts = baseParts.exec(endpoint); const protocol = parts[2] || 'https:'; const host = parts[3]; const port = parts[5]; const path = parts[6]; const rest = parts[7]; if (protocol === 'https:' || protocol === 'http:') { const flags = parseFlags(rest, httpFlags); return { name: flags.name, protocol, ipv4: flags.ipv4, ipv6: flags.ipv6, host, port, path, method: flags.post ? 'POST' : 'GET' }; } if (protocol === 'udp:' || protocol === 'udp4:' || protocol === 'udp6:') { const flags = parseFlags(rest, updFlags); const v6Parts = /^\[(.*)\]$/.exec(host); if (v6Parts && protocol === 'udp4:') { throw new Error(`Endpoint parsing error: Cannot use ipv6 host with udp4: (endpoint=${endpoint})`); } if (!v6Parts && protocol === 'udp6:') { throw new Error(`Endpoint parsing error: Incorrectly formatted host for udp6: (endpoint=${endpoint})`); } if (v6Parts) { return new UDP6Endpoint({ protocol: 'udp6:', ipv6: v6Parts[1], port, pk: flags.pk, name: flags.name }); } return new UDP4Endpoint({ protocol: 'udp4:', ipv4: host, port, pk: flags.pk, name: flags.name }); } throw new InvalidProtocolError(protocol, endpoint); } function parseFlags(rest, regex) { regex.lastIndex = 0; const result = {}; while (true) { const match = regex.exec(rest); if (!match) break; if (match[2]) { result[match[3].toLowerCase()] = match[4]; } else { result[match[1].toLowerCase()] = true; } } return result; } class InvalidProtocolError extends Error { constructor(protocol, endpoint) { super(`Invalid Endpoint: unsupported protocol "${protocol}" for endpoint: ${endpoint}, supported protocols: ${supportedProtocols.join(', ')}`); this.protocol = protocol; this.endpoint = endpoint; } toJSON() { return { code: this.code, endpoint: this.endpoint, timeout: this.timeout }; } } exports.InvalidProtocolError = InvalidProtocolError; InvalidProtocolError.prototype.name = 'InvalidProtocolError'; InvalidProtocolError.prototype.code = 'EPROTOCOL'; const supportedProtocols = ['http:', 'https:', 'udp4:', 'udp6:']; exports.supportedProtocols = supportedProtocols; class BaseEndpoint { constructor(opts, isHTTP) { this.name = opts.name || null; this.protocol = opts.protocol; const port = typeof opts.port === 'string' ? opts.port = parseInt(opts.port, 10) : opts.port; if (port === undefined || port === null) { this.port = isHTTP ? this.protocol === 'https:' ? 443 : 80 : opts.pk ? 443 : 53; } else if (typeof port !== 'number' && !isNaN(port)) { throw new Error(`Invalid Endpoint: port "${opts.port}" needs to be a number: ${JSON.stringify(opts)}`); } else { this.port = port; } } toJSON() { return this.toString(); } } exports.BaseEndpoint = BaseEndpoint; class UDPEndpoint extends BaseEndpoint { constructor(opts) { super(opts, false); this.pk = opts.pk || null; } toString() { const port = this.port !== (this.pk ? 443 : 53) ? `:${this.port}` : ''; const pk = this.pk ? ` [pk=${this.pk}]` : ''; const name = this.name ? ` [name=${this.name}]` : ''; return `udp://${this.ipv4 || `[${this.ipv6}]`}${port}${pk}${name}`; } } exports.UDPEndpoint = UDPEndpoint; class UDP4Endpoint extends UDPEndpoint { constructor(opts) { super(Object.assign({ protocol: 'udp4:' }, opts)); if (!opts.ipv4 || typeof opts.ipv4 !== 'string') { throw new Error(`Invalid Endpoint: .ipv4 "${opts.ipv4}" needs to be set: ${JSON.stringify(opts)}`); } this.ipv4 = opts.ipv4; } } exports.UDP4Endpoint = UDP4Endpoint; class UDP6Endpoint extends UDPEndpoint { constructor(opts) { super(Object.assign({ protocol: 'udp6:' }, opts)); if (!opts.ipv6 || typeof opts.ipv6 !== 'string') { throw new Error(`Invalid Endpoint: .ipv6 "${opts.ipv6}" needs to be set: ${JSON.stringify(opts)}`); } this.ipv6 = opts.ipv6; } } exports.UDP6Endpoint = UDP6Endpoint; function safeHost(host) { return v6Regex.test(host) && !v4Regex.test(host) ? `[${host}]` : host; } class HTTPEndpoint extends BaseEndpoint { constructor(opts) { super(Object.assign({ protocol: 'https:' }, opts), true); if (!opts.host) { if (opts.ipv4) { opts.host = opts.ipv4; } if (opts.ipv6) { opts.host = `[${opts.ipv6}]`; } } if (!opts.host || typeof opts.host !== 'string') { throw new Error(`Invalid Endpoint: host "${opts.path}" needs to be set: ${JSON.stringify(opts)}`); } this.host = opts.host; this.path = opts.path || '/dns-query'; this.method = /^post$/i.test(opts.method) ? 'POST' : 'GET'; this.ipv4 = opts.ipv4; this.ipv6 = opts.ipv6; if (!this.ipv6) { const v6Parts = v6Regex.exec(this.host); if (v6Parts) { this.ipv6 = v6Parts[1]; } } if (!this.ipv4) { if (v4Regex.test(this.host)) { this.ipv4 = this.host; } } const url = `${this.protocol}//${safeHost(this.host)}:${this.port}${this.path}`; try { this.url = new URL(url); } catch (err) { throw new Error(err.message + ` [${url}]`); } } toString() { const protocol = this.protocol === 'https:' ? '' : 'http://'; const port = this.port !== (this.protocol === 'https:' ? 443 : 80) ? `:${this.port}` : ''; const method = this.method !== 'GET' ? ' [post]' : ''; const path = this.path === '/dns-query' ? '' : this.path; const name = this.name ? ` [name=${this.name}]` : ''; const ipv4 = this.ipv4 && this.ipv4 !== this.host ? ` [ipv4=${this.ipv4}]` : ''; const ipv6 = this.ipv6 && this.ipv6 !== this.host ? ` [ipv6=${this.ipv6}]` : ''; return `${protocol}${safeHost(this.host)}${port}${path}${method}${ipv4}${ipv6}${name}`; } } exports.HTTPEndpoint = HTTPEndpoint; function toEndpoint(input) { let opts; if (typeof input === 'string') { opts = parseEndpoint(input); } else { if (typeof input !== 'object' || input === null || Array.isArray(input)) { throw new Error(`Can not convert ${input} to an endpoint`); } else if (input instanceof BaseEndpoint) { return input; } opts = input; } if (opts.protocol === null || opts.protocol === undefined) { opts.protocol = 'https:'; } const protocol = opts.protocol; if (protocol === 'udp4:') { return new UDP4Endpoint(opts); } if (protocol === 'udp6:') { return new UDP6Endpoint(opts); } if (protocol === 'https:' || protocol === 'http:') { return new HTTPEndpoint(opts); } throw new InvalidProtocolError(protocol, JSON.stringify(opts)); }