/* ip.js —— IP 地址处理 * 提取输入文本中全部 IPv4 → 按 /24 段聚合输出(文本 / 高亮预览): * IP段 发现数量(段内去重IP数) IP段性质 * 1.2.3.0/24 8 公网 * 1.2.3.4 * * 「重点匹配 IP」:填写的 IP 命中后,其所在 /24 段优先置顶、对应 IP 红色标出。 * CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入,可在后台维护。 */ (function () { 'use strict'; function $(id) { return document.getElementById(id); } // 内网 / 保留地址段(IPv4) var PRIV_RANGES = [ ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.168.0.0', 16], ['192.0.2.0', 24], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4] ].map(function (r) { return { net: ipToInt(r[0]), prefix: r[1] }; }); function ipToInt(ip) { var p = String(ip).split('.'); return (((+p[0]) << 24) + ((+p[1]) << 16) + ((+p[2]) << 8) + (+p[3])) >>> 0; } function inRange(ipInt, net, prefix) { var mask = (prefix === 0) ? 0 : ((0xFFFFFFFF << (32 - prefix)) >>> 0); return ((ipInt & mask) >>> 0) === ((net & mask) >>> 0); } function isPrivate(ipInt) { for (var i = 0; i < PRIV_RANGES.length; i++) { if (inRange(ipInt, PRIV_RANGES[i].net, PRIV_RANGES[i].prefix)) return true; } return false; } /* ---------- CDN 段库解析 ---------- */ function parseCdnText(text) { var out = [], lines = String(text || '').split(/\r?\n/), i, m, p; for (i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line || line.charAt(0) === '#') continue; p = line.split(/\s+/); m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(p[0]); if (!m) continue; var a = +m[1], b = +m[2], c = +m[3], d = +m[4], pre = +m[5]; if (a > 255 || b > 255 || c > 255 || d > 255 || pre > 32) continue; out.push({ net: (((a << 24) + (b << 16) + (c << 8) + d) >>> 0), prefix: pre, label: p.slice(1).join(' ') || 'CDN' }); } return out; } function cdnOf(ipInt, ranges) { for (var i = 0; i < ranges.length; i++) { if (inRange(ipInt, ranges[i].net, ranges[i].prefix)) return ranges[i].label; } return null; } /* ---------- IPv4 提取 ---------- */ function extractIps(text) { var re = /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/g, m, found = []; while ((m = re.exec(text)) !== null) { var a = +m[1], b = +m[2], c = +m[3], d = +m[4]; if (a > 255 || b > 255 || c > 255 || d > 255) continue; found.push(a + '.' + b + '.' + c + '.' + d); } return found; } /* ---------- 重点 IP 解析 ---------- */ function parseFocus(text) { var set = {}; var parts = String(text || '').split(/[\s,,;;]+/); for (var i = 0; i < parts.length; i++) { var p = parts[i].trim(); if (!p) continue; var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(p); if (!m) continue; if (+m[1] > 255 || +m[2] > 255 || +m[3] > 255 || +m[4] > 255) continue; set[m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]] = true; } return set; } function ipLess(a, b) { var aa = a.split('.').map(Number), bb = b.split('.').map(Number), k; for (k = 0; k < 4; k++) { if (aa[k] !== bb[k]) return aa[k] - bb[k]; } return 0; } function keyLess(a, b) { var aa = a.split('.').map(Number), bb = b.split('.').map(Number), k; for (k = 0; k < 3; k++) { if (aa[k] !== bb[k]) return aa[k] - bb[k]; } return 0; } var TYPE_ZH = { priv: '内网', cdn: 'CDN', pub: '公网' }; /* 返回 { segs:[{line, hit, ips:[{ip,hit}]}], segTotals, totalIps, totalSeg, hitSeg, hitIps, rangeCount } */ function analyze(text, cdnRaw, focusSet) { var all = extractIps(text); if (!all.length) return null; var ranges = parseCdnText(cdnRaw); var segMap = {}, segOrder = []; for (var i = 0; i < all.length; i++) { var ip = all[i]; var key = ip.substring(0, ip.lastIndexOf('.')); if (!segMap[key]) { segMap[key] = { key: key, ips: [], classes: {} }; segOrder.push(key); } var seg = segMap[key]; if (seg.ips.indexOf(ip) === -1) { seg.ips.push(ip); var int = ipToInt(ip); var vendor = cdnOf(int, ranges); var cls = vendor ? 'cdn' : (isPrivate(int) ? 'priv' : 'pub'); seg.classes[cls] = (seg.classes[cls] || 0) + 1; } } segOrder.sort(keyLess); function segLabel(s) { var keys = Object.keys(s.classes); if (keys.length === 1) return TYPE_ZH[keys[0]]; var best = keys.slice().sort(function (x, y) { return (s.classes[y] - s.classes[x]) || (TYPE_ZH[x] < TYPE_ZH[y] ? -1 : 1); })[0]; return TYPE_ZH[best] + '为主(混合)'; } var segs = segOrder.map(function (k) { return segMap[k]; }); segs.forEach(function (s) { s.label = segLabel(s); }); // 命中重点 IP 的段排前,组内段号升序 var focusSegs = [], otherSegs = []; segs.forEach(function (s) { s.hit = s.ips.some(function (ip) { return focusSet[ip]; }); (s.hit ? focusSegs : otherSegs).push(s); }); segs = focusSegs.concat(otherSegs); var totalIps = 0, hitIps = 0, hitSeg = 0; segs.forEach(function (s) { totalIps += s.ips.length; if (s.hit) hitSeg++; s.ips.sort(ipLess); var h = [], o = []; s.ips.forEach(function (ip) { if (focusSet[ip]) { h.push(ip); hitIps++; } else o.push(ip); }); s.details = h.concat(o); s.line = s.key + '.0/24\t' + s.ips.length + '\t' + s.label; }); var cntSeg = { priv: 0, cdn: 0, pub: 0 }; segs.forEach(function (s) { var lab = s.label; if (lab.indexOf('内网') === 0) cntSeg.priv++; else if (lab.indexOf('CDN') === 0) cntSeg.cdn++; else cntSeg.pub++; }); return { segs: segs, totalIps: totalIps, totalSeg: segs.length, hitSeg: hitSeg, hitIps: hitIps, cntSeg: cntSeg, rangeCount: ranges.length }; } /* ---------- 文本 / 高亮输出 ---------- */ function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function plainText(data) { var lines = ['IP段\t发现数量\tIP段性质']; data.segs.forEach(function (s) { lines.push(s.line); s.details.forEach(function (ip) { lines.push('\t' + ip); }); lines.push(''); }); return lines.join('\n'); } function previewHtml(data) { var parts = []; parts.push(esc('IP段\t发现数量\tIP段性质').replace(/\t/g, ' ')); data.segs.forEach(function (s) { var lineTxt = s.line.replace(/\t/g, ' '); parts.push(s.hit ? '★ ' + esc(lineTxt) + '' : esc(lineTxt)); s.details.forEach(function (ip) { parts.push(focusCur[ip] ? '' + esc(ip) + '' : esc(ip)); }); parts.push(''); }); return parts.join('\n'); } var focusCur = {}; /* ---------- 事件 ---------- */ var lastData = null; function run() { var input = $('ipInput'); var msg = $('ipMsg'); msg.innerHTML = ''; var text = input.value; if (!text.trim()) { msg.innerHTML = '