Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dnsLookupIpVersion option #1264

Merged
merged 26 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c4e4ae4
Added documentation
Giotino May 14, 2020
749e0e9
Renamed `family` to `ipFamily`
Giotino May 14, 2020
3b984ca
Renamed `ipFamily` to `ipVersion`
Giotino May 14, 2020
0a2ea29
Removed ipVersion from Defaults
Giotino May 14, 2020
095d0b4
Added some tests
Giotino May 14, 2020
236b5fa
Better DNS and IPv6 tests
Giotino May 14, 2020
b731b96
Optional "IPv6 request" test
Giotino May 14, 2020
48a7ade
Removed useless type
Giotino May 14, 2020
31d27ef
Renamed `ipVersion` to `dnsIpVersion`
Giotino May 15, 2020
c9de7e5
Merge branch 'master' into issue-1263
Giotino May 15, 2020
ebddaa3
Removed export from DnsIpFamily
Giotino May 15, 2020
15f9298
Added example to the documentation
Giotino May 16, 2020
e693c07
Update source/core/utils/dns-ip-version.ts
Giotino May 16, 2020
8947194
dns-ip-version.ts cleanup
Giotino May 16, 2020
28a74f3
Update readme.md
sindresorhus May 24, 2020
1f78181
Applied requested changes
Giotino May 24, 2020
c2c5865
Update test/http.ts
Giotino May 27, 2020
5d57c1d
Added IPv6 detection for tests
Giotino May 28, 2020
0d30db6
Update source/core/utils/dns-ip-version.ts
Giotino May 28, 2020
7e92ebc
Throw if `dnsLookupIpVersion` is invalid
Giotino May 28, 2020
5ef0ebf
Merge branch 'master' into issue-1263
Giotino May 28, 2020
7274e02
Some cleanups
Giotino May 28, 2020
54cf845
Added `family` deprecation warning
Giotino May 28, 2020
0f19d4c
Test cleanups
Giotino May 28, 2020
0237e67
Fixed IPv6 detection for tests
Giotino May 29, 2020
4a636e3
Fixed tests
Giotino Jun 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.\
Giotino marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1398,6 +1406,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