1.省流版
第一次做算小工具的完整项目,来练手
感觉还不是很完善
用法可以看readme.md(应该没人会用,dirsearch好用的多)
不过可以看看源码
2.核心原理
目录扫描本质上就是三个步骤:
字典 = ["admin", "api", "login", ".env", ...]
扩展名 = ["", ".php", ".html", ".bak"]
对每个字典词 × 每个扩展名:
拼接 URL: https://target.com/admin
https://target.com/admin.php
https://target.com/admin.html
...
发送 HTTP GET 请求
如果状态码是 200/301/403 → 记录(说明路径存在)
如果状态码是 404 → 忽略
用代码表示就是两层循环:
for word in wordlist:
for ext in extensions:
url = f"{base}/{word}{ext}" # 笛卡尔积
resp = await client.get(url) # 发请求
if resp.status_code in [200, 301, 403]:
results.append(url)
3.项目结构
dirscanner_simple/
├── scanner.py # 核心引擎:并发请求 + 过滤
├── web.py # Web 后端:FastAPI 路由
├── templates/
│ └── index.html # 前端界面:配置面板 + 结果表格
├── main.py # 入口:一行 uvicorn.run()
└── requirements.txt # 4 个依赖
4. 关键技术点
4.1 笛卡尔积生成 URL
scanner.py:147-157 --- 这 10 行代码把 wordlist × extensions 展开为完整 URL:
for word in self.config.wordlist: # ["admin", "api", ...]
path = f"/{word}"
for ext in self.config.extensions: # ["", ".php", ".html", ...]
if ext and not path.endswith(ext):
full_path = f"{path}{ext}" # /admin.php
else:
full_path = path # /admin
url = f"{base}{full_path}" # https://target.com/admin.php
4.2 异步并发
scanner.py:168-181 --- 关键代码:
async with httpx.AsyncClient(limits=limits, timeout=...) as client:
sem = asyncio.Semaphore(concurrency) # 最多同时 20 个请求
workers = [
asyncio.create_task(self._worker(i, client, queue, sem))
for i in range(concurrency) # 启动 20 个协程
]
AsyncClient复用 HTTP 连接,比每次新建连接快很多Semaphore(20)就像只有 20 张入场券,用完要归还asyncio.create_task()创建协程任务,不是线程
4.3 Worker 工作循环
scanner.py:215-273 --- 每个 worker 的循环:
while not self._cancel.is_set():
url = await asyncio.wait_for(queue.get(), timeout=0.5)
async with sem:
resp = await client.get(url)
if _should_report(resp, self.config):
results.append(...)
queue.task_done()
timeout=0.5 是关键------不是永久阻塞取任务,每隔 0.5 秒检查一次"用户是不是点了停止"。
4.4 三级过滤
scanner.py:94-114 --- 短路径求值:
def _should_report(resp, config):
# ↓ 第1级:状态码过滤
if resp.status_code in exclude: return False # 是 404 就丢
if resp.status_code not in include: return False # 不在白名单就丢
# ↓ 第2级:大小过滤
if config.min_size and len(resp.content) < config.min_size: return False
# ↓ 第3级:关键词过滤(可选)
if config.keyword and config.keyword not in resp.text: return False
return True # 三级全通过,这页面值得报告
短路逻辑利用 return False 提前退出,后面更昂贵的操作(比如读取 resp.text)能省就省。
4.5 前端轮询
index.html:288-341 --- 不用 WebSocket,用最简单的方式:
// 启动时:开始每秒轮询
timer = setInterval(poll, 1000);
async function poll() {
const status = await fetch('/api/scan/status'); // 拿进度
const results = await fetch('/api/scan/results'); // 拿结果
更新进度条和表格;
if (!status.running) { // 扫描结束
clearInterval(timer); // 停轮询
btnExport.disabled = false; // 启用导出按钮
}
}