document.addEventListener('DOMContentLoaded', function() {
const scanForm = document.getElementById('scanForm');
const scanButton = document.getElementById('scanButton');
const clearButton = document.getElementById('clearButton');
const loading = document.getElementById('loading');
const results = document.getElementById('results');
const error = document.getElementById('error');
const resultsContent = document.getElementById('resultsContent');
const errorMessage = document.getElementById('errorMessage');
const scanWarning = document.getElementById('scanWarning');
const portsInput = document.getElementById('ports');
const scanAllPortsCheckbox = document.getElementById('scanAllPorts');
const target = document.getElementById('target');
const optionCheckboxes = document.querySelectorAll('input[name="options"]');
const tasksGrid = document.getElementById('tasksGrid');
const threadsInUse = document.getElementById('threadsInUse');
const parallelTasksSlider = document.getElementById('parallelTasksSlider');
const parallelTasks = document.getElementById('parallelTasks');
const threadModes = document.querySelectorAll('.thread-mode');
const DEFAULT_THREADS = 8;
const MIN_THREADS = 4;
const MAX_THREADS = 16;
const TASK_STATUS = {
'pending': '等待中',
'task_running': '扫描中',
'task_progress': '扫描中',
'task_completed': '已完成',
'task_error': '出错'
};
let socket;
let currentScanId = null;
let reconnectAttempts = 0;
let isConnected = false;
const maxReconnectAttempts = 5;
let activeTasks = {};
let totalTasks = 0;
let completedTasks = 0;
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
progressContainer.innerHTML = `
`;
loading.insertBefore(progressContainer, document.querySelector('#tasksOverview'));
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const completedTasksCount = document.getElementById('completedTasksCount');
const totalTasksCount = document.getElementById('totalTasksCount');
const liveOutputContainer = document.createElement('div');
liveOutputContainer.className = 'live-output';
liveOutputContainer.innerHTML = `
实时扫描输出
`;
loading.appendChild(liveOutputContainer);
const liveOutput = document.getElementById('liveOutput');
const cancelButton = document.createElement('button');
cancelButton.type = 'button';
cancelButton.id = 'cancelScanButton';
cancelButton.className = 'btn cancel-btn';
cancelButton.innerHTML = ' 取消扫描';
cancelButton.style.display = 'none';
loading.appendChild(cancelButton);
const formElements = document.querySelectorAll('.form-group, .form-actions');
formElements.forEach((element, index) => {
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
element.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
setTimeout(() => {
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
}, 100 + index * 100);
});
function initThreadControl() {
parallelTasksSlider.addEventListener('input', function() {
parallelTasks.value = this.value;
updateThreadModeSelection(this.value);
threadsInUse.textContent = this.value;
});
parallelTasks.addEventListener('input', function() {
let value = parseInt(this.value);
if (isNaN(value) || value < MIN_THREADS) value = MIN_THREADS;
if (value > MAX_THREADS) value = MAX_THREADS;
this.value = value;
parallelTasksSlider.value = value;
updateThreadModeSelection(value);
threadsInUse.textContent = value;
});
threadModes.forEach(mode => {
mode.addEventListener('click', function() {
const value = parseInt(this.dataset.value);
parallelTasksSlider.value = value;
parallelTasks.value = value;
updateThreadModeSelection(value);
threadsInUse.textContent = value;
});
});
updateThreadModeSelection(DEFAULT_THREADS);
}
function updateThreadModeSelection(value) {
threadModes.forEach(mode => {
if (parseInt(mode.dataset.value) === parseInt(value)) {
mode.classList.add('active');
} else {
mode.classList.remove('active');
}
});
}
function initWebSocket() {
if (socket && socket.connected) {
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
console.log(`使用WebSocket协议: ${protocol}`);
const host = window.location.host;
const wsUrl = `${protocol}//${host}`;
console.log(`WebSocket连接URL: ${wsUrl}`);
socket = io(wsUrl, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: maxReconnectAttempts,
transports: ['websocket'],
upgrade: false
});
socket.on('connect', function() {
console.log('WebSocket连接已建立');
isConnected = true;
reconnectAttempts = 0;
updateSocketStatus(true, protocol === 'wss:');
if (currentScanId) {
joinScanRoom(currentScanId);
}
});
socket.on('connection_response', function(data) {
console.log('连接状态:', data.status);
console.log('传输方式:', data.transport);
console.log('会话ID:', data.sid);
const isWebSocket = data.transport === 'websocket';
const isSecure = window.location.protocol === 'https:';
updateSocketStatus(true, isWebSocket && isSecure);
showInfo(`已连接到服务器`);
});
socket.on('scan_started', function(data) {
console.log('扫描已开始,ID:', data.scan_id, '线程数:', data.parallel_tasks);
currentScanId = data.scan_id;
threadsInUse.textContent = data.parallel_tasks;
sessionStorage.setItem('currentScanId', currentScanId);
});
socket.on('scan_update', function(data) {
console.log('扫描更新:', data.status, data);
switch(data.status) {
case 'starting':
updateProgress(5);
updateLiveOutput(`正在使用 ${data.threads} 个线程开始扫描...\n`);
break;
case 'tasks_created':
activeTasks = {};
totalTasks = data.tasks.length;
completedTasks = 0;
totalTasksCount.textContent = totalTasks;
completedTasksCount.textContent = completedTasks;
createTaskCards(data.tasks);
updateLiveOutput(`已创建 ${totalTasks} 个扫描子任务\n`);
break;
case 'task_running':
updateTaskStatus(data.task_id, 'running');
updateLiveOutput(`${data.message}\n`);
break;
case 'task_progress':
if (data.partial_result) {
updateLiveOutput(data.partial_result);
}
increaseProgress();
break;
case 'task_completed':
updateTaskStatus(data.task_id, 'completed');
completedTasks++;
completedTasksCount.textContent = completedTasks;
const percentComplete = Math.round((completedTasks / totalTasks) * 100);
updateProgress(Math.min(percentComplete, 99));
break;
case 'task_error':
updateTaskStatus(data.task_id, 'error');
updateLiveOutput(`错误: ${data.message}\n`);
break;
case 'completed':
hideLoading();
showResults(data.result);
resetScanButton();
currentScanId = null;
sessionStorage.removeItem('currentScanId');
break;
case 'error':
hideLoading();
showError(data.message || data.error);
resetScanButton();
currentScanId = null;
sessionStorage.removeItem('currentScanId');
break;
case 'cancelled':
hideLoading();
showInfo('扫描已取消');
resetScanButton();
currentScanId = null;
sessionStorage.removeItem('currentScanId');
break;
}
});
socket.on('disconnect', function() {
console.log('WebSocket连接已断开');
isConnected = false;
updateSocketStatus(false, false);
if (reconnectAttempts < maxReconnectAttempts) {
console.log(`尝试重新连接 (${reconnectAttempts + 1}/${maxReconnectAttempts})...`);
reconnectAttempts++;
} else if (currentScanId) {
hideLoading();
showError('与服务器的连接已丢失,无法接收实时扫描更新。请检查您的网络连接并重试。');
resetScanButton();
}
});
}
function updateSocketStatus(connected, isSecure) {
const statusIndicator = document.getElementById('socketStatus');
if (connected) {
statusIndicator.classList.add('connected');
statusIndicator.title = isSecure ?
'已建立安全WebSocket连接 (WSS)' :
'已建立WebSocket连接 (WS)';
if (isSecure) {
statusIndicator.classList.add('secure');
} else {
statusIndicator.classList.remove('secure');
}
} else {
statusIndicator.classList.remove('connected', 'secure');
statusIndicator.title = '未连接到服务器';
}
}
function createTaskCards(tasks) {
tasksGrid.innerHTML = '';
tasks.forEach(task => {
const taskCard = document.createElement('div');
taskCard.className = 'task-item';
taskCard.id = `task-${task.task_id}`;
taskCard.innerHTML = `
${task.task_id}
${task.target}
${task.ports}
`;
tasksGrid.appendChild(taskCard);
// 记录任务状态
activeTasks[task.task_id] = {
status: 'pending',
element: taskCard
};
});
}
// 更新任务状态
function updateTaskStatus(taskId, status) {
if (activeTasks[taskId]) {
activeTasks[taskId].status = status;
const taskCard = document.getElementById(`task-${taskId}`);
if (taskCard) {
const statusElement = taskCard.querySelector('.task-status');
// 移除所有状态类
statusElement.classList.remove('pending', 'running', 'completed', 'error');
// 添加新状态类
statusElement.classList.add(status);
// 更新提示文本
statusElement.title = TASK_STATUS[status] || status;
}
}
}
// 加入扫描房间
function joinScanRoom(scanId) {
if (socket && socket.connected) {
socket.emit('join_scan', { scan_id: scanId });
}
}
// 端口输入与全端口扫描选项互斥
scanAllPortsCheckbox.addEventListener('change', function() {
if (this.checked) {
portsInput.disabled = true;
portsInput.placeholder = "已选择全端口扫描";
portsInput.parentElement.classList.add('disabled');
} else {
portsInput.disabled = false;
portsInput.placeholder = "例如: 80,443 或 1-1000";
portsInput.parentElement.classList.remove('disabled');
}
});
// 为选项添加动画效果
const scanTypeRadios = document.querySelectorAll('input[name="scan_type"]');
const scanSpeedRadios = document.querySelectorAll('input[name="scan_speed"]');
function addRadioAnimations(radioButtons) {
// 清除所有选中样式的函数
function clearSelectionStyles(name) {
document.querySelectorAll(`input[name="${name}"]`).forEach(radio => {
const item = radio.closest('.option-item');
if (item) {
item.classList.remove('option-selected');
}
});
}
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
// 清除同组中所有单选按钮的选中样式
clearSelectionStyles(this.name);
if (this.checked) {
const optionItem = this.closest('.option-item');
// 添加选中样式
optionItem.classList.add('option-selected');
// 添加一个简单的选中动画
const ripple = document.createElement('span');
ripple.classList.add('option-ripple');
optionItem.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 500);
}
});
// 初始化选中状态
if (radio.checked) {
radio.closest('.option-item').classList.add('option-selected');
}
});
}
// 初始化单选按钮动画
addRadioAnimations(scanTypeRadios);
addRadioAnimations(scanSpeedRadios);
// 表单验证
target.addEventListener('input', validateForm);
portsInput.addEventListener('input', validateForm);
scanAllPortsCheckbox.addEventListener('change', validateForm);
function validateForm() {
const targetValue = target.value.trim();
const isValid = targetValue.length > 0;
if (isValid) {
scanButton.disabled = false;
target.classList.remove('input-error');
} else {
scanButton.disabled = true;
if (targetValue === '' && target.classList.contains('was-validated')) {
target.classList.add('input-error');
}
}
}
target.addEventListener('blur', function() {
this.classList.add('was-validated');
validateForm();
});
// 表单提交处理
scanForm.addEventListener('submit', function(e) {
e.preventDefault();
// 检查WebSocket连接
if (!socket || !socket.connected) {
showError('未能连接到扫描服务器,请刷新页面重试');
return;
}
// 添加表单提交动画
scanButton.innerHTML = ' 扫描中...';
// 收集表单数据
const targetValue = target.value.trim();
const ports = portsInput.value.trim();
const scanAllPorts = scanAllPortsCheckbox.checked;
const threadCount = parseInt(parallelTasks.value) || DEFAULT_THREADS;
// 获取选中的扫描类型和速度
const selectedScanType = document.querySelector('input[name="scan_type"]:checked')?.value || "-sS";
const selectedScanSpeed = document.querySelector('input[name="scan_speed"]:checked')?.value || "-T3";
const selectedOptions = [selectedScanType, selectedScanSpeed];
// 验证目标
if (!targetValue) {
showError('请输入有效的目标地址');
resetScanButton();
return;
}
// 显示加载状态与取消按钮
showLoading(scanAllPorts);
cancelButton.style.display = 'inline-block';
// 重置进度和状态
updateProgress(0);
completedTasks = 0;
totalTasks = 0;
completedTasksCount.textContent = "0";
totalTasksCount.textContent = "0";
tasksGrid.innerHTML = '';
liveOutput.textContent = '等待扫描开始...\n';
// 准备请求数据
const requestData = {
target: targetValue,
ports: ports,
options: selectedOptions,
scan_all_ports: scanAllPorts,
parallel_tasks: threadCount
};
// 记住最近扫描的目标 (本地存储)
saveRecentScan(targetValue);
// 使用WebSocket发送扫描请求
socket.emit('start_scan', requestData);
});
// 更新进度条
function updateProgress(percentage) {
progressFill.style.width = `${percentage}%`;
progressText.textContent = `${percentage}%`;
}
// 缓慢增加进度,模拟扫描进展
function increaseProgress() {
if (totalTasks === 0) return; // 防止除以零
const currentCompleted = completedTasks;
const taskPercentage = (currentCompleted / totalTasks) * 100;
// 在任务完成百分比和当前进度条之间找一个中间值
const currentWidth = parseFloat(progressFill.style.width) || 0;
// 如果实际完成的任务百分比大于进度条,直接更新到任务百分比
if (taskPercentage > currentWidth) {
updateProgress(Math.round(taskPercentage));
return;
}
// 否则微微增加当前进度
let increment;
if (currentWidth < 30) {
increment = 0.5;
} else if (currentWidth < 60) {
increment = 0.3;
} else if (currentWidth < 80) {
increment = 0.2;
} else if (currentWidth < 90) {
increment = 0.1;
} else {
increment = 0.05;
}
// 不要超过99%,留给最终完成状态
const newWidth = Math.min(99, currentWidth + increment);
updateProgress(Math.round(newWidth));
}
// 更新实时输出
function updateLiveOutput(text) {
// 将新文本添加到当前内容
liveOutput.textContent += text;
// 自动滚动到底部
liveOutput.scrollTop = liveOutput.scrollHeight;
}
function resetScanButton() {
scanButton.innerHTML = ' 开始扫描';
cancelButton.style.display = 'none';
}
// 取消扫描
cancelButton.addEventListener('click', function() {
if (currentScanId && socket && socket.connected) {
socket.emit('cancel_scan', { scan_id: currentScanId });
}
});
// 保存最近扫描的目标
function saveRecentScan(targetValue) {
let recentScans = JSON.parse(localStorage.getItem('recentScans') || '[]');
// 避免重复
recentScans = recentScans.filter(scan => scan !== targetValue);
recentScans.unshift(targetValue); // 添加到开头
// 最多保存5个
recentScans = recentScans.slice(0, 5);
localStorage.setItem('recentScans', JSON.stringify(recentScans));
}
// 清除结果按钮
clearButton.addEventListener('click', function() {
hideResults();
hideError();
// 添加清除动画效果
this.classList.add('btn-active');
setTimeout(() => {
this.classList.remove('btn-active');
}, 300);
});
// 展示信息提示
function showInfo(message) {
// 创建一个临时消息提示
const infoToast = document.createElement('div');
infoToast.className = 'info-toast';
infoToast.innerHTML = ` ${message}`;
document.body.appendChild(infoToast);
// 显示动画
setTimeout(() => {
infoToast.classList.add('show');
}, 10);
// 自动消失
setTimeout(() => {
infoToast.classList.remove('show');
setTimeout(() => {
document.body.removeChild(infoToast);
}, 300);
}, 3000);
}
// 展示加载状态
function showLoading(isAllPorts) {
hideResults();
hideError();
loading.style.display = 'block';
scanButton.disabled = true;
// 淡入动画
loading.style.opacity = 0;
setTimeout(() => {
loading.style.opacity = 1;
}, 10);
// 如果是全端口扫描,显示额外警告
if (isAllPorts) {
scanWarning.style.display = 'block';
} else {
scanWarning.style.display = 'none';
}
}
// 隐藏加载状态
function hideLoading() {
loading.style.opacity = 0;
setTimeout(() => {
loading.style.display = 'none';
}, 300);
scanButton.disabled = false;
}
// 展示结果
function showResults(resultText) {
results.style.display = 'block';
resultsContent.textContent = resultText;
// 淡入动画
results.style.opacity = 0;
setTimeout(() => {
results.style.opacity = 1;
}, 10);
// 为结果添加语法高亮
highlightScanResults();
// 滚动到结果区域
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// 隐藏结果
function hideResults() {
if (results.style.display !== 'none') {
results.style.opacity = 0;
setTimeout(() => {
results.style.display = 'none';
}, 300);
}
}
// 展示错误
function showError(message) {
error.style.display = 'block';
errorMessage.textContent = message;
// 淡入动画
error.style.opacity = 0;
setTimeout(() => {
error.style.opacity = 1;
}, 10);
// 滚动到错误区域
error.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// 隐藏错误
function hideError() {
if (error.style.display !== 'none') {
error.style.opacity = 0;
setTimeout(() => {
error.style.display = 'none';
}, 300);
}
}
// 简单的结果高亮
function highlightScanResults() {
const content = resultsContent.textContent;
let highlighted = content;
// 高亮扫描头部
highlighted = highlighted.replace(/(Starting Nmap.*?)(?=\n)/g,
'');
// 高亮端口状态表头
highlighted = highlighted.replace(/(PORT\s+STATE\s+SERVICE)/g,
'');
// 高亮目标标记
highlighted = highlighted.replace(/(目标: .*?)(?=\n)/g,
'$1');
// 高亮端口状态
highlighted = highlighted.replace(/(\d+\/\w+)\s+(open|closed|filtered)\s+(.*?)(?=\n|$)/g, function(match, port, state, service) {
let stateClass = 'hl-state-' + state;
return `${port} ${state} ${service}`;
});
// 高亮IP地址
highlighted = highlighted.replace(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g,
'$1');
// 高亮域名
highlighted = highlighted.replace(/([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z0-9][-a-zA-Z0-9]*/g, function(match) {
// 避免重复高亮已经处理过的元素
if (match.includes('${match}`;
});
// 高亮服务版本信息
highlighted = highlighted.replace(/(Running|Service Info):(.*?)(?=\n|$)/g,
'$1:$2');
// 高亮总结信息
highlighted = highlighted.replace(/(Nmap 多线程扫描完成:.*?)(?=$)/g,
'$1');
if (highlighted !== content) {
resultsContent.innerHTML = highlighted;
}
}
// 检查是否有之前的扫描
function checkPreviousScan() {
const savedScanId = sessionStorage.getItem('currentScanId');
if (savedScanId) {
currentScanId = savedScanId;
showLoading(false);
updateLiveOutput('正在恢复之前的扫描状态...\n');
// 当WebSocket连接建立后会自动加入此房间
}
}
// 初始化
function init() {
initWebSocket();
initThreadControl();
validateForm();
checkPreviousScan();
}
// 启动初始化
init();
});