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

feat(axios): support ipv6 for isPrivate #160

Merged
merged 1 commit into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 7 additions & 38 deletions packages/axios/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
<T = any>(method: Method, url: string, config?: AxiosRequestConfig): Promise<T>
axios<T = any>(config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
Expand Down Expand Up @@ -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) {
Expand Down
86 changes: 86 additions & 0 deletions packages/axios/src/ip.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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
}