diff --git a/packages/axios/src/index.ts b/packages/axios/src/index.ts index 39b2f6e1..e7bf9c89 100644 --- a/packages/axios/src/index.ts +++ b/packages/axios/src/index.ts @@ -5,6 +5,7 @@ import { ClientRequestArgs } from 'http' import mimedb from 'mime-db' import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios' import * as Axios from 'axios' +import { isPrivate } from './ip' declare module 'cordis' { interface Context { @@ -18,41 +19,6 @@ declare module 'cordis' { } } -/* eslint-disable no-multi-spaces */ -const ranges = [ - '0.0.0.0/8', // RFC 1122 'this' network - '10.0.0.0/8', // RFC 1918 private space - '100.64.0.0/10', // RFC 6598 Carrier grade nat space - '127.0.0.0/8', // RFC 1122 localhost - '169.254.0.0/16', // RFC 3927 link local - '172.16.0.0/12', // RFC 1918 private space - '192.0.2.0/24', // RFC 5737 TEST-NET-1 - '192.88.99.0/24', // RFC 7526 6to4 anycast relay - '192.168.0.0/16', // RFC 1918 private space - '198.18.0.0/15', // RFC 2544 benchmarking - '198.51.100.0/24', // RFC 5737 TEST-NET-2 - '203.0.113.0/24', // RFC 5737 TEST-NET-3 - '224.0.0.0/4', // multicast - '240.0.0.0/4', // reserved -] -/* eslint-enable no-multi-spaces */ - -function addressToNumber(address: string) { - return address.split('.').reduce((a, b) => (a << 8n) + BigInt(b), 0n) -} - -function isPrivate(hostname: string) { - if (!/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.test(hostname)) return false - for (const cidr of ranges) { - const [address, length] = cidr.split('/') - const mask = -1n << BigInt(32 - +length) - const value1 = addressToNumber(hostname) & mask - const value2 = addressToNumber(address) & mask - if (value1 === value2) return true - } - return false -} - export interface Quester { (method: Method, url: string, config?: AxiosRequestConfig): Promise axios(config?: AxiosRequestConfig): Promise> @@ -152,10 +118,13 @@ export class Quester { return { mime, filename: name, data } } - isPrivate(url: string) { - const { hostname, protocol } = new URL(url) + async isPrivate(url: string) { + let { hostname, protocol } = new URL(url) if (protocol !== 'http:' && protocol !== 'https:') return true - return isPrivate(hostname) + if (/^\[.+\]$/.test(hostname)) { + hostname = hostname.slice(1, -1) + } + return await isPrivate(hostname) } async toPublic(url: string) { diff --git a/packages/axios/src/ip.ts b/packages/axios/src/ip.ts new file mode 100644 index 00000000..b6e51acf --- /dev/null +++ b/packages/axios/src/ip.ts @@ -0,0 +1,86 @@ +import { promises as dns } from 'dns' + +/* eslint-disable no-multi-spaces */ +const bogonV4 = [ + '0.0.0.0/8', // RFC 1122 'this' network + '10.0.0.0/8', // RFC 1918 private space + '100.64.0.0/10', // RFC 6598 Carrier grade nat space + '127.0.0.0/8', // RFC 1122 localhost + '169.254.0.0/16', // RFC 3927 link local + '172.16.0.0/12', // RFC 1918 private space + '192.0.2.0/24', // RFC 5737 TEST-NET-1 + '192.88.99.0/24', // RFC 7526 6to4 anycast relay + '192.168.0.0/16', // RFC 1918 private space + '198.18.0.0/15', // RFC 2544 benchmarking + '198.51.100.0/24', // RFC 5737 TEST-NET-2 + '203.0.113.0/24', // RFC 5737 TEST-NET-3 + '224.0.0.0/4', // multicast + '240.0.0.0/4', // reserved +] + +const bogonV6 = [ + '::/8', // RFC 4291 IPv4-compatible, loopback, et al + '0100::/64', // RFC 6666 Discard-Only + '2001:2::/48', // RFC 5180 BMWG + '2001:10::/28', // RFC 4843 ORCHID + '2001:db8::/32', // RFC 3849 documentation + '2002::/16', // RFC 7526 6to4 anycast relay + '3ffe::/16', // RFC 3701 old 6bone + 'fc00::/7', // RFC 4193 unique local unicast + 'fe80::/10', // RFC 4291 link local unicast + 'fec0::/10', // RFC 3879 old site local unicast + 'ff00::/8', // RFC 4291 multicast +] +/* eslint-enable no-multi-spaces */ + +export async function isPrivate(hostname: string): Promise { + try { + const { address, family } = await dns.lookup(hostname) + if (family !== 4 && family !== 6) return false + const { bogons, length, parse } = family === 4 + ? { bogons: bogonV4, length: 32, parse: parseIPv4 } + : { bogons: bogonV6, length: 128, parse: parseIPv6 } + const num = parse(address) + for (const bogon of bogons) { + const [prefix, cidr] = bogon.split('/') + const mask = ((1n << BigInt(cidr)) - 1n) << BigInt(length - +cidr) + if ((num & mask) === parse(prefix)) return true + } + return false + } catch (e) { + return false + } +} + +function parseIPv4(ip: string): bigint { + return ip.split('.').reduce((a, b) => (a << 8n) + BigInt(b), 0n) +} + +function parseIPv6(ip: string): bigint { + const exp = ip.indexOf('::') + let num = 0n + // :: 左边有内容 + if (exp !== -1 && exp !== 0) { + ip.slice(0, exp).split(':').forEach((piece, i) => { + num |= BigInt(`0x${piece}`) << BigInt((7 - i) * 16) + }) + } + // :: 在最右边 + if (exp === ip.length - 2) { + return num + } + // :: 右边的内容 + const rest = exp === -1 ? ip : ip.slice(exp + 2) + const v4 = rest.includes('.') + const pieces = rest.split(':') + let start = 0 + if (v4) { + start += 2 + const [addr] = pieces.splice(-1, 1) + num |= parseIPv4(addr) + } + pieces.reverse().forEach((piece, i) => { + num |= BigInt(`0x${piece}`) << BigInt((start + i) * 8) + }) + return num +}