UNPKG

dns-query

Version:

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

308 lines (281 loc) 8.92 kB
let AbortError = typeof global !== 'undefined' ? global.AbortError : typeof window !== 'undefined' ? window.AbortError : null if (!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 export { AbortError, URL } export 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 } } } HTTPStatusError.prototype.name = 'HTTPStatusError' HTTPStatusError.prototype.code = 'HTTP_STATUS' export 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) } } } ResponseError.prototype.name = 'ResponseError' ResponseError.prototype.code = 'RESPONSE_ERR' export class TimeoutError extends Error { constructor (timeout) { super('Timeout (t=' + timeout + ').') this.timeout = timeout } toJSON () { return { code: this.code, endpoint: this.endpoint, timeout: this.timeout } } } 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 export 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 export 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 } export 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 } } } InvalidProtocolError.prototype.name = 'InvalidProtocolError' InvalidProtocolError.prototype.code = 'EPROTOCOL' export const supportedProtocols = ['http:', 'https:', 'udp4:', 'udp6:'] export 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() } } export 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}` } } export 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 } } export 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 } } function safeHost (host) { return v6Regex.test(host) && !v4Regex.test(host) ? `[${host}]` : host } export 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}` } } export 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)) }