75 lines
1.8 KiB
TypeScript
75 lines
1.8 KiB
TypeScript
import IP2Region from 'ip2region'
|
|
import { existsSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { isPrivateIp, normalizeIpCandidate } from './clientIp'
|
|
|
|
interface IpGeodata {
|
|
country: string
|
|
province: string
|
|
city: string
|
|
isp: string
|
|
}
|
|
|
|
const ipLocationLabelCache = new Map<string, string>()
|
|
let ip2Region: IP2Region | null | undefined
|
|
|
|
export function getIpGeodataLabel(ip?: string | null) {
|
|
const normalizedIp = normalizeIpCandidate(ip)
|
|
if (!normalizedIp) {
|
|
return null
|
|
}
|
|
|
|
const cachedLabel = ipLocationLabelCache.get(normalizedIp)
|
|
if (cachedLabel) {
|
|
return cachedLabel
|
|
}
|
|
|
|
const label = resolveIpGeodataLabel(normalizedIp)
|
|
ipLocationLabelCache.set(normalizedIp, label)
|
|
return label
|
|
}
|
|
|
|
function resolveIpGeodataLabel(ip: string) {
|
|
if (isPrivateIp(ip)) {
|
|
return '内网地址'
|
|
}
|
|
|
|
try {
|
|
const geodata = getIp2Region()?.search(ip) as IpGeodata | null
|
|
const parts = uniqueNonEmptyValues([
|
|
geodata?.country,
|
|
geodata?.province,
|
|
geodata?.city,
|
|
geodata?.isp
|
|
])
|
|
|
|
return parts.length > 0 ? parts.join(' ') : '未知归属地'
|
|
} catch {
|
|
return '未知归属地'
|
|
}
|
|
}
|
|
|
|
function getIp2Region() {
|
|
if (ip2Region !== undefined) {
|
|
return ip2Region
|
|
}
|
|
|
|
const dataDir = join(process.cwd(), 'node_modules', 'ip2region', 'data')
|
|
const ipv4db = join(dataDir, 'ip2region.db')
|
|
const ipv6db = join(dataDir, 'ipv6wry.db')
|
|
|
|
if (!existsSync(ipv4db) || !existsSync(ipv6db)) {
|
|
console.warn(`[geodata] ip2region database files not found in ${dataDir}`)
|
|
ip2Region = null
|
|
return ip2Region
|
|
}
|
|
|
|
ip2Region = new IP2Region({ ipv4db, ipv6db })
|
|
return ip2Region
|
|
}
|
|
|
|
function uniqueNonEmptyValues(values: Array<string | undefined>) {
|
|
return Array.from(new Set(values
|
|
.map(value => value?.trim())
|
|
.filter((value): value is string => !!value && value !== '0')))
|
|
} |