327 lines
11 KiB
JavaScript
327 lines
11 KiB
JavaScript
const { exec } = require('child_process')
|
||
const os = require('os')
|
||
const net = require('net')
|
||
const iconv = require('iconv-lite')
|
||
const axios = require('axios')
|
||
const request = axios.create({ timeout: 3000 })
|
||
|
||
// 为空时请求本地IP
|
||
const getNetIPInfo = async (searchIp = '') => {
|
||
console.log('searchIp:', searchIp)
|
||
if (isLocalIP(searchIp)) {
|
||
return {
|
||
ip: searchIp,
|
||
country: '本地',
|
||
city: '局域网',
|
||
error: null
|
||
}
|
||
}
|
||
try {
|
||
let date = Date.now()
|
||
let ipUrls = [
|
||
// 45次/分钟&支持中文(无限制)
|
||
`http://ip-api.com/json/${ searchIp }?lang=zh-CN`,
|
||
// 10000次/月&支持中文(依赖IP计算调用次数)
|
||
`http://ipwho.is/${ searchIp }?lang=zh-CN`,
|
||
// 1500次/天(依赖密钥, 超出自行注册)
|
||
`https://api.ipdata.co/${ searchIp }?api-key=c6d4d04d5f11f2cd0839ee03c47c58621d74e361c945b5c1b4f668f3`,
|
||
// 50000/月(依赖密钥, 超出自行注册)
|
||
`https://ipinfo.io/${ searchIp }/json?token=41c48b54f6d78f`,
|
||
// 1000次/天(依赖密钥, 超出自行注册)
|
||
`https://api.ipgeolocation.io/ipgeo?apiKey=105fc2c7e8864ec08b98e1ad4e8cbc6d&ip=${ searchIp }`,
|
||
// 1000次/天(依赖IP计算调用次数)
|
||
`https://ipapi.co${ searchIp ? `/${ searchIp }` : '' }/json`,
|
||
// 国内IP138提供(无限制)
|
||
`https://sp1.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=${ searchIp }&resource_id=5809`
|
||
]
|
||
let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
|
||
|
||
let [ipApi, ipwho, ipdata, ipinfo, ipgeolocation, ipApi01, ip138] = result
|
||
|
||
let searchResult = []
|
||
if (ipApi.status === 'fulfilled') {
|
||
let { query: ip, country, regionName, city } = ipApi.value?.data || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
|
||
if (ipwho.status === 'fulfilled') {
|
||
let { ip, country, region: regionName, city } = ipwho.value?.data || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
|
||
if (ipdata.status === 'fulfilled') {
|
||
let { ip, country_name: country, region: regionName, city } = ipdata.value?.data || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
|
||
if (ipinfo.status === 'fulfilled') {
|
||
let { ip, country, region: regionName, city } = ipinfo.value?.data || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
|
||
if (ipgeolocation.status === 'fulfilled') {
|
||
let { ip, country_name: country, state_prov: regionName, city } = ipgeolocation.value?.data || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
|
||
if (ipApi01.status === 'fulfilled') {
|
||
let { ip, country_name: country, region: regionName, city } = ipApi01.value?.data || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
|
||
if (ip138.status === 'fulfilled') {
|
||
let [res] = ip138.value?.data?.data || []
|
||
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
|
||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||
}
|
||
console.log(searchResult)
|
||
let validInfo = searchResult.find(item => Boolean(item.country))
|
||
consola.info('查询IP信息:', validInfo)
|
||
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
|
||
} catch (error) {
|
||
// consola.error('getIpInfo Error: ', error)
|
||
return {
|
||
ip: '未知',
|
||
country: '未知',
|
||
city: '未知',
|
||
error
|
||
}
|
||
}
|
||
}
|
||
|
||
const getLocalNetIP = async () => {
|
||
try {
|
||
let ipUrls = [
|
||
'http://whois.pconline.com.cn/ipJson.jsp?json=true',
|
||
'https://www.ip.cn/api/index?ip=&type=0',
|
||
'https://freeipapi.com/api/json'
|
||
]
|
||
let result = await Promise.allSettled(ipUrls.map(url => axios.get(url)))
|
||
let [pconline, ipCN, freeipapi] = result
|
||
if (pconline.status === 'fulfilled') {
|
||
let ip = pconline.value?.data?.ip
|
||
if (ip) return ip
|
||
}
|
||
if (ipCN.status === 'fulfilled') {
|
||
let ip = ipCN.value?.data?.ip
|
||
consola.log('ipCN:', ip)
|
||
if (ip) return ip
|
||
}
|
||
if (freeipapi.status === 'fulfilled') {
|
||
let ip = pconline.value?.data?.ipAddress
|
||
if (ip) return ip
|
||
}
|
||
return null
|
||
} catch (error) {
|
||
console.error('getIpInfo Error: ', error?.message || error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
function isLocalIP(ip) {
|
||
// Check if IPv4 or IPv6 address
|
||
const isIPv4 = net.isIPv4(ip)
|
||
const isIPv6 = net.isIPv6(ip)
|
||
|
||
// Local IPv4 ranges
|
||
const localIPv4Ranges = [
|
||
{ start: '10.0.0.0', end: '10.255.255.255' },
|
||
{ start: '172.16.0.0', end: '172.31.255.255' },
|
||
{ start: '192.168.0.0', end: '192.168.255.255' },
|
||
{ start: '127.0.0.0', end: '127.255.255.255' } // Loopback
|
||
]
|
||
|
||
// Local IPv6 ranges
|
||
const localIPv6Ranges = [
|
||
'::1', // Loopback
|
||
'fc00::', // Unique local address
|
||
'fd00::' // Unique local address
|
||
]
|
||
|
||
function isInRange(ip, start, end) {
|
||
const ipNum = ipToNumber(ip)
|
||
return ipNum >= ipToNumber(start) && ipNum <= ipToNumber(end)
|
||
}
|
||
|
||
function ipToNumber(ip) {
|
||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0)
|
||
}
|
||
|
||
if (isIPv4) {
|
||
for (const range of localIPv4Ranges) {
|
||
if (isInRange(ip, range.start, range.end)) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isIPv6) {
|
||
if (localIPv6Ranges.includes(ip)) {
|
||
return true
|
||
}
|
||
|
||
// Handle IPv4-mapped IPv6 addresses (e.g., ::ffff:192.168.1.1)
|
||
if (ip.startsWith('::ffff:')) {
|
||
const ipv4Part = ip.split('::ffff:')[1]
|
||
if (ipv4Part && net.isIPv4(ipv4Part)) {
|
||
for (const range of localIPv4Ranges) {
|
||
if (isInRange(ipv4Part, range.start, range.end)) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
|
||
const err = new Error(msg)
|
||
err.status = status // 主动抛错
|
||
throw err
|
||
}
|
||
|
||
const isIP = (ip = '') => {
|
||
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
|
||
const isIPv6 = /^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/
|
||
return isIPv4.test(ip) || isIPv6.test(ip)
|
||
}
|
||
|
||
const randomStr = (len) => {
|
||
len = len || 16
|
||
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
||
a = str.length,
|
||
res = ''
|
||
for (let i = 0; i < len; i++) res += str.charAt(Math.floor(Math.random() * a))
|
||
return res
|
||
}
|
||
|
||
// 获取UTC-x时间
|
||
const getUTCDate = (num = 8) => {
|
||
let date = new Date()
|
||
let now_utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(),
|
||
date.getUTCDate(), date.getUTCHours() + num,
|
||
date.getUTCMinutes(), date.getUTCSeconds())
|
||
return new Date(now_utc)
|
||
}
|
||
|
||
const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
||
if (typeof (timestamp) !== 'number') return '--'
|
||
let date = new Date(timestamp)
|
||
let padZero = (num) => String(num).padStart(2, '0')
|
||
let year = date.getFullYear()
|
||
let mounth = padZero(date.getMonth() + 1)
|
||
let day = padZero(date.getDate())
|
||
let hours = padZero(date.getHours())
|
||
let minute = padZero(date.getMinutes())
|
||
let second = padZero(date.getSeconds())
|
||
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||
let week = weekday[date.getDay()]
|
||
switch (format) {
|
||
case 'date':
|
||
return `${ year }-${ mounth }-${ day }`
|
||
case 'week':
|
||
return `${ year }-${ mounth }-${ day } ${ week }`
|
||
case 'hour':
|
||
return `${ year }-${ mounth }-${ day } ${ hours }`
|
||
case 'time':
|
||
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
||
default:
|
||
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
||
}
|
||
}
|
||
|
||
function resolvePath(dir, path) {
|
||
return path.resolve(dir, path)
|
||
}
|
||
|
||
let shellThrottle = (fn, delay = 1000) => {
|
||
let timer = null
|
||
let args = null
|
||
function throttled() {
|
||
args = arguments
|
||
if (!timer) {
|
||
timer = setTimeout(() => {
|
||
fn(...args)
|
||
timer = null
|
||
}, delay)
|
||
}
|
||
}
|
||
function delayMs() {
|
||
return new Promise(resolve => setTimeout(resolve, delay))
|
||
}
|
||
throttled.last = async () => {
|
||
await delayMs()
|
||
fn(...args)
|
||
}
|
||
return throttled
|
||
}
|
||
|
||
const isProd = () => {
|
||
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
||
return EXEC_ENV === 'production'
|
||
}
|
||
|
||
let allowedIPs = process.env.ALLOWED_IPS ? process.env.ALLOWED_IPS.split(',') : ''
|
||
if (allowedIPs) consola.warn('allowedIPs:', allowedIPs)
|
||
const isAllowedIp = (requestIP) => {
|
||
if (allowedIPs.length === 0) return true
|
||
let flag = allowedIPs.some(item => requestIP.includes(item))
|
||
if (!flag) consola.warn('requestIP:', requestIP, '不在允许的IP列表中')
|
||
return flag
|
||
}
|
||
|
||
const ping = (ip, timeout = 5000) => {
|
||
return new Promise((resolve) => {
|
||
setTimeout(() => {
|
||
resolve({ success: false, msg: 'ping timeout!' })
|
||
}, timeout)
|
||
let isWin = os.platform() === 'win32'
|
||
const command = isWin ? `ping -n 1 ${ ip }` : `ping -c 1 ${ ip }`
|
||
const options = isWin ? { encoding: 'buffer' } : {}
|
||
|
||
exec(command, options, (error, stdout) => {
|
||
if (error) {
|
||
resolve({ success: false, msg: 'ping error!' })
|
||
return
|
||
}
|
||
let output
|
||
if (isWin) {
|
||
output = iconv.decode(stdout, 'cp936')
|
||
} else {
|
||
output = stdout.toString()
|
||
}
|
||
// console.log('output:', output)
|
||
let match
|
||
if (isWin) {
|
||
match = output.match(/平均 = (\d+)ms/)
|
||
if (!match) {
|
||
match = output.match(/Average = (\d+)ms/)
|
||
}
|
||
} else {
|
||
match = output.match(/rtt min\/avg\/max\/mdev = [\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+/)
|
||
}
|
||
if (match) {
|
||
resolve({ success: true, time: parseFloat(match[1]) })
|
||
} else {
|
||
resolve({ success: false, msg: 'Could not find time in ping output!' })
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
module.exports = {
|
||
getNetIPInfo,
|
||
getLocalNetIP,
|
||
throwError,
|
||
isIP,
|
||
randomStr,
|
||
getUTCDate,
|
||
formatTimestamp,
|
||
resolvePath,
|
||
shellThrottle,
|
||
isProd,
|
||
isAllowedIp,
|
||
ping
|
||
} |