/* ip.js —— IP 地址处理 * 提取输入文本中全部 IPv4 → 按 /24 段聚合输出(文本 / 高亮预览): * IP段 发现数量(段内去重IP数) IP段性质 出现次数 * 1.2.3.0/24 8 公网 56 * 1.2.3.4 出现次数 * * 段按出现次数(段内全部 IP 原文出现次数之和)从高到低排列; * 「重点匹配 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, count, ips:[...], details:[ip]}], totalCount, totalIps, totalSeg, hitSeg, hitIps, rangeCount, counts } */ function analyze(text, cdnRaw, focusSet) { var all = extractIps(text); if (!all.length) return null; var ranges = parseCdnText(cdnRaw); var counts = {}; for (var ci = 0; ci < all.length; ci++) { counts[all[ci]] = (counts[all[ci]] || 0) + 1; } 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 在原文出现次数之和 s.count = 0; s.ips.forEach(function (ip) { s.count += counts[ip] || 0; }); }); // 命中重点 IP 的段置顶;其余段按出现次数降序(同次数按段号升序) segs.forEach(function (s) { s.hit = s.ips.some(function (ip) { return focusSet[ip]; }); }); segs.sort(function (a, b) { if (a.hit !== b.hit) return a.hit ? -1 : 1; if (a.count !== b.count) return b.count - a.count; return keyLess(a.key, b.key); }); var totalIps = 0, hitIps = 0, hitSeg = 0, totalCount = 0; segs.forEach(function (s) { totalIps += s.ips.length; totalCount += s.count; 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 + '\t' + s.count; }); 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, totalCount: totalCount, totalIps: totalIps, totalSeg: segs.length, hitSeg: hitSeg, hitIps: hitIps, cntSeg: cntSeg, rangeCount: ranges.length, counts: counts }; } /* ---------- 文本 / 高亮输出 ---------- */ function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function plainText(data) { var lines = ['IP段 发现数量 IP段性质 出现次数']; data.segs.forEach(function (s) { lines.push(s.line); s.details.forEach(function (ip) { lines.push(ip + ' ' + (data.counts[ip] || 1)); }); lines.push(''); }); return lines.join('\n'); } function previewHtml(data) { var parts = []; parts.push(esc('IP段 发现数量 IP段性质 出现次数').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) { var cnt = data.counts[ip] || 1; var row = esc(ip) + ' ' + cnt; parts.push(focusCur[ip] ? '' + row + '' : row); }); 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 = '
请先粘贴要分析的日志 / 文本内容。
'; return; } var cdnRaw = ($('ipCdnRaw') && $('ipCdnRaw').value) || ''; var fs = parseFocus($('ipFocus').value); focusCur = fs; lastData = analyze(text, cdnRaw, fs); if (!lastData) { $('ipResult').hidden = true; msg.innerHTML = '
未在输入内容中解析到有效的 IPv4 地址。
'; return; } var d = lastData; $('ipPreview').innerHTML = previewHtml(d); var st = d.cntSeg; var note = 'IP 共出现 ' + d.totalCount + ' 次,去重 IP ' + d.totalIps + ' 个,涉及 ' + d.totalSeg + ' 个 /24 段(内网 ' + st.priv + ' 段 / CDN ' + st.cdn + ' 段 / 公网 ' + st.pub + ' 段),按段出现次数降序排列。'; if (Object.keys(parseFocus($('ipFocus').value)).length === 0) { note += ' 未填写重点 IP。'; } else if (d.hitIps === 0) { note += ' 填写的重点 IP 均未在输入内容中命中。'; } else { note += ' 命中重点 IP ' + d.hitIps + ' 个,' + d.hitSeg + ' 个 /24 段已优先置顶。'; } if (d.rangeCount === 0) note += ' CDN 段库为空,可在后台补充。'; $('ipStat').textContent = note; $('ipResult').hidden = false; msg.innerHTML = ''; } function copyText(text) { function done(ok) { var btn = $('ipCopy'); var old = btn.textContent; btn.textContent = ok ? '✓ 已复制' : '复制失败'; setTimeout(function () { btn.textContent = old; }, 1200); } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { done(true); }, function () { fallback(text, done); }); } else { fallback(text, done); } } function fallback(text, done) { var ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;'; document.body.appendChild(ta); ta.select(); try { done(document.execCommand('copy')); } catch (e) { done(false); } document.body.removeChild(ta); } function clearAll() { $('ipInput').value = ''; $('ipFile').value = ''; $('ipFocus').value = ''; $('ipMsg').innerHTML = ''; $('ipResult').hidden = true; $('ipInput').focus(); } /* ---------- 上传文本文件识别 ---------- */ var FILE_MAX = 20 * 1024 * 1024; // ≤ 20MB var FILE_EXTS = ['.txt', '.json', '.log', '.md', '.csv', '.conf', '.ini']; var fileInput = $('ipFile'); if (fileInput) { fileInput.addEventListener('change', function () { var f = fileInput.files && fileInput.files[0]; var msg = $('ipMsg'); msg.innerHTML = ''; if (!f) return; var name = (f.name || '').toLowerCase(); var okExt = false; for (var ei = 0; ei < FILE_EXTS.length; ei++) { if (name.slice(-FILE_EXTS[ei].length) === FILE_EXTS[ei]) { okExt = true; break; } } if (!okExt) { msg.innerHTML = '
不支持的文件格式。请上传 txt / json / log / md / csv / conf / ini 文本文件。
'; fileInput.value = ''; return; } if (f.size > FILE_MAX) { msg.innerHTML = '
文件超过 20MB 限制,请拆分后再分析。
'; fileInput.value = ''; return; } var rd = new FileReader(); rd.onload = function () { $('ipInput').value = String(rd.result || ''); msg.innerHTML = '
已读取「' + esc(f.name) + '」(' + (f.size / 1024).toFixed(1) + ' KB),正在分析…
'; run(); }; rd.onerror = function () { msg.innerHTML = '
文件读取失败,请重试或改用文本粘贴。
'; }; rd.readAsText(f, 'UTF-8'); }); } /* ---------- 导出 TXT ---------- */ function stamp() { var d = new Date(), p = function (n) { return (n < 10 ? '0' : '') + n; }; return '' + d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds()); } function exportTxt() { if (!lastData) return; var text = plainText(lastData); var blob = new Blob(['\ufeff' + text], { type: 'text/plain;charset=utf-8' }); // BOM 便于记事本识别 UTF-8 var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'ip_result_' + stamp() + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 1500); } $('ipBtn').addEventListener('click', run); $('ipCopy').addEventListener('click', function () { if (lastData) copyText(plainText(lastData)); }); $('ipExport').addEventListener('click', exportTxt); $('ipClear').addEventListener('click', clearAll); $('ipInput').addEventListener('keydown', function (ev) { if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run(); }); })();