290 lines
11 KiB
JavaScript
290 lines
11 KiB
JavaScript
/* 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, '<').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 ? '<span class="ip-seg-hit">★ ' + esc(lineTxt) + '</span>' : esc(lineTxt));
|
||
s.details.forEach(function (ip) {
|
||
parts.push(focusCur[ip] ? '<span class="ip-hit">' + esc(ip) + '</span>' : 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 = '<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.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 = '';
|
||
$('ipFocus').value = '';
|
||
$('ipMsg').innerHTML = '';
|
||
$('ipResult').hidden = true;
|
||
$('ipInput').focus();
|
||
}
|
||
|
||
$('ipBtn').addEventListener('click', run);
|
||
$('ipCopy').addEventListener('click', function () {
|
||
if (lastData) copyText(plainText(lastData));
|
||
});
|
||
$('ipClear').addEventListener('click', clearAll);
|
||
$('ipInput').addEventListener('keydown', function (ev) {
|
||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run();
|
||
});
|
||
})();
|