增加威胁域名审计
This commit is contained in:
+417
-239
@@ -3,7 +3,8 @@
|
||||
// 全局变量
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let logsPerPage = 30; // 默认显示30条记录
|
||||
let totalRecords = 0; // 总记录数
|
||||
let logsPerPage = 10; // 默认显示 10 条记录
|
||||
let currentFilter = '';
|
||||
let currentSearch = '';
|
||||
let logsChart = null;
|
||||
@@ -18,6 +19,12 @@ if (typeof ipGeolocationCache === 'undefined') {
|
||||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期 24 小时
|
||||
}
|
||||
|
||||
// 跟踪器检查缓存(优化性能,避免重复检查)
|
||||
const trackerCache = new Map();
|
||||
const trackerCacheTimestamp = new Map();
|
||||
const TRACKER_CHECK_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期 24 小时
|
||||
const TRACKER_CHECK_CACHE_MAX_SIZE = 1000; // 最大缓存 1000 条记录
|
||||
|
||||
// LRU 缓存辅助函数:更新缓存顺序,将指定 IP 移到最后(最近使用)
|
||||
function updateCacheOrder(ip) {
|
||||
const index = ipGeolocationCacheOrder.indexOf(ip);
|
||||
@@ -269,22 +276,45 @@ async function getDomainInfoFromAPI(domain) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息
|
||||
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息(带缓存优化)
|
||||
async function isDomainInTrackerDatabase(domain) {
|
||||
// 检查缓存
|
||||
const cached = trackerCache.get(domain);
|
||||
const timestamp = trackerCacheTimestamp.get(domain);
|
||||
|
||||
if (cached !== undefined && timestamp && (Date.now() - timestamp) < TRACKER_CHECK_CACHE_EXPIRY) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 确保数据库已加载
|
||||
if (!trackersDatabase || !trackersLoaded) {
|
||||
await loadTrackersDatabase();
|
||||
}
|
||||
|
||||
if (!trackersDatabase || !trackersDatabase.trackers) {
|
||||
// 缓存空结果
|
||||
trackerCache.set(domain, null);
|
||||
trackerCacheTimestamp.set(domain, Date.now());
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查域名是否直接作为跟踪器键存在
|
||||
if (trackersDatabase.trackers.hasOwnProperty(domain)) {
|
||||
return trackersDatabase.trackers[domain];
|
||||
const result = trackersDatabase.trackers[domain];
|
||||
// 缓存结果
|
||||
trackerCache.set(domain, result);
|
||||
trackerCacheTimestamp.set(domain, Date.now());
|
||||
// 限制缓存大小
|
||||
if (trackerCache.size > TRACKER_CHECK_CACHE_MAX_SIZE) {
|
||||
const oldestKey = trackerCache.keys().next().value;
|
||||
trackerCache.delete(oldestKey);
|
||||
trackerCacheTimestamp.delete(oldestKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查域名是否在跟踪器URL中
|
||||
// 检查域名是否在跟踪器 URL 中
|
||||
let result = null;
|
||||
for (const trackerKey in trackersDatabase.trackers) {
|
||||
if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) {
|
||||
const tracker = trackersDatabase.trackers[trackerKey];
|
||||
@@ -292,16 +322,28 @@ async function isDomainInTrackerDatabase(domain) {
|
||||
try {
|
||||
const trackerUrl = new URL(tracker.url);
|
||||
if (trackerUrl.hostname === domain) {
|
||||
return tracker;
|
||||
result = tracker;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略无效URL
|
||||
// 忽略无效 URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// 缓存结果
|
||||
trackerCache.set(domain, result);
|
||||
trackerCacheTimestamp.set(domain, Date.now());
|
||||
|
||||
// 限制缓存大小
|
||||
if (trackerCache.size > TRACKER_CHECK_CACHE_MAX_SIZE) {
|
||||
const oldestKey = trackerCache.keys().next().value;
|
||||
trackerCache.delete(oldestKey);
|
||||
trackerCacheTimestamp.delete(oldestKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 根据域名查找对应的网站信息(使用 API)
|
||||
@@ -662,6 +704,12 @@ function initResizableColumns() {
|
||||
// 初始化查询日志页面
|
||||
function initLogsPage() {
|
||||
|
||||
// 【优化】预加载跟踪器数据库(不阻塞页面初始化)
|
||||
if (typeof loadTrackersDatabase === 'function') {
|
||||
loadTrackersDatabase();
|
||||
console.log('📦 [优化] 跟踪器数据库预加载已启动');
|
||||
}
|
||||
|
||||
// 加载日志统计数据
|
||||
loadLogsStats();
|
||||
|
||||
@@ -748,15 +796,30 @@ function bindLogsEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
// 自定义记录数量
|
||||
const perPageSelect = document.getElementById('logs-per-page');
|
||||
if (perPageSelect) {
|
||||
perPageSelect.addEventListener('change', () => {
|
||||
logsPerPage = parseInt(perPageSelect.value);
|
||||
currentPage = 1;
|
||||
loadLogs();
|
||||
});
|
||||
}
|
||||
// 自定义记录数量(顶部和底部选择器同步)
|
||||
const perPageSelectTop = document.getElementById('logs-per-page');
|
||||
const perPageSelectBottom = document.getElementById('logs-per-page-bottom');
|
||||
|
||||
// 同步两个选择器的事件
|
||||
[perPageSelectTop, perPageSelectBottom].forEach(select => {
|
||||
if (select) {
|
||||
select.addEventListener('change', () => {
|
||||
const newValue = parseInt(select.value);
|
||||
console.log('每页数量改变:', logsPerPage, '->', newValue);
|
||||
logsPerPage = newValue;
|
||||
currentPage = 1;
|
||||
// 同步两个选择器的值
|
||||
if (perPageSelectTop && perPageSelectTop !== select) {
|
||||
perPageSelectTop.value = newValue;
|
||||
}
|
||||
if (perPageSelectBottom && perPageSelectBottom !== select) {
|
||||
perPageSelectBottom.value = newValue;
|
||||
}
|
||||
console.log('开始加载日志,limit:', logsPerPage);
|
||||
loadLogs();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 分页按钮
|
||||
const prevBtn = document.getElementById('logs-prev-page');
|
||||
@@ -916,18 +979,60 @@ function loadLogsStats() {
|
||||
});
|
||||
}
|
||||
|
||||
// 更新日志统计数据UI
|
||||
// 更新日志统计数据 UI
|
||||
function updateLogsStatsUI(data) {
|
||||
if (!data) return;
|
||||
|
||||
// 更新统计卡片
|
||||
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||||
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||||
document.getElementById('logs-active-ips').textContent = data.activeIPs;
|
||||
|
||||
// 计算屏蔽率
|
||||
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
|
||||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||||
if (!data) return;
|
||||
|
||||
// 更新统计卡片
|
||||
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||||
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||||
document.getElementById('logs-active-ips').textContent = data.activeIPs;
|
||||
|
||||
// 计算屏蔽率
|
||||
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
|
||||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||||
|
||||
// 更新归档统计信息(如果存在)
|
||||
if (data.archiveCount !== undefined) {
|
||||
const archiveInfoEl = document.getElementById('logs-archive-info');
|
||||
if (archiveInfoEl) {
|
||||
const archiveCount = data.archiveCount || 0;
|
||||
const archiveTotalRecords = data.archiveTotalRecords || 0;
|
||||
const archiveTotalSize = data.archiveTotalCompressedSize || 0;
|
||||
const grandTotal = data.grandTotalRecords || data.totalQueries;
|
||||
|
||||
if (archiveCount > 0) {
|
||||
archiveInfoEl.innerHTML = `
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="fa fa-archive text-primary mr-2"></i>
|
||||
<span>已包含 <strong>${archiveCount}</strong> 个归档文件,共 <strong>${formatNumber(archiveTotalRecords)}</strong> 条历史数据</span>
|
||||
<span class="mx-2">|</span>
|
||||
<span>主库:<strong>${formatNumber(data.totalQueries)}</strong> 条</span>
|
||||
<span class="mx-2">|</span>
|
||||
<span>总计:<strong>${formatNumber(grandTotal)}</strong> 条</span>
|
||||
<span class="mx-2">|</span>
|
||||
<span>归档大小:<strong>${formatFileSize(archiveTotalSize)}</strong></span>
|
||||
<button id="view-archives-btn" class="ml-4 px-3 py-1 bg-primary text-white rounded-md hover:bg-blue-600 transition-colors text-xs">
|
||||
<i class="fa fa-list mr-1"></i>查看归档
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定查看归档按钮事件
|
||||
const viewArchivesBtn = document.getElementById('view-archives-btn');
|
||||
if (viewArchivesBtn) {
|
||||
viewArchivesBtn.addEventListener('click', showArchiveListModal);
|
||||
}
|
||||
} else {
|
||||
archiveInfoEl.innerHTML = `
|
||||
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<i class="fa fa-archive mr-2"></i>
|
||||
<span>暂无归档文件</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日志详情
|
||||
@@ -961,8 +1066,8 @@ async function loadLogs() {
|
||||
}
|
||||
*/
|
||||
|
||||
// 构建请求URL
|
||||
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||||
// 构建请求 URL,使用 page 参数代替 offset
|
||||
let endpoint = `/logs/query?page=${currentPage}&limit=${logsPerPage}`;
|
||||
|
||||
// 添加过滤条件
|
||||
if (currentFilter) {
|
||||
@@ -980,7 +1085,7 @@ async function loadLogs() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用封装的apiRequest函数进行API调用
|
||||
// 使用封装的 apiRequest 函数进行 API 调用
|
||||
const logsData = await apiRequest(endpoint);
|
||||
|
||||
if (logsData && logsData.error) {
|
||||
@@ -992,19 +1097,37 @@ async function loadLogs() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载日志总数
|
||||
const [logs, countData] = await Promise.all([
|
||||
Promise.resolve(logsData || []), // 确保logsData是数组
|
||||
apiRequest('/logs/count')
|
||||
]);
|
||||
// 处理新的返回格式:{logs: [...], total: 123, page: 1, limit: 30, totalPages: 5}
|
||||
let logsArray = [];
|
||||
|
||||
// 确保logs是数组
|
||||
const logsArray = Array.isArray(logs) ? logs : [];
|
||||
// 确保countData是有效的
|
||||
const totalLogs = countData && countData.count ? countData.count : logsArray.length;
|
||||
if (logsData && typeof logsData === 'object') {
|
||||
// 如果是对象格式,提取所有字段
|
||||
logsArray = Array.isArray(logsData.logs) ? logsData.logs : [];
|
||||
totalRecords = logsData.total || 0;
|
||||
|
||||
// 直接使用后端返回的分页信息
|
||||
if (logsData.totalPages) {
|
||||
totalPages = logsData.totalPages;
|
||||
} else {
|
||||
// 如果没有返回 totalPages,则自己计算
|
||||
totalPages = totalRecords > 0 ? Math.ceil(totalRecords / logsPerPage) : 1;
|
||||
}
|
||||
|
||||
// 验证当前页码是否有效
|
||||
if (logsData.page && logsData.page !== currentPage) {
|
||||
console.log('后端返回的页码:', logsData.page, '当前页码:', currentPage);
|
||||
}
|
||||
} else if (Array.isArray(logsData)) {
|
||||
// 如果是数组格式(旧格式),直接使用
|
||||
logsArray = logsData;
|
||||
totalRecords = logsArray.length;
|
||||
totalPages = totalRecords > 0 ? Math.ceil(totalRecords / logsPerPage) : 1;
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||||
// 确保 logsArray 是数组
|
||||
if (!Array.isArray(logsArray)) {
|
||||
logsArray = [];
|
||||
}
|
||||
|
||||
// 禁用缓存存储,每次都从服务器获取最新数据
|
||||
/*
|
||||
@@ -1055,233 +1178,174 @@ async function loadLogs() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新日志表格
|
||||
// 更新日志表格(优化版:并行处理 + 文档片段)
|
||||
async function updateLogsTable(logs) {
|
||||
const startTime = performance.now();
|
||||
console.log('🚀 [性能] 开始渲染日志表格...');
|
||||
|
||||
const tableBody = document.getElementById('logs-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
// 清空表格
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (logs.length === 0) {
|
||||
// 显示空状态
|
||||
const emptyRow = document.createElement('tr');
|
||||
emptyRow.innerHTML = `
|
||||
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||
<div>暂无查询日志</div>
|
||||
</td>
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||
<div>暂无查询日志</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tableBody.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测是否为移动设备
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
// 填充表格
|
||||
for (const log of logs) {
|
||||
const trackerCheckStartTime = performance.now();
|
||||
const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
|
||||
const trackerResults = await Promise.all(trackerChecks);
|
||||
const trackerCheckEndTime = performance.now();
|
||||
console.log(`✅ [性能] 跟踪器检查完成,耗时:${(trackerCheckEndTime - trackerCheckStartTime).toFixed(2)}ms`);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i];
|
||||
const trackerInfo = trackerResults[i];
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
||||
row.className = `border-b border-gray-100 hover:bg-gray-50 transition-colors ${log.result === 'blocked' ? 'bg-red-50' : ''}`;
|
||||
|
||||
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
|
||||
const time = new Date(log.timestamp);
|
||||
const formattedDate = time.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
const formattedTime = time.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
const formattedDate = time.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
const formattedTime = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
|
||||
// 根据结果添加不同的背景色
|
||||
let rowClass = '';
|
||||
switch (log.result) {
|
||||
case 'blocked':
|
||||
rowClass = 'bg-red-50'; // 淡红色填充
|
||||
break;
|
||||
case 'allowed':
|
||||
// 检查是否是规则允许项目
|
||||
if (log.blockRule && log.blockRule.includes('allow')) {
|
||||
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
|
||||
} else {
|
||||
rowClass = ''; // 允许的不填充
|
||||
}
|
||||
break;
|
||||
default:
|
||||
rowClass = '';
|
||||
}
|
||||
let statusText = '', statusClass = '';
|
||||
if (log.result === 'blocked') { statusText = '被屏蔽'; statusClass = 'text-danger'; }
|
||||
else if (log.result === 'allowed') { statusText = '允许'; statusClass = 'text-success'; }
|
||||
else if (log.result === 'error') { statusText = '错误'; statusClass = 'text-warning'; }
|
||||
|
||||
// 添加行背景色
|
||||
if (rowClass) {
|
||||
row.classList.add(rowClass);
|
||||
}
|
||||
const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
|
||||
const isBlocked = log.result === 'blocked';
|
||||
|
||||
// 添加被屏蔽或允许显示,并增加颜色
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
switch (log.result) {
|
||||
case 'blocked':
|
||||
statusText = '被屏蔽';
|
||||
statusClass = 'text-danger';
|
||||
break;
|
||||
case 'allowed':
|
||||
statusText = '允许';
|
||||
statusClass = 'text-success';
|
||||
break;
|
||||
case 'error':
|
||||
statusText = '错误';
|
||||
statusClass = 'text-warning';
|
||||
break;
|
||||
default:
|
||||
statusText = '';
|
||||
statusClass = '';
|
||||
}
|
||||
const trackerTooltip = isTracker ? `
|
||||
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||
<div class="font-semibold mb-2">已知跟踪器</div>
|
||||
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
|
||||
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
|
||||
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// 检查域名是否在跟踪器数据库中
|
||||
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
// 构建行内容 - 根据设备类型决定显示内容
|
||||
// 添加缓存状态显示
|
||||
const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
|
||||
const cacheStatusText = log.fromCache ? '缓存' : '非缓存';
|
||||
|
||||
// 检查域名是否被拦截
|
||||
const isBlocked = log.result === 'blocked';
|
||||
|
||||
// 构建跟踪器浮窗内容
|
||||
const trackerTooltip = isTracker ? `
|
||||
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||
<div class="font-semibold mb-2">已知跟踪器</div>
|
||||
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
|
||||
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
|
||||
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
|
||||
if (isMobile) {
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm" colspan="4">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC 已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
${log.domain}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
if (isMobile) {
|
||||
// 移动设备只显示时间和请求信息
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm" colspan="4">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
${log.domain}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||
<div class="text-xs text-gray-500 mt-1">客户端: ${log.clientIP}</div>
|
||||
</td>
|
||||
`;
|
||||
} else {
|
||||
// 桌面设备显示完整信息
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium">${log.clientIP}</div>
|
||||
<div class="text-xs text-gray-500 mt-1 location-${log.clientIP.replace(/[.:]/g, '-')}">${log.location || '未知 未知'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC 已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
<img src="images/whois.svg" alt="WHOIS" class="w-4 h-4 mr-1 inline-block cursor-pointer hover:opacity-75" onclick="event.stopPropagation(); window.location.href = window.location.pathname + '#whois'; setTimeout(() => { const input = document.getElementById('whois-domain-input'); if(input && '${log.domain}') { input.value = '${log.domain}'; if(typeof searchDomainInfo === 'function') { searchDomainInfo(); } } }, 200);" title="查看域名信息" />
|
||||
${log.domain}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.fromCache ? '缓存' : '非缓存'}</span>${log.dnssec ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.edns ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">${log.responseTime}ms</td>
|
||||
<td class="py-3 px-4 text-sm text-center">
|
||||
${isBlocked ?
|
||||
`<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${log.domain}">放行</button>` :
|
||||
`<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${log.domain}">拦截</button>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 更新IP地理位置信息
|
||||
const locationElement = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
|
||||
if (locationElement) {
|
||||
// 调用getIpGeolocation函数获取地理位置
|
||||
getIpGeolocation(log.clientIP).then(location => {
|
||||
locationElement.textContent = location;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加跟踪器图标悬停事件
|
||||
if (isTracker) {
|
||||
const iconContainer = row.querySelector('.tracker-icon-container');
|
||||
const tooltip = iconContainer.querySelector('.tracker-tooltip');
|
||||
if (iconContainer && tooltip) {
|
||||
// 移除内联样式,使用CSS类控制显示
|
||||
tooltip.removeAttribute('style');
|
||||
|
||||
iconContainer.addEventListener('mouseenter', () => {
|
||||
tooltip.classList.add('visible');
|
||||
});
|
||||
|
||||
iconContainer.addEventListener('mouseleave', () => {
|
||||
tooltip.classList.remove('visible');
|
||||
});
|
||||
}
|
||||
}
|
||||
<div class="text-xs text-gray-500 mt-1">类型:${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||
<div class="text-xs text-gray-500 mt-1">客户端:${log.clientIP}</div>
|
||||
</td>
|
||||
`;
|
||||
} else {
|
||||
const domainEscaped = log.domain.replace(/'/g, "\\'");
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium">${log.clientIP}</div>
|
||||
<div class="text-xs text-gray-500 mt-1 location-${log.clientIP.replace(/[.:]/g, '-')}">${log.location || '未知 未知'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC 已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
<img src="images/whois.svg" alt="WHOIS" class="w-4 h-4 mr-1 inline-block cursor-pointer hover:opacity-75" onclick="event.stopPropagation(); window.location.href = window.location.pathname + '#whois'; setTimeout(() => { const input = document.getElementById('whois-domain-input'); if(input && '${domainEscaped}') { input.value = '${domainEscaped}'; if(typeof searchDomainInfo === 'function') { searchDomainInfo(); } } }, 200);" title="查看域名信息" />
|
||||
${log.domain}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">类型:${log.queryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.fromCache ? '缓存' : '非缓存'}</span>${log.dnssec ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.edns ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">DNS 服务器:${log.dnsServer || '无'}, DNSSEC 专用:${log.dnssecServer || '无'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">${log.responseTime}ms</td>
|
||||
<td class="py-3 px-4 text-sm text-center">
|
||||
${isBlocked ? `<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${domainEscaped}">放行</button>` : `<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${domainEscaped}">拦截</button>`}
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 绑定按钮事件
|
||||
const blockBtn = row.querySelector('.block-btn');
|
||||
if (blockBtn) {
|
||||
blockBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const domain = e.currentTarget.dataset.domain;
|
||||
blockDomain(domain);
|
||||
if (!isPrivateIP(log.clientIP)) {
|
||||
getIpGeolocation(log.clientIP).then(location => {
|
||||
const locationEl = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
|
||||
if (locationEl) locationEl.textContent = location;
|
||||
});
|
||||
}
|
||||
|
||||
const unblockBtn = row.querySelector('.unblock-btn');
|
||||
if (unblockBtn) {
|
||||
unblockBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const domain = e.currentTarget.dataset.domain;
|
||||
unblockDomain(domain);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定日志详情点击事件
|
||||
row.addEventListener('click', (e) => {
|
||||
// 如果点击的是按钮,不触发详情弹窗
|
||||
if (e.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
showLogDetailModal(log);
|
||||
});
|
||||
}
|
||||
|
||||
tableBody.appendChild(row);
|
||||
const blockBtn = row.querySelector('.block-btn');
|
||||
if (blockBtn) blockBtn.addEventListener('click', e => { e.preventDefault(); blockDomain(e.currentTarget.dataset.domain); });
|
||||
|
||||
const unblockBtn = row.querySelector('.unblock-btn');
|
||||
if (unblockBtn) unblockBtn.addEventListener('click', e => { e.preventDefault(); unblockDomain(e.currentTarget.dataset.domain); });
|
||||
|
||||
row.addEventListener('click', e => { if (e.target.closest('button')) return; showLogDetailModal(log); });
|
||||
|
||||
if (isTracker) {
|
||||
const iconContainer = row.querySelector('.tracker-icon-container');
|
||||
const tooltip = iconContainer && iconContainer.querySelector('.tracker-tooltip');
|
||||
if (iconContainer && tooltip) {
|
||||
tooltip.removeAttribute('style');
|
||||
iconContainer.addEventListener('mouseenter', () => tooltip.classList.add('visible'));
|
||||
iconContainer.addEventListener('mouseleave', () => tooltip.classList.remove('visible'));
|
||||
}
|
||||
}
|
||||
|
||||
fragment.appendChild(row);
|
||||
}
|
||||
|
||||
tableBody.appendChild(fragment);
|
||||
|
||||
const endTime = performance.now();
|
||||
console.log(`✅ [性能] 日志表格渲染完成,总耗时:${(endTime - startTime).toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
function updateLogsPagination() {
|
||||
// 更新页码显示
|
||||
document.getElementById('logs-current-page').textContent = currentPage;
|
||||
document.getElementById('logs-total-pages').textContent = totalPages;
|
||||
// 更新总记录数显示
|
||||
const totalCountEl = document.getElementById('logs-total-count');
|
||||
if (totalCountEl) {
|
||||
totalCountEl.textContent = totalRecords;
|
||||
}
|
||||
|
||||
// 更新页码显示(顶部和底部同步)
|
||||
const currentPageEl = document.getElementById('logs-current-page');
|
||||
const totalPagesEl = document.getElementById('logs-total-pages');
|
||||
const totalPagesBottomEl = document.getElementById('logs-total-pages-bottom');
|
||||
|
||||
if (currentPageEl) {
|
||||
currentPageEl.textContent = currentPage;
|
||||
}
|
||||
if (totalPagesEl) {
|
||||
totalPagesEl.textContent = totalPages;
|
||||
}
|
||||
if (totalPagesBottomEl) {
|
||||
totalPagesBottomEl.textContent = totalPages;
|
||||
}
|
||||
|
||||
// 更新页码输入框
|
||||
const pageInput = document.getElementById('logs-page-input');
|
||||
@@ -1920,7 +1984,7 @@ async function showLogDetailModal(log) {
|
||||
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||
recordsDiv.innerHTML = `
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">解析记录</div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-all bg-gray-50 dark:bg-gray-700 p-3 rounded-md border border-gray-200 dark:border-gray-600">${dnsRecords}</div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words font-mono bg-gray-50 dark:bg-gray-700 p-3 rounded-md border border-gray-200 dark:border-gray-600 overflow-x-auto">${dnsRecords}</div>
|
||||
`;
|
||||
|
||||
// DNS服务器
|
||||
@@ -2205,3 +2269,117 @@ function initLogDetailModal() {
|
||||
}
|
||||
|
||||
|
||||
// 格式化数字(添加千分位分隔符)
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === null || bytes === undefined || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 显示归档列表弹窗
|
||||
async function showArchiveListModal() {
|
||||
try {
|
||||
const archives = await apiRequest('/logs/archives');
|
||||
|
||||
if (!archives || archives.error) {
|
||||
const errorMsg = archives && archives.error ? archives.error : '未知错误';
|
||||
showNotification('获取归档列表失败:' + errorMsg, 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
|
||||
modalContainer.style.zIndex = '9999';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto animate-slide-in';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-between items-center';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'text-xl font-semibold text-gray-900 dark:text-gray-100';
|
||||
title.textContent = '归档文件列表';
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.innerHTML = '<i class="fa fa-times text-xl"></i>';
|
||||
closeButton.className = 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none transition-colors';
|
||||
closeButton.onclick = () => closeModal();
|
||||
|
||||
header.appendChild(title);
|
||||
header.appendChild(closeButton);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'p-6';
|
||||
|
||||
if (archives.length === 0) {
|
||||
content.innerHTML = '<div class="text-center py-12"><i class="fa fa-archive text-6xl text-gray-300 mb-4"></i><p class="text-gray-500 dark:text-gray-400">暂无归档文件</p></div>';
|
||||
} else {
|
||||
const table = document.createElement('table');
|
||||
table.className = 'w-full text-sm text-left text-gray-500 dark:text-gray-400';
|
||||
|
||||
const thead = document.createElement('thead');
|
||||
thead.className = 'text-xs text-gray-700 dark:text-gray-400 uppercase bg-gray-50 dark:bg-gray-700';
|
||||
thead.innerHTML = '<tr><th class="px-6 py-3">归档日期</th><th class="px-6 py-3">月份</th><th class="px-6 py-3">记录数</th><th class="px-6 py-3">原始大小</th><th class="px-6 py-3">压缩后大小</th><th class="px-6 py-3">压缩比</th><th class="px-6 py-3">时间范围</th></tr>';
|
||||
table.appendChild(thead);
|
||||
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
archives.forEach(archive => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
|
||||
const archiveDate = new Date(archive.archiveDate);
|
||||
const startTime = new Date(archive.startTime);
|
||||
const endTime = new Date(archive.endTime);
|
||||
const compressionRatio = archive.originalSize > 0 ? (archive.originalSize / archive.compressedSize).toFixed(2) : 'N/A';
|
||||
|
||||
row.innerHTML = '<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">' + archiveDate.toLocaleDateString('zh-CN') + '</td><td class="px-6 py-4">' + archive.month + '</td><td class="px-6 py-4">' + formatNumber(archive.recordCount) + '</td><td class="px-6 py-4">' + formatFileSize(archive.originalSize) + '</td><td class="px-6 py-4">' + formatFileSize(archive.compressedSize) + '</td><td class="px-6 py-4 text-green-600 dark:text-green-400">' + compressionRatio + 'x</td><td class="px-6 py-4"><div class="text-xs">' + startTime.toLocaleDateString('zh-CN') + '</div><div class="text-xs text-gray-400">' + startTime.toLocaleTimeString('zh-CN') + ' - ' + endTime.toLocaleTimeString('zh-CN') + '</div></td>';
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
content.appendChild(table);
|
||||
}
|
||||
|
||||
modalContent.appendChild(header);
|
||||
modalContent.appendChild(content);
|
||||
modalContainer.appendChild(modalContent);
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
function closeModal() {
|
||||
modalContainer.classList.add('animate-fade-out');
|
||||
modalContent.classList.add('animate-slide-out');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(modalContainer);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
modalContainer.addEventListener('click', (e) => {
|
||||
if (e.target === modalContainer) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
const handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
|
||||
} catch (error) {
|
||||
console.error('显示归档列表失败:', error);
|
||||
showNotification('显示归档列表失败:' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -105,7 +105,6 @@ function initPageByHash() {
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('gfwlist-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('domain-content'),
|
||||
document.getElementById('whois-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content'),
|
||||
@@ -134,7 +133,6 @@ function initPageByHash() {
|
||||
'hosts': 'Hosts 管理',
|
||||
'gfwlist': 'GFWList 管理',
|
||||
'query': 'DNS 屏蔽查询',
|
||||
'domain': '域名查询',
|
||||
'whois': '域名信息',
|
||||
'logs': '查询日志',
|
||||
'config': '系统设置',
|
||||
|
||||
+52
-5
@@ -838,12 +838,16 @@ function populateThreatList(filteredThreats = null) {
|
||||
<span class="px-2 py-1 rounded-full text-xs ${statusInfo.class}">${statusInfo.text}</span>
|
||||
</td>
|
||||
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">
|
||||
${!threat.resolved ? `
|
||||
${threat.status === 'blocked' ? `
|
||||
<button class="alert-action-btn block px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600" data-alert-id="${threat.id}" data-action="allowed" data-domain="${threat.domain}">放行</button>
|
||||
` : threat.status === 'allowed' ? `
|
||||
<button class="alert-action-btn block px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600" data-alert-id="${threat.id}" data-action="blocked" data-domain="${threat.domain}">屏蔽</button>
|
||||
` : `
|
||||
<div class="flex space-x-2">
|
||||
<button class="alert-action-btn block px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600" data-alert-id="${threat.id}" data-action="blocked">屏蔽</button>
|
||||
<button class="alert-action-btn block px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600" data-alert-id="${threat.id}" data-action="allowed">放行</button>
|
||||
<button class="alert-action-btn block px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600" data-alert-id="${threat.id}" data-action="blocked" data-domain="${threat.domain}">屏蔽</button>
|
||||
<button class="alert-action-btn block px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600" data-alert-id="${threat.id}" data-action="allowed" data-domain="${threat.domain}">放行</button>
|
||||
</div>
|
||||
` : '<span class="text-gray-500 text-xs">已处理</span>'}
|
||||
`}
|
||||
</td>
|
||||
`;
|
||||
|
||||
@@ -1058,8 +1062,9 @@ function bindAlertActionEvents() {
|
||||
button.addEventListener('click', async function() {
|
||||
const alertId = this.getAttribute('data-alert-id');
|
||||
const action = this.getAttribute('data-action');
|
||||
const domain = this.getAttribute('data-domain');
|
||||
|
||||
console.log(`处理告警 ${alertId},动作: ${action}`);
|
||||
console.log(`处理告警 ${alertId},动作: ${action},域名: ${domain}`);
|
||||
|
||||
// 显示加载状态
|
||||
const originalText = this.textContent;
|
||||
@@ -1082,6 +1087,15 @@ function bindAlertActionEvents() {
|
||||
console.log('告警解决 API 返回:', response);
|
||||
|
||||
if (response.status === 'success') {
|
||||
// 将规则加入到屏蔽管理的自定义规则列表
|
||||
if (action === 'blocked') {
|
||||
// 添加到屏蔽规则
|
||||
await addToCustomRules(domain, 'block');
|
||||
} else if (action === 'allowed') {
|
||||
// 添加到允许规则
|
||||
await addToCustomRules(domain, 'allow');
|
||||
}
|
||||
|
||||
// 刷新威胁告警数据
|
||||
await refreshThreatData();
|
||||
console.log('告警处理成功');
|
||||
@@ -1099,6 +1113,39 @@ function bindAlertActionEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
// 添加到自定义规则列表
|
||||
async function addToCustomRules(domain, action) {
|
||||
try {
|
||||
console.log(`添加 ${action} 规则: ${domain}`);
|
||||
|
||||
// 根据操作类型选择 HTTP 方法
|
||||
const method = action === 'block' ? 'POST' : 'DELETE';
|
||||
|
||||
// 发送请求到屏蔽规则 API
|
||||
const response = await fetch('/api/shield', {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
rule: domain
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('添加规则 API 返回:', result);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || '添加规则失败');
|
||||
}
|
||||
|
||||
console.log('规则添加成功');
|
||||
} catch (error) {
|
||||
console.error('添加规则失败:', error);
|
||||
// 即使添加规则失败,也继续处理告警
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新威胁告警数据
|
||||
async function refreshThreatData() {
|
||||
try {
|
||||
|
||||
+613
-51
@@ -3,6 +3,52 @@
|
||||
// 全局变量
|
||||
let whoisChart = null;
|
||||
let currentDomain = '';
|
||||
let isLoading = false;
|
||||
let currentPageSize = 10; // 当前每页显示数量
|
||||
|
||||
// IP 地理位置缓存(与 logs.js 共享)
|
||||
if (typeof ipGeolocationCache === 'undefined') {
|
||||
var ipGeolocationCache = {};
|
||||
var ipGeolocationCacheOrder = [];
|
||||
var GEOLOCATION_CACHE_MAX_SIZE = 1000;
|
||||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
// LRU 缓存辅助函数:更新缓存顺序,将指定 IP 移到最后(最近使用)
|
||||
function updateCacheOrder(ip) {
|
||||
// 确保变量存在
|
||||
if (typeof ipGeolocationCacheOrder === 'undefined') {
|
||||
window.ipGeolocationCacheOrder = [];
|
||||
}
|
||||
const index = ipGeolocationCacheOrder.indexOf(ip);
|
||||
if (index > -1) {
|
||||
// 已存在,移除旧位置
|
||||
ipGeolocationCacheOrder.splice(index, 1);
|
||||
}
|
||||
// 添加到末尾(最近使用)
|
||||
ipGeolocationCacheOrder.push(ip);
|
||||
}
|
||||
|
||||
// LRU 缓存辅助函数:淘汰最少使用的缓存项
|
||||
function evictLRUCache() {
|
||||
// 确保变量存在
|
||||
if (typeof ipGeolocationCacheOrder === 'undefined' || typeof GEOLOCATION_CACHE_MAX_SIZE === 'undefined') {
|
||||
return; // 变量未初始化,直接返回
|
||||
}
|
||||
while (ipGeolocationCacheOrder.length >= GEOLOCATION_CACHE_MAX_SIZE) {
|
||||
// 淘汰最久未使用的(数组第一个)
|
||||
const oldestIP = ipGeolocationCacheOrder.shift();
|
||||
if (oldestIP) {
|
||||
delete ipGeolocationCache[oldestIP];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LRU 缓存辅助函数:清理过期缓存
|
||||
function cleanupExpiredCache() {
|
||||
// 简化处理:缓存直接存储 API 返回的数据对象,不检查过期时间
|
||||
// 仅通过 LRU 机制在 evictLRUCache 中控制缓存大小
|
||||
}
|
||||
|
||||
// 模拟的 ICP 备案信息(作为备用)
|
||||
function generateMockICPInfo(domain) {
|
||||
@@ -38,7 +84,7 @@ function generateMockICPInfo(domain) {
|
||||
return mockICP[index];
|
||||
}
|
||||
|
||||
// 模拟的 DNS 解析结果
|
||||
// 模拟的 DNS 解析结果(作为备用)
|
||||
function generateMockDNSResults(domain) {
|
||||
const locations = ['中国 - 江苏省', '中国 - 广东省', '中国 - 浙江省', '中国 - 上海市', '中国 - 北京市'];
|
||||
const carriers = ['中国电信', '中国联通', '中国移动', '中国教育网', '中国科技网'];
|
||||
@@ -61,6 +107,18 @@ function generateMockDNSResults(domain) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 模拟的 WHOIS 信息(作为备用)
|
||||
function generateMockWhoisInfo(domain) {
|
||||
return {
|
||||
registrant: '该域名已开通隐私保护',
|
||||
created: '2020-01-01 00:00:00',
|
||||
email: '该域名已开通隐私保护',
|
||||
expires: '2027-01-01 00:00:00',
|
||||
registrar: '未知注册商',
|
||||
nameservers: 'ns1.example.com, ns2.example.com'
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟的访问趋势数据
|
||||
function generateMockTrendData() {
|
||||
const labels = [];
|
||||
@@ -93,14 +151,47 @@ function generateMockTrendData() {
|
||||
};
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
isLoading = true;
|
||||
const searchBtn = document.getElementById('whois-search-btn');
|
||||
if (searchBtn) {
|
||||
searchBtn.disabled = true;
|
||||
searchBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>查询中...';
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
const loadingDiv = document.getElementById('whois-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏加载状态
|
||||
function hideLoading() {
|
||||
isLoading = false;
|
||||
const searchBtn = document.getElementById('whois-search-btn');
|
||||
if (searchBtn) {
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.innerHTML = '查询';
|
||||
}
|
||||
|
||||
// 隐藏加载提示
|
||||
const loadingDiv = document.getElementById('whois-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 WHOIS API
|
||||
async function fetchWhoisInfo(domain) {
|
||||
try {
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/whois?domain=${encodeURIComponent(domain)}`, {
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/whois?format=json&domain=${encodeURIComponent(domain)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer uapi-pnhxbxhkCYUhyS-3r45avJLjTB-qEB8HaNMDzmrT'
|
||||
}
|
||||
},
|
||||
timeout: 10000 // 10秒超时
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -111,18 +202,20 @@ async function fetchWhoisInfo(domain) {
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('获取 WHOIS 信息失败:', error);
|
||||
throw error;
|
||||
// 返回 null 而不是抛出错误,让上层处理
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 ICP 备案信息 API
|
||||
async function fetchICPInfo(domain) {
|
||||
try {
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/icp?domain=${encodeURIComponent(domain)}`, {
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/icp?domain=${encodeURIComponent(domain)}&format=json`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer uapi-pnhxbxhkCYUhyS-3r45avJLjTB-qEB8HaNMDzmrT'
|
||||
}
|
||||
},
|
||||
timeout: 10000 // 10秒超时
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -133,7 +226,129 @@ async function fetchICPInfo(domain) {
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('获取 ICP 备案信息失败:', error);
|
||||
throw error;
|
||||
// 返回 null 而不是抛出错误,让上层处理
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 IP 地理位置 API
|
||||
async function fetchIPInfo(ip) {
|
||||
try {
|
||||
// 确保缓存变量已初始化
|
||||
if (typeof ipGeolocationCache === 'undefined') {
|
||||
console.log('缓存变量未初始化,正在初始化...');
|
||||
window.ipGeolocationCache = {};
|
||||
window.ipGeolocationCacheOrder = [];
|
||||
window.GEOLOCATION_CACHE_MAX_SIZE = 1000;
|
||||
window.GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
console.log('缓存变量状态:', {
|
||||
cache: typeof ipGeolocationCache,
|
||||
order: typeof ipGeolocationCacheOrder,
|
||||
orderValue: ipGeolocationCacheOrder
|
||||
});
|
||||
|
||||
// 清理过期缓存
|
||||
cleanupExpiredCache();
|
||||
|
||||
// 检查缓存(缓存直接存储数据对象)
|
||||
if (ipGeolocationCache[ip]) {
|
||||
console.log('缓存命中:', ip);
|
||||
updateCacheOrder(ip);
|
||||
// 直接返回缓存的数据对象(扁平结构)
|
||||
return ipGeolocationCache[ip];
|
||||
}
|
||||
|
||||
console.log('缓存未命中,请求 API:', ip);
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/ipinfo?ip=${encodeURIComponent(ip)}&source=commercial`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer uapi-pnhxbxhkCYUhyS-3r45avJLjTB-qEB8HaNMDzmrT'
|
||||
},
|
||||
timeout: 5000 // 5 秒超时
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API 返回数据:', data);
|
||||
|
||||
// 存储到缓存(直接存储数据对象,不嵌套)
|
||||
ipGeolocationCache[ip] = data;
|
||||
console.log('存储缓存前 order:', ipGeolocationCacheOrder);
|
||||
updateCacheOrder(ip);
|
||||
console.log('存储缓存后 order:', ipGeolocationCacheOrder);
|
||||
evictLRUCache();
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`获取 IP 地理位置信息失败 (${ip}):`, error);
|
||||
// 返回默认值(扁平结构)
|
||||
return {
|
||||
region: '未知',
|
||||
isp: '未知'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最近 7 天的 DNS 解析记录
|
||||
async function fetchDNSLogs(domain) {
|
||||
try {
|
||||
// 构建请求 URL
|
||||
const endpoint = `/api/logs/query?search=${encodeURIComponent(domain)}&limit=1000`;
|
||||
|
||||
console.log('请求 DNS 日志:', endpoint);
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(endpoint, {
|
||||
timeout: 10000 // 10 秒超时
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const logs = await response.json();
|
||||
console.log('获取到 DNS 日志:', logs.length, '条');
|
||||
|
||||
// 过滤出包含 IP 地址的记录
|
||||
const ipRecords = [];
|
||||
const seenIPs = new Set();
|
||||
|
||||
// 遍历日志记录
|
||||
for (const log of logs) {
|
||||
// 检查是否包含 answers 字段
|
||||
if (log.answers && Array.isArray(log.answers)) {
|
||||
// 遍历 answers 数组
|
||||
for (const answer of log.answers) {
|
||||
// 检查 answer 是否有 value 字段
|
||||
if (answer.value) {
|
||||
// 提取 IP 地址
|
||||
const ipMatches = answer.value.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g);
|
||||
if (ipMatches) {
|
||||
console.log(`从 answer.value 提取到 IP:`, ipMatches);
|
||||
for (const ip of ipMatches) {
|
||||
if (!seenIPs.has(ip)) {
|
||||
seenIPs.add(ip);
|
||||
ipRecords.push({
|
||||
ip: ip,
|
||||
timestamp: log.timestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('最终提取到 IP 记录:', ipRecords.length, '个');
|
||||
return ipRecords;
|
||||
} catch (error) {
|
||||
console.error('获取 DNS 解析记录失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,18 +388,61 @@ function parseWhoisString(whoisString) {
|
||||
}
|
||||
}
|
||||
|
||||
// 注册时间
|
||||
else if (trimmedLine.startsWith('Registration Time: ')) {
|
||||
const value = trimmedLine.substring('Registration Time: '.length).trim();
|
||||
// 注册时间(多种格式)
|
||||
else if (trimmedLine.startsWith('Registration Time: ') ||
|
||||
trimmedLine.startsWith('Creation Date: ') ||
|
||||
trimmedLine.startsWith('Created Date: ')) {
|
||||
let prefix = '';
|
||||
if (trimmedLine.startsWith('Registration Time: ')) {
|
||||
prefix = 'Registration Time: ';
|
||||
} else if (trimmedLine.startsWith('Creation Date: ')) {
|
||||
prefix = 'Creation Date: ';
|
||||
} else if (trimmedLine.startsWith('Created Date: ')) {
|
||||
prefix = 'Created Date: ';
|
||||
}
|
||||
|
||||
let value = trimmedLine.substring(prefix.length).trim();
|
||||
if (value) {
|
||||
// 处理 ISO 格式的时间 (YYYY-MM-DDTHH:MM:SSZ 或 YYYY-MM-DDTHH:MM:SS+0000)
|
||||
if (value.includes('T')) {
|
||||
value = value.replace('T', ' ');
|
||||
if (value.includes('Z')) {
|
||||
value = value.replace('Z', '');
|
||||
} else if (value.includes('+')) {
|
||||
value = value.split('+')[0];
|
||||
}
|
||||
}
|
||||
result.created = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 过期时间
|
||||
else if (trimmedLine.startsWith('Expiration Time: ')) {
|
||||
const value = trimmedLine.substring('Expiration Time: '.length).trim();
|
||||
// 过期时间(多种格式)
|
||||
else if (trimmedLine.startsWith('Expiration Time: ') ||
|
||||
trimmedLine.startsWith('Registry Expiry Date: ') ||
|
||||
trimmedLine.startsWith('Registrar Registration Expiration Date: ') ||
|
||||
trimmedLine.startsWith('Expiry Date: ')) {
|
||||
let prefix = '';
|
||||
if (trimmedLine.startsWith('Expiration Time: ')) {
|
||||
prefix = 'Expiration Time: ';
|
||||
} else if (trimmedLine.startsWith('Registry Expiry Date: ')) {
|
||||
prefix = 'Registry Expiry Date: ';
|
||||
} else if (trimmedLine.startsWith('Registrar Registration Expiration Date: ')) {
|
||||
prefix = 'Registrar Registration Expiration Date: ';
|
||||
} else if (trimmedLine.startsWith('Expiry Date: ')) {
|
||||
prefix = 'Expiry Date: ';
|
||||
}
|
||||
|
||||
let value = trimmedLine.substring(prefix.length).trim();
|
||||
if (value) {
|
||||
// 处理 ISO 格式的时间 (YYYY-MM-DDTHH:MM:SSZ 或 YYYY-MM-DDTHH:MM:SS+0000)
|
||||
if (value.includes('T')) {
|
||||
value = value.replace('T', ' ');
|
||||
if (value.includes('Z')) {
|
||||
value = value.replace('Z', '');
|
||||
} else if (value.includes('+')) {
|
||||
value = value.split('+')[0];
|
||||
}
|
||||
}
|
||||
result.expires = value;
|
||||
}
|
||||
}
|
||||
@@ -356,6 +614,23 @@ function updateDNSResults(results, page = 1, pageSize = 10) {
|
||||
const tbody = document.getElementById('dns-results-body');
|
||||
if (!tbody) return;
|
||||
|
||||
// 如果没有数据,显示一行"-"
|
||||
if (!results || results.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white text-center" colspan="3">-</td>
|
||||
</tr>
|
||||
`;
|
||||
document.getElementById('dns-results-total').textContent = '共计 0 条';
|
||||
|
||||
// 清空分页
|
||||
const pagination = document.getElementById('dns-results-pagination');
|
||||
if (pagination) {
|
||||
pagination.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(results.length / pageSize);
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
@@ -366,9 +641,9 @@ function updateDNSResults(results, page = 1, pageSize = 10) {
|
||||
pageResults.forEach(result => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-3 text-sm text-gray-900">${result.ip}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900">${result.location}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900">${result.carrier}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">${result.ip}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">${result.location}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">${result.carrier}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
@@ -389,9 +664,43 @@ function updateDNSPagination(results, currentPage, pageSize) {
|
||||
|
||||
pagination.innerHTML = '';
|
||||
|
||||
// 创建分页容器(使用 flex 布局)
|
||||
const container = document.createElement('div');
|
||||
container.className = 'flex items-center space-x-2 flex-wrap gap-2';
|
||||
|
||||
// 每页数量筛选器
|
||||
const pageSizeLabel = document.createElement('span');
|
||||
pageSizeLabel.className = 'text-sm text-gray-600 dark:text-gray-400';
|
||||
pageSizeLabel.textContent = '每页:';
|
||||
container.appendChild(pageSizeLabel);
|
||||
|
||||
const pageSizeSelect = document.createElement('select');
|
||||
pageSizeSelect.className = 'px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-primary';
|
||||
pageSizeSelect.value = pageSize;
|
||||
[3, 5, 10, 20, 50, 100].forEach(size => {
|
||||
const option = document.createElement('option');
|
||||
option.value = size;
|
||||
option.textContent = size;
|
||||
pageSizeSelect.appendChild(option);
|
||||
});
|
||||
pageSizeSelect.onchange = () => {
|
||||
const newSize = parseInt(pageSizeSelect.value);
|
||||
console.log('改变每页数量:', currentPageSize, '->', newSize);
|
||||
console.log('当前结果数量:', results ? results.length : 0);
|
||||
currentPageSize = newSize;
|
||||
updateDNSResults(results, 1, currentPageSize);
|
||||
};
|
||||
container.appendChild(pageSizeSelect);
|
||||
|
||||
// 分隔符
|
||||
const separator = document.createElement('span');
|
||||
separator.className = 'text-sm text-gray-600 dark:text-gray-400 mx-2';
|
||||
separator.textContent = '|';
|
||||
container.appendChild(separator);
|
||||
|
||||
// 上一页按钮
|
||||
const prevBtn = document.createElement('button');
|
||||
prevBtn.className = `px-3 py-1 rounded-md ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`;
|
||||
prevBtn.className = `px-3 py-1 rounded-md ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}`;
|
||||
prevBtn.textContent = '<';
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
prevBtn.onclick = () => {
|
||||
@@ -399,22 +708,70 @@ function updateDNSPagination(results, currentPage, pageSize) {
|
||||
updateDNSResults(results, currentPage - 1, pageSize);
|
||||
}
|
||||
};
|
||||
pagination.appendChild(prevBtn);
|
||||
container.appendChild(prevBtn);
|
||||
|
||||
// 页码按钮
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
// 页码按钮(智能显示,避免过多)
|
||||
const maxVisiblePages = 5; // 最多显示的页码数
|
||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
// 调整起始页,确保始终显示 maxVisiblePages 个页码(如果可能)
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
// 显示第一页
|
||||
if (startPage > 1) {
|
||||
const firstBtn = document.createElement('button');
|
||||
firstBtn.className = `px-3 py-1 rounded-md ${1 === currentPage ? 'bg-primary text-white' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}`;
|
||||
firstBtn.textContent = '1';
|
||||
firstBtn.onclick = () => {
|
||||
updateDNSResults(results, 1, pageSize);
|
||||
};
|
||||
container.appendChild(firstBtn);
|
||||
|
||||
// 显示省略号
|
||||
if (startPage > 2) {
|
||||
const ellipsis = document.createElement('span');
|
||||
ellipsis.className = 'text-gray-600 dark:text-gray-400 px-2';
|
||||
ellipsis.textContent = '...';
|
||||
container.appendChild(ellipsis);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示中间的页码
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const pageBtn = document.createElement('button');
|
||||
pageBtn.className = `px-3 py-1 rounded-md ${i === currentPage ? 'bg-primary text-white' : 'text-gray-700 hover:bg-gray-100'}`;
|
||||
pageBtn.className = `px-3 py-1 rounded-md ${i === currentPage ? 'bg-primary text-white' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}`;
|
||||
pageBtn.textContent = i;
|
||||
pageBtn.onclick = () => {
|
||||
updateDNSResults(results, i, pageSize);
|
||||
};
|
||||
pagination.appendChild(pageBtn);
|
||||
container.appendChild(pageBtn);
|
||||
}
|
||||
|
||||
// 显示最后页
|
||||
if (endPage < totalPages) {
|
||||
// 显示省略号
|
||||
if (endPage < totalPages - 1) {
|
||||
const ellipsis = document.createElement('span');
|
||||
ellipsis.className = 'text-gray-600 dark:text-gray-400 px-2';
|
||||
ellipsis.textContent = '...';
|
||||
container.appendChild(ellipsis);
|
||||
}
|
||||
|
||||
const lastBtn = document.createElement('button');
|
||||
lastBtn.className = `px-3 py-1 rounded-md ${totalPages === currentPage ? 'bg-primary text-white' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}`;
|
||||
lastBtn.textContent = totalPages;
|
||||
lastBtn.onclick = () => {
|
||||
updateDNSResults(results, totalPages, pageSize);
|
||||
};
|
||||
container.appendChild(lastBtn);
|
||||
}
|
||||
|
||||
// 下一页按钮
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.className = `px-3 py-1 rounded-md ${currentPage === totalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`;
|
||||
nextBtn.className = `px-3 py-1 rounded-md ${currentPage === totalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}`;
|
||||
nextBtn.textContent = '>';
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
nextBtn.onclick = () => {
|
||||
@@ -422,7 +779,46 @@ function updateDNSPagination(results, currentPage, pageSize) {
|
||||
updateDNSResults(results, currentPage + 1, pageSize);
|
||||
}
|
||||
};
|
||||
pagination.appendChild(nextBtn);
|
||||
container.appendChild(nextBtn);
|
||||
|
||||
// 跳转输入框
|
||||
const jumpLabel = document.createElement('span');
|
||||
jumpLabel.className = 'text-sm text-gray-600 dark:text-gray-400 mx-2';
|
||||
jumpLabel.textContent = '跳转:';
|
||||
container.appendChild(jumpLabel);
|
||||
|
||||
const jumpInput = document.createElement('input');
|
||||
jumpInput.type = 'number';
|
||||
jumpInput.min = '1';
|
||||
jumpInput.max = totalPages;
|
||||
jumpInput.className = 'w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-primary';
|
||||
jumpInput.value = currentPage;
|
||||
jumpInput.onkeypress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const pageNum = parseInt(jumpInput.value);
|
||||
if (pageNum >= 1 && pageNum <= totalPages) {
|
||||
updateDNSResults(results, pageNum, pageSize);
|
||||
} else {
|
||||
jumpInput.value = currentPage;
|
||||
}
|
||||
}
|
||||
};
|
||||
container.appendChild(jumpInput);
|
||||
|
||||
const jumpBtn = document.createElement('button');
|
||||
jumpBtn.className = 'px-3 py-1 text-sm bg-primary text-white rounded-md hover:bg-primary/90';
|
||||
jumpBtn.textContent = '确定';
|
||||
jumpBtn.onclick = () => {
|
||||
const pageNum = parseInt(jumpInput.value);
|
||||
if (pageNum >= 1 && pageNum <= totalPages) {
|
||||
updateDNSResults(results, pageNum, pageSize);
|
||||
} else {
|
||||
jumpInput.value = currentPage;
|
||||
}
|
||||
};
|
||||
container.appendChild(jumpBtn);
|
||||
|
||||
pagination.appendChild(container);
|
||||
}
|
||||
|
||||
// 更新访问趋势图表
|
||||
@@ -505,8 +901,10 @@ function showWhoisError(message) {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 提取主域名(去除 www 等前缀)
|
||||
// 提取主域名(去除所有子域名)
|
||||
function extractMainDomain(domain) {
|
||||
if (!domain) return domain;
|
||||
|
||||
const parts = domain.split('.');
|
||||
|
||||
// 如果只有两部分(如 example.com),直接返回
|
||||
@@ -514,27 +912,106 @@ function extractMainDomain(domain) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
// 常见的子域前缀
|
||||
const commonSubdomains = ['www', 'mail', 'news', 'map', 'image', 'video', 'cdn', 'api', 'blog', 'shop', 'cloud', 'docs', 'help', 'support', 'dev', 'test', 'staging', 'm', 'mobile', 'admin', 'static', 'img', 'assets'];
|
||||
// 常见的子域前缀(更多类型)
|
||||
const commonSubdomains = [
|
||||
// 常见子域
|
||||
'www', 'mail', 'news', 'map', 'image', 'video', 'cdn', 'api', 'blog', 'shop',
|
||||
'cloud', 'docs', 'help', 'support', 'dev', 'test', 'staging', 'm', 'mobile',
|
||||
'admin', 'static', 'img', 'assets', 'web', 'app', 'store', 'service',
|
||||
// 日志和监控相关
|
||||
'log', 'logs', 'event', 'events', 'eventlog', 'beacon', 'beacons', 'tracking',
|
||||
'track', 'analytics', 'stat', 'stats', 'statistics', 'metric', 'metrics',
|
||||
'monitor', 'monitoring', 'sentry', 'ingest', 'collector', 'collect',
|
||||
// 服务和功能相关
|
||||
'auth', 'login', 'sso', 'oauth', 'account', 'user', 'users', 'profile',
|
||||
'payment', 'pay', 'billing', 'order', 'orders', 'cart', 'checkout',
|
||||
'search', 'find', 'discovery', 'recommend', 'push', 'notification',
|
||||
// 内容相关
|
||||
'content', 'file', 'files', 'download', 'upload', 'resource', 'resources',
|
||||
'data', 'db', 'database', 'cache', 'redis', 'memcache', 'storage',
|
||||
// 网络和基础设施
|
||||
'proxy', 'gateway', 'router', 'switch', 'lb', 'nlb', 'alb', 'elb',
|
||||
'edge', 'node', 'server', 'host', 'vm', 'container', 'k8s', 'kubernetes',
|
||||
// 开发相关
|
||||
'git', 'gitlab', 'github', 'jenkins', 'ci', 'cd', 'build', 'release',
|
||||
'npm', 'yarn', 'pkg', 'package', 'registry', 'repo', 'repository',
|
||||
// 其他常见子域
|
||||
'beta', 'alpha', 'preview', 'demo', 'sandbox', 'uat', 'prod', 'production',
|
||||
'internal', 'external', 'public', 'private', 'secure', 'ssl', 'tls',
|
||||
'old', 'new', 'v1', 'v2', 'v3', 'api1', 'api2', 'api3'
|
||||
];
|
||||
|
||||
// 如果第一部分是常见子域,去掉它
|
||||
if (commonSubdomains.includes(parts[0].toLowerCase())) {
|
||||
return parts.slice(1).join('.');
|
||||
}
|
||||
// 处理特殊情况,如 co.uk, co.jp 等(复合顶级域)
|
||||
const compoundTLDs = [
|
||||
'co.uk', 'org.uk', 'me.uk', 'ltd.uk', 'plc.uk', 'net.uk',
|
||||
'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'go.jp', 'ad.jp', 'ed.jp', 'gr.jp', 'lg.jp',
|
||||
'co.kr', 'ne.kr', 'or.kr', 'ac.kr', 'go.kr', 'ms.kr',
|
||||
'co.in', 'co.in', 'firm.in', 'net.in', 'org.in', 'gen.in', 'ind.in',
|
||||
'co.ca', 'net.ca', 'org.ca',
|
||||
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
||||
'co.nz', 'net.nz', 'org.nz', 'geek.nz', 'maori.nz', 'ac.nz', 'school.nz', 'govt.nz', 'mil.nz',
|
||||
'co.th', 'ac.th', 'go.th', 'or.th', 'net.th', 'in.th',
|
||||
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
|
||||
'com.my', 'net.my', 'org.my', 'gov.my', 'edu.my', 'mil.my', 'name.my',
|
||||
'co.id', 'ac.id', 'go.id', 'mil.id', 'or.id', 'net.id', 'web.id', 'sch.id', 'my.id',
|
||||
'co.za', 'net.za', 'org.za', 'web.za', 'school.za', 'gov.za', 'mil.za',
|
||||
'com.cn', 'net.cn', 'org.cn', 'edu.cn', 'gov.cn', 'mil.cn', 'ac.cn',
|
||||
'org.cn', 'net.cn', 'firm.cn', 'store.cn', 'arts.cn', 'rec.cn', 'info.cn', 'nom.cn',
|
||||
'com.hk', 'net.hk', 'org.hk', 'edu.hk', 'gov.hk', 'idv.hk',
|
||||
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'mil.tw', 'idv.tw', 'game.tw', 'ebiz.tw', 'club.tw',
|
||||
'com.vn', 'net.vn', 'org.vn', 'edu.vn', 'gov.vn', 'int.vn', 'ac.vn', 'biz.vn', 'info.vn', 'name.vn', 'pro.vn', 'health.vn'
|
||||
];
|
||||
|
||||
// 处理特殊情况,如 co.uk, co.jp 等
|
||||
const countryTLDs = ['co.uk', 'co.jp', 'co.kr', 'co.in', 'co.ca', 'co.au', 'co.nz', 'co.th', 'co.sg', 'co.my', 'co.id', 'co.za', 'com.cn', 'org.cn', 'net.cn', 'gov.cn', 'edu.cn'];
|
||||
|
||||
for (const tld of countryTLDs) {
|
||||
if (domain.endsWith('.' + tld)) {
|
||||
const mainParts = domain.split('.');
|
||||
return mainParts.slice(-tld.split('.').length - 1).join('.');
|
||||
// 检查是否是复合顶级域
|
||||
for (const compoundTLD of compoundTLDs) {
|
||||
if (domain.endsWith('.' + compoundTLD)) {
|
||||
const tldParts = compoundTLD.split('.');
|
||||
// 保留主域名 + 复合顶级域(2 部分)
|
||||
const keepParts = tldParts.length + 1;
|
||||
return parts.slice(-keepParts).join('.');
|
||||
}
|
||||
}
|
||||
|
||||
// 默认情况:如果超过 2 部分,去掉第一部分
|
||||
// 检查第一部分是否是常见子域
|
||||
const firstPart = parts[0].toLowerCase();
|
||||
if (commonSubdomains.includes(firstPart)) {
|
||||
// 去掉第一个子域,递归处理
|
||||
return extractMainDomain(parts.slice(1).join('.'));
|
||||
}
|
||||
|
||||
// 检查最后两部分是否是有效的顶级域组合
|
||||
const tld = parts[parts.length - 1].toLowerCase();
|
||||
const sld = parts[parts.length - 2].toLowerCase();
|
||||
const validTLDs = ['com', 'net', 'org', 'io', 'xyz', 'cn', 'info', 'biz', 'me', 'tv', 'cc', 'top',
|
||||
'vip', 'pro', 'club', 'site', 'online', 'store', 'tech', 'app', 'dev', 'ai',
|
||||
'cloud', 'live', 'world', 'today', 'news', 'blog', 'shop', 'life', 'love',
|
||||
'work', 'design', 'art', 'music', 'game', 'games', 'video', 'photo', 'pics',
|
||||
'image', 'images', 'media', 'space', 'website', 'link', 'click', 'email',
|
||||
'mail', 'tel', 'phone', 'mobi', 'mobile', 'asia', 'eu', 'us', 'uk', 'de',
|
||||
'fr', 'jp', 'ru', 'br', 'in', 'au', 'ca', 'it', 'es', 'nl', 'pl', 'se',
|
||||
'no', 'fi', 'dk', 'ch', 'at', 'be', 'pt', 'gr', 'cz', 'ie', 'nz', 'za',
|
||||
'mx', 'ar', 'cl', 'co', 'pe', 've', 'sg', 'hk', 'tw', 'kr', 'th', 'my',
|
||||
'id', 'ph', 'vn', 'ae', 'sa', 'il', 'tr', 'ua', 'ro', 'hu', 'sk', 'si',
|
||||
'hr', 'rs', 'bg', 'lt', 'lv', 'ee', 'name', 'cat', 'tel', 'travel', 'pro',
|
||||
'aero', 'coop', 'museum', 'mil', 'int', 'gov', 'edu'];
|
||||
|
||||
// 如果最后两部分是有效的顶级域组合,保留它们和前面的主域名
|
||||
if (validTLDs.includes(tld)) {
|
||||
// 检查二级域名是否是常见的商业/组织前缀
|
||||
const commonSLD = ['co', 'com', 'net', 'org', 'edu', 'gov', 'mil', 'ac', 'ne', 'or', 'go', 'ad', 'gr', 'lg', 'firm', 'store', 'arts', 'rec', 'info', 'nom', 'idv', 'game', 'ebiz', 'club', 'health', 'web', 'my', 'biz'];
|
||||
if (commonSLD.includes(sld) && sld !== tld) {
|
||||
// 这是一个复合结构,如 example.co.uk,保留最后 3 部分
|
||||
if (parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
}
|
||||
// 普通情况,保留最后 2 部分(主域名 + 顶级域)
|
||||
return parts.slice(-2).join('.');
|
||||
}
|
||||
|
||||
// 默认情况:如果超过 2 部分,去掉第一部分,递归处理
|
||||
if (parts.length > 2) {
|
||||
return parts.slice(1).join('.');
|
||||
return extractMainDomain(parts.slice(1).join('.'));
|
||||
}
|
||||
|
||||
return domain;
|
||||
@@ -550,6 +1027,9 @@ async function searchDomainInfo() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading();
|
||||
|
||||
// 自动裁剪顶级域,提取主域名
|
||||
const mainDomain = extractMainDomain(domain);
|
||||
currentDomain = mainDomain;
|
||||
@@ -563,19 +1043,28 @@ async function searchDomainInfo() {
|
||||
document.getElementById('trend-section').classList.add('hidden');
|
||||
|
||||
try {
|
||||
// 并行获取 WHOIS 和 ICP 信息
|
||||
const [whoisData, icpData] = await Promise.all([
|
||||
// 并行获取 WHOIS 和 ICP 信息,以及 DNS 日志
|
||||
const [whoisData, icpData, dnsLogs] = await Promise.all([
|
||||
fetchWhoisInfo(mainDomain),
|
||||
fetchICPInfo(mainDomain)
|
||||
fetchICPInfo(mainDomain),
|
||||
fetchDNSLogs(mainDomain)
|
||||
]);
|
||||
|
||||
console.log('API 返回数据:', { whoisData, icpData });
|
||||
console.log('API 返回数据:', { whoisData, icpData, dnsLogs });
|
||||
|
||||
// 处理 WHOIS 信息
|
||||
let whoisInfo;
|
||||
if (!whoisData || !whoisData.whois) {
|
||||
showWhoisError(`未找到域名 ${mainDomain} 的注册信息,请检查域名是否正确`);
|
||||
whoisInfo = { registrant: '-', created: '-', email: '-', expires: '-', registrar: '-', nameservers: '-' };
|
||||
console.log('未找到 WHOIS 信息,显示为 -');
|
||||
// 没有数据时显示为 -
|
||||
whoisInfo = {
|
||||
registrant: '-',
|
||||
created: '-',
|
||||
email: '-',
|
||||
expires: '-',
|
||||
registrar: '-',
|
||||
nameservers: '-'
|
||||
};
|
||||
} else {
|
||||
whoisInfo = formatWhoisInfo(whoisData);
|
||||
console.log('格式化后的 WHOIS 信息:', whoisInfo);
|
||||
@@ -584,8 +1073,14 @@ async function searchDomainInfo() {
|
||||
// 处理 ICP 备案信息
|
||||
let icpInfo;
|
||||
if (!icpData || icpData.code !== '200') {
|
||||
console.log('未找到 ICP 备案信息,使用模拟数据');
|
||||
icpInfo = generateMockICPInfo(mainDomain);
|
||||
console.log('未找到 ICP 备案信息,显示为 -');
|
||||
// 没有数据时显示为 -
|
||||
icpInfo = {
|
||||
company: '-',
|
||||
license: '-',
|
||||
type: '-',
|
||||
date: '-'
|
||||
};
|
||||
} else {
|
||||
icpInfo = formatICPInfo(icpData);
|
||||
console.log('格式化后的 ICP 信息:', icpInfo);
|
||||
@@ -599,6 +1094,45 @@ async function searchDomainInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 DNS 解析结果
|
||||
let dnsResults;
|
||||
if (dnsLogs && dnsLogs.length > 0) {
|
||||
console.log('找到 DNS 解析记录:', dnsLogs.length, '条');
|
||||
// 获取每个 IP 的地理位置信息
|
||||
const ipInfoPromises = dnsLogs.map(async (record) => {
|
||||
const ipInfo = await fetchIPInfo(record.ip);
|
||||
console.log(`IP ${record.ip} 的地理位置信息:`, ipInfo);
|
||||
let location = '-';
|
||||
let carrier = '-';
|
||||
|
||||
// 检查 API 返回的数据
|
||||
if (ipInfo && ipInfo.region) {
|
||||
// 直接使用 ipInfo 作为数据对象(API 直接返回数据)
|
||||
// 使用 API 返回的 region 字段作为地理位置
|
||||
location = ipInfo.region;
|
||||
// 使用 API 返回的 isp 字段作为运营商
|
||||
if (ipInfo.isp) {
|
||||
carrier = ipInfo.isp;
|
||||
}
|
||||
} else {
|
||||
console.log(`IP ${record.ip} 没有 region 字段,ipInfo:`, ipInfo);
|
||||
}
|
||||
|
||||
return {
|
||||
ip: record.ip,
|
||||
location: location,
|
||||
carrier: carrier
|
||||
};
|
||||
});
|
||||
|
||||
dnsResults = await Promise.all(ipInfoPromises);
|
||||
console.log('处理后的 DNS 解析结果:', dnsResults);
|
||||
} else {
|
||||
// 没有查询到 DNS 解析记录,显示空数组
|
||||
dnsResults = [];
|
||||
console.log('未找到 DNS 解析记录');
|
||||
}
|
||||
|
||||
// 更新显示
|
||||
updateWhoisInfo(whoisInfo);
|
||||
document.getElementById('whois-info-section').classList.remove('hidden');
|
||||
@@ -606,9 +1140,8 @@ async function searchDomainInfo() {
|
||||
updateICPInfo(icpInfo);
|
||||
document.getElementById('icp-info-section').classList.remove('hidden');
|
||||
|
||||
// 生成并显示 DNS 解析结果(模拟数据)
|
||||
const dnsResults = generateMockDNSResults(mainDomain);
|
||||
updateDNSResults(dnsResults, 1, 10);
|
||||
// 显示 DNS 解析结果
|
||||
updateDNSResults(dnsResults, 1, currentPageSize);
|
||||
document.getElementById('dns-results-section').classList.remove('hidden');
|
||||
|
||||
// 生成并显示访问趋势图表(模拟数据)
|
||||
@@ -618,6 +1151,26 @@ async function searchDomainInfo() {
|
||||
} catch (error) {
|
||||
console.error('查询域名信息失败:', error);
|
||||
showWhoisError('查询失败,请稍后重试');
|
||||
|
||||
// 使用模拟数据作为备用
|
||||
const whoisInfo = generateMockWhoisInfo(mainDomain);
|
||||
const icpInfo = generateMockICPInfo(mainDomain);
|
||||
const dnsResults = generateMockDNSResults(mainDomain);
|
||||
|
||||
updateWhoisInfo(whoisInfo);
|
||||
document.getElementById('whois-info-section').classList.remove('hidden');
|
||||
|
||||
updateICPInfo(icpInfo);
|
||||
document.getElementById('icp-info-section').classList.remove('hidden');
|
||||
|
||||
updateDNSResults(dnsResults, 1, currentPageSize);
|
||||
document.getElementById('dns-results-section').classList.remove('hidden');
|
||||
|
||||
updateTrendChart(mainDomain);
|
||||
document.getElementById('trend-section').classList.remove('hidden');
|
||||
} finally {
|
||||
// 隐藏加载状态
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,6 +1191,15 @@ function initWhoisPage() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有域名参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const domainParam = urlParams.get('domain');
|
||||
if (domainParam && domainInput) {
|
||||
domainInput.value = domainParam;
|
||||
// 自动查询
|
||||
setTimeout(searchDomainInfo, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时自动初始化
|
||||
|
||||
Reference in New Issue
Block a user