isolate scan strategy runtime state

This commit is contained in:
ZacharyZcR
2026-05-18 16:33:47 +08:00
parent adb3ac5b74
commit 13f7997d16
6 changed files with 109 additions and 72 deletions
+6 -7
View File
@@ -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) { func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
// 验证扫描目标(需要同时检查 -h 和 -hf 参数) // 验证扫描目标(需要同时检查 -h 和 -hf 参数)
if info.Host == "" && session.Params.HostsFile == "" { if info.Host == "" && session.Params.HostsFile == "" {
common.LogError(i18n.GetText("parse_error_target_empty")) session.LogError(i18n.GetText("parse_error_target_empty"))
return return
} }
@@ -65,7 +65,7 @@ func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSes
s.performAliveScan(ctx, info, session) s.performAliveScan(ctx, info, session)
// 输出统计信息 // 输出统计信息
s.outputStats() s.outputStats(session)
} }
// performAliveScan 执行存活探测 // 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) hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
if err != nil { if err != nil {
common.LogError(i18n.Tr("parse_target_failed", err)) session.LogError(i18n.Tr("parse_target_failed", err))
return return
} }
if len(hosts) == 0 { if len(hosts) == 0 {
common.LogError(i18n.GetText("parse_error_no_hosts")) session.LogError(i18n.GetText("parse_error_no_hosts"))
return return
} }
@@ -87,7 +87,6 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho
s.stats.AliveHosts = 0 s.stats.AliveHosts = 0
s.stats.DeadHosts = 0 s.stats.DeadHosts = 0
// 执行存活检测 // 执行存活检测
aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测 aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测
@@ -103,10 +102,10 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho
} }
// outputStats 输出统计信息(精简版) // outputStats 输出统计信息(精简版)
func (s *AliveScanStrategy) outputStats() { func (s *AliveScanStrategy) outputStats(session *common.ScanSession) {
// 只输出存活主机列表,不输出冗余统计 // 只输出存活主机列表,不输出冗余统计
for _, host := range s.stats.AliveHostList { for _, host := range s.stats.AliveHostList {
common.LogSuccess(fmt.Sprintf("alive %s", host)) session.LogSuccess(fmt.Sprintf("alive %s", host))
} }
} }
+5 -4
View File
@@ -170,7 +170,7 @@ func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomM
} }
// LogPluginInfo 输出插件信息 // LogPluginInfo 输出插件信息
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) { func (b *BaseScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
allPlugins, isCustomMode := b.GetPlugins(config) allPlugins, isCustomMode := b.GetPlugins(config)
var prefix string var prefix string
@@ -189,6 +189,7 @@ func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
_ = allPlugins _ = allPlugins
_ = isCustomMode _ = isCustomMode
_ = prefix _ = prefix
_ = session
} }
// formatPluginList 格式化插件列表(超过5个时精简显示) // formatPluginList 格式化插件列表(超过5个时精简显示)
@@ -205,14 +206,14 @@ func (b *BaseScanStrategy) ValidateConfiguration() error {
} }
// LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示) // LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示)
func (b *BaseScanStrategy) LogScanStart() { func (b *BaseScanStrategy) LogScanStart(session *common.ScanSession) {
// 服务扫描模式下不显示(插件信息已足够说明) // 服务扫描模式下不显示(插件信息已足够说明)
// 仅在本地/Web等特殊模式下显示 // 仅在本地/Web等特殊模式下显示
switch b.filterType { switch b.filterType {
case FilterLocal: case FilterLocal:
common.LogInfo(i18n.GetText("start_local_scan")) session.LogInfo(i18n.GetText("start_local_scan"))
case FilterWeb: case FilterWeb:
common.LogInfo(i18n.GetText("start_web_scan")) session.LogInfo(i18n.GetText("start_web_scan"))
} }
} }
+7 -7
View File
@@ -22,12 +22,12 @@ func NewLocalScanStrategy() *LocalScanStrategy {
} }
// LogPluginInfo 重写以只显示通过-local指定的插件 // LogPluginInfo 重写以只显示通过-local指定的插件
func (s *LocalScanStrategy) LogPluginInfo(config *common.Config) { func (s *LocalScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
localPlugin := config.LocalPlugin localPlugin := config.LocalPlugin
if localPlugin != "" { if localPlugin != "" {
common.LogInfo(i18n.Tr("local_plugin_info", localPlugin)) session.LogInfo(i18n.Tr("local_plugin_info", localPlugin))
} else { } 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 config := session.Config
// 输出扫描开始信息 // 输出扫描开始信息
s.LogScanStart() s.LogScanStart(session)
// 验证插件配置 // 验证插件配置
if err := s.ValidateConfiguration(); err != nil { if err := s.ValidateConfiguration(); err != nil {
common.LogError(err.Error()) session.LogError(err.Error())
return return
} }
// 验证本地插件是否存在 // 验证本地插件是否存在
if config.LocalPlugin != "" { if config.LocalPlugin != "" {
if !plugins.Exists(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 return
} }
} }
// 输出插件信息 // 输出插件信息
s.LogPluginInfo(config) s.LogPluginInfo(config, session)
// 准备目标(本地扫描通常只有一个目标,即本机) // 准备目标(本地扫描通常只有一个目标,即本机)
targets := s.PrepareTargets(info) targets := s.PrepareTargets(info)
+17 -18
View File
@@ -25,27 +25,27 @@ func NewServiceScanStrategy() *ServiceScanStrategy {
} }
// LogPluginInfo 重写以提供基于端口的插件过滤 // LogPluginInfo 重写以提供基于端口的插件过滤
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) { func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
// 需要从命令行参数获取端口信息来进行过滤 // 需要从命令行参数获取端口信息来进行过滤
// 如果没有指定端口,使用默认端口进行过滤显示 // 如果没有指定端口,使用默认端口进行过滤显示
ports := config.Target.Ports ports := config.Target.Ports
if ports == "" || ports == "all" { if ports == "" || ports == "all" {
// 默认端口扫描:显示所有插件 // 默认端口扫描:显示所有插件
s.BaseScanStrategy.LogPluginInfo(config) s.BaseScanStrategy.LogPluginInfo(config, session)
} else { } else {
// 指定端口扫描:只显示匹配的插件 // 指定端口扫描:只显示匹配的插件
s.showPluginsForSpecifiedPorts(config) s.showPluginsForSpecifiedPorts(config, session)
} }
} }
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件 // showPluginsForSpecifiedPorts 显示指定端口的匹配插件
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config) { func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config, session *common.ScanSession) {
allPlugins, isCustomMode := s.GetPlugins(config) allPlugins, isCustomMode := s.GetPlugins(config)
// 解析端口 // 解析端口
ports := s.parsePortList(config.Target.Ports) ports := s.parsePortList(config.Target.Ports)
if len(ports) == 0 { if len(ports) == 0 {
s.BaseScanStrategy.LogPluginInfo(config) s.BaseScanStrategy.LogPluginInfo(config, session)
return return
} }
@@ -71,12 +71,12 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config
if len(applicablePlugins) > 0 { if len(applicablePlugins) > 0 {
pluginStr := formatPluginList(applicablePlugins) pluginStr := formatPluginList(applicablePlugins)
if isCustomMode { if isCustomMode {
common.LogInfo(i18n.Tr("service_plugin_custom", pluginStr)) session.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
} else { } else {
common.LogInfo(i18n.Tr("service_plugin_info", pluginStr)) session.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
} }
} else { } 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 参数) // 验证扫描目标(需要同时检查 -h 和 -hf 参数)
if info.Host == "" && session.Params.HostsFile == "" { if info.Host == "" && session.Params.HostsFile == "" {
common.LogError(i18n.GetText("parse_error_target_empty")) session.LogError(i18n.GetText("parse_error_target_empty"))
return return
} }
// 输出扫描开始信息 // 输出扫描开始信息
s.LogScanStart() s.LogScanStart(session)
// 验证插件配置 // 验证插件配置
if err := s.ValidateConfiguration(); err != nil { if err := s.ValidateConfiguration(); err != nil {
common.LogError(err.Error()) session.LogError(err.Error())
return return
} }
// 输出插件信息(重写以提供端口过滤) // 输出插件信息(重写以提供端口过滤)
s.LogPluginInfo(config) s.LogPluginInfo(config, session)
// 执行主机扫描流程 // 执行主机扫描流程
s.performHostScan(ctx, session, info, ch, wg) 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) hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
if err != nil { 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 return
} }
// 主机存活检测 // 主机存活检测
if s.shouldPerformLivenessCheck(hosts, config) { if s.shouldPerformLivenessCheck(hosts, config) {
hosts = CheckLive(ctx, hosts, false, session) 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 { 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) targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session)
if err != nil { if err != nil {
common.LogError(err.Error()) session.LogError(err.Error())
return nil return nil
} }
return targetInfos return targetInfos
@@ -291,7 +291,7 @@ func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput str
// 主机存活检测 // 主机存活检测
if s.shouldPerformLivenessCheck(hosts, config) { if s.shouldPerformLivenessCheck(hosts, config) {
hosts = CheckLive(ctx, hosts, false, session) 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() hostPorts := state.GetHostPorts()
if len(hostPorts) > 0 { if len(hostPorts) > 0 {
alivePorts = mergeHostPorts(alivePorts, hostPorts) alivePorts = mergeHostPorts(alivePorts, hostPorts)
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts))) session.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
state.ClearHostPorts() state.ClearHostPorts()
} }
@@ -390,4 +390,3 @@ func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo comm
return infos return infos
} }
+20 -35
View File
@@ -16,29 +16,6 @@ import (
gmtls "github.com/tjfoc/gmsm/gmtls" 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兼容 // WebPortDetector 简化的Web检测器 - 保持API兼容
type WebPortDetector struct{} type WebPortDetector struct{}
@@ -91,7 +68,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config, session *com
} }
// TLS和GM TLS都失败,尝试HTTP // TLS和GM TLS都失败,尝试HTTP
client := getSharedHTTPClient(config) client := createHTTPClient(config, session)
// 使用HEAD请求(更轻量) // 使用HEAD请求(更轻量)
httpURL := fmt.Sprintf("http://%s", addr) 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和代理 // 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 timeout := config.Network.WebTimeout
// 创建基础Transport,配置连接和 TLS 超时 // 创建基础Transport,配置连接和 TLS 超时
@@ -128,14 +105,14 @@ func createHTTPClient(config *common.Config) *http.Client {
if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil { if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil {
transport.Proxy = http.ProxyURL(proxyURL) transport.Proxy = http.ProxyURL(proxyURL)
} else { } else {
common.LogError(i18n.Tr("http_proxy_config_error", err)) session.LogError(i18n.Tr("http_proxy_config_error", err))
} }
} else if networkConfig.Socks5Proxy != "" { } else if networkConfig.Socks5Proxy != "" {
// 使用SOCKS5代理 - 需要特殊处理 // 使用SOCKS5代理 - 需要特殊处理
if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil { if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil {
// SOCKS5代理需要使用代理管理器 // SOCKS5代理需要使用代理管理器
// 这里先记录警告,建议使用HTTP代理进行Web检测 // 这里先记录警告,建议使用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 return false
} }
client := getSharedHTTPClient(config) client := createHTTPClient(config, session)
// 尝试HTTP // 尝试HTTP
if w.tryHTTP(client, host, port, "http") { if w.tryHTTP(client, host, port, "http") {
@@ -330,19 +307,19 @@ func (s *WebScanStrategy) Description() string {
// Execute 执行Web扫描策略 // Execute 执行Web扫描策略
func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { 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 { if err := s.ValidateConfiguration(); err != nil {
common.LogError(err.Error()) session.LogError(err.Error())
return return
} }
// 准备URL目标 // 准备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) ExecuteScanTasks(ctx, session, targets, s, ch, wg)
@@ -350,12 +327,16 @@ func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSessi
// PrepareTargets 准备URL目标列表 // PrepareTargets 准备URL目标列表
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common.State) []common.HostInfo { 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 var targetInfos []common.HostInfo
// 首先从State获取URL目标 // 首先从State获取URL目标
urls := state.GetURLs() urls := state.GetURLs()
for _, urlStr := range urls { for _, urlStr := range urls {
urlInfo := s.createTargetFromURL(baseInfo, urlStr) urlInfo := s.createTargetFromURLWithSession(baseInfo, urlStr, session)
if urlInfo != nil { if urlInfo != nil {
targetInfos = append(targetInfos, *urlInfo) targetInfos = append(targetInfos, *urlInfo)
} }
@@ -363,7 +344,7 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common
// 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL // 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL
if len(targetInfos) == 0 && baseInfo.URL != "" { if len(targetInfos) == 0 && baseInfo.URL != "" {
urlInfo := s.createTargetFromURL(baseInfo, baseInfo.URL) urlInfo := s.createTargetFromURLWithSession(baseInfo, baseInfo.URL, session)
if urlInfo != nil { if urlInfo != nil {
targetInfos = append(targetInfos, *urlInfo) targetInfos = append(targetInfos, *urlInfo)
} }
@@ -374,6 +355,10 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common
// createTargetFromURL 从URL创建目标信息 // createTargetFromURL 从URL创建目标信息
func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo { 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包含协议头 // 确保URL包含协议头
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
urlStr = "http://" + urlStr urlStr = "http://" + urlStr
@@ -382,7 +367,7 @@ func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr s
// 解析URL获取Host和Port信息 // 解析URL获取Host和Port信息
parsedURL, err := url.Parse(urlStr) parsedURL, err := url.Parse(urlStr)
if err != nil { if err != nil {
common.LogError(i18n.Tr("url_parse_failed", urlStr, err)) session.LogError(i18n.Tr("url_parse_failed", urlStr, err))
return nil return nil
} }
+54 -1
View File
@@ -25,7 +25,6 @@ web_scanner_test.go - WebScanner核心逻辑测试
4. 指纹缓存 - SetFingerprints, GetFingerprints 4. 指纹缓存 - SetFingerprints, GetFingerprints
不测试的部分(需要集成测试): 不测试的部分(需要集成测试):
- createHTTPClient - 依赖全局配置
- tryHTTP, DetectHTTPServiceOnly - 网络IO - tryHTTP, DetectHTTPServiceOnly - 网络IO
- Execute - 完整流程 - 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()
}