fscan/core/WebDetection.go
ZacharyZcR 9ad397f58f perf: 使用单例模式消除重复协议检测
核心修复:
- 将WebPortDetector改为全局单例,消除多实例问题
- 所有协议检测共享同一个缓存,避免重复TCP连接
- 使用sync.Once确保线程安全的单例初始化

性能提升:
- 每个端口的协议检测从多次减少到1次
- 大幅降低TCP连接数,减少网络开销
- 缓存命中率显著提升

技术实现:
- GetWebPortDetector() 替代 NewWebPortDetector()
- newWebPortDetector() 改为私有方法
- BaseScanStrategy统一使用单例实例

这是数据结构决定性能的经典案例 - 通过正确的实例管理
彻底解决了重复检测问题
2025-09-02 00:06:17 +00:00

447 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/common"
)
// WebPortDetector Web端口检测器
type WebPortDetector struct {
// 常见Web端口列表
commonWebPorts map[int]bool
// HTTP检测超时时间
httpTimeout time.Duration
// HTTP客户端
httpClient *http.Client
// HTTPS客户端
httpsClient *http.Client
// 检测结果缓存(避免重复检测)
detectionCache map[string]bool
// 协议检测缓存避免重复TCP连接
protocolCache map[string]bool
// 缓存互斥锁
cacheMutex sync.RWMutex
}
var (
// 全局单例实例
webDetectorInstance *WebPortDetector
webDetectorOnce sync.Once
)
// GetWebPortDetector 获取Web端口检测器单例
func GetWebPortDetector() *WebPortDetector {
webDetectorOnce.Do(func() {
webDetectorInstance = newWebPortDetector()
})
return webDetectorInstance
}
// newWebPortDetector 创建Web端口检测器私有方法
func newWebPortDetector() *WebPortDetector {
timeout := 3 * time.Second
// 只保留最基本的Web端口用于快速路径优化
commonPorts := map[int]bool{
80: true, // HTTP
443: true, // HTTPS
}
// 创建HTTP客户端
httpClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true,
MaxConnsPerHost: 5,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 不跟随重定向,减少检测时间
},
}
// 创建HTTPS客户端跳过证书验证
httpsClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true,
MaxConnsPerHost: 5,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return &WebPortDetector{
commonWebPorts: commonPorts,
httpTimeout: timeout,
httpClient: httpClient,
httpsClient: httpsClient,
detectionCache: make(map[string]bool),
protocolCache: make(map[string]bool),
}
}
// IsWebService 智能检测端口是否运行Web服务
func (w *WebPortDetector) IsWebService(host string, port int) bool {
// 1. 快速路径常见Web端口直接返回true
if w.IsCommonWebPort(port) {
common.LogDebug(fmt.Sprintf("端口 %d 是常见Web端口启用Web插件", port))
return true
}
// 2. 缓存检查:避免重复检测同一端口
cacheKey := fmt.Sprintf("%s:%d", host, port)
w.cacheMutex.RLock()
if cached, exists := w.detectionCache[cacheKey]; exists {
w.cacheMutex.RUnlock()
common.LogDebug(fmt.Sprintf("端口 %d 使用缓存结果: %v", port, cached))
return cached
}
w.cacheMutex.RUnlock()
// 3. 智能路径对非常见端口进行HTTP协议探测
common.LogDebug(fmt.Sprintf("对端口 %d 进行智能Web检测", port))
result := w.detectHTTPService(host, port)
// 4. 缓存结果
w.cacheMutex.Lock()
w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock()
common.LogDebug(fmt.Sprintf("端口 %d 智能检测结果: %v", port, result))
return result
}
// DetectHTTPServiceOnly 仅通过HTTP协议检测端口不使用预定义端口列表
func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int) bool {
// 缓存检查:避免重复检测同一端口
cacheKey := fmt.Sprintf("%s:%d", host, port)
w.cacheMutex.RLock()
if cached, exists := w.detectionCache[cacheKey]; exists {
w.cacheMutex.RUnlock()
return cached
}
w.cacheMutex.RUnlock()
// 跳过预定义端口检查,直接进行协议探测
result := w.detectHTTPService(host, port)
// 缓存结果
w.cacheMutex.Lock()
w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock()
return result
}
// IsCommonWebPort 检查是否为常见Web端口
func (w *WebPortDetector) IsCommonWebPort(port int) bool {
return w.commonWebPorts[port]
}
// detectHTTPService 检测HTTP服务真正的智能检测
func (w *WebPortDetector) detectHTTPService(host string, port int) bool {
// 创建检测上下文,避免长时间阻塞
ctx, cancel := context.WithTimeout(context.Background(), w.httpTimeout)
defer cancel()
// 先进行一次协议预检查,避免重复检测
if !w.quickProtocolCheck(ctx, host, port) {
return false
}
// 并发检测HTTP和HTTPS
httpChan := make(chan bool, 1)
httpsChan := make(chan bool, 1)
// 检测HTTP跳过协议检查因为已经检查过了
go func() {
httpChan <- w.tryHTTPConnectionDirect(ctx, host, port, "http")
}()
// 检测HTTPS跳过协议检查因为已经检查过了
go func() {
httpsChan <- w.tryHTTPConnectionDirect(ctx, host, port, "https")
}()
// 等待任一协议检测成功
select {
case result := <-httpChan:
if result {
common.LogDebug(fmt.Sprintf("端口 %d 检测到HTTP服务", port))
return true
}
case <-ctx.Done():
return false
}
select {
case result := <-httpsChan:
if result {
common.LogDebug(fmt.Sprintf("端口 %d 检测到HTTPS服务", port))
return true
}
case <-ctx.Done():
return false
}
return false
}
// tryHTTPConnection 尝试HTTP连接带协议检查
func (w *WebPortDetector) tryHTTPConnection(ctx context.Context, host string, port int, protocol string) bool {
// 先进行简单的协议嗅探避免向非HTTP服务发送HTTP请求
if !w.quickProtocolCheck(ctx, host, port) {
return false
}
return w.tryHTTPConnectionDirect(ctx, host, port, protocol)
}
// tryHTTPConnectionDirect 直接尝试HTTP连接跳过协议检查
func (w *WebPortDetector) tryHTTPConnectionDirect(ctx context.Context, host string, port int, protocol string) bool {
// 构造URL
var url string
if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") {
url = fmt.Sprintf("%s://%s", protocol, host)
} else {
url = fmt.Sprintf("%s://%s:%d", protocol, host, port)
}
// 选择客户端
var client *http.Client
if protocol == "https" {
client = w.httpsClient
} else {
client = w.httpClient
}
// 发送HEAD请求最小化网络开销
req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
if err != nil {
return false
}
req.Header.Set("User-Agent", "fscan-web-detector/2.2")
req.Header.Set("Accept", "*/*")
resp, err := client.Do(req)
if err != nil {
// 检查错误类型某些错误也表明是HTTP服务
return w.analyzeHTTPError(err)
}
defer resp.Body.Close()
// 分析HTTP响应
return w.analyzeHTTPResponse(resp, url)
}
// analyzeHTTPError 分析HTTP错误判断是否表明存在HTTP服务
func (w *WebPortDetector) analyzeHTTPError(err error) bool {
errStr := strings.ToLower(err.Error())
// 先检查明确的非Web服务错误
nonWebErrors := []string{
"connection refused",
"no such host",
"network unreachable",
"timeout",
"deadline exceeded",
"connection reset",
"eof",
}
for _, nonWebErr := range nonWebErrors {
if strings.Contains(errStr, nonWebErr) {
return false
}
}
// 这些错误表明连接到了HTTP服务器只是协议或其他问题
webIndicators := []string{
"ssl handshake",
"certificate",
"tls",
"bad request",
"method not allowed",
"server gave http response",
}
// 特别处理malformed HTTP response通常表明是非HTTP协议
if strings.Contains(errStr, "malformed http response") {
// 检查是否包含明显的二进制数据如MySQL greeting包
if strings.Contains(errStr, "\\x00") || strings.Contains(errStr, "\\x") {
common.LogDebug(fmt.Sprintf("检测到二进制协议响应非HTTP服务: %s", err.Error()))
return false
}
// 如果是文本形式的malformed response可能仍是HTTP服务的变种
}
for _, indicator := range webIndicators {
if strings.Contains(errStr, indicator) {
return true
}
}
return false
}
// analyzeHTTPResponse 分析HTTP响应判断是否为Web服务
func (w *WebPortDetector) analyzeHTTPResponse(resp *http.Response, url string) bool {
// 检查是否为有效的HTTP响应
if resp.StatusCode <= 0 {
return false
}
// 检查状态码是否合理(避免协议混淆导致的假阳性)
if resp.StatusCode < 100 || resp.StatusCode >= 600 {
common.LogDebug(fmt.Sprintf("端口返回无效HTTP状态码: %d", resp.StatusCode))
return false
}
// 更严格的验证检查是否有基本的HTTP头部
if len(resp.Header) == 0 {
common.LogDebug(fmt.Sprintf("端口返回HTTP响应但缺少HTTP头部: %s", url))
return false
}
// 检查是否包含标准HTTP头部字段
hasValidHeaders := false
standardHeaders := []string{"Server", "Content-Type", "Content-Length", "Date", "Connection", "Cache-Control"}
for _, header := range standardHeaders {
if resp.Header.Get(header) != "" {
hasValidHeaders = true
break
}
}
if !hasValidHeaders {
common.LogDebug(fmt.Sprintf("端口返回响应但缺少标准HTTP头部: %s", url))
return false
}
// 分析响应头获取更多信息
serverHeader := resp.Header.Get("Server")
contentType := resp.Header.Get("Content-Type")
// 特殊处理某些非Web服务也会返回HTTP响应
if w.isNonWebHTTPService(serverHeader, contentType) {
common.LogDebug(fmt.Sprintf("端口返回HTTP响应但可能不是Web服务: %s", url))
return false
}
// 记录检测到的Web服务器信息
if serverHeader != "" {
common.LogDebug(fmt.Sprintf("检测到Web服务器: %s (Server: %s)", url, serverHeader))
}
return true
}
// isNonWebHTTPService 判断是否为非Web的HTTP服务
func (w *WebPortDetector) isNonWebHTTPService(serverHeader, contentType string) bool {
// 简化:只基于实际响应内容判断,不用硬编码列表
return false
}
// quickProtocolCheck 快速协议检查避免向非HTTP服务发送HTTP请求
func (w *WebPortDetector) quickProtocolCheck(ctx context.Context, host string, port int) bool {
// 检查协议缓存
protocolKey := fmt.Sprintf("%s:%d:protocol", host, port)
w.cacheMutex.RLock()
if cached, exists := w.protocolCache[protocolKey]; exists {
w.cacheMutex.RUnlock()
return cached
}
w.cacheMutex.RUnlock()
// 执行实际的协议检测
result := w.doProtocolCheck(ctx, host, port)
// 缓存结果
w.cacheMutex.Lock()
w.protocolCache[protocolKey] = result
w.cacheMutex.Unlock()
return result
}
// doProtocolCheck 执行实际的协议检测
func (w *WebPortDetector) doProtocolCheck(ctx context.Context, host string, port int) bool {
// 建立TCP连接
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 2*time.Second)
if err != nil {
return false
}
defer conn.Close()
// 设置读取超时
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
// 读取服务器的初始响应(如果有)
buffer := make([]byte, 512)
n, err := conn.Read(buffer)
if err != nil {
// 大多数HTTP服务不会主动发送数据这是正常的
// 超时或EOF表示服务器在等待客户端请求HTTP特征
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return true // HTTP服务通常不主动发送数据
}
if err.Error() == "EOF" {
return true // 连接正常但无数据可能是HTTP
}
return true // 其他错误也可能是HTTP服务
}
if n > 0 {
// 如果服务器主动发送数据,检查是否包含明显的二进制内容
// 简单规则如果包含太多不可打印字符很可能不是HTTP协议
nonPrintableCount := 0
for i := 0; i < n && i < 100; i++ { // 只检查前100字节
b := buffer[i]
if b < 32 && b != 9 && b != 10 && b != 13 { // 排除tab、换行、回车
nonPrintableCount++
}
}
// 如果不可打印字符太多超过20%),很可能是二进制协议
if nonPrintableCount > n/5 {
common.LogDebug(fmt.Sprintf("端口 %d 检测到二进制协议(不可打印字符: %d/%d", port, nonPrintableCount, n))
return false
}
}
return true
}
// min 辅助函数
func min(a, b int) int {
if a < b {
return a
}
return b
}