mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
isolate scan strategy runtime state
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+17
-18
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+20
-35
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user