NewHome/assets/js/ip.js

366 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 隐藏文本框中func/ip.html可直接用编辑器增补厂商。
*/
(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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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 ? '<span class="ip-seg-hit">★ ' + esc(lineTxt) + '</span>' : esc(lineTxt));
s.details.forEach(function (ip) {
var cnt = data.counts[ip] || 1;
var row = esc(ip) + ' ' + cnt;
parts.push(focusCur[ip] ? '<span class="ip-hit">' + row + '</span>' : 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 = '<div class="err-msg">请先粘贴要分析的日志 / 文本内容。</div>';
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 = '<div class="err-msg">未在输入内容中解析到有效的 IPv4 地址。</div>';
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 = '<div class="err-msg">不支持的文件格式。请上传 txt / json / log / md / csv / conf / ini 文本文件。</div>';
fileInput.value = '';
return;
}
if (f.size > FILE_MAX) {
msg.innerHTML = '<div class="err-msg">文件超过 20MB 限制,请拆分后再分析。</div>';
fileInput.value = '';
return;
}
var rd = new FileReader();
rd.onload = function () {
$('ipInput').value = String(rd.result || '');
msg.innerHTML = '<div class="ok-msg">已读取「' + esc(f.name) + '」(' + (f.size / 1024).toFixed(1) + ' KB正在分析…</div>';
run();
};
rd.onerror = function () {
msg.innerHTML = '<div class="err-msg">文件读取失败,请重试或改用文本粘贴。</div>';
};
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();
});
})();