diff --git a/core/alive_scanner.go b/core/alive_scanner.go index a8a66ff..5b04f9c 100644 --- a/core/alive_scanner.go +++ b/core/alive_scanner.go @@ -57,7 +57,7 @@ func (s *AliveScanStrategy) Description() string { func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { // 验证扫描目标(需要同时检查 -h 和 -hf 参数) if info.Host == "" && session.Params.HostsFile == "" { - common.LogError(i18n.GetText("parse_error_target_empty")) + session.LogError(i18n.GetText("parse_error_target_empty")) return } @@ -65,7 +65,7 @@ func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSes s.performAliveScan(ctx, info, session) // 输出统计信息 - s.outputStats() + s.outputStats(session) } // performAliveScan 执行存活探测 @@ -73,12 +73,12 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho // 解析目标主机 hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts) if err != nil { - common.LogError(i18n.Tr("parse_target_failed", err)) + session.LogError(i18n.Tr("parse_target_failed", err)) return } if len(hosts) == 0 { - common.LogError(i18n.GetText("parse_error_no_hosts")) + session.LogError(i18n.GetText("parse_error_no_hosts")) return } @@ -87,7 +87,6 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho s.stats.AliveHosts = 0 s.stats.DeadHosts = 0 - // 执行存活检测 aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测 @@ -103,10 +102,10 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho } // outputStats 输出统计信息(精简版) -func (s *AliveScanStrategy) outputStats() { +func (s *AliveScanStrategy) outputStats(session *common.ScanSession) { // 只输出存活主机列表,不输出冗余统计 for _, host := range s.stats.AliveHostList { - common.LogSuccess(fmt.Sprintf("alive %s", host)) + session.LogSuccess(fmt.Sprintf("alive %s", host)) } } diff --git a/core/base_scan_strategy.go b/core/base_scan_strategy.go index 0b5c69f..1861e46 100644 --- a/core/base_scan_strategy.go +++ b/core/base_scan_strategy.go @@ -170,7 +170,7 @@ func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomM } // LogPluginInfo 输出插件信息 -func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) { +func (b *BaseScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { allPlugins, isCustomMode := b.GetPlugins(config) var prefix string @@ -189,6 +189,7 @@ func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) { _ = allPlugins _ = isCustomMode _ = prefix + _ = session } // formatPluginList 格式化插件列表(超过5个时精简显示) @@ -205,14 +206,14 @@ func (b *BaseScanStrategy) ValidateConfiguration() error { } // LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示) -func (b *BaseScanStrategy) LogScanStart() { +func (b *BaseScanStrategy) LogScanStart(session *common.ScanSession) { // 服务扫描模式下不显示(插件信息已足够说明) // 仅在本地/Web等特殊模式下显示 switch b.filterType { case FilterLocal: - common.LogInfo(i18n.GetText("start_local_scan")) + session.LogInfo(i18n.GetText("start_local_scan")) case FilterWeb: - common.LogInfo(i18n.GetText("start_web_scan")) + session.LogInfo(i18n.GetText("start_web_scan")) } } diff --git a/core/local_scanner.go b/core/local_scanner.go index c132a8d..a33ee20 100644 --- a/core/local_scanner.go +++ b/core/local_scanner.go @@ -22,12 +22,12 @@ func NewLocalScanStrategy() *LocalScanStrategy { } // LogPluginInfo 重写以只显示通过-local指定的插件 -func (s *LocalScanStrategy) LogPluginInfo(config *common.Config) { +func (s *LocalScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { localPlugin := config.LocalPlugin if localPlugin != "" { - common.LogInfo(i18n.Tr("local_plugin_info", localPlugin)) + session.LogInfo(i18n.Tr("local_plugin_info", localPlugin)) } else { - common.LogError(i18n.GetText("local_plugin_not_specified")) + session.LogError(i18n.GetText("local_plugin_not_specified")) } } @@ -46,24 +46,24 @@ func (s *LocalScanStrategy) Execute(ctx context.Context, session *common.ScanSes config := session.Config // 输出扫描开始信息 - s.LogScanStart() + s.LogScanStart(session) // 验证插件配置 if err := s.ValidateConfiguration(); err != nil { - common.LogError(err.Error()) + session.LogError(err.Error()) return } // 验证本地插件是否存在 if config.LocalPlugin != "" { if !plugins.Exists(config.LocalPlugin) { - common.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin)) + session.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin)) return } } // 输出插件信息 - s.LogPluginInfo(config) + s.LogPluginInfo(config, session) // 准备目标(本地扫描通常只有一个目标,即本机) targets := s.PrepareTargets(info) diff --git a/core/service_scanner.go b/core/service_scanner.go index 8924d0a..29f4270 100644 --- a/core/service_scanner.go +++ b/core/service_scanner.go @@ -25,27 +25,27 @@ func NewServiceScanStrategy() *ServiceScanStrategy { } // LogPluginInfo 重写以提供基于端口的插件过滤 -func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) { +func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { // 需要从命令行参数获取端口信息来进行过滤 // 如果没有指定端口,使用默认端口进行过滤显示 ports := config.Target.Ports if ports == "" || ports == "all" { // 默认端口扫描:显示所有插件 - s.BaseScanStrategy.LogPluginInfo(config) + s.BaseScanStrategy.LogPluginInfo(config, session) } else { // 指定端口扫描:只显示匹配的插件 - s.showPluginsForSpecifiedPorts(config) + s.showPluginsForSpecifiedPorts(config, session) } } // showPluginsForSpecifiedPorts 显示指定端口的匹配插件 -func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config) { +func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config, session *common.ScanSession) { allPlugins, isCustomMode := s.GetPlugins(config) // 解析端口 ports := s.parsePortList(config.Target.Ports) if len(ports) == 0 { - s.BaseScanStrategy.LogPluginInfo(config) + s.BaseScanStrategy.LogPluginInfo(config, session) return } @@ -71,12 +71,12 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config if len(applicablePlugins) > 0 { pluginStr := formatPluginList(applicablePlugins) if isCustomMode { - common.LogInfo(i18n.Tr("service_plugin_custom", pluginStr)) + session.LogInfo(i18n.Tr("service_plugin_custom", pluginStr)) } else { - common.LogInfo(i18n.Tr("service_plugin_info", pluginStr)) + session.LogInfo(i18n.Tr("service_plugin_info", pluginStr)) } } else { - common.LogInfo(i18n.GetText("service_plugin_none")) + session.LogInfo(i18n.GetText("service_plugin_none")) } } @@ -118,21 +118,21 @@ func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanS // 验证扫描目标(需要同时检查 -h 和 -hf 参数) if info.Host == "" && session.Params.HostsFile == "" { - common.LogError(i18n.GetText("parse_error_target_empty")) + session.LogError(i18n.GetText("parse_error_target_empty")) return } // 输出扫描开始信息 - s.LogScanStart() + s.LogScanStart(session) // 验证插件配置 if err := s.ValidateConfiguration(); err != nil { - common.LogError(err.Error()) + session.LogError(err.Error()) return } // 输出插件信息(重写以提供端口过滤) - s.LogPluginInfo(config) + s.LogPluginInfo(config, session) // 执行主机扫描流程 s.performHostScan(ctx, session, info, ch, wg) @@ -147,14 +147,14 @@ func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *comm // 解析目标主机 hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts) if err != nil { - common.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err)) + session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err)) return } // 主机存活检测 if s.shouldPerformLivenessCheck(hosts, config) { hosts = CheckLive(ctx, hosts, false, session) - common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) + session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) } if len(hosts) == 0 && len(state.GetHostPorts()) == 0 { @@ -218,7 +218,7 @@ func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *comm // 发现目标主机和端口 targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session) if err != nil { - common.LogError(err.Error()) + session.LogError(err.Error()) return nil } return targetInfos @@ -291,7 +291,7 @@ func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput str // 主机存活检测 if s.shouldPerformLivenessCheck(hosts, config) { hosts = CheckLive(ctx, hosts, false, session) - common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) + session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) } // 端口扫描 @@ -325,7 +325,7 @@ func (s *ServiceScanStrategy) discoverAlivePorts(ctx context.Context, hosts []st hostPorts := state.GetHostPorts() if len(hostPorts) > 0 { alivePorts = mergeHostPorts(alivePorts, hostPorts) - common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts))) + session.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts))) state.ClearHostPorts() } @@ -390,4 +390,3 @@ func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo comm return infos } - diff --git a/core/web_scanner.go b/core/web_scanner.go index ff6dcbe..ad3463e 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -16,29 +16,6 @@ import ( gmtls "github.com/tjfoc/gmsm/gmtls" ) -// =============================== -// Web服务检测 -// =============================== - -// 全局共享 HTTP Client,复用连接池减少 TLS 握手和 TCP 建连开销 -var ( - sharedHTTPClientOnce sync.Once - sharedHTTPClient *http.Client -) - -func getSharedHTTPClient(config *common.Config) *http.Client { - sharedHTTPClientOnce.Do(func() { - sharedHTTPClient = createHTTPClient(config) - // 启用 keep-alive 复用连接 - if t, ok := sharedHTTPClient.Transport.(*http.Transport); ok { - t.DisableKeepAlives = false - t.MaxIdleConns = 100 - t.MaxIdleConnsPerHost = 2 - } - }) - return sharedHTTPClient -} - // WebPortDetector 简化的Web检测器 - 保持API兼容 type WebPortDetector struct{} @@ -91,7 +68,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config, session *com } // TLS和GM TLS都失败,尝试HTTP - client := getSharedHTTPClient(config) + client := createHTTPClient(config, session) // 使用HEAD请求(更轻量) httpURL := fmt.Sprintf("http://%s", addr) @@ -106,7 +83,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config, session *com } // createHTTPClient 创建统一的HTTP客户端 - 支持HTTP/HTTPS和代理 -func createHTTPClient(config *common.Config) *http.Client { +func createHTTPClient(config *common.Config, session *common.ScanSession) *http.Client { timeout := config.Network.WebTimeout // 创建基础Transport,配置连接和 TLS 超时 @@ -128,14 +105,14 @@ func createHTTPClient(config *common.Config) *http.Client { if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil { transport.Proxy = http.ProxyURL(proxyURL) } else { - common.LogError(i18n.Tr("http_proxy_config_error", err)) + session.LogError(i18n.Tr("http_proxy_config_error", err)) } } else if networkConfig.Socks5Proxy != "" { // 使用SOCKS5代理 - 需要特殊处理 if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil { // SOCKS5代理需要使用代理管理器 // 这里先记录警告,建议使用HTTP代理进行Web检测 - common.LogError(i18n.GetText("socks5_not_supported_web")) + session.LogError(i18n.GetText("socks5_not_supported_web")) } } @@ -156,7 +133,7 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c return false } - client := getSharedHTTPClient(config) + client := createHTTPClient(config, session) // 尝试HTTP if w.tryHTTP(client, host, port, "http") { @@ -330,19 +307,19 @@ func (s *WebScanStrategy) Description() string { // Execute 执行Web扫描策略 func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { // 输出扫描开始信息 - s.LogScanStart() + s.LogScanStart(session) // 验证插件配置 if err := s.ValidateConfiguration(); err != nil { - common.LogError(err.Error()) + session.LogError(err.Error()) return } // 准备URL目标 - targets := s.PrepareTargets(info, session.State) + targets := s.prepareTargets(info, session.State, session) // 输出插件信息 - s.LogPluginInfo(session.Config) + s.LogPluginInfo(session.Config, session) // 执行扫描任务 ExecuteScanTasks(ctx, session, targets, s, ch, wg) @@ -350,12 +327,16 @@ func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSessi // PrepareTargets 准备URL目标列表 func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common.State) []common.HostInfo { + return s.prepareTargets(baseInfo, state, nil) +} + +func (s *WebScanStrategy) prepareTargets(baseInfo common.HostInfo, state *common.State, session *common.ScanSession) []common.HostInfo { var targetInfos []common.HostInfo // 首先从State获取URL目标 urls := state.GetURLs() for _, urlStr := range urls { - urlInfo := s.createTargetFromURL(baseInfo, urlStr) + urlInfo := s.createTargetFromURLWithSession(baseInfo, urlStr, session) if urlInfo != nil { targetInfos = append(targetInfos, *urlInfo) } @@ -363,7 +344,7 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common // 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL if len(targetInfos) == 0 && baseInfo.URL != "" { - urlInfo := s.createTargetFromURL(baseInfo, baseInfo.URL) + urlInfo := s.createTargetFromURLWithSession(baseInfo, baseInfo.URL, session) if urlInfo != nil { targetInfos = append(targetInfos, *urlInfo) } @@ -374,6 +355,10 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common // createTargetFromURL 从URL创建目标信息 func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo { + return s.createTargetFromURLWithSession(baseInfo, urlStr, nil) +} + +func (s *WebScanStrategy) createTargetFromURLWithSession(baseInfo common.HostInfo, urlStr string, session *common.ScanSession) *common.HostInfo { // 确保URL包含协议头 if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { urlStr = "http://" + urlStr @@ -382,7 +367,7 @@ func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr s // 解析URL获取Host和Port信息 parsedURL, err := url.Parse(urlStr) if err != nil { - common.LogError(i18n.Tr("url_parse_failed", urlStr, err)) + session.LogError(i18n.Tr("url_parse_failed", urlStr, err)) return nil } diff --git a/core/web_scanner_test.go b/core/web_scanner_test.go index ae5ae5e..1597a73 100644 --- a/core/web_scanner_test.go +++ b/core/web_scanner_test.go @@ -25,7 +25,6 @@ web_scanner_test.go - WebScanner核心逻辑测试 4. 指纹缓存 - SetFingerprints, GetFingerprints 不测试的部分(需要集成测试): -- createHTTPClient - 依赖全局配置 - tryHTTP, DetectHTTPServiceOnly - 网络IO - Execute - 完整流程 @@ -765,3 +764,57 @@ func TestDetectHTTPScheme(t *testing.T) { } }) } + +func TestCreateHTTPClientUsesPerSessionProxy(t *testing.T) { + cfgA := common.NewConfig() + cfgA.Network.WebTimeout = time.Second + cfgA.Network.HTTPProxy = "http://127.0.0.1:18080" + sessionA := common.NewScanSession(cfgA, common.NewState(), &common.FlagVars{}) + + cfgB := common.NewConfig() + cfgB.Network.WebTimeout = time.Second + cfgB.Network.HTTPProxy = "http://127.0.0.1:28080" + sessionB := common.NewScanSession(cfgB, common.NewState(), &common.FlagVars{}) + + clientA := createHTTPClient(cfgA, sessionA) + clientB := createHTTPClient(cfgB, sessionB) + if clientA == clientB { + t.Fatal("createHTTPClient reused a process-wide client") + } + + proxyA := proxyForTest(t, clientA) + proxyB := proxyForTest(t, clientB) + if proxyA == proxyB { + t.Fatalf("proxy URLs should be per config, both were %q", proxyA) + } + if proxyA != "http://127.0.0.1:18080" { + t.Fatalf("proxyA = %q, want http://127.0.0.1:18080", proxyA) + } + if proxyB != "http://127.0.0.1:28080" { + t.Fatalf("proxyB = %q, want http://127.0.0.1:28080", proxyB) + } +} + +func proxyForTest(t *testing.T, client *http.Client) string { + t.Helper() + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("client transport is not *http.Transport") + } + if transport.Proxy == nil { + t.Fatal("client proxy is nil") + } + req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) + if err != nil { + t.Fatal(err) + } + proxyURL, err := transport.Proxy(req) + if err != nil { + t.Fatal(err) + } + if proxyURL == nil { + t.Fatal("proxy URL is nil") + } + return proxyURL.String() +}