dns-query
Advanced tools
Comparing version 0.8.0 to 0.9.0
343
common.js
@@ -1,84 +0,303 @@ | ||
'use strict' | ||
"use strict"; | ||
function inherit (ctor, superCtor) { | ||
Object.defineProperty(ctor, 'super_', { | ||
value: superCtor, | ||
writable: true, | ||
configurable: true | ||
}) | ||
Object.setPrototypeOf(ctor.prototype, superCtor.prototype) | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.URL = exports.UDPEndpoint = exports.UDP6Endpoint = exports.UDP4Endpoint = exports.TimeoutError = exports.ResponseError = 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); | ||
} | ||
}; | ||
} | ||
let AbortError = typeof global !== 'undefined' ? global.AbortError : typeof window !== 'undefined' ? window.AbortError : null | ||
if (!AbortError) { | ||
AbortError = function () { | ||
Error.captureStackTrace(this, AbortError) | ||
this.message = 'Request aborted.' | ||
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; | ||
} | ||
inherit(AbortError, Error) | ||
AbortError.prototype.code = 'ABORT_ERR' | ||
AbortError.prototype.name = 'AbortError' | ||
toJSON() { | ||
return { | ||
code: this.code, | ||
uri: this.uri, | ||
status: this.status, | ||
method: this.method | ||
}; | ||
} | ||
} | ||
function HTTPStatusError (uri, code, method) { | ||
Error.captureStackTrace(this, HTTPStatusError) | ||
this.message = 'status=' + code + ' while requesting ' + uri + ' [' + method + ']' | ||
this.uri = uri | ||
this.status = code | ||
this.method = method | ||
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, | ||
code: this.code, | ||
cause: reduceError(this.cause) | ||
}; | ||
} | ||
} | ||
inherit(HTTPStatusError, Error) | ||
HTTPStatusError.prototype.code = 'HTTP_STATUS' | ||
HTTPStatusError.prototype.name = 'StatusError' | ||
function ResponseError (message, cause) { | ||
Error.captureStackTrace(this, ResponseError) | ||
this.message = message | ||
this.cause = 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, | ||
timeout: this.timeout | ||
}; | ||
} | ||
} | ||
inherit(ResponseError, Error) | ||
ResponseError.prototype.code = 'RESPONSE_ERR' | ||
ResponseError.prototype.name = 'ResponseError' | ||
function TimeoutError (timeout) { | ||
Error.captureStackTrace(this, TimeoutError) | ||
this.message = 'Timeout (t=' + timeout + ').' | ||
this.timeout = 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; | ||
} | ||
inherit(TimeoutError, Error) | ||
TimeoutError.prototype.code = 'ETIMEOUT' | ||
TimeoutError.prototype.name = 'TimeoutError' | ||
function Endpoint (opts) { | ||
if (!opts.protocol) { | ||
opts.protocol = 'https:' | ||
} else if (!['http:', 'https:', 'udp4:', 'udp6:'].includes(opts.protocol)) { | ||
throw new Error(`Invalid Endpoint: unsupported protocol "${opts.protocol}" for endpoint: ${JSON.stringify(opts)}`) | ||
function parseEndpoint(endpoint) { | ||
const parts = /^(([^:]+?:)\/\/)?([^/]*?)(\/.*?)?(\s\[(post|get)\])?(\s\[pk=(.*)\])?(\s\[cors\])?(\s\[name=(.*)\])?$/i.exec(endpoint); | ||
const protocol = parts[2] || 'https:'; | ||
let family = 1; | ||
let host; | ||
let port; | ||
const ipv6Parts = v6Regex.exec(parts[3]); | ||
if (ipv6Parts) { | ||
const ipv4Parts = v4Regex.exec(parts[3]); | ||
if (ipv4Parts) { | ||
host = ipv4Parts[1]; | ||
if (ipv4Parts[4]) { | ||
port = parseInt(ipv4Parts[4]); | ||
} | ||
} else { | ||
family = 2; | ||
host = ipv6Parts[1]; | ||
if (ipv6Parts[9]) { | ||
port = parseInt(ipv6Parts[10]); | ||
} | ||
} | ||
} else { | ||
const portParts = /^([^:]*)(:(.*))?$/.exec(parts[3]); | ||
host = portParts[1]; | ||
if (portParts[3]) { | ||
port = parseInt(portParts[3]); | ||
} | ||
} | ||
if (typeof opts.host !== 'string') { | ||
throw new Error(`Invalid Endpoint: host "${opts.host}" needs to be a string: ${JSON.stringify(opts)}`) | ||
if (protocol === 'udp:' && family === 2 || protocol === 'udp6:') { | ||
return toEndpoint({ | ||
name: parts[11], | ||
protocol: 'udp6:', | ||
ipv6: host, | ||
pk: parts[8], | ||
port | ||
}); | ||
} | ||
if (typeof opts.port !== 'number' && !isNaN(opts.port)) { | ||
throw new Error(`Invalid Endpoint: port "${opts.port}" needs to be a number: ${JSON.stringify(opts)}`) | ||
if (protocol === 'udp:' && family === 1 || protocol === 'udp4:') { | ||
return toEndpoint({ | ||
name: parts[11], | ||
protocol: 'udp4:', | ||
ipv4: host, | ||
pk: parts[8], | ||
port | ||
}); | ||
} | ||
for (const key in opts) { | ||
if (opts[key] !== undefined) { | ||
this[key] = opts[key] | ||
return toEndpoint({ | ||
name: parts[11], | ||
protocol, | ||
host, | ||
port, | ||
path: parts[4], | ||
method: parts[6], | ||
cors: !!parts[9] | ||
}); | ||
} | ||
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; | ||
} | ||
} | ||
} | ||
const rawEndpoints = require('./endpoints.json') | ||
const endpoints = {} | ||
for (const name in rawEndpoints) { | ||
endpoints[name] = new Endpoint(rawEndpoints[name]) | ||
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}`; | ||
} | ||
} | ||
module.exports = { | ||
endpoints, | ||
AbortError: AbortError, | ||
HTTPStatusError: HTTPStatusError, | ||
ResponseError: ResponseError, | ||
TimeoutError: TimeoutError, | ||
Endpoint: Endpoint | ||
exports.UDPEndpoint = UDPEndpoint; | ||
class UDP4Endpoint extends UDPEndpoint { | ||
constructor(opts) { | ||
super(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(opts); | ||
if (!opts.ipv6 || typeof opts.ipv6 !== 'string') { | ||
throw new Error(`Invalid Endpoint: .ipv6 "${opts.ipv6}" needs to be set: ${JSON.stringify(opts)}`); | ||
} | ||
} | ||
} | ||
exports.UDP6Endpoint = UDP6Endpoint; | ||
class HTTPEndpoint extends BaseEndpoint { | ||
constructor(opts) { | ||
super(opts, true); | ||
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.cors = !!opts.cors; | ||
this.path = opts.path || '/dns-query'; | ||
this.method = /^post$/i.test(opts.method) ? 'POST' : 'GET'; | ||
this.ipv4 = opts.ipv4; | ||
this.ipv6 = opts.ipv6; | ||
const urlHost = v6Regex.test(this.host) && !v4Regex.test(this.host) ? `[${this.host}]` : this.host; | ||
this.url = new URL(`${this.protocol}//${urlHost}:${this.port}${this.path}`); | ||
} | ||
toString() { | ||
const port = this.port !== (this.protocol === 'https:' ? 443 : 80) ? `:${this.port}` : ''; | ||
const method = this.method !== 'GET' ? ' [post]' : ''; | ||
const cors = this.cors ? ' [cors]' : ''; | ||
const path = this.path === '/dns-query' ? '' : this.path; | ||
const name = this.name ? ` [name=${this.name}]` : ''; | ||
return `${this.protocol}//${this.host}${port}${path}${method}${cors}${name}`; | ||
} | ||
} | ||
exports.HTTPEndpoint = HTTPEndpoint; | ||
function toEndpoint(opts) { | ||
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 Error(`Invalid Endpoint: unsupported protocol "${opts.protocol}" for endpoint: ${JSON.stringify(opts)}, supported protocols: ${supportedProtocols.join(', ')}`); | ||
} |
481
index.js
@@ -1,178 +0,359 @@ | ||
'use strict' | ||
const packet = require('dns-packet') | ||
const lib = require('./lib.node.js') | ||
const common = require('./common.js') | ||
const AbortError = common.AbortError | ||
const ResponseError = common.ResponseError | ||
const Endpoint = common.Endpoint | ||
const endpoints = common.endpoints | ||
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 | ||
"use strict"; | ||
function queryOne (endpoint, query, timeout, abortSignal) { | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
Object.defineProperty(exports, "AbortError", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.AbortError; | ||
} | ||
}); | ||
Object.defineProperty(exports, "BaseEndpoint", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.BaseEndpoint; | ||
} | ||
}); | ||
Object.defineProperty(exports, "HTTPEndpoint", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.HTTPEndpoint; | ||
} | ||
}); | ||
Object.defineProperty(exports, "HTTPStatusError", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.HTTPStatusError; | ||
} | ||
}); | ||
Object.defineProperty(exports, "ResponseError", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.ResponseError; | ||
} | ||
}); | ||
exports.Session = void 0; | ||
Object.defineProperty(exports, "TimeoutError", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.TimeoutError; | ||
} | ||
}); | ||
Object.defineProperty(exports, "UDP4Endpoint", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.UDP4Endpoint; | ||
} | ||
}); | ||
Object.defineProperty(exports, "UDP6Endpoint", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.UDP6Endpoint; | ||
} | ||
}); | ||
exports.backup = void 0; | ||
exports.endpoints = endpoints; | ||
exports.loadEndpoints = loadEndpoints; | ||
Object.defineProperty(exports, "parseEndpoint", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.parseEndpoint; | ||
} | ||
}); | ||
exports.query = query; | ||
Object.defineProperty(exports, "toEndpoint", { | ||
enumerable: true, | ||
get: function () { | ||
return _common.toEndpoint; | ||
} | ||
}); | ||
exports.wellknown = wellknown; | ||
var packet = _interopRequireWildcard(require("@leichtgewicht/dns-packet"), true); | ||
var lib = _interopRequireWildcard(require("./lib.js"), true); | ||
var _resolvers = require("./resolvers.js"); | ||
var _common = require("./common.js"); | ||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||
function resolversToWellknown(res) { | ||
const resolvers = res.data.map(resolver => { | ||
resolver.endpoint = (0, _common.toEndpoint)(Object.assign({ | ||
name: resolver.name | ||
}, resolver.endpoint)); | ||
return resolver; | ||
}); | ||
const endpoints = resolvers.map(resolver => resolver.endpoint); | ||
return lib.processWellknown({ | ||
data: { | ||
resolvers, | ||
resolverByName: resolvers.reduce((byName, resolver) => { | ||
byName[resolver.name] = resolver; | ||
return byName; | ||
}, {}), | ||
endpoints, | ||
endpointByName: endpoints.reduce((byName, endpoint) => { | ||
byName[endpoint.name] = endpoint; | ||
return byName; | ||
}, {}) | ||
}, | ||
time: res.time === null || res.time === undefined ? Date.now() : res.time | ||
}); | ||
} | ||
const backup = resolversToWellknown(_resolvers.resolvers); | ||
exports.backup = backup; | ||
function toMultiQuery(singleQuery) { | ||
const query = Object.assign({ | ||
type: 'query' | ||
}, singleQuery); | ||
delete query.question; | ||
query.questions = []; | ||
if (singleQuery.question) { | ||
query.questions.push(singleQuery.question); | ||
} | ||
return query; | ||
} | ||
function queryOne(endpoint, query, timeout, abortSignal) { | ||
if (abortSignal && abortSignal.aborted) { | ||
return Promise.reject(new AbortError()) | ||
return Promise.reject(new _common.AbortError()); | ||
} | ||
if (endpoint.protocol === 'udp4:' || endpoint.protocol === 'udp6:') { | ||
return lib.queryDns(endpoint, query, timeout, abortSignal) | ||
return lib.queryDns(endpoint, toMultiQuery(query), timeout, abortSignal).then(result => { | ||
result.question = result.questions[0]; | ||
delete result.questions; | ||
return result; | ||
}); | ||
} | ||
return queryDoh(endpoint, query, timeout, abortSignal) | ||
return queryDoh(endpoint, query, timeout, abortSignal); | ||
} | ||
function queryDoh (endpoint, query, timeout, abortSignal) { | ||
const protocol = endpoint.protocol || 'https:' | ||
return new Promise(function (resolve, reject) { | ||
lib.request( | ||
protocol, | ||
endpoint.host, | ||
endpoint.port ? parseInt(endpoint.port, 10) : (protocol === 'https:' ? 443 : 80), | ||
endpoint.path || '/dns-query', | ||
/^post$/i.test(endpoint.method) ? 'POST' : 'GET', | ||
packet.encode(Object.assign({ | ||
flags: packet.RECURSION_DESIRED, | ||
type: 'query' | ||
}, query)), | ||
timeout, | ||
abortSignal, | ||
function (error, data, response) { | ||
let decoded | ||
if (error === null) { | ||
if (data.length === 0) { | ||
error = new ResponseError('Empty.') | ||
} else { | ||
try { | ||
decoded = packet.decode(data) | ||
} catch (err) { | ||
error = new ResponseError('Invalid packet (cause=' + err.message + ')', err) | ||
} | ||
} | ||
function queryDoh(endpoint, query, timeout, abortSignal) { | ||
return lib.request(endpoint.url, endpoint.method, packet.query.encode(Object.assign({ | ||
flags: packet.RECURSION_DESIRED | ||
}, query)), timeout, abortSignal).then(function (res) { | ||
const data = res.data; | ||
const response = res.response; | ||
let error = res.error; | ||
if (error === undefined) { | ||
if (data.length === 0) { | ||
error = new _common.ResponseError('Empty.'); | ||
} else { | ||
try { | ||
const decoded = packet.response.decode(data); | ||
decoded.endpoint = endpoint; | ||
decoded.response = response; | ||
return decoded; | ||
} catch (err) { | ||
error = new _common.ResponseError('Invalid packet (cause=' + err.message + ')', err); | ||
} | ||
if (error !== null) { | ||
reject(Object.assign(error, { response, endpoint })) | ||
} else { | ||
decoded.endpoint = endpoint | ||
decoded.response = response | ||
resolve(decoded) | ||
} | ||
} | ||
) | ||
}) | ||
} | ||
throw Object.assign(error, { | ||
response, | ||
endpoint | ||
}); | ||
}, error => { | ||
throw Object.assign(error, { | ||
endpoint | ||
}); | ||
}); | ||
} | ||
function query (q, opts) { | ||
opts = Object.assign({ | ||
retries: 5, | ||
timeout: 30000 | ||
}, opts) | ||
let endpoints | ||
try { | ||
if (opts.endpoints === 'doh') { | ||
endpoints = lib.endpoints({ doh: true, dns: false }) | ||
} else if (opts.endpoints === 'dns') { | ||
endpoints = lib.endpoints({ doh: false, dns: true }) | ||
} else { | ||
endpoints = parseEndpoints(opts.endpoints) || lib.endpoints({ doh: true, dns: true }) | ||
const UPDATE_URL = new _common.URL('https://martinheidegger.github.io/dns-query/resolvers.json'); | ||
class Session { | ||
constructor(opts) { | ||
this.opts = Object.assign({ | ||
retries: 5, | ||
timeout: 30000, | ||
// 30 seconds | ||
update: true, | ||
updateURL: UPDATE_URL, | ||
persist: false, | ||
localStoragePrefix: 'dnsquery_', | ||
maxAge: 300000 // 5 minutes | ||
}, opts); | ||
this._wellknownP = null; | ||
} | ||
_wellknown(force, outdated) { | ||
if (!force && this._wellknownP !== null) { | ||
return this._wellknownP.then(res => { | ||
if (res.time < Date.now() - this.opts.maxAge) { | ||
return this._wellknown(true, res); | ||
} | ||
return res; | ||
}); | ||
} | ||
if (!endpoints || endpoints.length === 0) { | ||
throw new Error('No endpoints defined.') | ||
} | ||
} catch (error) { | ||
return Promise.reject(error) | ||
this._wellknownP = !this.opts.update ? Promise.resolve(backup) : lib.loadJSON(this.opts.updateURL, this.opts.persist ? { | ||
name: 'resolvers.json', | ||
localStoragePrefix: this.opts.localStoragePrefix, | ||
maxTime: Date.now() - this.opts.maxAge | ||
} : null, this.opts.timeout).then(res => resolversToWellknown({ | ||
data: res.data.resolvers, | ||
time: res.time | ||
})).catch(() => outdated || backup); | ||
return this._wellknownP; | ||
} | ||
return queryN(endpoints, q, opts) | ||
wellknown() { | ||
return this._wellknown(false).then(data => data.data); | ||
} | ||
endpoints() { | ||
return this.wellknown().then(data => data.endpoints); | ||
} | ||
query(q, opts) { | ||
opts = Object.assign({}, this.opts, opts); | ||
if (!q.question) return Promise.reject(new Error('To request data you need to specify a .question!')); | ||
return loadEndpoints(this, opts.endpoints).then(endpoints => queryN(endpoints, q, opts)); | ||
} | ||
} | ||
function queryN (endpoints, q, opts) { | ||
const endpoint = endpoints.length === 1 | ||
? endpoints[0] | ||
: endpoints[Math.floor(Math.random() * endpoints.length) % endpoints.length] | ||
return queryOne(endpoint, q, opts.timeout, opts.signal) | ||
.then( | ||
data => { | ||
// Add the endpoint to give a chance to identify which endpoint returned the result | ||
data.endpoint = endpoint | ||
return data | ||
}, | ||
err => { | ||
if (err.name === 'AbortError' || opts.retries === 0) { | ||
throw err | ||
} | ||
if (opts.retries > 0) { | ||
opts.retries -= 1 | ||
} | ||
return query(q, opts) | ||
} | ||
) | ||
exports.Session = Session; | ||
const defautSession = new Session(); | ||
function query(q, opts) { | ||
return defautSession.query(q, opts); | ||
} | ||
function parseEndpoints (input) { | ||
if (!input) { | ||
return | ||
function endpoints() { | ||
return defautSession.endpoints(); | ||
} | ||
function wellknown() { | ||
return defautSession.wellknown(); | ||
} | ||
function queryN(endpoints, q, opts) { | ||
if (endpoints.length === 0) { | ||
throw new Error('No endpoints defined.'); | ||
} | ||
if (typeof input[Symbol.iterator] !== 'function' || typeof input === 'string') { | ||
throw new Error('Endpoints needs to be iterable.') | ||
} | ||
const result = [] | ||
for (let endpoint of input) { | ||
if (typeof endpoint === 'object') { | ||
if (!(endpoint instanceof Endpoint)) { | ||
endpoint = new Endpoint(endpoint) | ||
} | ||
result.push(endpoint) | ||
} else if (typeof endpoint === 'string') { | ||
result.push(endpoints[endpoint] || parseEndpoint(endpoint)) | ||
const endpoint = endpoints.length === 1 ? endpoints[0] : endpoints[Math.floor(Math.random() * endpoints.length) % endpoints.length]; | ||
return queryOne(endpoint, q, opts.timeout, opts.signal).then(data => { | ||
// Add the endpoint to give a chance to identify which endpoint returned the result | ||
data.endpoint = endpoint; | ||
return data; | ||
}, err => { | ||
if (err.name === 'AbortError' || opts.retries === 0) { | ||
throw err; | ||
} | ||
} | ||
return result | ||
if (opts.retries > 0) { | ||
opts.retries -= 1; | ||
} | ||
return query(q, opts); | ||
}); | ||
} | ||
function parseEndpoint (endpoint) { | ||
const parts = /^(([^:]+?:)\/\/)?([^/]*?)(\/.*?)?(\s\[(post|get)\])?$/i.exec(endpoint) | ||
let protocol = parts[2] || 'https:' | ||
let family = 1 | ||
let host | ||
let port | ||
const ipv6Parts = v6Regex.exec(parts[3]) | ||
if (ipv6Parts) { | ||
const ipv4Parts = v4Regex.exec(parts[3]) | ||
if (ipv4Parts) { | ||
host = ipv4Parts[1] | ||
if (ipv4Parts[4]) { | ||
port = parseInt(ipv4Parts[4]) | ||
function filterEndpoints(filter) { | ||
return function (endpoints) { | ||
const result = []; | ||
for (const name in endpoints) { | ||
const endpoint = endpoints[name]; | ||
if (filter(endpoint)) { | ||
result.push(endpoint); | ||
} | ||
} else { | ||
family = 2 | ||
host = ipv6Parts[1] | ||
if (ipv6Parts[9]) { | ||
port = parseInt(ipv6Parts[10]) | ||
} | ||
} | ||
} else { | ||
const portParts = /^([^:]*)(:(.*))?$/.exec(parts[3]) | ||
host = portParts[1] | ||
if (portParts[3]) { | ||
port = parseInt(portParts[3]) | ||
} | ||
return result; | ||
}; | ||
} | ||
const filterDoh = filterEndpoints(function filterDoh(endpoint) { | ||
return endpoint.protocol === 'https:' || endpoint.protocol === 'http:'; | ||
}); | ||
const filterDns = filterEndpoints(function filterDns(endpoint) { | ||
return endpoint.protocol === 'udp4:' || endpoint.protocol === 'udp6:'; | ||
}); | ||
function isPromise(input) { | ||
if (input === null) { | ||
return false; | ||
} | ||
if (protocol === 'udp:') { | ||
protocol = family === 2 ? 'udp6:' : 'udp4:' | ||
if (typeof input !== 'object') { | ||
return false; | ||
} | ||
return new Endpoint({ | ||
protocol: protocol, | ||
host, | ||
port, | ||
path: parts[4], | ||
method: parts[6] | ||
}) | ||
return typeof input.then === 'function'; | ||
} | ||
module.exports = { | ||
query: query, | ||
endpoints: endpoints, | ||
parseEndpoints: parseEndpoints, | ||
AbortError: AbortError, | ||
ResponseError: ResponseError, | ||
TimeoutError: common.TimeoutError, | ||
HTTPStatusError: common.HTTPStatusError, | ||
Endpoint: Endpoint | ||
function isString(entry) { | ||
return typeof entry === 'string'; | ||
} | ||
function loadEndpoints(session, input) { | ||
const p = isPromise(input) ? input : Promise.resolve(input); | ||
return p.then(function (endpoints) { | ||
if (endpoints === 'doh') { | ||
return session.endpoints().then(filterDoh); | ||
} | ||
if (endpoints === 'dns') { | ||
return session.endpoints().then(filterDns); | ||
} | ||
const type = typeof endpoints; | ||
if (type === 'function') { | ||
return session.endpoints().then(filterEndpoints(endpoints)); | ||
} | ||
if (endpoints === null || endpoints === undefined) { | ||
return session.endpoints(); | ||
} | ||
if (type === 'string' || typeof endpoints[Symbol.iterator] !== 'function') { | ||
throw new Error(`Endpoints (${endpoints}) needs to be iterable.`); | ||
} | ||
endpoints = Array.from(endpoints).filter(Boolean); | ||
if (endpoints.findIndex(isString) === -1) { | ||
return endpoints.map(endpoint => { | ||
if (endpoint instanceof _common.BaseEndpoint) { | ||
return endpoint; | ||
} | ||
return (0, _common.toEndpoint)(endpoint); | ||
}); | ||
} | ||
return session.wellknown().then(wellknown => endpoints.map(endpoint => { | ||
if (endpoint instanceof _common.BaseEndpoint) { | ||
return endpoint; | ||
} | ||
if (typeof endpoint === 'string') { | ||
return wellknown.endpointByName[endpoint] || (0, _common.parseEndpoint)(endpoint); | ||
} | ||
return (0, _common.toEndpoint)(endpoint); | ||
})); | ||
}); | ||
} |
@@ -1,104 +0,179 @@ | ||
'use strict' | ||
/* global XMLHttpRequest */ | ||
const Buffer = require('buffer').Buffer | ||
const common = require('./common.js') | ||
const AbortError = common.AbortError | ||
const HTTPStatusError = common.HTTPStatusError | ||
const TimeoutError = common.TimeoutError | ||
const contentType = 'application/dns-message' | ||
const endpoints = Object.values(common.endpoints).filter(function (endpoint) { | ||
return !endpoint.filter && !endpoint.logging && endpoint.cors | ||
}) | ||
"use strict"; | ||
// https://tools.ietf.org/html/rfc8484 | ||
function toRFC8484 (buffer) { | ||
return buffer.toString('base64') | ||
.replace(/=/g, '') | ||
.replace(/\+/g, '-') | ||
.replace(/\//g, '_') | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.loadJSON = loadJSON; | ||
exports.processWellknown = processWellknown; | ||
exports.queryDns = queryDns; | ||
exports.request = request; | ||
var utf8Codec = _interopRequireWildcard(require("utf8-codec"), true); | ||
var _base64Codec = require("@leichtgewicht/base64-codec"); | ||
var _common = require("./common.js"); | ||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||
/* global XMLHttpRequest, localStorage */ | ||
const contentType = 'application/dns-message'; | ||
function noop() {} | ||
function queryDns() { | ||
throw new Error('Only "doh" endpoints are supported in the browser'); | ||
} | ||
function noop () {} | ||
async function loadJSON(url, cache, timeout, abortSignal) { | ||
const cacheKey = cache ? cache.localStoragePrefix + cache.name : null; | ||
function request (protocol, host, port, path, method, packet, timeout, abortSignal, cb) { | ||
const uri = protocol + '//' + host + ':' + port + path + (method === 'GET' ? '?dns=' + toRFC8484(packet) : '') | ||
const xhr = new XMLHttpRequest() | ||
xhr.open(method, uri, true) | ||
xhr.setRequestHeader('Accept', contentType) | ||
if (method === 'POST') { | ||
xhr.setRequestHeader('Content-Type', contentType) | ||
} | ||
xhr.responseType = 'arraybuffer' | ||
xhr.timeout = timeout | ||
xhr.ontimeout = ontimeout | ||
xhr.onreadystatechange = onreadystatechange | ||
xhr.onerror = onerror | ||
xhr.onload = onload | ||
if (method === 'GET') { | ||
xhr.send() | ||
} else { | ||
xhr.send(packet) | ||
} | ||
if (cacheKey) { | ||
try { | ||
const cached = JSON.parse(localStorage.getItem(cacheKey)); | ||
if (abortSignal) { | ||
abortSignal.addEventListener('abort', onabort) | ||
if (cached && cached.time > cache.maxTime) { | ||
return cached; | ||
} | ||
} catch (err) {} | ||
} | ||
function ontimeout () { | ||
finish(new TimeoutError(timeout)) | ||
const { | ||
data | ||
} = await requestRaw(url, 'GET', null, timeout, abortSignal); | ||
const result = { | ||
time: Date.now(), | ||
data: JSON.parse(utf8Codec.decode(data)) | ||
}; | ||
if (cacheKey) { | ||
try { | ||
xhr.abort() | ||
} catch (e) {} | ||
localStorage.setItem(cacheKey, JSON.stringify(result)); | ||
} catch (err) { | ||
result.time = null; | ||
} | ||
} | ||
function onload () { | ||
if (xhr.status !== 200) { | ||
finish(new HTTPStatusError(uri, xhr.status, method)) | ||
return result; | ||
} | ||
function requestRaw(url, method, data, timeout, abortSignal) { | ||
return new Promise((resolve, reject) => { | ||
const target = new _common.URL(url); | ||
if (method === 'GET' && data) { | ||
target.search = '?dns=' + _base64Codec.base64URL.decode(data); | ||
} | ||
const uri = target.toString(); | ||
const xhr = new XMLHttpRequest(); | ||
xhr.open(method, uri, true); | ||
xhr.setRequestHeader('Accept', contentType); | ||
if (method === 'POST') { | ||
xhr.setRequestHeader('Content-Type', contentType); | ||
} | ||
xhr.responseType = 'arraybuffer'; | ||
xhr.timeout = timeout; | ||
xhr.ontimeout = ontimeout; | ||
xhr.onreadystatechange = onreadystatechange; | ||
xhr.onerror = onerror; | ||
xhr.onload = onload; | ||
if (method === 'POST') { | ||
xhr.send(data); | ||
} else { | ||
finish(null, Buffer.from(xhr.response)) | ||
xhr.send(); | ||
} | ||
} | ||
function onreadystatechange () { | ||
if (xhr.readyState > 1 && xhr.status !== 200 && xhr.status !== 0) { | ||
finish(new HTTPStatusError(uri, xhr.status, method)) | ||
if (abortSignal) { | ||
abortSignal.addEventListener('abort', onabort); | ||
} | ||
function ontimeout() { | ||
finish(new _common.TimeoutError(timeout)); | ||
try { | ||
xhr.abort() | ||
xhr.abort(); | ||
} catch (e) {} | ||
} | ||
} | ||
let finish = function (error, data) { | ||
finish = noop | ||
if (abortSignal) { | ||
abortSignal.removeEventListener('abort', onabort) | ||
function onload() { | ||
if (xhr.status !== 200) { | ||
finish(new _common.HTTPStatusError(uri, xhr.status, method)); | ||
} else { | ||
let buf; | ||
if (typeof xhr.response === 'string') { | ||
buf = utf8Codec.encode(xhr.response); | ||
} else if (xhr.response instanceof Uint8Array) { | ||
buf = xhr.response; | ||
} else if (Array.isArray(xhr.response) || xhr.response instanceof ArrayBuffer) { | ||
buf = new Uint8Array(xhr.response); | ||
} else { | ||
throw new Error('Unprocessable response ' + xhr.response); | ||
} | ||
finish(null, buf); | ||
} | ||
} | ||
cb(error, data, xhr) | ||
} | ||
function onerror () { | ||
finish(xhr.status === 200 ? new Error('Inexplicable XHR Error') : new HTTPStatusError(uri, xhr.status, method)) | ||
} | ||
function onreadystatechange() { | ||
if (xhr.readyState > 1 && xhr.status !== 200 && xhr.status !== 0) { | ||
finish(new _common.HTTPStatusError(uri, xhr.status, method)); | ||
function onabort () { | ||
finish(new AbortError()) | ||
try { | ||
xhr.abort() | ||
} catch (e) {} | ||
} | ||
} | ||
try { | ||
xhr.abort(); | ||
} catch (e) {} | ||
} | ||
} | ||
module.exports = { | ||
request: request, | ||
queryDns: function () { | ||
throw new Error('Only "doh" endpoints are supported in the browser') | ||
}, | ||
endpoints: opts => { | ||
if (opts.doh) { | ||
return endpoints | ||
let finish = function (error, data) { | ||
finish = noop; | ||
if (abortSignal) { | ||
abortSignal.removeEventListener('abort', onabort); | ||
} | ||
if (error) { | ||
resolve({ | ||
error, | ||
response: xhr | ||
}); | ||
} else { | ||
resolve({ | ||
data, | ||
response: xhr | ||
}); | ||
} | ||
}; | ||
function onerror() { | ||
finish(xhr.status === 200 ? new Error('Inexplicable XHR Error') : new _common.HTTPStatusError(uri, xhr.status, method)); | ||
} | ||
if (opts.dns) { | ||
throw new Error('Only "doh" is supported in the browser') | ||
function onabort() { | ||
finish(new _common.AbortError()); | ||
try { | ||
xhr.abort(); | ||
} catch (e) {} | ||
} | ||
} | ||
}); | ||
} | ||
function request(url, method, packet, timeout, abortSignal) { | ||
return requestRaw(url, method, packet, timeout, abortSignal); | ||
} | ||
function processWellknown(wellknown) { | ||
return { | ||
time: wellknown.time, | ||
data: Object.assign({}, wellknown.data, { | ||
endpoints: wellknown.data.endpoints.filter(ep => ep.cors) | ||
}) | ||
}; | ||
} |
{ | ||
"name": "dns-query", | ||
"version": "0.8.0", | ||
"version": "0.9.0", | ||
"description": "Node & Browser tested, Non-JSON DNS over HTTPS fetching with minimal dependencies.", | ||
"main": "index.js", | ||
"types": "types", | ||
"module": "index.mjs", | ||
"types": "types/index.d.ts", | ||
"bin": { | ||
@@ -11,16 +12,43 @@ "dns-query": "./bin/dns-query" | ||
"scripts": { | ||
"lint": "npm run endpoints && standard *.js bin/* && dtslint --localTs node_modules/typescript/lib types", | ||
"endpoints": "node endpoints-json.js", | ||
"prepack": "npm run endpoints", | ||
"lint": "standard '**/*.mjs' bin/* test/env && dtslint types --localTs node_modules/typescript/lib", | ||
"update-resolvers": "node bin/update-resolvers && standard --fix resolvers.mjs && esm2umd", | ||
"prepare": "esm2umd", | ||
"test": "node test/env npm run test-impl", | ||
"test-impl": "fresh-tape test/all.js", | ||
"test-cov": "node test/env c8 -r lcov -r html npm run test-impl", | ||
"test-cjs": "node test/env fresh-tape test/all.js", | ||
"test-impl": "fresh-tape test/all.mjs", | ||
"test-cov": "node test/env c8 -r lcov -r html -r text npm run test-impl", | ||
"browser-test": "env TEST_ENV=browser node test/env npm run browser-test-impl -s", | ||
"browser-test-impl": "browserify -t envify --debug test/all.js | tape-run", | ||
"browser-test-cov": "env TEST_ENV=browser node test/env npm run browser-test-cov-impl -s", | ||
"browser-test-cov-impl": "browserify -t envify -t coverify --debug test/all.js | tape-run | coverify" | ||
"browser-test-impl": "browserify -t envify --debug test/all.js | tape-run" | ||
}, | ||
"browser": { | ||
"./lib.node.js": "./lib.browser.js" | ||
"./lib.js": "./lib.browser.js", | ||
"./lib.mjs": "./lib.browser.mjs" | ||
}, | ||
"exports": { | ||
".": { | ||
"import": "./index.mjs", | ||
"types": "./types/index.d.ts", | ||
"require": "./index.js" | ||
}, | ||
"./common.js": { | ||
"import": "./common.mjs", | ||
"types": "./common.d.ts", | ||
"require": "./common.js" | ||
}, | ||
"./resolvers.js": { | ||
"import": "./resolvers.mjs", | ||
"types": "./resolvers.d.ts", | ||
"require": "./resolvers.js" | ||
}, | ||
"./lib.js": { | ||
"import": "./lib.mjs", | ||
"types": "./lib.d.ts", | ||
"require": "./lib.js", | ||
"browser": { | ||
"import": "./lib.browser.mjs", | ||
"types": "./lib.d.ts", | ||
"require": "./lib.browser.js" | ||
} | ||
} | ||
}, | ||
"keywords": [ | ||
@@ -43,21 +71,23 @@ "dns", | ||
"dependencies": { | ||
"@leichtgewicht/ip-codec": "^2.0.2", | ||
"@types/dns-packet": "^5.2.0", | ||
"dns-packet": "^5.3.0", | ||
"dns-socket": "^4.2.2" | ||
"@leichtgewicht/base64-codec": "^1.0.0", | ||
"@leichtgewicht/dns-packet": "^6.0.1", | ||
"@leichtgewicht/dns-socket": "^5.0.0", | ||
"@leichtgewicht/ip-codec": "^2.0.4", | ||
"utf8-codec": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@definitelytyped/dtslint": "0.0.112", | ||
"@leichtgewicht/dnsstamp": "^1.1.4", | ||
"@leichtgewicht/esm2umd": "^0.4.0", | ||
"abort-controller": "^3.0.0", | ||
"browserify": "^17.0.0", | ||
"c8": "^7.7.3", | ||
"coverify": "^1.5.1", | ||
"dtslint": "^4.1.1", | ||
"c8": "^7.11.3", | ||
"envify": "^4.1.0", | ||
"fresh-tape": "^5.2.4", | ||
"markdown-it": "^12.1.0", | ||
"ngrok": "^4.0.1", | ||
"p-map": "^4.0.0", | ||
"standard": "^16.0.3", | ||
"tape-run": "^9.0.0", | ||
"typescript": "^4.3.5", | ||
"fresh-tape": "^5.5.3", | ||
"markdown-it": "^13.0.1", | ||
"ngrok": "^4.3.1", | ||
"p-filter": "^3.0.0", | ||
"standard": "^17.0.0", | ||
"tape-run": "^10.0.0", | ||
"typescript": "^4.6.4", | ||
"xhr2": "^0.2.1" | ||
@@ -64,0 +94,0 @@ }, |
172
README.md
@@ -15,5 +15,26 @@ # dns-query | ||
By default `dns-query` uses well-known public dns-over-https servers to execute | ||
queries! These servers come with caveats, please look at [`./endpoints.md`](./endpoints.md) | ||
for more information. | ||
queries [automatically compiled][] by the data from the [DNSCrypt][] project. | ||
[automatically compiled]: https://github.com/martinheidegger/dns-query/actions/workflows/update.yml | ||
[DNSCrypt]: https://dnscrypt.info/ | ||
The npm package comes with the list that was/is current on the time of the publication. | ||
It will will try to automatically download the list from [the dns-query website][] unless | ||
you set the `.update` property on a `Session` object. | ||
[the dns-query website]: https://martinheidegger.github.io/dns-query/resolvers.json | ||
These servers come with caveats that you should be aware of: | ||
- A server may filter, log or limit the requests it receives! | ||
- Filtering can be useful in case you want to avoid malware/ads/adult-content. | ||
- Logging may be required in some countries and limiting may be part of a business model. | ||
- Furthermore the different endpoints may or may not be distributed around the globe, | ||
making requests slower/faster depending on the client's location. | ||
- Not all endpoints supply CORS headers which means that the list is severly reduced if you use this | ||
library in the browser. | ||
If you are presenting this library to an end-user, you may want to allow them to decide what endpoint | ||
they want to use as it has privacy and usage implications! | ||
## DNS support | ||
@@ -37,11 +58,10 @@ | ||
endpoints = [{ host: 'cloudflare-dns.com' }] // Specify using properties | ||
endpoints = (endpoint) => endpoint.protocol === 'https:' // Use a filter against the well-known endpoints | ||
endpoints = Promise.resolve('doh') // The endpoints can also be a promise | ||
try { | ||
const { answers } = await query({ | ||
questions: [ | ||
{type: 'A', name: 'google.com'}, | ||
{type: 'A', name: 'twitter.com'} | ||
] | ||
question: {type: 'A', name: 'google.com'} | ||
}, { | ||
/* Options (optional) */ | ||
endpoints, | ||
endpoints: endpoints, | ||
retry: 3, // (optional) retries if a given endpoint fails; -1 = infinite retries; 0 = no retry | ||
@@ -69,26 +89,45 @@ timeout: 4000, // (optional, default=30000) timeout for single requests | ||
Execute a dns query over https. | ||
dns-query - Execute a dns query over https. | ||
Examples: | ||
USAGE: | ||
dns-query <Options> <Input> | ||
EXAMPLES: | ||
# Fetch from the google dns-over-https endpoint the A-name of google.com | ||
$ dns-query --json -e google \ | ||
'{ "questions": [{ "type": "A", "name": "google.com" }] }' | ||
'{ "question": { "type": "A", "name": "google.com" } }' | ||
$ echo '{ "questions": [{ "type": "A", "name": "google.com" }] }' \ | ||
# Fetch TXT entries for ipfs.io through regular dns | ||
$ dns-query --json --dns \ | ||
'{ "question": { "type": "TXT", "name": "ipfs.io" } }' | ||
# Pass the query through stdin | ||
$ echo '{ "question": { "type": "A", "name": "google.com" } }' \ | ||
| dns-query --stdin --endpoint cloudflare | ||
--help, -h ....... Show this help | ||
--version, -v .... Show the version | ||
--json ........... --type=json | ||
--base64 ......... --type=base64 | ||
--binary ......... --type=binary | ||
--type ........... Input type. Options: json, base64, binary; Default: json | ||
--out ............ Output type. Defaults to the input --type. | ||
--stdin .......... Get <input> from stdin instead of cli arguments | ||
--endpoint, -e ... Use a specific endpoint. Can be either the name of a known | ||
endpoint, a json object or an url. By default uses one of the known endpoints. | ||
If multiple are provided, one at random will be used. | ||
--endpoints ...... Lists all known endpoints as json. | ||
--retry .......... Number of retries to do in case a request fails, default: 3 | ||
--timeout ........ Timeout for the request in milliseconds, default: 30000 | ||
OPTIONS: | ||
--help, -h ....... Show this help | ||
--version, -v .... Show the version | ||
--json ........... --type=json | ||
--base64 ......... --type=base64 | ||
--binary ......... --type=binary | ||
--type ........... Input type. Options: json, base64, binary; Default: json | ||
--out ............ Output type. Defaults to the input --type. | ||
--stdin .......... Get <input> from stdin instead of cli arguments | ||
--dns ............ Use dns endpoints | ||
--doh ............ Use doh endpoints | ||
--endpoint, -e ... Use a specific endpoint. Can be either the name of a known | ||
endpoint, a json object or an url. By default uses one of the known endpoints. | ||
If multiple are provided, one at random will be used. | ||
--endpoints ...... Lists all known endpoints as json. | ||
--resolvers ...... List all known resolvers as json. | ||
--response ....... Show the http response in the result. | ||
--retries ........ Number of retries to do in case requests fails, default: 5 | ||
--timeout ........ Timeout for the request in milliseconds, default: 30000 (5 sec) | ||
--max-age ........ Max age of the persisted data, default: 300000 (5 min) | ||
--no-persist ..... Dont persist the the latest resolvers | ||
--offline ........ Do not update the resolver list | ||
``` | ||
@@ -101,23 +140,35 @@ | ||
```typescript | ||
interface EndpointProps { | ||
/* https is the default for DoH endpoints, udp4:/upd6: for regular dns endpoints and http for debug only! defaults to https: */ | ||
protocol?: 'http:' | 'https:' | 'udp4:' | 'udp6:'; | ||
type EndpointProps = { | ||
/* https is the default for DoH endpoints and http for debug only! If you don't specify a protocol, https is assumed */ | ||
protocol: 'https:' | 'http:' | ||
/* https port, defaults to 443 for https, 80 for http */ | ||
port?: number | string | null | ||
/* Host to look up */ | ||
host: string; | ||
/* Path, prefixed with /, defaults to /dns-query for the http/https protocol, ignored for udp */ | ||
path?: string; | ||
/* https port, defaults to 443 for https, 80 for http and 53 for udp*/ | ||
port?: number; | ||
/* true, if endpoint is known to log requests, defaults to false */ | ||
log?: boolean; | ||
host: string | ||
/* Known IPV4 address that can be used for the lookup */ | ||
ipv4?: string | ||
/* Known IPV6 address that can be used for the lookup */ | ||
ipv6?: string | ||
/* true, if endpoint supports http/https CORS headers, defaults to false */ | ||
cors?: boolean; | ||
/* true, if endpoint is known to filters/redirects DNS packets, defaults to false */ | ||
filter?: boolean; | ||
/* link to documentation, if available */ | ||
docs?: string; | ||
/* Known geographical location */ | ||
location?: string; | ||
cors?: boolean | ||
/* Path, prefixed with /, defaults to /dns-query for the http/https protocol */ | ||
path?: string | ||
/* Method to request in case of http/https, defaults to GET */ | ||
method?: 'post' | 'Post' | 'POST' | 'get' | 'Get' | 'GET'; | ||
method?: 'POST' | 'GET' | ||
} | { | ||
protocol: 'udp4:' | ||
/* ipv4 endpoint to connect-to */ | ||
ipv4: string | ||
/* https port, defaults to 53; 443 if pk is present */ | ||
port?: number | string | null | ||
/* dnscrypt public key */ | ||
pk?: string | null | ||
} | { | ||
protocol: 'udp6:' | ||
/* ipv4 endpoint to connect-to */ | ||
ipv6: string | ||
/* https port, defaults to 53; 443 if pk is present */ | ||
port?: number | string | null | ||
/* dnscrypt public key */ | ||
pk?: string | null | ||
} | ||
@@ -129,4 +180,5 @@ ``` | ||
Instead of passing an object you can also pass a string. If the string matches the name | ||
of one of the [endpoints](./endpoints.md), that endpoint will be used, else it needs | ||
to be a url, with a possible `[post]` or `[get]` suffix to indicate the method. | ||
of one of the endpoints, that endpoint will be used. If it doesnt match any endpoint, | ||
then it will be parsed using the `parseEndpoint` method understands an URL like structure | ||
with additional properties defined like flags (`[<name>]`). | ||
@@ -148,2 +200,32 @@ _Examples:_ | ||
### Persisting Resolvers | ||
Loading the latest list of resolvers from the servers will increase both the load | ||
on the server hosting the list and your application's responsiveness. `dns-query` comes | ||
with support for persisting the resolvers in order to ease the load. | ||
While the CLI does that by default, you need to enable it when using the JavaScript | ||
API. | ||
```js | ||
query(..., { | ||
persist: true | ||
}) | ||
``` | ||
In node.js this will try to persist the list of resolvers to the `node_modules` | ||
directory. | ||
In the browser it will use `localStorage` to store the copy of resolvers. By default | ||
it will use the `localStoragePrefix = 'dnsquery_'` option. You will be able to find | ||
the persisted resolvers under `localStorage.getItem('dnsquery_resolvers.json')`. | ||
You can change the prefix in the options. | ||
```js | ||
query(..., { | ||
localStoragePrefix: 'my_custom_prefix' | ||
}) | ||
``` | ||
## See Also | ||
@@ -150,0 +232,0 @@ |
@@ -1,7 +0,52 @@ | ||
import { Packet } from 'dns-packet'; | ||
import { IncomingMessage } from 'http'; | ||
import { SingleQuestionPacket } from '@leichtgewicht/dns-packet'; | ||
import { | ||
Endpoint, EndpointOpts | ||
} from '../common.js'; | ||
export interface Options { | ||
import { | ||
RawResolver | ||
} from '../resolvers.js'; | ||
export { | ||
TimeoutError, | ||
HTTPStatusError, | ||
AbortError, | ||
ResponseError, | ||
BaseEndpoint, | ||
BaseEndpointOpts, | ||
Endpoint, | ||
EndpointOpts, | ||
HTTPEndpoint, | ||
HTTPEndpointOpts, | ||
UDP4Endpoint, | ||
UDP4EndpointOpts, | ||
UDP6Endpoint, | ||
UDP6EndpointOpts, | ||
parseEndpoint, | ||
toEndpoint | ||
} from '../common.js'; | ||
export { | ||
RawResolver | ||
} from '../resolvers.js'; | ||
export type Resolver = RawResolver<Endpoint>; | ||
export const backup: { | ||
data: Resolver | ||
time: number | ||
}; | ||
export interface Wellknown { | ||
resolvers: Resolver[]; | ||
resolverByName: { [name: string]: Resolver }; | ||
endpoints: Endpoint[]; | ||
endpointByName: { [name: string]: Endpoint }; | ||
} | ||
export type OrPromise <T> = Promise<T> | T; | ||
export type EndpointInput = OrPromise<'doh' | 'dns' | ((endpoint: Endpoint) => boolean) | Iterable<Endpoint | EndpointOpts | string>>; | ||
export interface QueryOpts { | ||
/* Set of endpoints to lookup doh queries. */ | ||
endpoints?: 'doh' | 'dns' | Iterable<Endpoint | EndpointProps | string>; | ||
endpoints?: EndpointInput; | ||
/* Amount of retry's if a request fails, defaults to 5 */ | ||
@@ -15,139 +60,24 @@ retries?: number; | ||
export class Endpoint { | ||
/* https is the default for DoH endpoints, udp4:/upd6: for regular dns endpoints and http for debug only! defaults to https: */ | ||
protocol?: 'http:' | 'https:' | 'udp4:' | 'udp6:'; | ||
/* Host to look up */ | ||
host: string; | ||
/* Path, prefixed with /, defaults to /dns-query for the http/https protocol, ignored for udp */ | ||
path?: string; | ||
/* https port, defaults to 443 for https, 80 for http and 53 for udp*/ | ||
port?: number; | ||
/* true, if endpoint is known to log requests, defaults to false */ | ||
log?: boolean; | ||
/* true, if endpoint supports http/https CORS headers, defaults to false */ | ||
cors?: boolean; | ||
/* true, if endpoint is known to filters/redirects DNS packets, defaults to false */ | ||
filter?: boolean; | ||
/* link to documentation, if available */ | ||
docs?: string; | ||
/* Known geographical location */ | ||
location?: string; | ||
/* Method to request in case of http/https, defaults to GET */ | ||
method?: 'post' | 'Post' | 'POST' | 'get' | 'Get' | 'GET'; | ||
constructor(data: EndpointProps); | ||
} | ||
export type SessionOpts = Partial<{ | ||
retries: number | ||
timeout: number | ||
update: boolean | ||
updateURL: URL | ||
persist: boolean | ||
localStoragePrefix: string | ||
maxAge: number | ||
}>; | ||
export type EndpointProps = Omit<Endpoint, ''>; | ||
export type Response = undefined | XMLHttpRequest | IncomingMessage; | ||
export class Session { | ||
opts: SessionOpts; | ||
constructor(opts: SessionOpts); | ||
export function query(packet: Packet, options?: Options): Promise<Packet & { | ||
endpoint: Endpoint; | ||
response: Response; | ||
}>; | ||
wellknown(): Promise<Wellknown>; | ||
endpoints(): Promise<Endpoint[]>; | ||
query(query: SingleQuestionPacket, opts: QueryOpts): Promise<SingleQuestionPacket>; | ||
} | ||
export class AbortError extends Error { | ||
constructor(); | ||
code: 'ABORT_ERR'; | ||
name: 'AbortError'; | ||
} | ||
export class HTTPStatusError extends Error { | ||
constructor(uri: string, status: number, method: string); | ||
uri: string; | ||
status: number; | ||
method: 'POST' | 'GET'; | ||
code: 'HTTP_STATUS'; | ||
name: 'StatusError'; | ||
response: Response; | ||
endpoint: Endpoint; | ||
} | ||
export class ResponseError extends Error { | ||
constructor(message: string) | ||
code: 'RESPONSE_ERR'; | ||
name: 'ResponseError'; | ||
response: Response; | ||
endpoint: Endpoint; | ||
} | ||
export class TimeoutError extends Error { | ||
constructor(timeout: number) | ||
timeout: number; | ||
code: 'ETIMEOUT'; | ||
name: 'TimeoutError'; | ||
} | ||
export function parseEndpoints(endpoints?: Iterable<Endpoint | EndpointProps | string>): Endpoint[]; | ||
export const endpoints: { | ||
cloudflare: Endpoint; | ||
cloudflareFamily: Endpoint; | ||
cloudflareSecurity: Endpoint; | ||
cloudflareEth: Endpoint; | ||
aAndA: Endpoint; | ||
usablePrivacy: Endpoint; | ||
adguard: Endpoint; | ||
adguardFamily: Endpoint; | ||
adguardUnfiltered: Endpoint; | ||
ahadnsIn: Endpoint; | ||
ahadnsIt: Endpoint; | ||
ahadnsEs: Endpoint; | ||
ahadnsNo: Endpoint; | ||
ahadnsNl: Endpoint; | ||
ahadnsPl: Endpoint; | ||
ahadnsNy: Endpoint; | ||
ahadnsChi: Endpoint; | ||
ahadnsAu: Endpoint; | ||
ahadnsLa: Endpoint; | ||
alidns: Endpoint; | ||
amsNl: Endpoint; | ||
amsSe: Endpoint; | ||
amsEs: Endpoint; | ||
arapurayil: Endpoint; | ||
digitaleGesellschaft: Endpoint; | ||
dnsForFamily: Endpoint; | ||
dnsHome: Endpoint; | ||
blahDnsCh: Endpoint; | ||
blahDnsJp: Endpoint; | ||
blahDnsDe: Endpoint; | ||
blahDnsFi: Endpoint; | ||
cleanBrowsingSecurity: Endpoint; | ||
cleanBrowsingFamily: Endpoint; | ||
cleanBrowsingAdult: Endpoint; | ||
appliedPrivacy: Endpoint; | ||
ffmuc: Endpoint; | ||
tiarap: Endpoint; | ||
tiarapJp: Endpoint; | ||
google: Endpoint; | ||
he: Endpoint; | ||
iij: Endpoint; | ||
libredns: Endpoint; | ||
librednsAds: Endpoint; | ||
njalla: Endpoint; | ||
opendns: Endpoint; | ||
opendnsFamily: Endpoint; | ||
sebyVultr: Endpoint; | ||
sebyOVH: Endpoint; | ||
quad9: Endpoint; | ||
quad9Ads: Endpoint; | ||
switchCh: Endpoint; | ||
yepdns: Endpoint; | ||
lavaDnsEU1: Endpoint; | ||
controlId: Endpoint; | ||
controlIdMw: Endpoint; | ||
controlIdAds: Endpoint; | ||
controlIdSoc: Endpoint; | ||
uncensoredAny: Endpoint; | ||
uncensoredUni: Endpoint; | ||
dnssbGlobal: Endpoint; | ||
dbssbDeDus: Endpoint; | ||
dnssbDeFra: Endpoint; | ||
dnssbNlAms: Endpoint; | ||
dnssbNlAms2: Endpoint; | ||
dnssbEeTll: Endpoint; | ||
dnssbJpKix: Endpoint; | ||
dnssbHkHkg: Endpoint; | ||
dnssbAuSyd: Endpoint; | ||
dnssbUsChi: Endpoint; | ||
dnssbInBlr: Endpoint; | ||
dnssbSgSin: Endpoint; | ||
dnssbKrSel: Endpoint; | ||
dnssbRuMow: Endpoint; | ||
ethlink: Endpoint; | ||
handshake: Endpoint; | ||
}; | ||
export function query(query: SingleQuestionPacket, opts: QueryOpts): Promise<SingleQuestionPacket>; | ||
export function wellknown(): Promise<Wellknown>; | ||
export function endpoints(): Promise<Endpoint[]>; | ||
export function loadEndpoints(session: Session, input: EndpointInput): Promise<Endpoint[]>; |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
155639
19
4333
235
5
15
2
9
+ Addedutf8-codec@^1.0.0
+ Added@leichtgewicht/base64-codec@1.0.0(transitive)
+ Added@leichtgewicht/dns-packet@6.0.3(transitive)
+ Added@leichtgewicht/dns-socket@5.0.0(transitive)
+ Addedbytes.js@0.0.2(transitive)
+ Addedutf8-bytes@0.0.1(transitive)
+ Addedutf8-codec@1.0.0(transitive)
+ Addedutf8-length@0.0.1(transitive)
+ Addedutf8-string-bytes@1.0.3(transitive)
- Removed@types/dns-packet@^5.2.0
- Removeddns-packet@^5.3.0
- Removeddns-socket@^4.2.2
- Removed@types/dns-packet@5.6.5(transitive)
- Removed@types/node@20.12.10(transitive)
- Removeddns-packet@5.6.1(transitive)
- Removeddns-socket@4.2.2(transitive)
- Removedundici-types@5.26.5(transitive)