From 03a5631b07d6a9d8acd219a30d13f23dd06e1763 Mon Sep 17 00:00:00 2001 From: kimbo Date: Sat, 14 Mar 2020 15:46:24 -0600 Subject: [PATCH 1/5] timeout param for DohResolver.query & sendDohMsg --- lib/index.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/index.js b/lib/index.js index 2340ece..33ce856 100755 --- a/lib/index.js +++ b/lib/index.js @@ -67,16 +67,17 @@ resolver.query('example.com', 'A') *
* IMPORTANT: If you don't provide the "Accept: application/dns-message" header, you probably won't get the response you're hoping for. * See [RFC 8484 examples](https://tools.ietf.org/html/rfc8484#section-4.1.1) for examples of HTTPS headers for both GET and POST requests. + * @param timeout {number} the number of milliseconds to wait for a response before aborting the request * @throws {MethodNotAllowedError} If the method is not allowed (i.e. if it's not "GET" or "POST"), a MethodNotAllowedError will be thrown. * @returns {Promise} The DNS response received */ - query(qname, qtype='A', method='POST', headers=null) { + query(qname, qtype='A', method='POST', headers=null, timeout) { return new Promise((resolve, reject) => { if (!isMethodAllowed(method)) { return reject(new MethodNotAllowedError(`Request method ${method} not allowed. Must be either 'GET' or 'POST'`)) } let dnsMessage = makeQuery(qname, qtype); - sendDohMsg(dnsMessage, this.nameserver_url, method, headers) + sendDohMsg(dnsMessage, this.nameserver_url, method, headers, timeout) .then(resolve) .catch(reject) }); @@ -131,23 +132,24 @@ function makeQuery(qname, qtype='A') { * @param url {string} the url to send the DNS message to * @param method {string} the request method to use ("GET" or "POST") * @param headers {object} headers to send in the DNS request. The default headers for GET requests are + * @param timeout {number} the number of milliseconds to wait for a response before aborting the request * @returns {Promise} the response (if we got any) * @example -// imports -const {makeQuery, sendDohMsg} = require('dohjs'); + // imports + const {makeQuery, sendDohMsg} = require('dohjs'); -const url = 'https://cloudflare-dns.com/dns-query'; -const method = 'GET'; + const url = 'https://cloudflare-dns.com/dns-query'; + const method = 'GET'; -// create a query message -let msg = makeQuery('example.com', 'TXT'); + // create a query message + let msg = makeQuery('example.com', 'TXT'); -// send it and print out the response to the console -sendDohMsg(msg, url, method) -.then(response => response.answers.forEach(ans => console.log(ans.data.toString()))) -.catch(console.error); + // send it and print out the response to the console + sendDohMsg(msg, url, method) + .then(response => response.answers.forEach(ans => console.log(ans.data.toString()))) + .catch(console.error); */ -function sendDohMsg(packet, url, method, headers) { +function sendDohMsg(packet, url, method, headers, timeout) { return new Promise((resolve, reject) => { const transportModule = url.startsWith('https://') ? https : http; const buf = dnsPacket.encode(packet); @@ -175,6 +177,9 @@ function sendDohMsg(packet, url, method, headers) { path: url.pathname + url.search, headers: headers }; + if (timeout) { + requestOptions.timeout = timeout; + } let data; const request = transportModule.request(requestOptions, (response) => { response.on('data', (d) => { @@ -193,6 +198,10 @@ function sendDohMsg(packet, url, method, headers) { request.abort(); return reject(err); }); + request.on('timeout', () => { + request.abort(); + return reject(`Query timed out after ${timeout} milliseconds`); + }); if (method === 'POST') { request.write(buf) } From 698ea6359ab24c3d98c1dc87bd5271df0cc30dc8 Mon Sep 17 00:00:00 2001 From: kimbo Date: Sat, 14 Mar 2020 15:47:31 -0600 Subject: [PATCH 2/5] added test for timeout param --- lib/index.spec.js | 50 ++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/lib/index.spec.js b/lib/index.spec.js index f4d05c8..fba2c56 100644 --- a/lib/index.spec.js +++ b/lib/index.spec.js @@ -22,6 +22,17 @@ test('DNS query message for example.com matches expected', () => { questions: [ { type: 'A', name: 'example.com' } ]}); }); +test('sendDohMsg() works (and the example.com zone still has an A record)', () => { + let msg = makeQuery('example.com', 'A'); + sendDohMsg(msg, 'https://1.1.1.1/dns-query', 'GET') + .then(response => { + expect(response).toHaveProperty('answers'); + }) + .catch(err => { + throw err; + }); +}); + test('DohResolver should be created', () => { expect(new DohResolver("https://example.com/dns-query")).toBeTruthy() }); @@ -47,35 +58,20 @@ test('DohResolver.query() for example.com TXT contains answers', () => { const resolver = new DohResolver("https://dns.google/dns-query"); resolver.query('example.com', 'TXT') .then(response => { - expect(response).toContain('answers') + expect(response).toHaveProperty('answers') }) - .catch(err => expect.assertions(0)) + .catch(err => { + throw err; + }); }); -test('sendDohMsg() works (and the example.com zone still has an A record)', () => { +test('timeout works properly (and cloudflare doesn\'t respond within 1 millisecond)', () => { let msg = makeQuery('example.com'); - sendDohMsg(msg, 'https://cloudflare-dns.com/dns-query', 'GET', null).then( - response => { - expect(response.answers.length).toBeGreaterThanOrEqual(1); - } - ) -}); - -test('DohResolver.query() works with no/null headers', () => { - const resolver = new DohResolver('https://cloudflare-dns.com/dns-query'); - resolver.query('example.com', 'A', 'POST') - .then(response => expect(response.answers.length).toBeGreaterThanOrEqual(1)); -}); - -test('DohResolver.query() works with custom headers', () => { - const resolver = new DohResolver('https://cloudflare-dns.com/dns-query'); - const headers = { - /* without the "Accept: application/dns-message" header, it probably won't work */ - 'Accept': 'application/dns-message', - 'User-Agent': 'some user agent', - 'Favorite-Color': 'purple', - 'Least-Favorite-Color': 'gray' - }; - resolver.query('example.com', 'A', 'GET', headers) - .then(response => expect(response.answers.length).toBeGreaterThanOrEqual(1)); + sendDohMsg(msg, 'https://1.1.1.1/dns-query', 'GET', null, 1) + .then(ans => { + throw Error("Test should have failed"); + }) + .catch(err => { + expect(err).toMatch(/.*timed out.*/); + }); }); From 143c0433883eb7b17d1c5f60159b368322b973f8 Mon Sep 17 00:00:00 2001 From: kimbo Date: Sat, 14 Mar 2020 15:47:53 -0600 Subject: [PATCH 3/5] fixed build script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1114e4d..1f1e93a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test": "./test.sh", "start": "npx live-server", "prestart": "npm run build", - "build": "npm run dohjs && npm run doh-elems", + "build": "npm run dohjs", "prepare": "npm run build", "docs": "jsdoc2md lib/index.js > docs/README.md", "dohjs": "browserify lib/index.js --standalone doh -o dist/doh.js && npx terser dist/doh.js --compress --mangle --output=dist/doh.min.js" From 0ad31c4d8933f3bc88d605c18e2ae8e37c65caab Mon Sep 17 00:00:00 2001 From: kimbo Date: Sat, 14 Mar 2020 15:53:06 -0600 Subject: [PATCH 4/5] update docs --- docs/README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/README.md b/docs/README.md index b2864b1..be03d02 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,7 +31,7 @@ Allowed method are "GET" and "POST"


The recursion desired flag will be set, and the ID in the header will be set to zero, per the RFC (section 4.1).

-
sendDohMsg(packet, url, method, headers)Promise.<object>
+
sendDohMsg(packet, url, method, headers, timeout)Promise.<object>

Send a DNS message over HTTPS to url using the given request method

@@ -52,7 +52,7 @@ A super lame DNS over HTTPS stub resolver * [DohResolver](#DohResolver) * [new DohResolver(nameserver_url)](#new_DohResolver_new) - * [.query(qname, qtype, method, headers)](#DohResolver+query) ⇒ Promise.<object> + * [.query(qname, qtype, method, headers, timeout)](#DohResolver+query) ⇒ Promise.<object> @@ -82,7 +82,7 @@ resolver.query('example.com', 'A') ``` -### dohResolver.query(qname, qtype, method, headers) ⇒ Promise.<object> +### dohResolver.query(qname, qtype, method, headers, timeout) ⇒ Promise.<object> Perform a DNS lookup for the given query name and type. **Kind**: instance method of [DohResolver](#DohResolver) @@ -98,6 +98,7 @@ Perform a DNS lookup for the given query name and type. | qtype | string | "A" | the type of record we're looking for (e.g. A, AAAA, TXT, MX) | | method | string | "POST" | Must be either "GET" or "POST" | | headers | object | | define HTTP headers to use in the DNS query
IMPORTANT: If you don't provide the "Accept: application/dns-message" header, you probably won't get the response you're hoping for. See [RFC 8484 examples](https://tools.ietf.org/html/rfc8484#section-4.1.1) for examples of HTTPS headers for both GET and POST requests. | +| timeout | number | | the number of milliseconds to wait for a response before aborting the request | @@ -151,7 +152,7 @@ console.log(msg); ``` -## sendDohMsg(packet, url, method, headers) ⇒ Promise.<object> +## sendDohMsg(packet, url, method, headers, timeout) ⇒ Promise.<object> Send a DNS message over HTTPS to `url` using the given request method **Kind**: global function @@ -163,20 +164,21 @@ Send a DNS message over HTTPS to `url` using the given request method | url | string | the url to send the DNS message to | | method | string | the request method to use ("GET" or "POST") | | headers | object | headers to send in the DNS request. The default headers for GET requests are | +| timeout | number | the number of milliseconds to wait for a response before aborting the request | **Example** ```js // imports -const {makeQuery, sendDohMsg} = require('dohjs'); + const {makeQuery, sendDohMsg} = require('dohjs'); -const url = 'https://cloudflare-dns.com/dns-query'; -const method = 'GET'; + const url = 'https://cloudflare-dns.com/dns-query'; + const method = 'GET'; -// create a query message -let msg = makeQuery('example.com', 'TXT'); + // create a query message + let msg = makeQuery('example.com', 'TXT'); -// send it and print out the response to the console -sendDohMsg(msg, url, method) -.then(response => response.answers.forEach(ans => console.log(ans.data.toString()))) -.catch(console.error); + // send it and print out the response to the console + sendDohMsg(msg, url, method) + .then(response => response.answers.forEach(ans => console.log(ans.data.toString()))) + .catch(console.error); ``` From 713b0c0d1e1021f1eaaf324085ea94ffcd84780d Mon Sep 17 00:00:00 2001 From: kimbo Date: Sat, 14 Mar 2020 15:57:24 -0600 Subject: [PATCH 5/5] added example with timeout --- examples/basic/DohResolver.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/basic/DohResolver.js b/examples/basic/DohResolver.js index fad3441..3abe6d1 100755 --- a/examples/basic/DohResolver.js +++ b/examples/basic/DohResolver.js @@ -11,3 +11,18 @@ resolver.query('example.com', 'A') response.answers.forEach(ans => console.log(ans.data)); }) .catch(err => console.error(err)); + + +// Now we'll do the same query, but this time, +// it's going to be a GET request, and +// we're going to set a timeout for one second + +// timeout in milliseconds +const timeout = 1000; + +// lookup the A records for example.com +resolver.query('example.com', 'A', 'GET', null, timeout) + .then(response => { + response.answers.forEach(ans => console.log(ans.data)); + }) + .catch(err => console.error(err));