267 lines
4.9 KiB
JavaScript
267 lines
4.9 KiB
JavaScript
|
|
import {promisify} from 'node:util';
|
||
|
|
import dgram from 'node:dgram';
|
||
|
|
import dns from 'dns-socket';
|
||
|
|
import got, {CancelError} from 'got';
|
||
|
|
import {isIPv6, isIPv4} from 'is-ip';
|
||
|
|
import {createPublicIp, IpNotFoundError} from './core.js';
|
||
|
|
|
||
|
|
export {IpNotFoundError} from './core.js';
|
||
|
|
|
||
|
|
const defaults = {
|
||
|
|
timeout: 5000,
|
||
|
|
onlyHttps: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
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',
|
||
|
|
},
|
||
|
|
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.replaceAll('"', ''),
|
||
|
|
},
|
||
|
|
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.replaceAll('"', ''),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
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/',
|
||
|
|
],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const queryDns = (version, options) => {
|
||
|
|
const data = type[version];
|
||
|
|
|
||
|
|
const socket = dns({
|
||
|
|
retries: 0,
|
||
|
|
maxQueries: 1,
|
||
|
|
socket: dgram.createSocket(version === 'v6' ? 'udp6' : 'udp4'),
|
||
|
|
timeout: options.timeout,
|
||
|
|
});
|
||
|
|
|
||
|
|
const socketQuery = promisify(socket.query.bind(socket));
|
||
|
|
|
||
|
|
const promise = (async () => {
|
||
|
|
let lastError;
|
||
|
|
|
||
|
|
for (const dnsServerInfo of data.dnsServers) {
|
||
|
|
const {servers, question} = dnsServerInfo;
|
||
|
|
for (const server of servers) {
|
||
|
|
if (socket.destroyed) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
const method = version === 'v6' ? isIPv6 : isIPv4;
|
||
|
|
|
||
|
|
if (ip && method(ip)) {
|
||
|
|
socket.destroy();
|
||
|
|
return ip;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
lastError = error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
socket.destroy();
|
||
|
|
|
||
|
|
throw new IpNotFoundError({cause: lastError});
|
||
|
|
})();
|
||
|
|
|
||
|
|
promise.cancel = () => {
|
||
|
|
socket.destroy();
|
||
|
|
};
|
||
|
|
|
||
|
|
return promise;
|
||
|
|
};
|
||
|
|
|
||
|
|
const queryHttps = (version, options) => {
|
||
|
|
let cancel;
|
||
|
|
|
||
|
|
const promise = (async () => {
|
||
|
|
try {
|
||
|
|
const requestOptions = {
|
||
|
|
dnsLookupIpVersion: version === 'v6' ? 6 : 4,
|
||
|
|
retry: {
|
||
|
|
limit: 0,
|
||
|
|
},
|
||
|
|
timeout: {
|
||
|
|
request: options.timeout,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const urls = [
|
||
|
|
...type[version].httpsUrls,
|
||
|
|
...(options.fallbackUrls ?? []),
|
||
|
|
];
|
||
|
|
|
||
|
|
let lastError;
|
||
|
|
for (const url of urls) {
|
||
|
|
try {
|
||
|
|
// Note: We use `.get` to allow for mocking.
|
||
|
|
const gotPromise = got.get(url, requestOptions);
|
||
|
|
cancel = gotPromise.cancel;
|
||
|
|
|
||
|
|
// eslint-disable-next-line no-await-in-loop
|
||
|
|
const response = await gotPromise;
|
||
|
|
|
||
|
|
const ip = response.body?.trim();
|
||
|
|
const method = version === 'v6' ? isIPv6 : isIPv4;
|
||
|
|
|
||
|
|
if (ip && method(ip)) {
|
||
|
|
return ip;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
lastError = error;
|
||
|
|
|
||
|
|
if (error instanceof CancelError) {
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new IpNotFoundError({cause: lastError});
|
||
|
|
} catch (error) {
|
||
|
|
// Don't throw a cancellation error for consistency with DNS
|
||
|
|
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;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const publicIp = createPublicIp(publicIpv4, publicIpv6);
|
||
|
|
|
||
|
|
export function publicIpv4(options) {
|
||
|
|
options = {
|
||
|
|
...defaults,
|
||
|
|
...options,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!options.onlyHttps) {
|
||
|
|
return queryAll('v4', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.onlyHttps) {
|
||
|
|
return queryHttps('v4', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
return queryDns('v4', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function publicIpv6(options) {
|
||
|
|
options = {
|
||
|
|
...defaults,
|
||
|
|
...options,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!options.onlyHttps) {
|
||
|
|
return queryAll('v6', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.onlyHttps) {
|
||
|
|
return queryHttps('v6', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
return queryDns('v6', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
export {CancelError} from 'got';
|