Skip to content

Commit

Permalink
Add dnsLookupIpVersion option (#1264)
Browse files Browse the repository at this point in the history
Co-authored-by: Szymon Marczak <[email protected]>
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
3 people authored Jun 1, 2020
1 parent e00dbbc commit 7f643bb
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 5 deletions.
25 changes: 25 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,31 @@ An instance of [`CacheableLookup`](https://github.com/szmarczak/cacheable-lookup
**Note:** This should stay disabled when making requests to internal hostnames such as `localhost`, `database.local` etc.\
`CacheableLookup` uses `dns.resolver4(..)` and `dns.resolver6(...)` under the hood and fall backs to `dns.lookup(...)` when the first two fail, which may lead to additional delay.

###### dnsLookupIpVersion

Type: `'auto' | 'ipv4' | 'ipv6'`\
Default: `'auto'`

Indicates which DNS record family to use.\
Values:
- `auto`: IPv4 (if present) or IPv6
- `ipv4`: Only IPv4
- `ipv6`: Only IPv6

Note: If you are using the undocumented option `family`, `dnsLookupIpVersion` will override it.

```js
// `api6.ipify.org` will be resolved as IPv4 and the request will be over IPv4 (the website will respond with your public IPv4)
await got('https://api6.ipify.org', {
dnsLookupIpVersion: 'ipv4'
});

// `api6.ipify.org` will be resolved as IPv6 and the request will be over IPv6 (the website will respond with your public IPv6)
await got('https://api6.ipify.org', {
dnsLookupIpVersion: 'ipv6'
});
```

###### request

Type: `Function`\
Expand Down
17 changes: 17 additions & 0 deletions source/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import timedOut, {Delays, TimeoutError as TimedOutTimeoutError} from './utils/ti
import urlToOptions from './utils/url-to-options';
import optionsToUrl, {URLOptions} from './utils/options-to-url';
import WeakableMap from './utils/weakable-map';
import {DnsLookupIpVersion, isDnsLookupIpVersion, dnsLookupIpVersionToFamily} from './utils/dns-ip-version';
import deprecationWarning from '../utils/deprecation-warning';

type HttpRequestFunction = typeof httpRequest;
Expand Down Expand Up @@ -150,6 +151,7 @@ export interface Options extends URLOptions {
lookup?: CacheableLookup['lookup'];
headers?: Headers;
methodRewriting?: boolean;
dnsLookupIpVersion?: DnsLookupIpVersion;

// From `http.RequestOptions`
localAddress?: string;
Expand Down Expand Up @@ -653,6 +655,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
assert.any([is.boolean, is.undefined], options.http2);
assert.any([is.boolean, is.undefined], options.allowGetBody);
assert.any([is.string, is.undefined], options.localAddress);
assert.any([isDnsLookupIpVersion, is.undefined], options.dnsLookupIpVersion);
assert.any([is.object, is.undefined], options.https);
assert.any([is.boolean, is.undefined], options.rejectUnauthorized);
if (options.https) {
Expand Down Expand Up @@ -871,6 +874,11 @@ export default class Request extends Duplex implements RequestEvents<Request> {
}
}

// DNS options
if ('family' in options) {
deprecationWarning('"options.family" was never documented, please use "options.dnsLookupIpVersion"');
}

// HTTPS options
if ('rejectUnauthorized' in options) {
deprecationWarning('"options.rejectUnauthorized" is now deprecated, please use "options.https.rejectUnauthorized"');
Expand Down Expand Up @@ -1397,6 +1405,15 @@ export default class Request extends Duplex implements RequestEvents<Request> {

const requestOptions = options as unknown as RealRequestOptions;

// If `dnsLookupIpVersion` is not present do not override `family`
if (options.dnsLookupIpVersion !== undefined) {
try {
requestOptions.family = dnsLookupIpVersionToFamily(options.dnsLookupIpVersion);
} catch {
throw new Error('Invalid `dnsLookupIpVersion` option value');
}
}

// HTTPS options remapping
if (options.https) {
if ('rejectUnauthorized' in options.https) {
Expand Down
20 changes: 20 additions & 0 deletions source/core/utils/dns-ip-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type DnsLookupIpVersion = 'auto' | 'ipv4' | 'ipv6';
type DnsIpFamily = 0 | 4 | 6;

const conversionTable = {
auto: 0,
ipv4: 4,
ipv6: 6
};

export const isDnsLookupIpVersion = (value: any): boolean => {
return value in conversionTable;
};

export const dnsLookupIpVersionToFamily = (dnsLookupIpVersion: DnsLookupIpVersion): DnsIpFamily => {
if (isDnsLookupIpVersion(dnsLookupIpVersion)) {
return conversionTable[dnsLookupIpVersion] as DnsIpFamily;
}

throw new Error('Invalid DNS lookup IP version');
};
100 changes: 98 additions & 2 deletions test/http.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import {STATUS_CODES, Agent} from 'http';
import test from 'ava';
import {Handler} from 'express';
import {isIPv4, isIPv6} from 'net';
import nock = require('nock');
import getStream = require('get-stream');
import pEvent = require('p-event');
import got, {HTTPError, UnsupportedProtocolError} from '../source';
import pEvent from 'p-event';
import got, {HTTPError, UnsupportedProtocolError, CancelableRequest} from '../source';
import withServer from './helpers/with-server';
import os = require('os');

const IPv6supported = Object.values(os.networkInterfaces()).some(iface => iface?.some(addr => !addr.internal && addr.family === 'IPv6'));

const echoIp: Handler = (request, response) => {
const address = request.connection.remoteAddress;
if (address === undefined) {
return response.end();
}

// IPv4 address mapped to IPv6
response.end(address === '::ffff:127.0.0.1' ? '127.0.0.1' : address);
};

test('simple request', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
Expand Down Expand Up @@ -255,3 +270,84 @@ test('does not destroy completed requests', withServer, async (t, server, got) =

t.pass();
});

if (IPv6supported) {
test('IPv6 request', withServer, async (t, server) => {
server.get('/ok', echoIp);

const response = await got(`http://[::1]:${server.port}/ok`);

t.is(response.body, '::1');
});
}

test('DNS auto', withServer, async (t, server, got) => {
server.get('/ok', echoIp);

const response = await got('ok', {
dnsLookupIpVersion: 'auto'
});

t.true(isIPv4(response.body));
});

test('DNS IPv4', withServer, async (t, server, got) => {
server.get('/ok', echoIp);

const response = await got('ok', {
dnsLookupIpVersion: 'ipv4'
});

t.true(isIPv4(response.body));
});

if (IPv6supported) {
test('DNS IPv6', withServer, async (t, server, got) => {
server.get('/ok', echoIp);

const response = await got('ok', {
dnsLookupIpVersion: 'ipv6'
});

t.true(isIPv6(response.body));
});
}

test('invalid dnsLookupIpVersion', withServer, async (t, server, got) => {
server.get('/ok', echoIp);

await t.throwsAsync(got('ok', {
dnsLookupIpVersion: 'test'
} as any));
});

test.serial('deprecated `family` option', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.end('ok');
});

await new Promise(resolve => {
let request: CancelableRequest;
(async () => {
const warning = await pEvent(process, 'warning');
t.is(warning.name, 'DeprecationWarning');
request!.cancel();
resolve();
})();

(async () => {
request = got({
family: '4'
} as any);

try {
await request;
t.fail();
} catch {
t.true(request!.isCanceled);
}

resolve();
})();
});
});
14 changes: 11 additions & 3 deletions test/https.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava';
import got from '../source';
import got, {CancelableRequest} from '../source';
import withServer from './helpers/with-server';
import {DetailedPeerCertificate} from 'tls';
import pEvent from 'p-event';
Expand Down Expand Up @@ -115,18 +115,26 @@ test.serial('deprecated `rejectUnauthorized` option', withServer, async (t, serv
});

await new Promise(resolve => {
let request: CancelableRequest;
(async () => {
const warning = await pEvent(process, 'warning');
t.is(warning.name, 'DeprecationWarning');
request!.cancel();
resolve();
})();

(async () => {
await got.secure({
request = got.secure({
rejectUnauthorized: false
});

t.fail();
try {
await request;
t.fail();
} catch {
t.true(request!.isCanceled);
}

resolve();
})();
});
Expand Down

0 comments on commit 7f643bb

Please sign in to comment.