Skip to content

Commit

Permalink
Merge pull request #43 from byu-imaal/kl/add-timeout
Browse files Browse the repository at this point in the history
Add timeout param (close #42)
  • Loading branch information
kimbo authored Mar 15, 2020
2 parents e448d1d + 713b0c0 commit 79e7d16
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 54 deletions.
28 changes: 15 additions & 13 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Allowed method are &quot;GET&quot; and &quot;POST&quot;</p>
<br>
The recursion desired flag will be set, and the ID in the header will be set to zero, per the RFC (<a href="https://tools.ietf.org/html/rfc8484#section-4.1">section 4.1</a>).</p>
</dd>
<dt><a href="#sendDohMsg">sendDohMsg(packet, url, method, headers)</a> ⇒ <code>Promise.&lt;object&gt;</code></dt>
<dt><a href="#sendDohMsg">sendDohMsg(packet, url, method, headers, timeout)</a> ⇒ <code>Promise.&lt;object&gt;</code></dt>
<dd><p>Send a DNS message over HTTPS to <code>url</code> using the given request method</p>
</dd>
</dl>
Expand All @@ -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) ⇒ <code>Promise.&lt;object&gt;</code>
* [.query(qname, qtype, method, headers, timeout)](#DohResolver+query) ⇒ <code>Promise.&lt;object&gt;</code>

<a name="new_DohResolver_new"></a>

Expand Down Expand Up @@ -82,7 +82,7 @@ resolver.query('example.com', 'A')
```
<a name="DohResolver+query"></a>

### dohResolver.query(qname, qtype, method, headers) ⇒ <code>Promise.&lt;object&gt;</code>
### dohResolver.query(qname, qtype, method, headers, timeout) ⇒ <code>Promise.&lt;object&gt;</code>
Perform a DNS lookup for the given query name and type.

**Kind**: instance method of [<code>DohResolver</code>](#DohResolver)
Expand All @@ -98,6 +98,7 @@ Perform a DNS lookup for the given query name and type.
| qtype | <code>string</code> | <code>&quot;A&quot;</code> | the type of record we're looking for (e.g. A, AAAA, TXT, MX) |
| method | <code>string</code> | <code>&quot;POST&quot;</code> | Must be either "GET" or "POST" |
| headers | <code>object</code> | <code></code> | define HTTP headers to use in the DNS query <br> <b><i>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.</i></b> |
| timeout | <code>number</code> | | the number of milliseconds to wait for a response before aborting the request |

<a name="ALLOWED_REQUEST_METHODS"></a>

Expand Down Expand Up @@ -151,7 +152,7 @@ console.log(msg);
```
<a name="sendDohMsg"></a>

## sendDohMsg(packet, url, method, headers) ⇒ <code>Promise.&lt;object&gt;</code>
## sendDohMsg(packet, url, method, headers, timeout) ⇒ <code>Promise.&lt;object&gt;</code>
Send a DNS message over HTTPS to `url` using the given request method

**Kind**: global function
Expand All @@ -163,20 +164,21 @@ Send a DNS message over HTTPS to `url` using the given request method
| url | <code>string</code> | the url to send the DNS message to |
| method | <code>string</code> | the request method to use ("GET" or "POST") |
| headers | <code>object</code> | headers to send in the DNS request. The default headers for GET requests are |
| timeout | <code>number</code> | 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);
```
15 changes: 15 additions & 0 deletions examples/basic/DohResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
35 changes: 22 additions & 13 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,17 @@ resolver.query('example.com', 'A')
* <br>
* <b><i>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.</i></b>
* @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<object>} 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)
});
Expand Down Expand Up @@ -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<object>} 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);
Expand Down Expand Up @@ -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) => {
Expand All @@ -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)
}
Expand Down
50 changes: 23 additions & 27 deletions lib/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
});
Expand All @@ -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.*/);
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 79e7d16

Please sign in to comment.