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 }