还不是很完善,还有一些问题:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IP Web 服务扫描器 (支持断点续扫)</title>
<style>
:root {
--primary-color: #2563eb;
--success-color: #16a34a; /* 新增绿色用于继续按钮 */
--danger-color: #ef4444;
--bg-color: #f8fafc;
--card-bg: #ffffff;
--text-color: #334155;
--border-color: #e2e8f0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 20px;
line-height: 1.5;
}
.container {
max-width: 900px;
margin: 0 auto;
background: var(--card-bg);
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: var(--primary-color);
margin-bottom: 30px;
}
.control-panel {
display: grid;
/* IP(3) 模式(2) 并发(1) 按钮(1.5) */
grid-template-columns: 3fr 2fr 1fr 1.5fr;
gap: 15px;
margin-bottom: 20px;
}
.input-group {
display: flex;
flex-direction: column;
}
label {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 5px;
}
input[type="text"],
input[type="number"],
select {
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 1rem;
outline: none;
height: 42px;
box-sizing: border-box;
width: 100%;
}
input[type="text"]:focus,
input[type="number"]:focus,
select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.btn-group {
display: flex;
gap: 10px;
align-self: end;
}
button {
color: white;
border: none;
border-radius: 6px;
padding: 0 15px;
font-size: 0.95rem;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s, opacity 0.2s;
height: 42px;
flex: 1;
white-space: nowrap;
}
.btn-primary {
background-color: var(--primary-color);
}
.btn-primary:hover {
background-color: #1d4ed8;
}
/* 继续按钮样式 */
.btn-continue {
background-color: var(--success-color);
}
.btn-continue:hover {
background-color: #15803d;
}
.btn-danger {
background-color: var(--danger-color);
}
.btn-danger:hover {
background-color: #dc2626;
}
button:disabled {
background-color: #94a3b8;
cursor: not-allowed;
opacity: 0.7;
}
.status-bar {
margin: 20px 0;
padding: 10px;
background: #f1f5f9;
border-radius: 6px;
font-size: 0.9rem;
display: flex;
justify-content: space-between;
}
.progress-container {
height: 4px;
background: #e2e8f0;
border-radius: 2px;
overflow: hidden;
margin-bottom: 20px;
}
.progress-bar {
height: 100%;
background: var(--primary-color);
width: 0%;
transition: width 0.3s ease;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: #f8fafc;
font-weight: 600;
}
.status-open {
color: #16a34a;
font-weight: bold;
background: #dcfce7;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.notice {
margin-top: 30px;
padding: 15px;
background-color: #fff7ed;
border-left: 4px solid #f97316;
font-size: 0.85rem;
color: #9a3412;
}
@media (max-width: 700px) {
.control-panel {
grid-template-columns: 1fr;
}
.btn-group {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Web 服务探测器</h1>
<div class="control-panel">
<div class="input-group">
<label for="targetIp">目标 IP 地址</label>
<input
type="text"
id="targetIp"
placeholder="例如: 192.168.1.1"
value="127.0.0.1"
oninput="resetScanState()"
/>
</div>
<div class="input-group">
<label for="portMode">扫描模式</label>
<select id="portMode" onchange="resetScanState()">
<option value="common">常用 Web 端口 (Top 20)</option>
<option value="extended">扩展 Web 端口 (Top 100)</option>
<option value="all">全部端口(1-65535)</option>
</select>
</div>
<div class="input-group">
<label for="concurrency">并发数</label>
<input
type="number"
id="concurrency"
value="20"
min="1"
max="1000"
title="同时发起的请求数量,建议 20-100"
/>
</div>
<div class="input-group">
<label> </label>
<div class="btn-group">
<button id="startBtn" class="btn-primary" onclick="handleStartClick()">开始扫描</button>
<button id="stopBtn" class="btn-danger" onclick="stopScan()" disabled>停止</button>
</div>
</div>
</div>
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="status-bar">
<span id="statusText">等待开始...</span>
<span id="foundCount">发现: 0</span>
</div>
<table>
<thead>
<tr>
<th>地址</th>
<th>端口</th>
<th>协议 (猜测)</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="resultTable">
<!-- 结果将在这里生成 -->
</tbody>
</table>
<div class="notice">
<strong>ℹ️ 提示:</strong>
点击"停止"后,您可以点击"继续扫描"从断点处恢复任务。如果修改了 IP 或模式,将重新开始。
</div>
</div>
<script>
// 常用 Web 端口列表
const COMMON_PORTS = [
80, 443, 8080, 8443, 3000, 5000, 8000, 8008, 8888, 9000, 9200, 7001, 81,
82, 88, 8181, 9090, 10000, 27017,
];
// 扩展端口列表 (模拟)
const EXTENDED_PORTS = [
...COMMON_PORTS,
5900,
6379,
1900,
53,
111,
445,
21,
22,
23,
25,
8081,
8082,
8083,
8090,
9001,
9999,
];
const ALL_PORTS = Array.from({ length: 65535 }, (_, i) => i + 1);
// 状态变量
let isScanning = false;
let shouldStop = false;
let isPaused = false;
// 断点续传相关
let pausedIndex = 0; // 记录暂停时的索引
let currentPortList = []; // 记录当前正在扫描的端口列表
let globalProcessed = 0; // 记录已处理数量
let globalFound = 0; // 记录已发现数量
/**
* 用户修改IP或模式时,重置为"全新开始"状态
*/
function resetScanState() {
if (isScanning) return; // 扫描中不重置,等待停止
isPaused = false;
pausedIndex = 0;
const startBtn = document.getElementById("startBtn");
startBtn.textContent = "开始扫描";
startBtn.className = "btn-primary";
document.getElementById("statusText").textContent = "准备就绪 (参数已变更)";
}
/**
* 点击"开始/继续"按钮的入口
*/
async function handleStartClick() {
if (isScanning) return;
const ipInput = document.getElementById("targetIp").value.trim();
if (!isValidIP(ipInput)) {
alert("请输入有效的 IP 地址!");
return;
}
const concurrencyInput = document.getElementById("concurrency").value;
let batchSize = parseInt(concurrencyInput, 10);
if (isNaN(batchSize) || batchSize < 1) {
alert("请输入有效的并发数!");
return;
}
// UI 设置
const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
isScanning = true;
shouldStop = false; // 重置停止标志
startBtn.disabled = true;
startBtn.textContent = "扫描中...";
stopBtn.disabled = false;
stopBtn.textContent = "停止";
// 判断是"新扫描"还是"继续扫描"
if (!isPaused) {
// === 新扫描初始化 ===
document.getElementById("resultTable").innerHTML = "";
document.getElementById("foundCount").textContent = "发现: 0";
globalFound = 0;
globalProcessed = 0;
pausedIndex = 0;
updateProgress(0);
// 生成端口列表
const mode = document.getElementById("portMode").value;
if (mode === "common") currentPortList = COMMON_PORTS;
else if (mode === "extended") currentPortList = EXTENDED_PORTS;
else currentPortList = ALL_PORTS;
// 去重排序
currentPortList = [...new Set(currentPortList)].sort((a, b) => a - b);
}
// 执行循环
await runScanLoop(ipInput, batchSize);
}
/**
* 核心扫描循环
*/
async function runScanLoop(ip, batchSize) {
const total = currentPortList.length;
// 从 pausedIndex 开始循环 (如果是新扫描,pausedIndex 为 0)
for (let i = pausedIndex; i < total; i += batchSize) {
// 1. 检查是否按下了停止
if (shouldStop) {
pausedIndex = i; // 记录当前位置
handleStopState(); // 处理 UI 变为暂停状态
return;
}
// 2. 准备并发任务
const batch = currentPortList.slice(i, i + batchSize);
const promises = batch.map((port) => checkPort(ip, port));
// 3. 等待当前批次完成
const results = await Promise.all(promises);
// 4. 处理结果
results.forEach((res) => {
if (res.isOpen) {
globalFound++;
addResultRow(res.ip, res.port, res.protocol);
}
});
// 5. 更新进度
globalProcessed += batch.length;
// 防止进度条回退或溢出(因 batchSize 切分可能导致最后一点计算误差)
const safeProcessed = Math.min(globalProcessed, total);
// 只有在没停止的时候才更新 UI,避免停止瞬间数字跳动
if (!shouldStop) {
updateProgress((i + batch.length) / total * 100); // 使用 i 计算进度更准确
document.getElementById("statusText").textContent =
`正在扫描: ${safeProcessed}/${total} (并发: ${batchSize})`;
document.getElementById("foundCount").textContent = `发现: ${globalFound}`;
}
}
// === 循环正常结束 ===
handleFinishState();
}
function stopScan() {
if (isScanning) {
shouldStop = true;
document.getElementById("stopBtn").textContent = "正在停止...";
}
}
/**
* 处理"暂停/停止"后的状态和 UI
*/
function handleStopState() {
isScanning = false;
isPaused = true; // 标记为暂停状态
const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
startBtn.disabled = false;
startBtn.textContent = "继续扫描";
startBtn.className = "btn-continue"; // 切换为绿色按钮
stopBtn.disabled = true;
stopBtn.textContent = "停止";
document.getElementById("statusText").textContent =
`已暂停 (进度: ${pausedIndex}/${currentPortList.length})`;
}
/**
* 处理"扫描完成"后的状态和 UI
*/
function handleFinishState() {
isScanning = false;
isPaused = false; // 任务完成,不再是暂停状态
pausedIndex = 0;
updateProgress(100);
const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
startBtn.disabled = false;
startBtn.textContent = "开始扫描"; // 变回开始
startBtn.className = "btn-primary"; // 变回蓝色
stopBtn.disabled = true;
stopBtn.textContent = "停止";
document.getElementById("statusText").textContent = "扫描完成";
}
function checkPort(ip, port) {
return new Promise((resolve) => {
const timeout = 1500;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const protocol = port === 443 || port === 8443 ? "https" : "http";
const url = `${protocol}://${ip}:${port}`;
fetch(url, {
mode: "no-cors",
signal: controller.signal,
})
.then(() => {
clearTimeout(id);
resolve({ isOpen: true, ip, port, protocol });
})
.catch((err) => {
clearTimeout(id);
resolve({ isOpen: false, ip, port, protocol });
});
});
}
function addResultRow(ip, port, protocol) {
const tbody = document.getElementById("resultTable");
const tr = document.createElement("tr");
const url = `${protocol}://${ip}:${port}`;
tr.innerHTML = `
<td>${ip}</td>
<td>${port}</td>
<td>${protocol.toUpperCase()}</td>
<td><span class="status-open">开放 / 可达</span></td>
<td><a href="${url}" target="_blank" style="color: var(--primary-color); text-decoration: none;">访问 →</a></td>
`;
tbody.appendChild(tr);
}
function updateProgress(percent) {
// 限制最大 100%
const p = Math.min(percent, 100);
document.getElementById("progressBar").style.width = `${p}%`;
}
function isValidIP(ip) {
return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ip) || ip === "localhost";
}
</script>
</body>
</html>