fscan/core/WebDetection.go
ZacharyZcR 57aa48be58 refactor: 简化HTTP协议检测,消除硬编码和重复检测
优化内容:
- 消除硬编码协议列表,使用通用的二进制字符比例判断
- 协议预检查只执行一次,HTTP和HTTPS检测复用结果
- 添加tryHTTPConnectionDirect函数,避免重复协议检查
- 简化isNonHTTPProtocol逻辑,基于统计学特征而非特定协议

技术改进:
- 检测性能提升:避免重复的TCP连接和协议分析
- 更好的可维护性:无需为每个新协议添加硬编码规则
- 更智能的判断:基于二进制内容比例,适用于所有未知协议
2025-09-02 00:03:42 +00:00

408 lines
11 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
// 缓存互斥锁
cacheMutex sync.RWMutex
}
// 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),
}
}
// 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 {
// 建立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
}