Skip to content

Commit

Permalink
Add redundancy, remove https option, add onlyHttps option… (#34)
Browse files Browse the repository at this point in the history
...and add `fallbackUrls` option.

Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
ghostnumber7 and sindresorhus committed Nov 22, 2019
1 parent 3c76d72 commit fdcbb7d
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 79 deletions.
32 changes: 16 additions & 16 deletions browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ const defaults = {
};

const urls = {
v4: 'https://ipv4.icanhazip.com/',
v6: 'https://ipv6.icanhazip.com/'
};

const fallbackUrls = {
v4: 'https://api.ipify.org',
v6: 'https://api6.ipify.org'
v4: [
'https://ipv4.icanhazip.com/',
'https://api.ipify.org/'
],
v6: [
'https://ipv6.icanhazip.com/',
'https://api6.ipify.org/'
]
};

let xhr;
Expand All @@ -27,7 +28,7 @@ const sendXhr = async (url, options, version) => {
const ip = xhr.responseText.trim();

if (!ip || !isIp[version](ip)) {
reject();
return reject();
}

resolve(ip);
Expand All @@ -41,17 +42,16 @@ const sendXhr = async (url, options, version) => {

const queryHttps = async (version, options) => {
let ip;
try {
ip = await sendXhr(urls[version], options, version);
} catch (_) {
const _urls = [].concat.apply(urls[version], options.fallbackUrls || []);
for (const url of _urls) {
try {
ip = await sendXhr(fallbackUrls[version], options, version);
} catch (_) {
throw new Error('Couldn\'t find your IP');
}
// eslint-disable-next-line no-await-in-loop
ip = await sendXhr(url, options, version);
return ip;
} catch (_) {}
}

return ip;
throw new Error('Couldn\'t find your IP');
};

queryHttps.cancel = () => {
Expand Down
15 changes: 11 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
declare namespace publicIp {
interface Options {
/**
Use a HTTPS check using the [icanhazip.com](https://github.com/major/icanhaz) service instead of the DNS query. This check is much more secure and tamper-proof, but also a lot slower. **This option is only available in the Node.js version**.
Use a HTTPS check using the [icanhazip.com](https://github.com/major/icanhaz) service instead of the DNS query. [ipify.org](https://www.ipify.org) is used as a fallback if `icanhazip.com` fails. This check is much more secure and tamper-proof, but also a lot slower. **This option is only available in the Node.js version**. Default behaviour is to check aginst DNS before using HTTPS fallback, if set as `true` it will *only* check against HTTPS.
@default false
*/
readonly https?: boolean;
readonly onlyHttps?: boolean;

/**
The time in milliseconds until a request is considered timed out.
@default 5000
*/
readonly timeout?: number;

/**
In case you want to add your own custom HTTPS endpoints to get public IP from (like [ifconfig.co](https://ifconfig.co), for example), you can set them here. They will only be used if everything else fails. Any service used as fallback *must* return the IP as a plain string.
@default []
*/
readonly fallbackUrls?: string[];
}

type CancelablePromise<T> = Promise<T> & {
Expand All @@ -24,7 +31,7 @@ declare const publicIp: {
/**
Get your public IP address - very fast!
In Node.js, it queries the DNS records of OpenDNS which has an entry with your IP address. In browsers, it uses the excellent [icanhaz](https://github.com/major/icanhaz) service through HTTPS.
In Node.js, it queries the DNS records of OpenDNS, Google DNS and HTTPS services to determine your IP address. In browsers, it uses the excellent [icanhaz](https://github.com/major/icanhaz) and [ipify](https://ipify.org) services through HTTPS.
@returns Your public IPv4 address. A `.cancel()` method is available on the promise, which can be used to cancel the request.
@throws On error or timeout.
Expand All @@ -44,7 +51,7 @@ declare const publicIp: {
/**
Get your public IP address - very fast!
In Node.js, it queries the DNS records of OpenDNS which has an entry with your IP address. In browsers, it uses the excellent [icanhaz](https://github.com/major/icanhaz) service through HTTPS.
In Node.js, it queries the DNS records of OpenDNS, Google DNS and HTTPS services to determine your IP address. In browsers, it uses the excellent [icanhaz](https://github.com/major/icanhaz) and [ipify](https://ipify.org) services through HTTPS.
@returns Your public IPv6 address. A `.cancel()` method is available on the promise, which can be used to cancel the request.
@throws On error or timeout.
Expand Down
189 changes: 144 additions & 45 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,79 @@
const {promisify} = require('util');
const dgram = require('dgram');
const dns = require('dns-socket');
const got = require('got');
const {get: got, CancelError} = require('got');
const isIp = require('is-ip');

const defaults = {
timeout: 5000,
https: false
onlyHttps: false
};

const type = {
v4: {
dnsServer: '208.67.222.222',
dnsQuestion: {
const dnsServers = [
{
v4: {
servers: [
'208.67.222.222',
'208.67.220.220',
'208.67.222.220',
'208.67.220.222'
],
name: 'myip.opendns.com',
type: 'A'
},
httpsUrl: 'https://ipv4.icanhazip.com/',
httpsFallbackUrl: 'https://api.ipify.org/'
},
v6: {
dnsServer: '2620:0:ccc::2',
dnsQuestion: {
v6: {
servers: [
'2620:0:ccc::2',
'2620:0:ccd::2'
],
name: 'myip.opendns.com',
type: 'AAAA'
}
},
{
v4: {
servers: [
'216.239.32.10',
'216.239.34.10',
'216.239.36.10',
'216.239.38.10'
],
name: 'o-o.myaddr.l.google.com',
type: 'TXT',
transform: ip => ip.replace(/"/g, '')
},
httpsUrl: 'https://ipv6.icanhazip.com/',
httpsFallbackUrl: 'https://api6.ipify.org/'
v6: {
servers: [
'2001:4860:4802:32::a',
'2001:4860:4802:34::a',
'2001:4860:4802:36::a',
'2001:4860:4802:38::a'
],
name: 'o-o.myaddr.l.google.com',
type: 'TXT',
transform: ip => ip.replace(/"/g, '')
}
}
];

const type = {
v4: {
dnsServers: dnsServers.map(({v4: {servers, ...question}}) => ({
servers, question
})),
httpsUrls: [
'https://icanhazip.com/',
'https://api.ipify.org/'
]
},
v6: {
dnsServers: dnsServers.map(({v6: {servers, ...question}}) => ({
servers, question
})),
httpsUrls: [
'https://icanhazip.com/',
'https://api6.ipify.org/'
]
}
};

Expand All @@ -42,21 +89,40 @@ const queryDns = (version, options) => {

const socketQuery = promisify(socket.query.bind(socket));

// eslint-disable-next-line promise/prefer-await-to-then
const promise = socketQuery({questions: [data.dnsQuestion]}, 53, data.dnsServer).then(({answers}) => {
socket.destroy();

const ip = ((answers[0] && answers[0].data) || '').trim();

if (!ip || !isIp[version](ip)) {
throw new Error('Couldn\'t find your IP');
const promise = (async () => {
for (const dnsServerInfo of data.dnsServers) {
const {servers, question} = dnsServerInfo;
for (const server of servers) {
try {
const {name, type, transform} = question;

// eslint-disable-next-line no-await-in-loop
const dnsResponse = await socketQuery({questions: [{name, type}]}, 53, server);

const {
answers: {
0: {
data
}
}
} = dnsResponse;

const response = (typeof data === 'string' ? data : data.toString()).trim();

const ip = transform ? transform(response) : response;

if (ip && isIp[version](ip)) {
socket.destroy();
return ip;
}
} catch (_) {}
}
}

return ip;
}).catch(error => { // TODO: Move both the `socket.destroy()` calls into a `Promise#finally()` handler when targeting Node.js 10.
socket.destroy();
throw error;
});

throw new Error('Couldn\'t find your IP');
})();

promise.cancel = () => {
socket.cancel();
Expand All @@ -76,36 +142,61 @@ const queryHttps = (version, options) => {
timeout: options.timeout
};

const gotPromise = got(type[version].httpsUrl, requestOptions);

cancel = gotPromise.cancel;

let response;
try {
response = await gotPromise;
} catch (_) {
const gotBackupPromise = got(type[version].httpsFallbackUrl, requestOptions);
const urls = [].concat.apply(type[version].httpsUrls, options.fallbackUrls || []);

cancel = gotBackupPromise.cancel;
for (const url of urls) {
try {
const gotPromise = got(url, requestOptions);
cancel = gotPromise.cancel;

response = await gotBackupPromise;
}
// eslint-disable-next-line no-await-in-loop
const response = await gotPromise;

const ip = (response.body || '').trim();
const ip = (response.body || '').trim();

if (!ip) {
throw new Error('Couldn\'t find your IP');
if (ip && isIp[version](ip)) {
return ip;
}
} catch (error) {
if (error instanceof CancelError) {
throw error;
}
}
}

return ip;
throw new Error('Couldn\'t find your IP');
} catch (error) {
// Don't throw a cancellation error for consistency with DNS
if (!(error instanceof got.CancelError)) {
if (!(error instanceof CancelError)) {
throw error;
}
}
})();

promise.cancel = function () {
return cancel.apply(this);
};

return promise;
};

const queryAll = (version, options) => {
let cancel;
const promise = (async () => {
let response;
const dnsPromise = queryDns(version, options);
cancel = dnsPromise.cancel;
try {
response = await dnsPromise;
} catch (_) {
const httpsPromise = queryHttps(version, options);
cancel = httpsPromise.cancel;
response = await httpsPromise;
}

return response;
})();

promise.cancel = cancel;

return promise;
Expand All @@ -117,7 +208,11 @@ module.exports.v4 = options => {
...options
};

if (options.https) {
if (!options.onlyHttps) {
return queryAll('v4', options);
}

if (options.onlyHttps) {
return queryHttps('v4', options);
}

Expand All @@ -130,7 +225,11 @@ module.exports.v6 = options => {
...options
};

if (options.https) {
if (!options.onlyHttps) {
return queryAll('v6', options);
}

if (options.onlyHttps) {
return queryHttps('v6', options);
}

Expand Down
6 changes: 4 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import publicIp = require('.');
const options: publicIp.Options = {};

expectType<publicIp.CancelablePromise<string>>(publicIp.v4());
expectType<publicIp.CancelablePromise<string>>(publicIp.v4({https: false}));
expectType<publicIp.CancelablePromise<string>>(publicIp.v4({onlyHttps: true}));
expectType<publicIp.CancelablePromise<string>>(publicIp.v4({timeout: 10}));
expectType<publicIp.CancelablePromise<string>>(publicIp.v4({fallbackUrls: ['https://ifconfig.io']}));
publicIp.v4().cancel();

expectType<publicIp.CancelablePromise<string>>(publicIp.v6());
expectType<publicIp.CancelablePromise<string>>(publicIp.v6({https: false}));
expectType<publicIp.CancelablePromise<string>>(publicIp.v6({onlyHttps: true}));
expectType<publicIp.CancelablePromise<string>>(publicIp.v6({timeout: 10}));
expectType<publicIp.CancelablePromise<string>>(publicIp.v6({fallbackUrls: ['https://ifconfig.io']}));
publicIp.v6().cancel();
4 changes: 4 additions & 0 deletions mocks/dns-socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';
const stub = require('./stub');

module.exports = stub(require('dns-socket').prototype, 'query', -2);
4 changes: 4 additions & 0 deletions mocks/got.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';
const stub = require('./stub');

module.exports = stub(require('got'), 'get', 0);
Loading

0 comments on commit fdcbb7d

Please sign in to comment.