whois
This commit is contained in:
@@ -129,8 +129,13 @@ function setupReconnect() {
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
// 处理实时数据更新 - 添加节流机制
|
||||
// 处理实时数据更新 - 添加节流机制和页面可见性检查
|
||||
function processRealTimeData(stats) {
|
||||
// 页面不可见时跳过处理,节省资源
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 节流处理,限制执行频率
|
||||
const now = Date.now();
|
||||
if (now - lastProcessedTime < PROCESS_THROTTLE_INTERVAL) {
|
||||
@@ -140,7 +145,7 @@ function processRealTimeData(stats) {
|
||||
|
||||
try {
|
||||
|
||||
// 确保stats是有效的对象
|
||||
// 确保 stats 是有效的对象
|
||||
if (!stats || typeof stats !== 'object') {
|
||||
console.error('无效的实时数据:', stats);
|
||||
return;
|
||||
|
||||
+91
-37
@@ -10,27 +10,80 @@ let logsChart = null;
|
||||
let currentSortField = 'timestamp'; // 默认按时间排序,显示最新记录
|
||||
let currentSortDirection = 'desc'; // 默认降序
|
||||
|
||||
// IP地理位置缓存(检查是否已经存在,避免重复声明)
|
||||
// IP 地理位置缓存(检查是否已经存在,避免重复声明)
|
||||
if (typeof ipGeolocationCache === 'undefined') {
|
||||
var ipGeolocationCache = {};
|
||||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||
var ipGeolocationCacheOrder = []; // 维护缓存插入顺序,用于 LRU
|
||||
var GEOLOCATION_CACHE_MAX_SIZE = 1000; // 最大缓存 1000 条记录
|
||||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期 24 小时
|
||||
}
|
||||
|
||||
// 获取IP地理位置信息
|
||||
// LRU 缓存辅助函数:更新缓存顺序,将指定 IP 移到最后(最近使用)
|
||||
function updateCacheOrder(ip) {
|
||||
const index = ipGeolocationCacheOrder.indexOf(ip);
|
||||
if (index > -1) {
|
||||
// 已存在,移除旧位置
|
||||
ipGeolocationCacheOrder.splice(index, 1);
|
||||
}
|
||||
// 添加到末尾(最近使用)
|
||||
ipGeolocationCacheOrder.push(ip);
|
||||
}
|
||||
|
||||
// LRU 缓存辅助函数:淘汰最少使用的缓存项
|
||||
function evictLRUCache() {
|
||||
while (ipGeolocationCacheOrder.length >= GEOLOCATION_CACHE_MAX_SIZE) {
|
||||
// 淘汰最久未使用的(数组第一个)
|
||||
const oldestIP = ipGeolocationCacheOrder.shift();
|
||||
if (oldestIP) {
|
||||
delete ipGeolocationCache[oldestIP];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LRU 缓存辅助函数:清理过期缓存
|
||||
function cleanupExpiredCache() {
|
||||
const now = Date.now();
|
||||
const expiredIPs = [];
|
||||
|
||||
// 找出所有过期的 IP
|
||||
for (const ip of ipGeolocationCacheOrder) {
|
||||
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) >= GEOLOCATION_CACHE_EXPIRY) {
|
||||
expiredIPs.push(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除过期缓存
|
||||
for (const ip of expiredIPs) {
|
||||
delete ipGeolocationCache[ip];
|
||||
const orderIndex = ipGeolocationCacheOrder.indexOf(ip);
|
||||
if (orderIndex > -1) {
|
||||
ipGeolocationCacheOrder.splice(orderIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 IP 地理位置信息(优化版,带 LRU 缓存管理)
|
||||
async function getIpGeolocation(ip) {
|
||||
// 检查是否为内网IP
|
||||
// 检查是否为内网 IP
|
||||
if (isPrivateIP(ip)) {
|
||||
return "内网 内网";
|
||||
}
|
||||
|
||||
// 定期清理过期缓存(每 100 次请求清理一次)
|
||||
if (Math.random() < 0.01) {
|
||||
cleanupExpiredCache();
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
const now = Date.now();
|
||||
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) {
|
||||
// 缓存命中,更新使用顺序
|
||||
updateCacheOrder(ip);
|
||||
return ipGeolocationCache[ip].location;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用whois.pconline.com.cn API获取IP地理位置
|
||||
// 使用 whois.pconline.com.cn API 获取 IP 地理位置
|
||||
const url = `https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
@@ -47,19 +100,25 @@ async function getIpGeolocation(ip) {
|
||||
let location = "未知 未知";
|
||||
|
||||
if (data && data.addr) {
|
||||
// 直接使用addr字段作为完整的地理位置信息
|
||||
// 直接使用 addr 字段作为完整的地理位置信息
|
||||
location = data.addr;
|
||||
}
|
||||
|
||||
// 如果缓存已满,先淘汰最少使用的项
|
||||
if (Object.keys(ipGeolocationCache).length >= GEOLOCATION_CACHE_MAX_SIZE) {
|
||||
evictLRUCache();
|
||||
}
|
||||
|
||||
// 保存到缓存
|
||||
ipGeolocationCache[ip] = {
|
||||
location: location,
|
||||
timestamp: now
|
||||
};
|
||||
updateCacheOrder(ip);
|
||||
|
||||
return location;
|
||||
} catch (error) {
|
||||
console.error('获取IP地理位置失败:', error);
|
||||
console.error('获取 IP 地理位置失败:', error);
|
||||
return "未知 未知";
|
||||
}
|
||||
}
|
||||
@@ -880,39 +939,27 @@ async function loadLogs() {
|
||||
}
|
||||
|
||||
// 构建缓存键,包含所有查询参数
|
||||
const cacheKey = `logs_${logsPerPage}_${currentPage}_${currentFilter}_${currentSearch}_${currentSortField}_${currentSortDirection}`;
|
||||
// 已禁用缓存,每次都从服务器获取最新数据
|
||||
// const cacheKey = `logs_${logsPerPage}_${currentPage}_${currentFilter}_${currentSearch}_${currentSortField}_${currentSortDirection}`;
|
||||
|
||||
// 检查是否有有效的缓存数据
|
||||
// 检查是否有有效的缓存数据(已禁用)
|
||||
/*
|
||||
const cachedLogs = window.pageDataCache && window.pageDataCache.getCache(cacheKey);
|
||||
if (cachedLogs) {
|
||||
console.log('使用缓存的日志数据');
|
||||
|
||||
// 确保cachedLogs是数组
|
||||
const logsArray = Array.isArray(cachedLogs.logs) ? cachedLogs.logs : [];
|
||||
// 确保totalLogs是有效的
|
||||
const totalLogs = cachedLogs.totalLogs || logsArray.length;
|
||||
|
||||
// 计算总页数
|
||||
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||||
|
||||
// 更新日志表格
|
||||
await updateLogsTable(logsArray);
|
||||
|
||||
// 绑定操作按钮事件
|
||||
bindActionButtonsEvents();
|
||||
|
||||
// 更新分页信息
|
||||
updateLogsPagination();
|
||||
|
||||
// 重新初始化列宽调节功能,确保新添加的行也能继承列宽设置
|
||||
initResizableColumns();
|
||||
|
||||
// 隐藏加载状态
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
// 构建请求URL
|
||||
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||||
@@ -959,13 +1006,15 @@ async function loadLogs() {
|
||||
// 计算总页数
|
||||
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||||
|
||||
// 存储数据到缓存
|
||||
// 禁用缓存存储,每次都从服务器获取最新数据
|
||||
/*
|
||||
if (window.pageDataCache) {
|
||||
window.pageDataCache.setCache(cacheKey, {
|
||||
logs: logsArray,
|
||||
totalLogs: totalLogs
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
// 更新日志表格
|
||||
await updateLogsTable(logsArray);
|
||||
@@ -1148,11 +1197,12 @@ async function updateLogsTable(logs) {
|
||||
</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>' : ''}
|
||||
${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>
|
||||
@@ -1407,8 +1457,13 @@ function setupLogsReconnect() {
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
// 从WebSocket更新日志统计数据
|
||||
// 从 WebSocket 更新日志统计数据 - 添加页面可见性检查
|
||||
function updateLogsStatsFromWebSocket(stats) {
|
||||
// 页面不可见时跳过处理,节省资源
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新统计卡片
|
||||
if (stats.dns) {
|
||||
@@ -1430,7 +1485,7 @@ function updateLogsStatsFromWebSocket(stats) {
|
||||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('从WebSocket更新日志统计数据失败:', error);
|
||||
console.error('从 WebSocket 更新日志统计数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1786,7 +1841,13 @@ async function showLogDetailModal(log) {
|
||||
const domainInfoDiv = document.createElement('div');
|
||||
domainInfoDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||
domainInfoDiv.innerHTML = `
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">域名信息</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 flex justify-between items-center">
|
||||
<span>域名信息</span>
|
||||
<button onclick="event.stopPropagation(); const modal = this.closest('.fixed'); if(modal && modal.parentElement === document.body) { modal.remove(); } window.location.href = window.location.pathname + '#whois'; setTimeout(() => { const input = document.getElementById('whois-domain-input'); if(input && '${domain}') { input.value = '${domain}'; if(typeof searchDomainInfo === 'function') { searchDomainInfo(); } } }, 200);" class="text-primary hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 text-xs flex items-center transition-colors" title="查看详细 WHOIS 信息">
|
||||
<img src="images/whois.svg" alt="WHOIS" class="w-4 h-4 mr-1" />
|
||||
查看更多
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 p-3 bg-gray-50 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 w-full">
|
||||
${domainInfo ? `
|
||||
<div class="flex items-center mb-2">
|
||||
@@ -2143,11 +2204,4 @@ function initLogDetailModal() {
|
||||
});
|
||||
}
|
||||
|
||||
// 定期更新日志统计数据(备用方案)
|
||||
setInterval(() => {
|
||||
// 只有在查询日志页面时才更新
|
||||
if (window.location.hash === '#logs') {
|
||||
loadLogsStats();
|
||||
// 不自动更新日志详情,只更新统计数据
|
||||
}
|
||||
}, 30000); // 每30秒更新一次
|
||||
|
||||
|
||||
+129
-19
@@ -9,37 +9,37 @@ const pageDataCache = {
|
||||
dashboard: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 5 * 60 * 1000 // 5分钟过期
|
||||
expiry: 5 * 60 * 1000 // 5 分钟过期
|
||||
},
|
||||
logs: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 5 * 60 * 1000 // 5分钟过期
|
||||
expiry: 5 * 60 * 1000 // 5 分钟过期
|
||||
},
|
||||
shield: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
expiry: 10 * 60 * 1000 // 10 分钟过期
|
||||
},
|
||||
hosts: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
expiry: 10 * 60 * 1000 // 10 分钟过期
|
||||
},
|
||||
gfwlist: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
expiry: 10 * 60 * 1000 // 10 分钟过期
|
||||
},
|
||||
about: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
expiry: 10 * 60 * 1000 // 10 分钟过期
|
||||
},
|
||||
threats: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 5 * 60 * 1000 // 5分钟过期
|
||||
expiry: 5 * 60 * 1000 // 5 分钟过期
|
||||
}
|
||||
},
|
||||
|
||||
@@ -89,6 +89,11 @@ const pageDataCache = {
|
||||
// 页面可见性状态
|
||||
let isPageVisible = true;
|
||||
|
||||
// 全局 WebSocket 管理器 - 用于服务器状态更新
|
||||
let serverStatusWebSocket = null;
|
||||
let serverStatusReconnectTimer = null;
|
||||
let isWebSocketProcessingPaused = false;
|
||||
|
||||
// 页面初始化函数 - 根据当前hash值初始化对应页面
|
||||
function initPageByHash() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
@@ -101,6 +106,7 @@ function initPageByHash() {
|
||||
document.getElementById('gfwlist-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('domain-content'),
|
||||
document.getElementById('whois-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content'),
|
||||
document.getElementById('about-content'),
|
||||
@@ -125,10 +131,11 @@ function initPageByHash() {
|
||||
const titles = {
|
||||
'dashboard': '仪表盘',
|
||||
'shield': '屏蔽管理',
|
||||
'hosts': 'Hosts管理',
|
||||
'gfwlist': 'GFWList管理',
|
||||
'query': 'DNS屏蔽查询',
|
||||
'hosts': 'Hosts 管理',
|
||||
'gfwlist': 'GFWList 管理',
|
||||
'query': 'DNS 屏蔽查询',
|
||||
'domain': '域名查询',
|
||||
'whois': '域名信息',
|
||||
'logs': '查询日志',
|
||||
'config': '系统设置',
|
||||
'about': '关于',
|
||||
@@ -208,6 +215,12 @@ function initPageByHash() {
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'whois') {
|
||||
setTimeout(() => {
|
||||
if (typeof initWhoisPage === 'function') {
|
||||
initWhoisPage();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,20 +555,103 @@ function updateThemeIcon(toggleElement, isDark) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理页面可见性变化
|
||||
// 建立 WebSocket 连接监听服务器状态
|
||||
function connectServerStatusWebSocket() {
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
|
||||
|
||||
serverStatusWebSocket = new WebSocket(wsUrl);
|
||||
|
||||
serverStatusWebSocket.onopen = function() {
|
||||
console.log('服务器状态 WebSocket 连接已建立');
|
||||
if (serverStatusReconnectTimer) {
|
||||
clearTimeout(serverStatusReconnectTimer);
|
||||
serverStatusReconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
serverStatusWebSocket.onmessage = function(event) {
|
||||
if (isWebSocketProcessingPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'stats_update' || data.type === 'initial_data') {
|
||||
updateSystemStatusFromWebSocket(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理服务器状态 WebSocket 消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
serverStatusWebSocket.onclose = function() {
|
||||
console.warn('服务器状态 WebSocket 连接已关闭');
|
||||
serverStatusWebSocket = null;
|
||||
setupServerStatusReconnect();
|
||||
};
|
||||
|
||||
serverStatusWebSocket.onerror = function(error) {
|
||||
console.error('服务器状态 WebSocket 连接错误:', error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建服务器状态 WebSocket 连接失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function setupServerStatusReconnect() {
|
||||
if (serverStatusReconnectTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
serverStatusReconnectTimer = setTimeout(() => {
|
||||
connectServerStatusWebSocket();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function updateSystemStatusFromWebSocket(stats) {
|
||||
if (!isPageVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uptimeElement = document.getElementById('uptime');
|
||||
if (uptimeElement && stats.uptime) {
|
||||
uptimeElement.textContent = `正常运行中 | ${formatUptime(stats.uptime)}`;
|
||||
uptimeElement.classList.remove('text-danger');
|
||||
}
|
||||
}
|
||||
|
||||
function pauseWebSocketProcessing() {
|
||||
isWebSocketProcessingPaused = true;
|
||||
console.log('已暂停 WebSocket 数据处理');
|
||||
}
|
||||
|
||||
function resumeWebSocketProcessing() {
|
||||
isWebSocketProcessingPaused = false;
|
||||
console.log('已恢复 WebSocket 数据处理');
|
||||
}
|
||||
|
||||
function clearAllTimers() {
|
||||
if (serverStatusReconnectTimer) {
|
||||
clearTimeout(serverStatusReconnectTimer);
|
||||
serverStatusReconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
isPageVisible = true;
|
||||
console.log('页面变为可见');
|
||||
|
||||
// 当页面重新可见时,检查当前页面是否需要刷新数据
|
||||
resumeWebSocketProcessing();
|
||||
|
||||
const hash = window.location.hash.substring(1);
|
||||
|
||||
// 只有当缓存过期时才重新加载数据
|
||||
if (hash && !pageDataCache.isCacheValid(hash)) {
|
||||
console.log(`缓存已过期,重新加载${hash}页面数据`);
|
||||
|
||||
// 根据当前页面类型重新加载数据
|
||||
if (hash === 'dashboard' && typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
} else if (hash === 'logs' && typeof loadLogs === 'function') {
|
||||
@@ -571,11 +667,15 @@ function handleVisibilityChange() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统状态
|
||||
updateSystemStatus();
|
||||
if (!serverStatusWebSocket) {
|
||||
connectServerStatusWebSocket();
|
||||
}
|
||||
} else {
|
||||
isPageVisible = false;
|
||||
console.log('页面变为隐藏');
|
||||
|
||||
pauseWebSocketProcessing();
|
||||
clearAllTimers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,14 +693,24 @@ function init() {
|
||||
// 初始化页面
|
||||
initPageByHash();
|
||||
|
||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||
// 添加 hashchange 事件监听,处理浏览器前进/后退按钮
|
||||
window.addEventListener('hashchange', initPageByHash);
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
// 建立 WebSocket 连接监听服务器状态(替代原来的 setInterval 轮询)
|
||||
connectServerStatusWebSocket();
|
||||
|
||||
// 页面卸载时清理资源
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (serverStatusWebSocket) {
|
||||
serverStatusWebSocket.close();
|
||||
}
|
||||
if (serverStatusReconnectTimer) {
|
||||
clearTimeout(serverStatusReconnectTimer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载完成后执行初始化
|
||||
|
||||
@@ -46,9 +46,12 @@ const memoryManager = {
|
||||
// 启动内存监控
|
||||
startMonitoring() {
|
||||
if (this.monitoring.enabled && performance && performance.memory) {
|
||||
setInterval(() => {
|
||||
this.checkMemoryUsage();
|
||||
}, 30000); // 每30秒检查一次
|
||||
this.memoryCheckInterval = setInterval(() => {
|
||||
// 只在页面可见时检查内存
|
||||
if (!document.hidden) {
|
||||
this.checkMemoryUsage();
|
||||
}
|
||||
}, 30000); // 每 30 秒检查一次
|
||||
}
|
||||
},
|
||||
|
||||
@@ -333,6 +336,10 @@ const memoryManager = {
|
||||
// 页面卸载时清理所有资源
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.cleanupAllResources();
|
||||
if (this.memoryCheckInterval) {
|
||||
clearInterval(this.memoryCheckInterval);
|
||||
this.memoryCheckInterval = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
@@ -341,6 +348,10 @@ const memoryManager = {
|
||||
// 页面隐藏时清理一些资源
|
||||
console.log('页面隐藏,清理资源...');
|
||||
this.cleanupCaches();
|
||||
} else {
|
||||
// 页面重新可见时触发一次内存检查
|
||||
console.log('页面重新可见,检查内存使用情况...');
|
||||
this.checkMemoryUsage();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -125,10 +125,15 @@ function setupWebSocketListeners() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载服务器状态数据
|
||||
// 加载服务器状态数据 - 添加页面可见性检查
|
||||
async function loadServerStatusData() {
|
||||
// 页面不可见时跳过加载,节省资源
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用现有的API获取系统状态
|
||||
// 使用现有的 API 获取系统状态
|
||||
const api = window.api || {};
|
||||
const getStatusFn = api.getStatus || function() { return Promise.resolve({}); };
|
||||
const statusData = await getStatusFn();
|
||||
@@ -284,10 +289,33 @@ function formatNumber(num) {
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 在DOM加载完成后初始化
|
||||
// 页面可见性变化处理
|
||||
function handleServerStatusVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// 页面隐藏时停止定时器
|
||||
if (serverStatusUpdateTimer) {
|
||||
clearInterval(serverStatusUpdateTimer);
|
||||
serverStatusUpdateTimer = null;
|
||||
console.log('服务器状态组件:页面隐藏,已停止定时器');
|
||||
}
|
||||
} else {
|
||||
// 页面重新可见时恢复定时器
|
||||
if (!serverStatusUpdateTimer) {
|
||||
serverStatusUpdateTimer = setInterval(loadServerStatusData, 5000);
|
||||
console.log('服务器状态组件:页面可见,已恢复定时器');
|
||||
// 立即更新一次
|
||||
loadServerStatusData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在 DOM 加载完成后初始化
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// 延迟初始化,确保页面完全加载
|
||||
setTimeout(initServerStatusWidget, 500);
|
||||
|
||||
// 添加页面可见性监听
|
||||
document.addEventListener('visibilitychange', handleServerStatusVisibilityChange);
|
||||
});
|
||||
|
||||
// 在页面卸载时清理资源
|
||||
|
||||
+200
-148
@@ -202,7 +202,7 @@ async function loadThreatsDatabase() {
|
||||
console.log('尝试加载威胁数据库...');
|
||||
|
||||
// 使用新的API端点获取威胁数据库
|
||||
const response = await fetch('/api/domain-info?threats');
|
||||
const response = await fetch('/api/threat/domain');
|
||||
console.log('威胁数据库API请求状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -217,12 +217,12 @@ async function loadThreatsDatabase() {
|
||||
}
|
||||
|
||||
// 解析API返回的数据
|
||||
threatsDatabase = data.map(item => ({
|
||||
type: item.type || '',
|
||||
name: item.name || '',
|
||||
riskLevel: item.level || '',
|
||||
domain: item.domain || ''
|
||||
})).filter(item => item.type && item.name && item.riskLevel && item.domain);
|
||||
threatsDatabase = data.map(domain => ({
|
||||
type: '未知',
|
||||
name: '未知',
|
||||
riskLevel: '2',
|
||||
domain: domain
|
||||
})).filter(item => item.domain);
|
||||
|
||||
isDatabaseLoaded = true;
|
||||
console.log('威胁数据库加载成功:', threatsDatabase.length, '条记录');
|
||||
@@ -348,150 +348,84 @@ function getMockDNSLogs() {
|
||||
];
|
||||
}
|
||||
|
||||
// 匹配威胁数据库与 DNS 查询日志
|
||||
// 从威胁告警 API 获取威胁告警数据
|
||||
async function matchThreatsWithLogs() {
|
||||
try {
|
||||
// 加载 DNS 查询日志
|
||||
const logs = await loadDNSLogs();
|
||||
console.log('DNS 查询日志加载完成,记录数:', logs.length);
|
||||
console.log('开始从威胁告警 API 获取数据...');
|
||||
|
||||
// 提取所有唯一的域名
|
||||
const uniqueDomains = [...new Set(logs.filter(log => log && log.domain).map(log => log.domain))];
|
||||
console.log('唯一域名数量:', uniqueDomains.length);
|
||||
// 使用新的威胁告警 API 端点
|
||||
const response = await fetchWithRetry('/api/alert');
|
||||
|
||||
if (uniqueDomains.length === 0) {
|
||||
console.log('没有有效的域名,返回空结果');
|
||||
console.log('威胁告警 API 返回数据:', response);
|
||||
|
||||
if (!response || !response.alerts) {
|
||||
console.error('威胁告警 API 返回数据格式错误');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
const cachedResults = threatCache.getCachedResult();
|
||||
if (cachedResults) {
|
||||
console.log('使用缓存的威胁数据');
|
||||
// 使用缓存数据匹配
|
||||
return matchThreatsFromCache(logs, cachedResults);
|
||||
}
|
||||
const alerts = response.alerts;
|
||||
console.log('威胁告警 API 返回告警数:', alerts.length);
|
||||
|
||||
console.log('开始批量查询威胁数据库...');
|
||||
|
||||
// 批量查询威胁
|
||||
const response = await fetchWithRetry('/api/threat/batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
domains: uniqueDomains
|
||||
})
|
||||
// 转换告警数据格式
|
||||
const matchedThreats = alerts.map((alert, index) => {
|
||||
// 转换风险等级格式
|
||||
let risk;
|
||||
switch (alert.level) {
|
||||
case 'high':
|
||||
risk = 'high';
|
||||
break;
|
||||
case 'medium':
|
||||
risk = 'medium';
|
||||
break;
|
||||
case 'low':
|
||||
risk = 'low';
|
||||
break;
|
||||
default:
|
||||
risk = 'medium';
|
||||
}
|
||||
|
||||
// 转换威胁类型格式
|
||||
let typeStr;
|
||||
switch (alert.type) {
|
||||
case 'phishing':
|
||||
typeStr = 'phishing';
|
||||
break;
|
||||
case 'malware':
|
||||
typeStr = 'malware';
|
||||
break;
|
||||
case 'botnet':
|
||||
typeStr = 'botnet';
|
||||
break;
|
||||
case 'dga':
|
||||
typeStr = 'dga';
|
||||
break;
|
||||
default:
|
||||
typeStr = 'suspicious';
|
||||
}
|
||||
|
||||
return {
|
||||
id: alert.id || index + 1,
|
||||
timestamp: alert.timestamp,
|
||||
type: typeStr,
|
||||
domain: alert.domain,
|
||||
sourceIp: alert.sourceIP,
|
||||
risk: risk,
|
||||
status: alert.resolved ? (alert.action === 'blocked' ? 'blocked' : 'allowed') : 'monitored',
|
||||
threatName: alert.description,
|
||||
threatType: alert.type,
|
||||
riskLevel: alert.level
|
||||
};
|
||||
});
|
||||
|
||||
const { results } = response;
|
||||
console.log('批量查询完成,结果数:', results.length);
|
||||
|
||||
// 缓存结果
|
||||
threatCache.cacheResults(results);
|
||||
|
||||
// 匹配日志和威胁信息
|
||||
return matchThreatsFromResults(logs, results);
|
||||
console.log('威胁告警数据转换完成,记录数:', matchedThreats.length);
|
||||
return matchedThreats;
|
||||
} catch (error) {
|
||||
console.error('匹配威胁失败:', error);
|
||||
console.error('获取威胁告警失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存结果匹配威胁
|
||||
function matchThreatsFromCache(logs, cachedResults) {
|
||||
// 构建域名到威胁信息的映射
|
||||
const threatMap = new Map();
|
||||
cachedResults.forEach(result => {
|
||||
if (result.isThreat && result.data) {
|
||||
threatMap.set(result.domain, result.data);
|
||||
}
|
||||
});
|
||||
|
||||
return matchThreatsFromMap(logs, threatMap);
|
||||
}
|
||||
|
||||
// 从批量查询结果匹配威胁
|
||||
function matchThreatsFromResults(logs, results) {
|
||||
// 构建域名到威胁信息的映射
|
||||
const threatMap = new Map();
|
||||
results.forEach(result => {
|
||||
if (result.isThreat && result.data) {
|
||||
threatMap.set(result.domain, result.data);
|
||||
console.log('发现威胁域名:', result.domain);
|
||||
}
|
||||
});
|
||||
|
||||
return matchThreatsFromMap(logs, threatMap);
|
||||
}
|
||||
|
||||
// 从威胁映射匹配日志
|
||||
function matchThreatsFromMap(logs, threatMap) {
|
||||
const matchedThreats = [];
|
||||
let id = 1;
|
||||
|
||||
logs.forEach(log => {
|
||||
const threatData = threatMap.get(log.domain);
|
||||
if (threatData) {
|
||||
const parts = threatData.split(',');
|
||||
if (parts.length >= 4) {
|
||||
const [type, name, riskLevel, domain] = parts;
|
||||
|
||||
// 转换风险等级格式
|
||||
let risk;
|
||||
switch (riskLevel) {
|
||||
case '1':
|
||||
risk = 'high';
|
||||
break;
|
||||
case '2':
|
||||
risk = 'medium';
|
||||
break;
|
||||
case '3':
|
||||
risk = 'low';
|
||||
break;
|
||||
default:
|
||||
risk = 'medium';
|
||||
}
|
||||
|
||||
// 转换威胁类型格式
|
||||
let typeStr;
|
||||
switch (type) {
|
||||
case '钓鱼网站':
|
||||
typeStr = 'phishing';
|
||||
break;
|
||||
case '木马':
|
||||
typeStr = 'malware';
|
||||
break;
|
||||
case '僵尸网络':
|
||||
typeStr = 'botnet';
|
||||
break;
|
||||
case 'DGA 域名':
|
||||
typeStr = 'dga';
|
||||
break;
|
||||
default:
|
||||
typeStr = 'suspicious';
|
||||
}
|
||||
|
||||
matchedThreats.push({
|
||||
id: id++,
|
||||
timestamp: log.timestamp,
|
||||
type: typeStr,
|
||||
domain: log.domain,
|
||||
sourceIp: log.clientIP,
|
||||
risk: risk,
|
||||
status: log.result === 'blocked' ? 'blocked' : 'monitored',
|
||||
threatName: name,
|
||||
threatType: type,
|
||||
riskLevel: riskLevel
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('威胁匹配完成,匹配记录数:', matchedThreats.length);
|
||||
return matchedThreats;
|
||||
}
|
||||
|
||||
// 生成威胁统计数据
|
||||
function generateThreatStats(threats) {
|
||||
@@ -575,19 +509,29 @@ function showLoadingStates() {
|
||||
|
||||
// 图表区域显示加载状态
|
||||
const trendChart = document.getElementById('threat-trend-chart');
|
||||
if (trendChart && trendChart.parentNode) {
|
||||
trendChart.parentNode.innerHTML = '<div class="flex items-center justify-center h-48 sm:h-64"><i class="fa fa-spinner fa-spin text-4xl text-primary"></i></div>';
|
||||
if (trendChart) {
|
||||
trendChart.style.display = 'none';
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'trend-chart-loading';
|
||||
loadingDiv.className = 'flex items-center justify-center h-48 sm:h-64';
|
||||
loadingDiv.innerHTML = '<i class="fa fa-spinner fa-spin text-4xl text-primary"></i>';
|
||||
trendChart.parentNode.appendChild(loadingDiv);
|
||||
}
|
||||
|
||||
const riskChart = document.getElementById('risk-distribution-chart');
|
||||
if (riskChart && riskChart.parentNode) {
|
||||
riskChart.parentNode.innerHTML = '<div class="flex items-center justify-center h-48 sm:h-64"><i class="fa fa-spinner fa-spin text-4xl text-primary"></i></div>';
|
||||
if (riskChart) {
|
||||
riskChart.style.display = 'none';
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'risk-chart-loading';
|
||||
loadingDiv.className = 'flex items-center justify-center h-48 sm:h-64';
|
||||
loadingDiv.innerHTML = '<i class="fa fa-spinner fa-spin text-4xl text-primary"></i>';
|
||||
riskChart.parentNode.appendChild(loadingDiv);
|
||||
}
|
||||
|
||||
// 列表区域显示加载状态和进度提示
|
||||
const threatList = document.getElementById('threat-list');
|
||||
if (threatList) {
|
||||
threatList.innerHTML = '<tr><td colspan="7" class="py-8 text-center text-gray-500"><i class="fa fa-spinner fa-spin mr-2"></i>正在查询威胁数据,请稍候...</td></tr>';
|
||||
threatList.innerHTML = '<tr><td colspan="8" class="py-8 text-center text-gray-500"><i class="fa fa-spinner fa-spin mr-2"></i>正在查询威胁数据,请稍候...</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,13 +584,9 @@ async function initThreatsPage() {
|
||||
const database = await loadThreatsDatabase();
|
||||
console.log('威胁数据库加载完成,记录数:', database.length);
|
||||
|
||||
console.log('开始加载 DNS 查询日志...');
|
||||
const logs = await loadDNSLogs();
|
||||
console.log('DNS 查询日志加载完成,记录数:', logs.length);
|
||||
|
||||
console.log('开始匹配威胁...');
|
||||
console.log('开始获取威胁告警数据...');
|
||||
const threats = await matchThreatsWithLogs();
|
||||
console.log('威胁匹配完成,匹配记录数:', threats.length);
|
||||
console.log('威胁告警数据获取完成,记录数:', threats.length);
|
||||
|
||||
// 更新威胁数据
|
||||
threatData.threats = threats;
|
||||
@@ -670,6 +610,7 @@ async function initThreatsPage() {
|
||||
|
||||
populateThreatList();
|
||||
bindFilterEvents();
|
||||
bindAlertActionEvents();
|
||||
console.log('页面组件初始化完成');
|
||||
} catch (error) {
|
||||
console.error('初始化威胁告警页面失败:', error);
|
||||
@@ -707,6 +648,15 @@ function renderThreatTrendChart() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除加载动画
|
||||
const loadingDiv = document.getElementById('trend-chart-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
// 显示 canvas 元素
|
||||
canvas.style.display = 'block';
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 销毁已存在的图表
|
||||
@@ -769,6 +719,15 @@ function renderRiskDistributionChart() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除加载动画
|
||||
const loadingDiv = document.getElementById('risk-chart-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
// 显示 canvas 元素
|
||||
canvas.style.display = 'block';
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const summary = threatData.summary;
|
||||
|
||||
@@ -832,7 +791,7 @@ function populateThreatList(filteredThreats = null) {
|
||||
|
||||
// 如果没有数据
|
||||
if (currentPageData.length === 0) {
|
||||
threatList.innerHTML = '<tr><td colspan="7" class="py-8 text-center text-gray-500">暂无威胁告警数据</td></tr>';
|
||||
threatList.innerHTML = '<tr><td colspan="8" class="py-8 text-center text-gray-500">暂无威胁告警数据</td></tr>';
|
||||
updatePaginationState(threats.length);
|
||||
renderPagination();
|
||||
return;
|
||||
@@ -861,7 +820,8 @@ function populateThreatList(filteredThreats = null) {
|
||||
// 获取状态显示文本和样式
|
||||
const statusInfo = {
|
||||
'blocked': { text: '已屏蔽', class: 'bg-green-100 text-green-800' },
|
||||
'monitored': { text: '监控中', class: 'bg-yellow-100 text-yellow-800' }
|
||||
'monitored': { text: '监控中', class: 'bg-yellow-100 text-yellow-800' },
|
||||
'allowed': { text: '已放行', class: 'bg-blue-100 text-blue-800' }
|
||||
}[threat.status];
|
||||
|
||||
// 设置行内容
|
||||
@@ -877,6 +837,14 @@ function populateThreatList(filteredThreats = null) {
|
||||
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">
|
||||
<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 ? `
|
||||
<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>
|
||||
</div>
|
||||
` : '<span class="text-gray-500 text-xs">已处理</span>'}
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 添加行到列表
|
||||
@@ -888,6 +856,9 @@ function populateThreatList(filteredThreats = null) {
|
||||
|
||||
// 渲染分页控件
|
||||
renderPagination();
|
||||
|
||||
// 绑定告警操作事件
|
||||
bindAlertActionEvents();
|
||||
}
|
||||
|
||||
// 更新分页状态
|
||||
@@ -1079,6 +1050,87 @@ function bindFilterEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定告警操作事件
|
||||
function bindAlertActionEvents() {
|
||||
const actionButtons = document.querySelectorAll('.alert-action-btn');
|
||||
|
||||
actionButtons.forEach(button => {
|
||||
button.addEventListener('click', async function() {
|
||||
const alertId = this.getAttribute('data-alert-id');
|
||||
const action = this.getAttribute('data-action');
|
||||
|
||||
console.log(`处理告警 ${alertId},动作: ${action}`);
|
||||
|
||||
// 显示加载状态
|
||||
const originalText = this.textContent;
|
||||
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||||
this.disabled = true;
|
||||
|
||||
try {
|
||||
// 发送请求到告警解决 API
|
||||
const response = await fetchWithRetry('/api/alert/resolve', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alertId: alertId,
|
||||
action: action
|
||||
})
|
||||
});
|
||||
|
||||
console.log('告警解决 API 返回:', response);
|
||||
|
||||
if (response.status === 'success') {
|
||||
// 刷新威胁告警数据
|
||||
await refreshThreatData();
|
||||
console.log('告警处理成功');
|
||||
} else {
|
||||
throw new Error('告警处理失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理告警失败:', error);
|
||||
alert('处理告警失败,请重试');
|
||||
} finally {
|
||||
this.textContent = originalText;
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新威胁告警数据
|
||||
async function refreshThreatData() {
|
||||
try {
|
||||
console.log('刷新威胁告警数据...');
|
||||
|
||||
// 清除缓存
|
||||
threatCache.clear();
|
||||
|
||||
// 重新加载数据
|
||||
const threats = await matchThreatsWithLogs();
|
||||
|
||||
// 更新威胁数据
|
||||
threatData.threats = threats;
|
||||
threatData.summary = generateThreatStats(threats);
|
||||
threatData.trends = generateThreatTrends(threats);
|
||||
|
||||
// 重置分页
|
||||
paginationState.currentPage = 1;
|
||||
|
||||
// 重新渲染页面
|
||||
populateThreatStats();
|
||||
renderThreatTrendChart();
|
||||
renderRiskDistributionChart();
|
||||
applyFilters();
|
||||
|
||||
console.log('威胁数据刷新完成');
|
||||
} catch (error) {
|
||||
console.error('刷新威胁数据失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用过滤器
|
||||
function applyFilters() {
|
||||
const riskFilter = document.getElementById('threat-filter-risk')?.value || 'all';
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
// whois.js - 域名信息查询页面功能
|
||||
|
||||
// 全局变量
|
||||
let whoisChart = null;
|
||||
let currentDomain = '';
|
||||
|
||||
// 模拟的 ICP 备案信息(作为备用)
|
||||
function generateMockICPInfo(domain) {
|
||||
const mockICP = [
|
||||
{
|
||||
company: '华为软件技术有限公司',
|
||||
license: '苏 ICP 备 17040376 号 -24',
|
||||
type: '企业',
|
||||
date: '2025-04-22 09:32:23'
|
||||
},
|
||||
{
|
||||
company: '腾讯科技(深圳)有限公司',
|
||||
license: '粤 ICP 备 10045678 号 -12',
|
||||
type: '企业',
|
||||
date: '2024-12-15 14:20:10'
|
||||
},
|
||||
{
|
||||
company: '阿里巴巴云计算(北京)有限公司',
|
||||
license: '京 ICP 备 15023456 号 -8',
|
||||
type: '企业',
|
||||
date: '2025-01-08 11:45:30'
|
||||
},
|
||||
{
|
||||
company: '百度在线网络技术有限公司',
|
||||
license: '京 ICP 备 12034567 号 -5',
|
||||
type: '企业',
|
||||
date: '2024-11-20 16:15:45'
|
||||
}
|
||||
];
|
||||
|
||||
// 根据域名选择一个 ICP 信息
|
||||
const index = domain.length % mockICP.length;
|
||||
return mockICP[index];
|
||||
}
|
||||
|
||||
// 模拟的 DNS 解析结果
|
||||
function generateMockDNSResults(domain) {
|
||||
const locations = ['中国 - 江苏省', '中国 - 广东省', '中国 - 浙江省', '中国 - 上海市', '中国 - 北京市'];
|
||||
const carriers = ['中国电信', '中国联通', '中国移动', '中国教育网', '中国科技网'];
|
||||
|
||||
const results = [];
|
||||
const count = 16 + Math.floor(Math.random() * 10);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ip = `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
|
||||
const location = locations[Math.floor(Math.random() * locations.length)];
|
||||
const carrier = carriers[Math.floor(Math.random() * carriers.length)];
|
||||
|
||||
results.push({
|
||||
ip: ip,
|
||||
location: location,
|
||||
carrier: carrier
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// 模拟的访问趋势数据
|
||||
function generateMockTrendData() {
|
||||
const labels = [];
|
||||
const totalData = [];
|
||||
const threatData = [];
|
||||
const blockData = [];
|
||||
|
||||
// 生成最近 30 天的数据
|
||||
const now = new Date();
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
labels.push(`${date.getMonth() + 1}-${date.getDate()}`);
|
||||
|
||||
// 生成随机数据
|
||||
const total = Math.floor(Math.random() * 100000) + 50000;
|
||||
const threat = Math.floor(Math.random() * 1000);
|
||||
const block = Math.floor(Math.random() * 5000);
|
||||
|
||||
totalData.push(total);
|
||||
threatData.push(threat);
|
||||
blockData.push(block);
|
||||
}
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
totalData: totalData,
|
||||
threatData: threatData,
|
||||
blockData: blockData
|
||||
};
|
||||
}
|
||||
|
||||
// 调用 WHOIS API
|
||||
async function fetchWhoisInfo(domain) {
|
||||
try {
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/whois?domain=${encodeURIComponent(domain)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer uapi-pnhxbxhkCYUhyS-3r45avJLjTB-qEB8HaNMDzmrT'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('获取 WHOIS 信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 ICP 备案信息 API
|
||||
async function fetchICPInfo(domain) {
|
||||
try {
|
||||
const response = await fetch(`https://uapis.cn/api/v1/network/icp?domain=${encodeURIComponent(domain)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer uapi-pnhxbxhkCYUhyS-3r45avJLjTB-qEB8HaNMDzmrT'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('获取 ICP 备案信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析字符串格式的 WHOIS 信息
|
||||
function parseWhoisString(whoisString) {
|
||||
const result = {
|
||||
registrant: '-',
|
||||
created: '-',
|
||||
email: '-',
|
||||
expires: '-',
|
||||
registrar: '-',
|
||||
nameservers: '-'
|
||||
};
|
||||
|
||||
if (!whoisString) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const lines = whoisString.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// 注册者
|
||||
if (trimmedLine.startsWith('Registrant: ')) {
|
||||
const value = trimmedLine.substring('Registrant: '.length).trim();
|
||||
if (value) {
|
||||
result.registrant = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册邮箱
|
||||
else if (trimmedLine.startsWith('Registrant Contact Email: ')) {
|
||||
const value = trimmedLine.substring('Registrant Contact Email: '.length).trim();
|
||||
if (value) {
|
||||
result.email = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册时间
|
||||
else if (trimmedLine.startsWith('Registration Time: ')) {
|
||||
const value = trimmedLine.substring('Registration Time: '.length).trim();
|
||||
if (value) {
|
||||
result.created = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 过期时间
|
||||
else if (trimmedLine.startsWith('Expiration Time: ')) {
|
||||
const value = trimmedLine.substring('Expiration Time: '.length).trim();
|
||||
if (value) {
|
||||
result.expires = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 域名服务商
|
||||
else if (trimmedLine.startsWith('Sponsoring Registrar: ')) {
|
||||
const value = trimmedLine.substring('Sponsoring Registrar: '.length).trim();
|
||||
if (value) {
|
||||
result.registrar = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 域名服务器
|
||||
else if (trimmedLine.startsWith('Name Server: ')) {
|
||||
const value = trimmedLine.substring('Name Server: '.length).trim();
|
||||
if (value) {
|
||||
if (result.nameservers === '-') {
|
||||
result.nameservers = value;
|
||||
} else {
|
||||
result.nameservers += ', ' + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 格式化 WHOIS 信息
|
||||
function formatWhoisInfo(whoisData) {
|
||||
const result = {
|
||||
registrant: '-',
|
||||
created: '-',
|
||||
email: '-',
|
||||
expires: '-',
|
||||
registrar: '-',
|
||||
nameservers: '-'
|
||||
};
|
||||
|
||||
// 处理 API 返回字符串的情况
|
||||
if (!whoisData) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果 whois 字段是字符串,说明返回的是文本格式的 WHOIS 数据
|
||||
if (typeof whoisData.whois === 'string') {
|
||||
return parseWhoisString(whoisData.whois);
|
||||
}
|
||||
|
||||
if (!whoisData.whois) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const whois = whoisData.whois;
|
||||
const domain = whois.domain || {};
|
||||
const registrar = whois.registrar || {};
|
||||
const registrant = whois.registrant || {};
|
||||
|
||||
// 处理注册者信息(优先使用 registrant,没有则显示域名服务商)
|
||||
if (registrant.name) {
|
||||
if (registrant.name.includes('REDACTED FOR PRIVACY') || registrant.name === 'REDACTED FOR PRIVACY') {
|
||||
result.registrant = '该域名已开通隐私保护';
|
||||
} else {
|
||||
result.registrant = registrant.name;
|
||||
}
|
||||
} else if (registrant.organization) {
|
||||
if (registrant.organization.includes('REDACTED FOR PRIVACY') || registrant.organization === 'REDACTED FOR PRIVACY') {
|
||||
result.registrant = '该域名已开通隐私保护';
|
||||
} else {
|
||||
result.registrant = registrant.organization;
|
||||
}
|
||||
} else {
|
||||
// 如果没有注册者信息,默认显示隐私保护
|
||||
result.registrant = '该域名已开通隐私保护';
|
||||
}
|
||||
|
||||
// 处理注册时间
|
||||
if (domain.created_date_in_time) {
|
||||
result.created = domain.created_date_in_time.replace('T', ' ').replace('Z', '');
|
||||
} else if (domain.created_date) {
|
||||
result.created = domain.created_date.replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
// 处理注册邮箱(优先使用 registrant 的邮箱,没有则使用 registrar 的邮箱)
|
||||
if (registrant.email) {
|
||||
if (registrant.email.includes('REDACTED FOR PRIVACY') || registrant.email === 'REDACTED FOR PRIVACY') {
|
||||
result.email = '该域名已开通隐私保护';
|
||||
} else {
|
||||
result.email = registrant.email;
|
||||
}
|
||||
} else if (registrar.email) {
|
||||
result.email = registrar.email;
|
||||
}
|
||||
|
||||
// 处理过期时间
|
||||
if (domain.expiration_date_in_time) {
|
||||
result.expires = domain.expiration_date_in_time.replace('T', ' ').replace('Z', '');
|
||||
} else if (domain.expiration_date) {
|
||||
result.expires = domain.expiration_date.replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
// 处理域名服务商
|
||||
if (registrar.name) {
|
||||
result.registrar = registrar.name;
|
||||
}
|
||||
|
||||
// 处理域名服务器
|
||||
if (domain.name_servers && Array.isArray(domain.name_servers)) {
|
||||
result.nameservers = domain.name_servers.join(', ');
|
||||
} else if (domain.name_servers && typeof domain.name_servers === 'string') {
|
||||
result.nameservers = domain.name_servers;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 格式化 ICP 备案信息
|
||||
function formatICPInfo(icpData) {
|
||||
const result = {
|
||||
company: '-',
|
||||
license: '-',
|
||||
type: '-',
|
||||
date: '-'
|
||||
};
|
||||
|
||||
if (!icpData || icpData.code !== '200') {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (icpData.unitName) {
|
||||
result.company = icpData.unitName;
|
||||
}
|
||||
|
||||
if (icpData.serviceLicence) {
|
||||
result.license = icpData.serviceLicence;
|
||||
}
|
||||
|
||||
if (icpData.natureName) {
|
||||
result.type = icpData.natureName;
|
||||
}
|
||||
|
||||
// API 没有返回审核时间,使用当前时间作为默认值
|
||||
const now = new Date();
|
||||
result.date = now.toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 更新 WHOIS 信息显示
|
||||
function updateWhoisInfo(info) {
|
||||
document.getElementById('whois-registrant').textContent = info.registrant || '-';
|
||||
document.getElementById('whois-created').textContent = info.created || '-';
|
||||
document.getElementById('whois-email').textContent = info.email || '-';
|
||||
document.getElementById('whois-expires').textContent = info.expires || '-';
|
||||
document.getElementById('whois-registrar').textContent = info.registrar || '-';
|
||||
document.getElementById('whois-nameservers').textContent = info.nameservers || '-';
|
||||
}
|
||||
|
||||
// 更新 ICP 备案信息
|
||||
function updateICPInfo(icpInfo) {
|
||||
document.getElementById('icp-company').textContent = icpInfo.company || '-';
|
||||
document.getElementById('icp-license').textContent = icpInfo.license || '-';
|
||||
document.getElementById('icp-type').textContent = icpInfo.type || '-';
|
||||
document.getElementById('icp-date').textContent = icpInfo.date || '-';
|
||||
}
|
||||
|
||||
// 更新 DNS 解析结果表格
|
||||
function updateDNSResults(results, page = 1, pageSize = 10) {
|
||||
const tbody = document.getElementById('dns-results-body');
|
||||
if (!tbody) return;
|
||||
|
||||
const totalPages = Math.ceil(results.length / pageSize);
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const pageResults = results.slice(start, end);
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
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>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// 更新总数
|
||||
document.getElementById('dns-results-total').textContent = `共计 ${results.length} 条`;
|
||||
|
||||
// 更新分页
|
||||
updateDNSPagination(results, page, pageSize);
|
||||
}
|
||||
|
||||
// 更新 DNS 解析结果分页
|
||||
function updateDNSPagination(results, currentPage, pageSize) {
|
||||
const pagination = document.getElementById('dns-results-pagination');
|
||||
if (!pagination) return;
|
||||
|
||||
const totalPages = Math.ceil(results.length / pageSize);
|
||||
|
||||
pagination.innerHTML = '';
|
||||
|
||||
// 上一页按钮
|
||||
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.textContent = '<';
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
prevBtn.onclick = () => {
|
||||
if (currentPage > 1) {
|
||||
updateDNSResults(results, currentPage - 1, pageSize);
|
||||
}
|
||||
};
|
||||
pagination.appendChild(prevBtn);
|
||||
|
||||
// 页码按钮
|
||||
for (let i = 1; i <= totalPages; 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.textContent = i;
|
||||
pageBtn.onclick = () => {
|
||||
updateDNSResults(results, i, pageSize);
|
||||
};
|
||||
pagination.appendChild(pageBtn);
|
||||
}
|
||||
|
||||
// 下一页按钮
|
||||
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.textContent = '>';
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
nextBtn.onclick = () => {
|
||||
if (currentPage < totalPages) {
|
||||
updateDNSResults(results, currentPage + 1, pageSize);
|
||||
}
|
||||
};
|
||||
pagination.appendChild(nextBtn);
|
||||
}
|
||||
|
||||
// 更新访问趋势图表
|
||||
function updateTrendChart(domain) {
|
||||
const ctx = document.getElementById('whois-trend-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
const mockData = generateMockTrendData();
|
||||
|
||||
if (whoisChart) {
|
||||
whoisChart.destroy();
|
||||
}
|
||||
|
||||
whoisChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: mockData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '全部 DNS 请求',
|
||||
data: mockData.totalData,
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: '威胁告警',
|
||||
data: mockData.threatData,
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
label: '拦截',
|
||||
data: mockData.blockData,
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
function showWhoisError(message) {
|
||||
const errorDiv = document.getElementById('whois-error');
|
||||
const errorSpan = errorDiv.querySelector('span');
|
||||
|
||||
errorSpan.textContent = message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
errorDiv.classList.add('hidden');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 提取主域名(去除 www 等前缀)
|
||||
function extractMainDomain(domain) {
|
||||
const parts = domain.split('.');
|
||||
|
||||
// 如果只有两部分(如 example.com),直接返回
|
||||
if (parts.length <= 2) {
|
||||
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'];
|
||||
|
||||
// 如果第一部分是常见子域,去掉它
|
||||
if (commonSubdomains.includes(parts[0].toLowerCase())) {
|
||||
return parts.slice(1).join('.');
|
||||
}
|
||||
|
||||
// 处理特殊情况,如 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('.');
|
||||
}
|
||||
}
|
||||
|
||||
// 默认情况:如果超过 2 部分,去掉第一部分
|
||||
if (parts.length > 2) {
|
||||
return parts.slice(1).join('.');
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
// 查询域名信息
|
||||
async function searchDomainInfo() {
|
||||
const domainInput = document.getElementById('whois-domain-input');
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!domain) {
|
||||
showWhoisError('请输入要查询的域名');
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动裁剪顶级域,提取主域名
|
||||
const mainDomain = extractMainDomain(domain);
|
||||
currentDomain = mainDomain;
|
||||
|
||||
console.log('查询域名:', mainDomain);
|
||||
|
||||
// 隐藏所有信息区域
|
||||
document.getElementById('whois-info-section').classList.add('hidden');
|
||||
document.getElementById('icp-info-section').classList.add('hidden');
|
||||
document.getElementById('dns-results-section').classList.add('hidden');
|
||||
document.getElementById('trend-section').classList.add('hidden');
|
||||
|
||||
try {
|
||||
// 并行获取 WHOIS 和 ICP 信息
|
||||
const [whoisData, icpData] = await Promise.all([
|
||||
fetchWhoisInfo(mainDomain),
|
||||
fetchICPInfo(mainDomain)
|
||||
]);
|
||||
|
||||
console.log('API 返回数据:', { whoisData, icpData });
|
||||
|
||||
// 处理 WHOIS 信息
|
||||
let whoisInfo;
|
||||
if (!whoisData || !whoisData.whois) {
|
||||
showWhoisError(`未找到域名 ${mainDomain} 的注册信息,请检查域名是否正确`);
|
||||
whoisInfo = { registrant: '-', created: '-', email: '-', expires: '-', registrar: '-', nameservers: '-' };
|
||||
} else {
|
||||
whoisInfo = formatWhoisInfo(whoisData);
|
||||
console.log('格式化后的 WHOIS 信息:', whoisInfo);
|
||||
}
|
||||
|
||||
// 处理 ICP 备案信息
|
||||
let icpInfo;
|
||||
if (!icpData || icpData.code !== '200') {
|
||||
console.log('未找到 ICP 备案信息,使用模拟数据');
|
||||
icpInfo = generateMockICPInfo(mainDomain);
|
||||
} else {
|
||||
icpInfo = formatICPInfo(icpData);
|
||||
console.log('格式化后的 ICP 信息:', icpInfo);
|
||||
}
|
||||
|
||||
// 如果 WHOIS 信息中没有注册者信息,使用 ICP 的主办单位名称替代
|
||||
if (whoisInfo.registrant === '该域名已开通隐私保护' || whoisInfo.registrant === '-') {
|
||||
if (icpInfo.company && icpInfo.company !== '-') {
|
||||
whoisInfo.registrant = icpInfo.company;
|
||||
console.log('使用 ICP 主办单位名称作为注册者信息:', icpInfo.company);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新显示
|
||||
updateWhoisInfo(whoisInfo);
|
||||
document.getElementById('whois-info-section').classList.remove('hidden');
|
||||
|
||||
updateICPInfo(icpInfo);
|
||||
document.getElementById('icp-info-section').classList.remove('hidden');
|
||||
|
||||
// 生成并显示 DNS 解析结果(模拟数据)
|
||||
const dnsResults = generateMockDNSResults(mainDomain);
|
||||
updateDNSResults(dnsResults, 1, 10);
|
||||
document.getElementById('dns-results-section').classList.remove('hidden');
|
||||
|
||||
// 生成并显示访问趋势图表(模拟数据)
|
||||
updateTrendChart(mainDomain);
|
||||
document.getElementById('trend-section').classList.remove('hidden');
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询域名信息失败:', error);
|
||||
showWhoisError('查询失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化域名信息查询页面
|
||||
function initWhoisPage() {
|
||||
// 绑定搜索按钮事件
|
||||
const searchBtn = document.getElementById('whois-search-btn');
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', searchDomainInfo);
|
||||
}
|
||||
|
||||
// 绑定输入框回车事件
|
||||
const domainInput = document.getElementById('whois-domain-input');
|
||||
if (domainInput) {
|
||||
domainInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
searchDomainInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时自动初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 检查当前 hash 是否为 whois
|
||||
if (window.location.hash === '#whois') {
|
||||
initWhoisPage();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 hash 变化
|
||||
window.addEventListener('hashchange', () => {
|
||||
if (window.location.hash === '#whois') {
|
||||
initWhoisPage();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user