diff --git a/common/config_builder.go b/common/config_builder.go new file mode 100644 index 0000000..32fedce --- /dev/null +++ b/common/config_builder.go @@ -0,0 +1,308 @@ +package common + +import ( + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + + "github.com/shadow1ng/fscan/common/config" + "github.com/shadow1ng/fscan/common/parsers" +) + +/* +config_builder.go - 统一配置构建入口 + +从 FlagVars 直接构建 Config 和 State,消除中间层。 +*/ + +// BuildConfig 从 FlagVars 构建完整的 Config 和 State +// 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables() +func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) { + // 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags) + cfg := BuildConfigFromFlags(fv) + + // 2. 创建 State + state := NewState() + + // 3. 解析凭据 + if err := parseCredentials(fv, cfg); err != nil { + return nil, nil, fmt.Errorf("凭据解析失败: %w", err) + } + + // 4. 解析目标(主机、端口、URL) + if err := parseTargets(fv, info, cfg, state); err != nil { + return nil, nil, fmt.Errorf("目标解析失败: %w", err) + } + + // 5. 应用日志级别 + applyLogLevelFromConfig(fv) + + return cfg, state, nil +} + +// ============================================================================= +// 凭据解析 +// ============================================================================= + +func parseCredentials(fv *FlagVars, cfg *Config) error { + // 解析用户名 + usernames := parseUsernames(fv) + if len(usernames) > 0 { + for serviceName := range cfg.Credentials.Userdict { + cfg.Credentials.Userdict[serviceName] = usernames + } + } + + // 解析密码 + passwords := parsePasswords(fv) + if len(passwords) > 0 { + cfg.Credentials.Passwords = passwords + } + + // 解析用户密码对 + pairs, err := parseUserPassPairs(fv) + if err != nil { + return err + } + if len(pairs) > 0 { + cfg.Credentials.UserPassPairs = pairs + } + + // 解析哈希 + hashValues, hashBytes, err := parseHashes(fv) + if err != nil { + return err + } + if len(hashValues) > 0 { + cfg.Credentials.HashValues = hashValues + cfg.Credentials.HashBytes = hashBytes + } + + return nil +} + +func parseUsernames(fv *FlagVars) []string { + var usernames []string + + // 命令行用户名 + if fv.Username != "" { + for _, u := range strings.Split(fv.Username, ",") { + u = strings.TrimSpace(u) + if u != "" { + usernames = append(usernames, u) + } + } + } + + // 从文件读取 + if fv.UsersFile != "" { + if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil { + usernames = append(usernames, lines...) + } + } + + // 额外用户名 + if fv.AddUsers != "" { + for _, u := range strings.Split(fv.AddUsers, ",") { + u = strings.TrimSpace(u) + if u != "" { + usernames = append(usernames, u) + } + } + } + + return removeDuplicate(usernames) +} + +func parsePasswords(fv *FlagVars) []string { + var passwords []string + + // 命令行密码 + if fv.Password != "" { + for _, p := range strings.Split(fv.Password, ",") { + passwords = append(passwords, p) + } + } + + // 从文件读取 + if fv.PasswordsFile != "" { + if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil { + passwords = append(passwords, lines...) + } + } + + // 额外密码 + if fv.AddPasswords != "" { + for _, p := range strings.Split(fv.AddPasswords, ",") { + passwords = append(passwords, p) + } + } + + return removeDuplicate(passwords) +} + +func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) { + var pairs []config.CredentialPair + + // 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个) + if fv.Username != "" && fv.Password != "" && + !strings.Contains(fv.Username, ",") && !strings.Contains(fv.Password, ",") && + fv.UsersFile == "" && fv.PasswordsFile == "" && fv.UserPassFile == "" { + pairs = append(pairs, config.CredentialPair{ + Username: strings.TrimSpace(fv.Username), + Password: fv.Password, + }) + return pairs, nil + } + + // 从文件读取用户密码对 + if fv.UserPassFile != "" { + filePairs, err := parsers.ParseUserPassFile(fv.UserPassFile) + if err != nil { + return nil, err + } + pairs = append(pairs, filePairs...) + } + + return pairs, nil +} + +func parseHashes(fv *FlagVars) ([]string, [][]byte, error) { + var hashValues []string + var hashBytes [][]byte + + // 命令行哈希 + if fv.HashValue != "" { + hash := strings.TrimSpace(fv.HashValue) + if len(hash) == 32 { + hashValues = append(hashValues, hash) + if hashByte, err := hex.DecodeString(hash); err == nil { + hashBytes = append(hashBytes, hashByte) + } + } + } + + // 从文件读取 + if fv.HashFile != "" { + fileHashes, fileHashBytes, err := parsers.ParseHashFile(fv.HashFile) + if err != nil { + return nil, nil, err + } + hashValues = append(hashValues, fileHashes...) + hashBytes = append(hashBytes, fileHashBytes...) + } + + return hashValues, hashBytes, nil +} + +// ============================================================================= +// 目标解析 +// ============================================================================= + +func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error { + // 检查是否为 host:port 格式 + ports := fv.Ports + if info.Host != "" && strings.Contains(info.Host, ":") { + if _, portStr, err := net.SplitHostPort(info.Host); err == nil { + if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 { + // 有效的 host:port 格式 + state.SetHostPorts([]string{info.Host}) + ports = "" // 清空端口,避免双重扫描 + } + } + } + + // 解析 URL + urls := parseURLs(fv) + if len(urls) > 0 { + state.SetURLs(urls) + if info.URL == "" && len(urls) == 1 { + info.URL = urls[0] + } + } + + // 更新端口配置 + if ports != "" { + cfg.Target.Ports = ports + } + + return nil +} + +func parseURLs(fv *FlagVars) []string { + var urls []string + + // 命令行 URL + if fv.TargetURL != "" { + for _, u := range strings.Split(fv.TargetURL, ",") { + u = strings.TrimSpace(u) + if u != "" { + urls = append(urls, normalizeURL(u)) + } + } + } + + // 从文件读取 + if fv.URLsFile != "" { + if lines, err := parsers.ReadLinesFromFile(fv.URLsFile); err == nil { + for _, line := range lines { + urls = append(urls, normalizeURL(line)) + } + } + } + + return removeDuplicate(urls) +} + +func normalizeURL(rawURL string) string { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return rawURL + } + if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { + return "http://" + rawURL + } + return rawURL +} + +// ============================================================================= +// 日志级别应用 +// ============================================================================= + +func applyLogLevelFromConfig(fv *FlagVars) { + if fv.LogLevel == "" { + return + } + // 调用已有的 applyLogLevel 函数 + applyLogLevel() +} + +// ============================================================================= +// 辅助函数 +// ============================================================================= + +func removeDuplicate(old []string) []string { + if len(old) <= 1 { + return old + } + + temp := make(map[string]struct{}, len(old)) + result := make([]string, 0, len(old)) + + for _, item := range old { + if _, exists := temp[item]; !exists { + temp[item] = struct{}{} + result = append(result, item) + } + } + + return result +} + +// ============================================================================= +// 保留 BuildConfigFromFlags 的原有实现(从 flag_config.go 移入) +// ============================================================================= + +// BuildConfigFromFlags 已在 flag_config.go 中定义,这里不重复 diff --git a/common/initialize.go b/common/initialize.go index 71c2573..f879463 100644 --- a/common/initialize.go +++ b/common/initialize.go @@ -7,7 +7,8 @@ import ( /* initialize.go - 统一初始化入口 -将分散的初始化步骤整合为单一入口,简化 main.go。 +简化后的流程: +命令行 → FlagVars → BuildConfig() → Config + State */ // InitResult 初始化结果 @@ -18,60 +19,22 @@ type InitResult struct { } // Initialize 统一初始化函数 -// 封装 Parse → InitGlobalConfigAndState → InitOutput 流程 -// 返回可直接使用的 Config 和 State 对象 +// 封装 BuildConfig → InitOutput 流程 func Initialize(info *HostInfo) (*InitResult, error) { - // 初始化日志系统 + // 1. 初始化日志系统 InitLogger() - // 解析和验证参数(会更新 globalConfig 的凭据信息) - if err := Parse(info); err != nil { - return nil, fmt.Errorf("参数解析失败: %w", err) + // 2. 从 FlagVars 构建 Config 和 State + cfg, state, err := BuildConfig(GetFlagVars(), info) + if err != nil { + return nil, fmt.Errorf("配置构建失败: %w", err) } - // 获取 Parse 更新过的凭据信息 - parsedCreds := GetGlobalConfig().Credentials - - // 从 FlagVars 构建 Config(新架构) - cfg := BuildConfigFromFlags(flagVars) - - // 关键修复:获取 Parse 阶段设置的全局状态数据 - // Parse 通过 updateGlobalVariables 将 URLs 和 HostPorts 设置到了全局状态 - // 需要保留这些数据到新状态中 - oldGlobalState := GetGlobalState() - state := NewState() - - // 迁移 Parse 阶段设置的目标数据 - if urls := oldGlobalState.GetURLs(); len(urls) > 0 { - state.SetURLs(urls) - } - if hostPorts := oldGlobalState.GetHostPorts(); len(hostPorts) > 0 { - state.SetHostPorts(hostPorts) - } - - // 关键修复:应用 Parse 解析的凭据结果到新 Config - // Parse 会根据 -user/-pwd/-usera/-pwda 等参数更新凭据 - if len(parsedCreds.UserPassPairs) > 0 { - cfg.Credentials.UserPassPairs = parsedCreds.UserPassPairs - } - if len(parsedCreds.Userdict) > 0 { - cfg.Credentials.Userdict = parsedCreds.Userdict - } - if len(parsedCreds.Passwords) > 0 { - cfg.Credentials.Passwords = parsedCreds.Passwords - } - if len(parsedCreds.HashValues) > 0 { - cfg.Credentials.HashValues = parsedCreds.HashValues - } - if len(parsedCreds.HashBytes) > 0 { - cfg.Credentials.HashBytes = parsedCreds.HashBytes - } - - // 设置全局实例 + // 3. 设置全局实例 SetGlobalConfig(cfg) SetGlobalState(state) - // 初始化输出系统 + // 4. 初始化输出系统 if err := InitOutput(); err != nil { return nil, fmt.Errorf("输出初始化失败: %w", err) } @@ -120,7 +83,6 @@ func ValidateExclusiveParams(info *HostInfo) error { } // Cleanup 清理资源 -// 应该在程序退出前调用 func Cleanup() error { return CloseOutput() } diff --git a/common/parse.go b/common/parse.go index 83991c2..008b855 100644 --- a/common/parse.go +++ b/common/parse.go @@ -1,364 +1,22 @@ package common import ( - "errors" - "fmt" - "net" "strconv" "strings" - "sync" - "time" - "github.com/shadow1ng/fscan/common/i18n" "github.com/shadow1ng/fscan/common/logging" - "github.com/shadow1ng/fscan/common/parsers" ) -// ParsedConfiguration 解析后的完整配置(兼容旧代码) -type ParsedConfiguration struct { - *parsers.ParsedConfig -} +/* +parse.go - 解析相关工具函数 -// Parser 主解析器 -type Parser struct { - mu sync.RWMutex - fileReader *parsers.FileReader - credentialParser *parsers.CredentialParser - targetParser *parsers.TargetParser - networkParser *parsers.NetworkParser - validationParser *parsers.ValidationParser - options *parsers.ParserOptions - initialized bool -} +重构后只保留: +- RemoveDuplicate - 字符串去重 +- applyLogLevel - 日志级别应用 +- 辅助函数 +*/ -// NewParser 创建新的解析器实例 -func NewParser(options *parsers.ParserOptions) *Parser { - if options == nil { - options = parsers.DefaultParserOptions() - } - - // 创建文件读取器 - fileReader := parsers.NewFileReader(nil) - - // 创建各个子解析器 - credentialParser := parsers.NewCredentialParser(fileReader, nil) - targetParser := parsers.NewTargetParser(fileReader, nil) - networkParser := parsers.NewNetworkParser(nil) - validationParser := parsers.NewValidationParser(nil) - - return &Parser{ - fileReader: fileReader, - credentialParser: credentialParser, - targetParser: targetParser, - networkParser: networkParser, - validationParser: validationParser, - options: options, - initialized: true, - } -} - -// 全局解析器实例 -var globalParser *Parser -var parseOnce sync.Once - -// getGlobalParser 获取全局解析器实例 -func getGlobalParser() *Parser { - parseOnce.Do(func() { - globalParser = NewParser(nil) - }) - return globalParser -} - -// Parse 主解析函数 - 保持与原版本兼容的接口 -func Parse(Info *HostInfo) error { - // 首先应用LogLevel配置到日志系统 - applyLogLevel() - - parser := getGlobalParser() - fv := GetFlagVars() // 从 FlagVars 获取命令行参数 - - // 检查是否为host:port格式,如果是则清空端口字段避免双重扫描 - ports := fv.Ports - if Info.Host != "" && strings.Contains(Info.Host, ":") { - if _, portStr, err := net.SplitHostPort(Info.Host); err == nil { - if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 { - // 这是有效的host:port格式,清空端口字段 - ports = "" - fv.Ports = "" // 更新 FlagVars,避免插件适用性检查使用默认端口 - } - } - } - - // 构建输入参数(从 FlagVars 读取) - input := &AllInputs{ - Credential: &parsers.CredentialInput{ - Username: fv.Username, - Password: fv.Password, - AddUsers: fv.AddUsers, - AddPasswords: fv.AddPasswords, - HashValue: fv.HashValue, - SSHKeyPath: fv.SSHKeyPath, - Domain: fv.Domain, - UsersFile: fv.UsersFile, - PasswordsFile: fv.PasswordsFile, - UserPassFile: fv.UserPassFile, - HashFile: fv.HashFile, - }, - Target: &parsers.TargetInput{ - Host: Info.Host, - HostsFile: fv.HostsFile, - ExcludeHosts: fv.ExcludeHosts, - ExcludeHostsFile: fv.ExcludeHostsFile, - Ports: ports, - PortsFile: fv.PortsFile, - AddPorts: fv.AddPorts, - ExcludePorts: fv.ExcludePorts, - TargetURL: fv.TargetURL, - URLsFile: fv.URLsFile, - HostPort: nil, // 由解析器填充 - LocalMode: fv.LocalPlugin != "", - }, - Network: &parsers.NetworkInput{ - HTTPProxy: fv.HTTPProxy, - Socks5Proxy: fv.Socks5Proxy, - Timeout: fv.TimeoutSec, - WebTimeout: fv.WebTimeout, - DisablePing: fv.DisablePing, - DNSLog: fv.DNSLog, - UserAgent: fv.UserAgent, - Cookie: fv.Cookie, - }, - } - - // 执行解析 - result, err := parser.ParseAll(input) - if err != nil { - return fmt.Errorf("配置解析失败: %w", err) - } - - // 检查解析结果中的错误(关键修复:防止静默失败) - if !result.Success || len(result.Errors) > 0 { - LogError("配置解析失败,发现以下错误:") - for i, parseErr := range result.Errors { - LogError(fmt.Sprintf(" [%d] %v", i+1, parseErr)) - } - return fmt.Errorf("配置解析失败,共%d个错误", len(result.Errors)) - } - - // 更新全局变量以保持兼容性 - if err := updateGlobalVariables(result.Config, Info); err != nil { - return fmt.Errorf("更新全局变量失败: %w", err) - } - - // 报告警告 - for _, warning := range result.Warnings { - LogBase(warning) - } - - // 显示解析结果摘要 - showParseSummary(result.Config) - - return nil -} - -// AllInputs 所有输入参数的集合 -type AllInputs struct { - Credential *parsers.CredentialInput `json:"credential"` - Target *parsers.TargetInput `json:"target"` - Network *parsers.NetworkInput `json:"network"` -} - -// ParseAll 解析所有配置 -func (p *Parser) ParseAll(input *AllInputs) (*parsers.ParseResult, error) { - if input == nil { - return nil, errors.New(i18n.GetText("parse_error_empty_input")) - } - - p.mu.Lock() - defer p.mu.Unlock() - - if !p.initialized { - return nil, errors.New(i18n.GetText("parse_error_parser_not_init")) - } - - startTime := time.Now() - result := &parsers.ParseResult{ - Config: &parsers.ParsedConfig{}, - Success: true, - } - - var allErrors []error - var allWarnings []string - - // 解析凭据配置 - if input.Credential != nil { - credResult, err := p.credentialParser.Parse(input.Credential, p.options) - if err != nil { - allErrors = append(allErrors, fmt.Errorf("凭据解析失败: %w", err)) - } else { - result.Config.Credentials = credResult.Config.Credentials - allErrors = append(allErrors, credResult.Errors...) - allWarnings = append(allWarnings, credResult.Warnings...) - } - } - - // 解析目标配置 - if input.Target != nil { - targetResult, err := p.targetParser.Parse(input.Target, p.options) - if err != nil { - allErrors = append(allErrors, fmt.Errorf("目标解析失败: %w", err)) - } else { - result.Config.Targets = targetResult.Config.Targets - allErrors = append(allErrors, targetResult.Errors...) - allWarnings = append(allWarnings, targetResult.Warnings...) - } - } - - // 解析网络配置 - if input.Network != nil { - networkResult, err := p.networkParser.Parse(input.Network, p.options) - if err != nil { - allErrors = append(allErrors, fmt.Errorf("网络配置解析失败: %w", err)) - } else { - result.Config.Network = networkResult.Config.Network - allErrors = append(allErrors, networkResult.Errors...) - allWarnings = append(allWarnings, networkResult.Warnings...) - } - } - - // 执行验证 - fv := GetFlagVars() - validationInput := &parsers.ValidationInput{ - ScanMode: fv.ScanMode, - LocalMode: fv.LocalPlugin != "", - HasHosts: input.Target != nil && (input.Target.Host != "" || input.Target.HostsFile != ""), - HasURLs: input.Target != nil && (input.Target.TargetURL != "" || input.Target.URLsFile != ""), - HasPorts: input.Target != nil && (input.Target.Ports != "" || input.Target.PortsFile != ""), - HasProxy: input.Network != nil && (input.Network.HTTPProxy != "" || input.Network.Socks5Proxy != ""), - DisablePing: input.Network != nil && input.Network.DisablePing, - HasCredentials: input.Credential != nil && (input.Credential.Username != "" || input.Credential.UsersFile != ""), - } - - validationResult, err := p.validationParser.Parse(validationInput, result.Config, p.options) - if err != nil { - allErrors = append(allErrors, fmt.Errorf("参数验证失败: %w", err)) - } else { - result.Config.Validation = validationResult.Config.Validation - allErrors = append(allErrors, validationResult.Errors...) - allWarnings = append(allWarnings, validationResult.Warnings...) - } - - // 汇总结果 - result.Errors = allErrors - result.Warnings = allWarnings - result.ParseTime = time.Since(startTime) - result.Success = len(allErrors) == 0 - - return result, nil -} - -// updateGlobalVariables 更新运行时数据和FlagVars以保持向后兼容性 -func updateGlobalVariables(config *parsers.ParsedConfig, info *HostInfo) error { - if config == nil { - return nil - } - - fv := GetFlagVars() - - // 更新全局Config的凭据数据 - globalCfg := GetGlobalConfig() - if config.Credentials != nil { - if len(config.Credentials.Usernames) > 0 { - // 更新全局Config中的用户字典 - for serviceName := range globalCfg.Credentials.Userdict { - globalCfg.Credentials.Userdict[serviceName] = config.Credentials.Usernames - } - } - - if len(config.Credentials.Passwords) > 0 { - globalCfg.Credentials.Passwords = config.Credentials.Passwords - } - - if len(config.Credentials.UserPassPairs) > 0 { - globalCfg.Credentials.UserPassPairs = config.Credentials.UserPassPairs - } - - if len(config.Credentials.HashValues) > 0 { - globalCfg.Credentials.HashValues = config.Credentials.HashValues - } - - if len(config.Credentials.HashBytes) > 0 { - globalCfg.Credentials.HashBytes = config.Credentials.HashBytes - } - } - - // 更新目标相关数据 - if config.Targets != nil { - state := GetGlobalState() - - if len(config.Targets.Hosts) > 0 { - // 如果info.Host已经有值,说明解析结果来自info.Host,不需要重复设置 - // 只有当info.Host为空时才设置(如从文件读取的情况) - if info.Host == "" { - info.Host = joinStrings(config.Targets.Hosts, ",") - } - } - - if len(config.Targets.URLs) > 0 { - state.SetURLs(config.Targets.URLs) - // 如果info.Url为空且只有一个URL,将其设置到info.URL - if info.URL == "" && len(config.Targets.URLs) == 1 { - info.URL = config.Targets.URLs[0] - } - } - - if len(config.Targets.Ports) > 0 { - fv.Ports = joinInts(config.Targets.Ports, ",") - } - - if len(config.Targets.ExcludePorts) > 0 { - fv.ExcludePorts = joinInts(config.Targets.ExcludePorts, ",") - } - - if len(config.Targets.HostPorts) > 0 { - state.SetHostPorts(config.Targets.HostPorts) - } - } - - // 更新网络相关FlagVars - if config.Network != nil { - if config.Network.HTTPProxy != "" { - fv.HTTPProxy = config.Network.HTTPProxy - } - - if config.Network.Socks5Proxy != "" { - fv.Socks5Proxy = config.Network.Socks5Proxy - } - - if config.Network.Timeout > 0 { - fv.TimeoutSec = int64(config.Network.Timeout.Seconds()) - } - - if config.Network.WebTimeout > 0 { - fv.WebTimeout = int64(config.Network.WebTimeout.Seconds()) - } - - if config.Network.UserAgent != "" { - fv.UserAgent = config.Network.UserAgent - } - - if config.Network.Cookie != "" { - fv.Cookie = config.Network.Cookie - } - - fv.DisablePing = config.Network.DisablePing - fv.DNSLog = config.Network.EnableDNSLog - } - - return nil -} - -// RemoveDuplicate 去重函数 - 恢复原始高效实现 +// RemoveDuplicate 去重函数 func RemoveDuplicate(old []string) []string { if len(old) <= 1 { return old @@ -377,8 +35,6 @@ func RemoveDuplicate(old []string) []string { return result } -// 辅助函数 - // joinStrings 连接字符串切片 func joinStrings(slice []string, sep string) string { return strings.Join(slice, sep) @@ -396,14 +52,8 @@ func joinInts(slice []int, sep string) string { return strings.Join(strs, sep) } -// showParseSummary 显示解析结果摘要(已精简,不再输出冗余信息) -func showParseSummary(config *parsers.ParsedConfig) { - // 不再输出开局配置信息,减少干扰 -} - -// logLevelMap 日志级别字符串到级别的映射(支持新旧格式) +// logLevelMap 日志级别字符串到级别的映射 var logLevelMap = map[string]logging.LogLevel{ - // 新格式(小写) LogLevelAll: logging.LevelAll, LogLevelError: logging.LevelError, LogLevelBase: logging.LevelBase, @@ -426,16 +76,14 @@ func applyLogLevel() { fv := GetFlagVars() logLevel := fv.LogLevel if logLevel == "" { - return // 使用默认级别 + return } - // 查找日志级别 level, ok := logLevelMap[logLevel] if !ok { - return // 无效的级别,保持默认 + return } - // 更新全局日志管理器的级别 if globalLogger != nil { config := &logging.LoggerConfig{ Level: level, @@ -447,12 +95,7 @@ func applyLogLevel() { } newLogger := logging.NewLogger(config) - - // 设置协调输出函数,使用LogWithProgress newLogger.SetCoordinatedOutput(LogWithProgress) - - // 更新全局日志管理器 globalLogger = newLogger - // status变量已移除,如需获取状态请直接调用newLogger.GetScanStatus() } } diff --git a/common/parsers/constants.go b/common/parsers/constants.go index 0c436f3..a61c68e 100644 --- a/common/parsers/constants.go +++ b/common/parsers/constants.go @@ -2,346 +2,52 @@ package parsers import ( "regexp" - "time" ) /* -constants.go - 解析器系统常量定义 +constants.go - 核心解析器常量 -统一管理common/parsers包中的所有常量,便于查看和编辑。 +精简后只保留 parsers.go 所需的常量。 */ // ============================================================================= -// 默认解析器选项常量 (从Types.go迁移) +// 端口常量 // ============================================================================= const ( - // DefaultEnableConcurrency 解析器默认启用并发 - DefaultEnableConcurrency = true - // DefaultMaxWorkers 默认最大工作线程数 - DefaultMaxWorkers = 4 - // DefaultTimeout 默认超时时间 - DefaultTimeout = 30 * time.Second - // DefaultEnableValidation 默认启用验证 - DefaultEnableValidation = true - // DefaultIgnoreErrors 默认不忽略错误 - DefaultIgnoreErrors = false - // DefaultFileMaxSize 默认文件最大大小100MB - DefaultFileMaxSize = 100 * 1024 * 1024 - // DefaultMaxTargets 默认最大目标数量10K - DefaultMaxTargets = 10000 -) - -// ============================================================================= -// 文件读取器常量 (从FileReader.go迁移) -// ============================================================================= - -const ( - // DefaultMaxCacheSize 默认最大缓存大小 - DefaultMaxCacheSize = 10 - // DefaultEnableCache 默认启用缓存 - DefaultEnableCache = true - // DefaultFileReaderMaxFileSize 文件读取器默认最大文件大小50MB - DefaultFileReaderMaxFileSize = 50 * 1024 * 1024 - // DefaultFileReaderTimeout 文件读取器默认超时时间 - DefaultFileReaderTimeout = 30 * time.Second - // DefaultFileReaderEnableValidation 文件读取器默认启用验证 - DefaultFileReaderEnableValidation = true - // DefaultTrimSpace 默认去除空格 - DefaultTrimSpace = true - // DefaultSkipEmpty 默认跳过空行 - DefaultSkipEmpty = true - // DefaultSkipComments 默认跳过注释 - DefaultSkipComments = true - - // MaxLineLength 单行最大字符数 - MaxLineLength = 1000 - // MaxValidRune 最小有效字符ASCII值 - MaxValidRune = 32 - // TabRune Tab字符 - TabRune = 9 - // NewlineRune 换行符 - NewlineRune = 10 - // CarriageReturnRune 回车符 - CarriageReturnRune = 13 - // CommentPrefix 注释前缀 - CommentPrefix = "#" -) - -// ============================================================================= -// 凭据解析器常量 (从CredentialParser.go迁移) -// ============================================================================= - -const ( - // DefaultMaxUsernameLength 凭据验证限制 - 默认最大用户名长度 - DefaultMaxUsernameLength = 64 - // DefaultMaxPasswordLength 默认最大密码长度 - DefaultMaxPasswordLength = 128 - // DefaultAllowEmptyPasswords 默认允许空密码 - DefaultAllowEmptyPasswords = true - // DefaultValidateHashes 默认验证哈希 - DefaultValidateHashes = true - // DefaultDeduplicateUsers 默认去重用户 - DefaultDeduplicateUsers = true - // DefaultDeduplicatePasswords 默认去重密码 - DefaultDeduplicatePasswords = true - - // HashRegexPattern MD5哈希正则表达式 - HashRegexPattern = `^[a-fA-F0-9]{32}$` - // HashValidationLength 有效哈希长度 - HashValidationLength = 32 - // InvalidUsernameChars 无效用户名字符 - InvalidUsernameChars = "\r\n\t" -) - -// ============================================================================= -// 网络解析器常量 (从NetworkParser.go迁移) -// ============================================================================= - -const ( - // DefaultValidateProxies 网络配置默认值 - 默认验证代理 - DefaultValidateProxies = true - // DefaultAllowInsecure 默认不允许不安全连接 - DefaultAllowInsecure = false - // DefaultNetworkTimeout 默认网络超时时间 - DefaultNetworkTimeout = 30 * time.Second - // DefaultWebTimeout 默认Web超时时间 - DefaultWebTimeout = 10 * time.Second - // DefaultUserAgent 默认用户代理字符串 - DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" - - // MaxTimeoutSeconds 超时限制 - 最大超时5分钟 - MaxTimeoutSeconds = 300 - // MaxWebTimeoutSeconds 最大Web超时2分钟 - MaxWebTimeoutSeconds = 120 - - // MaxUserAgentLength 字符串长度限制 - 最大用户代理长度 - MaxUserAgentLength = 512 - // MaxCookieLength 最大Cookie长度 - MaxCookieLength = 4096 - - // ProxyShortcut1 代理快捷配置 - 快捷方式1 - ProxyShortcut1 = "1" - // ProxyShortcut2 快捷方式2 - ProxyShortcut2 = "2" - // ProxyShortcutHTTP 快捷方式HTTP代理地址 - ProxyShortcutHTTP = "http://127.0.0.1:8080" - // ProxyShortcutSOCKS5 快捷方式SOCKS5代理地址 - ProxyShortcutSOCKS5 = "socks5://127.0.0.1:1080" - - // ProtocolHTTP 协议支持 - HTTP协议 - ProtocolHTTP = "http" - // ProtocolHTTPS HTTPS协议 - ProtocolHTTPS = "https" - // ProtocolSOCKS5 SOCKS5协议 - ProtocolSOCKS5 = "socks5" - // ProtocolPrefix 协议前缀分隔符 - ProtocolPrefix = "://" - // SOCKS5Prefix SOCKS5协议前缀 - SOCKS5Prefix = "socks5://" - // HTTPPrefix HTTP协议前缀 - HTTPPrefix = "http://" - - // MinPort 端口范围 - 最小端口号 + // MinPort 最小端口号 MinPort = 1 // MaxPort 最大端口号 MaxPort = 65535 - - // InvalidUserAgentChars 无效字符集 - 用户代理中的非法字符 - InvalidUserAgentChars = "\r\n\t" ) -// GetCommonBrowsers 获取常见浏览器标识列表 -func GetCommonBrowsers() []string { - return []string{ - "Mozilla", "Chrome", "Safari", "Firefox", "Edge", "Opera", - "AppleWebKit", "Gecko", "Trident", "Presto", - } -} - // ============================================================================= -// 目标解析器常量 (从TargetParser.go迁移) +// IP/主机解析常量 // ============================================================================= const ( - // DefaultTargetMaxTargets 目标解析器默认配置 - 默认最大目标数量 - DefaultTargetMaxTargets = 10000 - // DefaultMaxPortRange 默认最大端口范围(支持全端口扫描) - DefaultMaxPortRange = 65535 - // DefaultAllowPrivateIPs 默认允许私有IP - DefaultAllowPrivateIPs = true - // DefaultAllowLoopback 默认允许回环地址 - DefaultAllowLoopback = true - // DefaultValidateURLs 默认验证URL - DefaultValidateURLs = true - // DefaultResolveDomains 默认解析域名 - DefaultResolveDomains = false - - // IPv4RegexPattern 正则表达式模式 - IPv4地址正则 - IPv4RegexPattern = `^(\d{1,3}\.){3}\d{1,3}$` - // PortRangeRegexPattern 端口范围正则 - PortRangeRegexPattern = `^(\d+)(-(\d+))?$` - // URLValidationRegexPattern URL验证正则 - URLValidationRegexPattern = `^https?://[^\s]+$` - // DomainRegexPattern 域名正则 - DomainRegexPattern = `^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$` - // CookieRegexPattern Cookie正则 - CookieRegexPattern = `^[^=;\s]+(=[^;\s]*)?(\s*;\s*[^=;\s]+(=[^;\s]*)?)*$` - - // MaxIPv4OctetValue IP地址限制 - IPv4八位组最大值 - MaxIPv4OctetValue = 255 - // IPv4OctetCount IPv4八位组数量 - IPv4OctetCount = 4 - // MaxDomainLength 域名最大长度 - MaxDomainLength = 253 - - // PrivateNetwork192 CIDR网段简写 - 192私有网络前缀 - PrivateNetwork192 = "192" - // PrivateNetwork172 172私有网络前缀 - PrivateNetwork172 = "172" - // PrivateNetwork10 10私有网络前缀 - PrivateNetwork10 = "10" - // PrivateNetwork192CIDR 192私有网络CIDR - PrivateNetwork192CIDR = "192.168.0.0/16" - // PrivateNetwork172CIDR 172私有网络CIDR - PrivateNetwork172CIDR = "172.16.0.0/12" - // PrivateNetwork10CIDR 10私有网络CIDR - PrivateNetwork10CIDR = "10.0.0.0/8" - - // Private172StartSecondOctet 私有网络范围 - 172网段起始第二段 - Private172StartSecondOctet = 16 - // Private172EndSecondOctet 172网段结束第二段 - Private172EndSecondOctet = 31 - // Private192SecondOctet 192网段第二段 - Private192SecondOctet = 168 - - // Subnet8SamplingStep /8网段采样配置 - 采样步长 - Subnet8SamplingStep = 32 - // Subnet8ThirdOctetStep 第三段步长 - Subnet8ThirdOctetStep = 10 - - // IPFirstOctetShift IP地址计算位移 - 第一段位移 - IPFirstOctetShift = 24 - // IPSecondOctetShift 第二段位移 - IPSecondOctetShift = 16 - // IPThirdOctetShift 第三段位移 - IPThirdOctetShift = 8 - // IPOctetMask 八位组掩码 - IPOctetMask = 0xFF -) - -// GetCommonSecondOctets 获取常用第二段IP -func GetCommonSecondOctets() []int { - return []int{0, 1, 2, 10, 100, 200, 254} -} - -// ============================================================================= -// 简化解析器常量 (从Simple.go迁移) -// ============================================================================= - -const ( - // SimpleMaxHosts 端口和主机限制 - 最大主机数量 + // SimpleMaxHosts 最大主机数量限制 SimpleMaxHosts = 10000 - - // DefaultGatewayLastOctet 网段简写展开 - 默认网关最后一段 - DefaultGatewayLastOctet = 1 - // RouterSwitchLastOctet 路由器/交换机最后一段 - RouterSwitchLastOctet = 254 - // SamplingMinHost 采样最小主机号 - SamplingMinHost = 2 - // SamplingMaxHost 采样最大主机号 - SamplingMaxHost = 253 ) -// 端口组定义已迁移到 common/config/constants.go -// 此处保留引用函数以保持向后兼容 - // ============================================================================= -// 验证解析器常量 (从ValidationParser.go迁移) +// 哈希验证常量 // ============================================================================= const ( - // DefaultMaxErrorCount 验证解析器默认配置 - 默认最大错误数 - DefaultMaxErrorCount = 100 - // DefaultStrictMode 默认严格模式 - DefaultStrictMode = false - // DefaultAllowEmpty 默认允许空值 - DefaultAllowEmpty = true - // DefaultCheckConflicts 默认检查冲突 - DefaultCheckConflicts = true - // DefaultValidateTargets 默认验证目标 - DefaultValidateTargets = true - // DefaultValidateNetwork 默认验证网络配置 - DefaultValidateNetwork = true - - // MaxTargetsThreshold 性能警告阈值 - 最大目标数量阈值 - MaxTargetsThreshold = 100000 - // PortCountWarningThreshold 端口数量警告阈值(超过此值时警告) - PortCountWarningThreshold = 5000 - // MinTimeoutThreshold 最小超时阈值 - MinTimeoutThreshold = 1 * time.Second - // MaxTimeoutThreshold 最大超时阈值 - MaxTimeoutThreshold = 60 * time.Second + // HashRegexPattern MD5哈希正则表达式(32位十六进制) + HashRegexPattern = `^[a-fA-F0-9]{32}$` ) // ============================================================================= -// 错误类型常量 -// ============================================================================= - -const ( - // ErrorTypeInputError 解析错误类型 - 输入错误 - ErrorTypeInputError = "INPUT_ERROR" - // ErrorTypeFileError 文件错误 - ErrorTypeFileError = "FILE_ERROR" - // ErrorTypeTimeout 超时错误 - ErrorTypeTimeout = "TIMEOUT" - // ErrorTypeReadError 读取错误 - ErrorTypeReadError = "READ_ERROR" - // ErrorTypeUsernameError 用户名错误 - ErrorTypeUsernameError = "USERNAME_ERROR" - // ErrorTypePasswordError 密码错误 - ErrorTypePasswordError = "PASSWORD_ERROR" - // ErrorTypeHashError 哈希错误 - ErrorTypeHashError = "HASH_ERROR" - // ErrorTypeProxyError 代理错误 - ErrorTypeProxyError = "PROXY_ERROR" - // ErrorTypeUserAgentError 用户代理错误 - ErrorTypeUserAgentError = "USERAGENT_ERROR" - // ErrorTypeCookieError Cookie错误 - ErrorTypeCookieError = "COOKIE_ERROR" - // ErrorTypeHostError 主机错误 - ErrorTypeHostError = "HOST_ERROR" - // ErrorTypePortError 端口错误 - ErrorTypePortError = "PORT_ERROR" - // ErrorTypeExcludePortError 排除端口错误 - ErrorTypeExcludePortError = "EXCLUDE_PORT_ERROR" -) - -// ============================================================================= -// 编译时正则表达式 +// 预编译正则表达式 // ============================================================================= var ( - // CompiledHashRegex 预编译的正则表达式,提高性能 - MD5哈希正则 + // CompiledHashRegex 预编译的MD5哈希正则 CompiledHashRegex *regexp.Regexp - // CompiledIPv4Regex IPv4地址正则 - CompiledIPv4Regex *regexp.Regexp - // CompiledPortRegex 端口范围正则 - CompiledPortRegex *regexp.Regexp - // CompiledURLRegex URL验证正则 - CompiledURLRegex *regexp.Regexp - // CompiledDomainRegex 域名正则 - CompiledDomainRegex *regexp.Regexp - // CompiledCookieRegex Cookie正则 - CompiledCookieRegex *regexp.Regexp ) -// 在包初始化时编译正则表达式 func init() { CompiledHashRegex = regexp.MustCompile(HashRegexPattern) - CompiledIPv4Regex = regexp.MustCompile(IPv4RegexPattern) - CompiledPortRegex = regexp.MustCompile(PortRangeRegexPattern) - CompiledURLRegex = regexp.MustCompile(URLValidationRegexPattern) - CompiledDomainRegex = regexp.MustCompile(DomainRegexPattern) - CompiledCookieRegex = regexp.MustCompile(CookieRegexPattern) } diff --git a/common/parsers/credential.go b/common/parsers/credential.go deleted file mode 100644 index 7172bd4..0000000 --- a/common/parsers/credential.go +++ /dev/null @@ -1,422 +0,0 @@ -package parsers - -import ( - "encoding/hex" - "fmt" - "regexp" - "strings" - "sync" - "time" - - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" -) - -// CredentialParser 凭据解析器 -type CredentialParser struct { - fileReader *FileReader - mu sync.RWMutex //nolint:unused // reserved for future thread safety - hashRegex *regexp.Regexp - options *CredentialParserOptions -} - -// CredentialParserOptions 凭据解析器选项 -type CredentialParserOptions struct { - MaxUsernameLength int `json:"max_username_length"` - MaxPasswordLength int `json:"max_password_length"` - AllowEmptyPasswords bool `json:"allow_empty_passwords"` - ValidateHashes bool `json:"validate_hashes"` - DeduplicateUsers bool `json:"deduplicate_users"` - DeduplicatePasswords bool `json:"deduplicate_passwords"` -} - -// DefaultCredentialParserOptions 默认凭据解析器选项 -func DefaultCredentialParserOptions() *CredentialParserOptions { - return &CredentialParserOptions{ - MaxUsernameLength: DefaultMaxUsernameLength, - MaxPasswordLength: DefaultMaxPasswordLength, - AllowEmptyPasswords: DefaultAllowEmptyPasswords, - ValidateHashes: DefaultValidateHashes, - DeduplicateUsers: DefaultDeduplicateUsers, - DeduplicatePasswords: DefaultDeduplicatePasswords, - } -} - -// NewCredentialParser 创建凭据解析器 -func NewCredentialParser(fileReader *FileReader, options *CredentialParserOptions) *CredentialParser { - if options == nil { - options = DefaultCredentialParserOptions() - } - - // 编译哈希验证正则表达式 (MD5: 32位十六进制) - hashRegex := CompiledHashRegex - - return &CredentialParser{ - fileReader: fileReader, - hashRegex: hashRegex, - options: options, - } -} - -// CredentialInput 凭据输入参数 -type CredentialInput struct { - // 直接输入 - Username string `json:"username"` - Password string `json:"password"` - AddUsers string `json:"add_users"` - AddPasswords string `json:"add_passwords"` - HashValue string `json:"hash_value"` - SSHKeyPath string `json:"ssh_key_path"` - Domain string `json:"domain"` - - // 文件输入 - UsersFile string `json:"users_file"` - PasswordsFile string `json:"passwords_file"` - UserPassFile string `json:"user_pass_file"` // 用户名:密码对文件 - HashFile string `json:"hash_file"` -} - -// Parse 解析凭据配置 -func (cp *CredentialParser) Parse(input *CredentialInput, options *ParserOptions) (*ParseResult, error) { - if input == nil { - return nil, NewParseError(ErrorTypeInputError, "凭据输入为空", "", 0, ErrEmptyInput) - } - - startTime := time.Now() - result := &ParseResult{ - Config: &ParsedConfig{ - Credentials: &CredentialConfig{ - SSHKeyPath: input.SSHKeyPath, - Domain: input.Domain, - }, - }, - Success: true, - } - - var errors []error - var warnings []string - - // 解析用户名 - usernames, userErrors, userWarnings := cp.parseUsernames(input) - errors = append(errors, userErrors...) - warnings = append(warnings, userWarnings...) - - // 解析密码 - passwords, passErrors, passWarnings := cp.parsePasswords(input) - errors = append(errors, passErrors...) - warnings = append(warnings, passWarnings...) - - // 解析哈希值 - hashValues, hashBytes, hashErrors, hashWarnings := cp.parseHashes(input) - errors = append(errors, hashErrors...) - warnings = append(warnings, hashWarnings...) - - // 解析用户密码对 - userPassPairs, pairErrors, pairWarnings := cp.parseUserPassPairs(input) - errors = append(errors, pairErrors...) - warnings = append(warnings, pairWarnings...) - - // 更新配置 - result.Config.Credentials.Usernames = usernames - result.Config.Credentials.Passwords = passwords - result.Config.Credentials.UserPassPairs = userPassPairs - result.Config.Credentials.HashValues = hashValues - result.Config.Credentials.HashBytes = hashBytes - - // 设置结果状态 - result.Errors = errors - result.Warnings = warnings - result.ParseTime = time.Since(startTime) - result.Success = len(errors) == 0 - - return result, nil -} - -// parseUsernames 解析用户名 -func (cp *CredentialParser) parseUsernames(input *CredentialInput) ([]string, []error, []string) { - var usernames []string - var errors []error - var warnings []string - - // 解析命令行用户名 - if input.Username != "" { - users := strings.Split(input.Username, ",") - for _, user := range users { - if processedUser, valid, err := cp.validateUsername(strings.TrimSpace(user)); valid { - usernames = append(usernames, processedUser) - } else if err != nil { - errors = append(errors, NewParseError(ErrorTypeUsernameError, err.Error(), "command line", 0, err)) - } - } - } - - // 从文件读取用户名 - if input.UsersFile != "" { - fileResult, err := cp.fileReader.ReadFile(input.UsersFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取用户名文件失败", input.UsersFile, 0, err)) - } else { - for i, line := range fileResult.Lines { - if processedUser, valid, err := cp.validateUsername(line); valid { - usernames = append(usernames, processedUser) - } else if err != nil { - warnings = append(warnings, fmt.Sprintf("用户名文件第%d行无效: %s", i+1, err.Error())) - } - } - } - } - - // 处理额外用户名 - if input.AddUsers != "" { - extraUsers := strings.Split(input.AddUsers, ",") - for _, user := range extraUsers { - if processedUser, valid, err := cp.validateUsername(strings.TrimSpace(user)); valid { - usernames = append(usernames, processedUser) - } else if err != nil { - warnings = append(warnings, fmt.Sprintf("额外用户名无效: %s", err.Error())) - } - } - } - - // 去重 - if cp.options.DeduplicateUsers { - usernames = cp.removeDuplicateStrings(usernames) - } - - return usernames, errors, warnings -} - -// parsePasswords 解析密码 -func (cp *CredentialParser) parsePasswords(input *CredentialInput) ([]string, []error, []string) { - var passwords []string - var errors []error - var warnings []string - - // 解析命令行密码 - if input.Password != "" { - passes := strings.Split(input.Password, ",") - for _, pass := range passes { - if processedPass, valid, err := cp.validatePassword(pass); valid { - passwords = append(passwords, processedPass) - } else if err != nil { - errors = append(errors, NewParseError(ErrorTypePasswordError, err.Error(), "command line", 0, err)) - } - } - } - - // 从文件读取密码 - if input.PasswordsFile != "" { - fileResult, err := cp.fileReader.ReadFile(input.PasswordsFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取密码文件失败", input.PasswordsFile, 0, err)) - } else { - for i, line := range fileResult.Lines { - if processedPass, valid, err := cp.validatePassword(line); valid { - passwords = append(passwords, processedPass) - } else if err != nil { - warnings = append(warnings, fmt.Sprintf("密码文件第%d行无效: %s", i+1, err.Error())) - } - } - } - } - - // 处理额外密码 - if input.AddPasswords != "" { - extraPasses := strings.Split(input.AddPasswords, ",") - for _, pass := range extraPasses { - if processedPass, valid, err := cp.validatePassword(pass); valid { - passwords = append(passwords, processedPass) - } else if err != nil { - warnings = append(warnings, fmt.Sprintf("额外密码无效: %s", err.Error())) - } - } - } - - // 去重 - if cp.options.DeduplicatePasswords { - passwords = cp.removeDuplicateStrings(passwords) - } - - return passwords, errors, warnings -} - -// parseHashes 解析哈希值 -func (cp *CredentialParser) parseHashes(input *CredentialInput) ([]string, [][]byte, []error, []string) { - var hashValues []string - var hashBytes [][]byte - var errors []error - var warnings []string - - // 解析单个哈希值 - if input.HashValue != "" { - if valid, err := cp.validateHash(input.HashValue); valid { - hashValues = append(hashValues, input.HashValue) - } else { - errors = append(errors, NewParseError(ErrorTypeHashError, err.Error(), "command line", 0, err)) - } - } - - // 从文件读取哈希值 - if input.HashFile != "" { - fileResult, err := cp.fileReader.ReadFile(input.HashFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取哈希文件失败", input.HashFile, 0, err)) - } else { - for i, line := range fileResult.Lines { - if valid, err := cp.validateHash(line); valid { - hashValues = append(hashValues, line) - } else { - warnings = append(warnings, fmt.Sprintf("哈希文件第%d行无效: %s", i+1, err.Error())) - } - } - } - } - - // 转换哈希值为字节数组 - for _, hash := range hashValues { - if hashByte, err := hex.DecodeString(hash); err == nil { - hashBytes = append(hashBytes, hashByte) - } else { - warnings = append(warnings, fmt.Sprintf("哈希值解码失败: %s", hash)) - } - } - - return hashValues, hashBytes, errors, warnings -} - -// validateUsername 验证用户名 -func (cp *CredentialParser) validateUsername(username string) (string, bool, error) { - if len(username) == 0 { - return "", false, nil // 允许空用户名,但不添加到列表 - } - - if len(username) > cp.options.MaxUsernameLength { - return "", false, fmt.Errorf("username length %d exceeds maximum %d", len(username), cp.options.MaxUsernameLength) - } - - // 检查特殊字符 - if strings.ContainsAny(username, InvalidUsernameChars) { - return "", false, fmt.Errorf("%s", i18n.GetText("parser_username_invalid_chars")) - } - - return username, true, nil -} - -// validatePassword 验证密码 -func (cp *CredentialParser) validatePassword(password string) (string, bool, error) { - if len(password) == 0 && !cp.options.AllowEmptyPasswords { - return "", false, fmt.Errorf("%s", i18n.GetText("parser_password_empty")) - } - - if len(password) > cp.options.MaxPasswordLength { - return "", false, fmt.Errorf("password length %d exceeds maximum %d", len(password), cp.options.MaxPasswordLength) - } - - return password, true, nil -} - -// validateHash 验证哈希值 -func (cp *CredentialParser) validateHash(hash string) (bool, error) { - if !cp.options.ValidateHashes { - return true, nil - } - - hash = strings.TrimSpace(hash) - if len(hash) == 0 { - return false, fmt.Errorf("%s", i18n.GetText("parser_hash_empty")) - } - - if !cp.hashRegex.MatchString(hash) { - return false, fmt.Errorf("%s", i18n.GetText("parser_hash_invalid_format")) - } - - return true, nil -} - -// removeDuplicateStrings 去重字符串切片 -func (cp *CredentialParser) removeDuplicateStrings(slice []string) []string { - seen := make(map[string]struct{}) - var result []string - - for _, item := range slice { - if _, exists := seen[item]; !exists { - seen[item] = struct{}{} - result = append(result, item) - } - } - - return result -} - -// parseUserPassPairs 解析用户名密码对 -func (cp *CredentialParser) parseUserPassPairs(input *CredentialInput) ([]config.CredentialPair, []error, []string) { - var pairs []config.CredentialPair - var errors []error - var warnings []string - - // 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个), - // 将其视为精确的用户密码对,而不是做笛卡尔积 - if input.Username != "" && input.Password != "" && - !strings.Contains(input.Username, ",") && !strings.Contains(input.Password, ",") && - input.UsersFile == "" && input.PasswordsFile == "" && input.UserPassFile == "" { - pairs = append(pairs, config.CredentialPair{ - Username: strings.TrimSpace(input.Username), - Password: input.Password, // 密码不trim,可能包含空格 - }) - return pairs, errors, warnings - } - - if input.UserPassFile == "" { - return pairs, errors, warnings - } - - fileResult, err := cp.fileReader.ReadFile(input.UserPassFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取用户密码对文件失败", input.UserPassFile, 0, err)) - return pairs, errors, warnings - } - - for i, line := range fileResult.Lines { - // 只在第一个 : 处分割,后面的都是密码部分 - idx := strings.Index(line, ":") - if idx == -1 { - warnings = append(warnings, fmt.Sprintf("用户密码对文件第%d行格式错误,缺少冒号分隔符: %s", i+1, line)) - continue - } - - user := strings.TrimSpace(line[:idx]) - pass := line[idx+1:] // 密码不 trim,可能包含空格 - - if user == "" { - warnings = append(warnings, fmt.Sprintf("用户密码对文件第%d行用户名为空", i+1)) - continue - } - - // 验证用户名 - if _, valid, err := cp.validateUsername(user); !valid { - if err != nil { - warnings = append(warnings, fmt.Sprintf("用户密码对文件第%d行用户名无效: %s", i+1, err.Error())) - } - continue - } - - // 验证密码 - if _, valid, err := cp.validatePassword(pass); !valid { - if err != nil { - warnings = append(warnings, fmt.Sprintf("用户密码对文件第%d行密码无效: %s", i+1, err.Error())) - } - continue - } - - pairs = append(pairs, config.CredentialPair{ - Username: user, - Password: pass, - }) - } - - return pairs, errors, warnings -} - -// ============================================================================================= -// 已删除的死代码(未使用):Validate 和 GetStatistics 方法 -// ============================================================================================= diff --git a/common/parsers/credential_test.go b/common/parsers/credential_test.go deleted file mode 100644 index ab7596a..0000000 --- a/common/parsers/credential_test.go +++ /dev/null @@ -1,766 +0,0 @@ -package parsers - -import ( - "strings" - "testing" -) - -// ============================================================================= -// CredentialParser 构造函数测试 -// ============================================================================= - -func TestNewCredentialParser(t *testing.T) { - fileReader := NewFileReader(nil) - - tests := []struct { - name string - options *CredentialParserOptions - wantNil bool - }{ - { - name: "使用默认选项", - options: nil, - wantNil: false, - }, - { - name: "使用自定义选项", - options: &CredentialParserOptions{ - MaxUsernameLength: 32, - MaxPasswordLength: 64, - AllowEmptyPasswords: false, - }, - wantNil: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewCredentialParser(fileReader, tt.options) - - if tt.wantNil && parser != nil { - t.Error("期望parser为nil,但不是") - } - if !tt.wantNil && parser == nil { - t.Error("期望parser不为nil,但是nil") - } - - if parser != nil { - if parser.options == nil { - t.Error("parser.options为nil") - } - if parser.hashRegex == nil { - t.Error("parser.hashRegex为nil") - } - } - }) - } -} - -// ============================================================================= -// Parse 主函数测试 -// ============================================================================= - -func TestCredentialParser_Parse(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewCredentialParser(fileReader, nil) - - tests := []struct { - name string - input *CredentialInput - wantSuccess bool - wantUsernames int - wantPasswords int - wantHashes int - wantError bool - }{ - { - name: "空输入", - input: nil, - wantSuccess: false, - wantError: true, - }, - { - name: "单个用户名", - input: &CredentialInput{ - Username: "admin", - }, - wantSuccess: true, - wantUsernames: 1, - }, - { - name: "多个用户名(逗号分隔)", - input: &CredentialInput{ - Username: "admin,root,user", - }, - wantSuccess: true, - wantUsernames: 3, - }, - { - name: "单个密码", - input: &CredentialInput{ - Password: "password123", - }, - wantSuccess: true, - wantPasswords: 1, - }, - { - name: "多个密码", - input: &CredentialInput{ - Password: "pass1,pass2,pass3", - }, - wantSuccess: true, - wantPasswords: 3, - }, - { - name: "用户名和密码组合", - input: &CredentialInput{ - Username: "admin,root", - Password: "123456,password", - }, - wantSuccess: true, - wantUsernames: 2, - wantPasswords: 2, - }, - { - name: "有效MD5哈希", - input: &CredentialInput{ - HashValue: "5f4dcc3b5aa765d61d8327deb882cf99", - }, - wantSuccess: true, - wantHashes: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parser.Parse(tt.input, nil) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if result == nil { - t.Fatal("result为nil") - } - - if result.Success != tt.wantSuccess { - t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess) - } - - if tt.wantUsernames > 0 && len(result.Config.Credentials.Usernames) != tt.wantUsernames { - t.Errorf("用户名数量 = %d, want %d", len(result.Config.Credentials.Usernames), tt.wantUsernames) - } - - if tt.wantPasswords > 0 && len(result.Config.Credentials.Passwords) != tt.wantPasswords { - t.Errorf("密码数量 = %d, want %d", len(result.Config.Credentials.Passwords), tt.wantPasswords) - } - - if tt.wantHashes > 0 && len(result.Config.Credentials.HashValues) != tt.wantHashes { - t.Errorf("哈希数量 = %d, want %d", len(result.Config.Credentials.HashValues), tt.wantHashes) - } - }) - } -} - -// ============================================================================= -// validateUsername 测试 -// ============================================================================= - -func TestCredentialParser_ValidateUsername(t *testing.T) { - fileReader := NewFileReader(nil) - - tests := []struct { - name string - username string - options *CredentialParserOptions - wantValid bool - }{ - { - name: "有效用户名", - username: "admin", - options: nil, - wantValid: true, - }, - { - name: "带数字的用户名", - username: "user123", - options: nil, - wantValid: true, - }, - { - name: "空用户名", - username: "", - options: nil, - wantValid: false, - }, - { - name: "过长的用户名", - username: strings.Repeat("a", 100), - options: &CredentialParserOptions{ - MaxUsernameLength: 64, - }, - wantValid: false, - }, - { - name: "包含换行符的用户名(无效)", - username: "admin\ntest", - options: nil, - wantValid: false, - }, - { - name: "包含制表符的用户名(无效)", - username: "admin\ttest", - options: nil, - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewCredentialParser(fileReader, tt.options) - - _, valid, err := parser.validateUsername(tt.username) - - if valid != tt.wantValid { - t.Errorf("validateUsername() = %v (err: %v), want %v", valid, err, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// validatePassword 测试 -// ============================================================================= - -func TestCredentialParser_ValidatePassword(t *testing.T) { - fileReader := NewFileReader(nil) - - tests := []struct { - name string - password string - options *CredentialParserOptions - wantValid bool - }{ - { - name: "有效密码", - password: "password123", - options: nil, - wantValid: true, - }, - { - name: "复杂密码", - password: "P@ssw0rd!#$", - options: nil, - wantValid: true, - }, - { - name: "空密码(允许)", - password: "", - options: &CredentialParserOptions{ - AllowEmptyPasswords: true, - }, - wantValid: true, - }, - { - name: "空密码(不允许)", - password: "", - options: &CredentialParserOptions{ - AllowEmptyPasswords: false, - }, - wantValid: false, - }, - { - name: "过长的密码", - password: strings.Repeat("a", 200), - options: &CredentialParserOptions{ - MaxPasswordLength: 128, - }, - wantValid: false, - }, - { - name: "带空格的密码", - password: "my password", - options: nil, - wantValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewCredentialParser(fileReader, tt.options) - - _, valid, err := parser.validatePassword(tt.password) - - if valid != tt.wantValid { - t.Errorf("validatePassword() = %v (err: %v), want %v", valid, err, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// validateHash 测试 -// ============================================================================= - -func TestCredentialParser_ValidateHash(t *testing.T) { - fileReader := NewFileReader(nil) - - tests := []struct { - name string - hash string - options *CredentialParserOptions - wantValid bool - }{ - { - name: "有效MD5哈希(小写)", - hash: "5f4dcc3b5aa765d61d8327deb882cf99", - options: nil, - wantValid: true, - }, - { - name: "有效MD5哈希(大写)", - hash: "5F4DCC3B5AA765D61D8327DEB882CF99", - options: nil, - wantValid: true, - }, - { - name: "有效MD5哈希(混合大小写)", - hash: "5f4DcC3b5Aa765d61D8327dEb882Cf99", - options: nil, - wantValid: true, - }, - { - name: "空哈希", - hash: "", - options: nil, - wantValid: false, - }, - { - name: "过短的哈希", - hash: "5f4dcc3b5aa765d61d8327deb882cf", - options: nil, - wantValid: false, - }, - { - name: "过长的哈希", - hash: "5f4dcc3b5aa765d61d8327deb882cf9900", - options: nil, - wantValid: false, - }, - { - name: "包含非法字符的哈希", - hash: "5f4dcc3b5aa765d61d8327deb882cfgg", - options: nil, - wantValid: false, - }, - { - name: "禁用哈希验证", - hash: "invalid-hash", - options: &CredentialParserOptions{ - ValidateHashes: false, - }, - wantValid: true, // 禁用验证时,任何哈希都有效 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewCredentialParser(fileReader, tt.options) - - valid, err := parser.validateHash(tt.hash) - - if valid != tt.wantValid { - t.Errorf("validateHash() = %v (err: %v), want %v", valid, err, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// 文件解析测试 -// ============================================================================= - -func TestCredentialParser_ParseFromFile(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewCredentialParser(fileReader, nil) - - t.Run("用户名文件", func(t *testing.T) { - usersFile := createTestFile(t, `admin -root -user -# 这是注释 -test`) - - input := &CredentialInput{ - UsersFile: usersFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该有4个用户名(注释被跳过) - if len(result.Config.Credentials.Usernames) != 4 { - t.Errorf("用户名数量 = %d, want 4", len(result.Config.Credentials.Usernames)) - } - }) - - t.Run("密码文件", func(t *testing.T) { - passFile := createTestFile(t, `password1 -password2 -# 注释 -password3 -`) - - input := &CredentialInput{ - PasswordsFile: passFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if len(result.Config.Credentials.Passwords) != 3 { - t.Errorf("密码数量 = %d, want 3", len(result.Config.Credentials.Passwords)) - } - }) - - t.Run("用户密码对文件", func(t *testing.T) { - pairsFile := createTestFile(t, `admin:admin123 -root:toor -user:password -# test:test123 (注释) -guest:guest`) - - input := &CredentialInput{ - UserPassFile: pairsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该有4对用户名密码 - if len(result.Config.Credentials.UserPassPairs) != 4 { - t.Errorf("用户密码对数量 = %d, want 4", len(result.Config.Credentials.UserPassPairs)) - } - - // 验证第一对 - if result.Config.Credentials.UserPassPairs[0].Username != "admin" { - t.Errorf("第一对用户名 = %s, want admin", result.Config.Credentials.UserPassPairs[0].Username) - } - if result.Config.Credentials.UserPassPairs[0].Password != "admin123" { - t.Errorf("第一对密码 = %s, want admin123", result.Config.Credentials.UserPassPairs[0].Password) - } - }) - - t.Run("哈希文件", func(t *testing.T) { - hashFile := createTestFile(t, `5f4dcc3b5aa765d61d8327deb882cf99 -e99a18c428cb38d5f260853678922e03 -# 注释 -098f6bcd4621d373cade4e832627b4f6`) - - input := &CredentialInput{ - HashFile: hashFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if len(result.Config.Credentials.HashValues) != 3 { - t.Errorf("哈希数量 = %d, want 3", len(result.Config.Credentials.HashValues)) - } - - // 验证哈希字节数组也被生成 - if len(result.Config.Credentials.HashBytes) != 3 { - t.Errorf("哈希字节数组数量 = %d, want 3", len(result.Config.Credentials.HashBytes)) - } - }) -} - -// ============================================================================= -// 去重功能测试 -// ============================================================================= - -func TestCredentialParser_Deduplication(t *testing.T) { - fileReader := NewFileReader(nil) - - t.Run("用户名去重(启用)", func(t *testing.T) { - opts := DefaultCredentialParserOptions() - opts.DeduplicateUsers = true - parser := NewCredentialParser(fileReader, opts) - - input := &CredentialInput{ - Username: "admin,root,admin,user,root", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该去重为3个用户名 - if len(result.Config.Credentials.Usernames) != 3 { - t.Errorf("用户名数量 = %d, want 3", len(result.Config.Credentials.Usernames)) - } - }) - - t.Run("用户名去重(禁用)", func(t *testing.T) { - opts := DefaultCredentialParserOptions() - opts.DeduplicateUsers = false - parser := NewCredentialParser(fileReader, opts) - - input := &CredentialInput{ - Username: "admin,root,admin", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 不去重,应该有3个用户名 - if len(result.Config.Credentials.Usernames) != 3 { - t.Errorf("用户名数量 = %d, want 3", len(result.Config.Credentials.Usernames)) - } - }) - - t.Run("密码去重(启用)", func(t *testing.T) { - opts := DefaultCredentialParserOptions() - opts.DeduplicatePasswords = true - parser := NewCredentialParser(fileReader, opts) - - input := &CredentialInput{ - Password: "123456,password,123456,admin,password", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该去重为3个密码 - if len(result.Config.Credentials.Passwords) != 3 { - t.Errorf("密码数量 = %d, want 3", len(result.Config.Credentials.Passwords)) - } - }) -} - -// ============================================================================= -// 混合输入测试 -// ============================================================================= - -func TestCredentialParser_MixedInput(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewCredentialParser(fileReader, nil) - - t.Run("命令行+文件混合", func(t *testing.T) { - usersFile := createTestFile(t, "user1\nuser2") - passFile := createTestFile(t, "pass1\npass2") - - input := &CredentialInput{ - Username: "admin,root", - UsersFile: usersFile, - Password: "123456", - PasswordsFile: passFile, - AddUsers: "guest", - AddPasswords: "guest123", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 用户名: 2(命令行) + 2(文件) + 1(AddUsers) = 5 - if len(result.Config.Credentials.Usernames) != 5 { - t.Errorf("用户名数量 = %d, want 5", len(result.Config.Credentials.Usernames)) - } - - // 密码: 1(命令行) + 2(文件) + 1(AddPasswords) = 4 - if len(result.Config.Credentials.Passwords) != 4 { - t.Errorf("密码数量 = %d, want 4", len(result.Config.Credentials.Passwords)) - } - }) -} - -// ============================================================================= -// 错误处理测试 -// ============================================================================= - -func TestCredentialParser_ErrorHandling(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewCredentialParser(fileReader, nil) - - t.Run("用户密码对格式错误", func(t *testing.T) { - pairsFile := createTestFile(t, `admin:admin123 -invalidformat -root:toor`) - - input := &CredentialInput{ - UserPassFile: pairsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该有警告 - if len(result.Warnings) == 0 { - t.Error("期望有警告,但没有") - } - - // 应该只解析出2对有效的 - if len(result.Config.Credentials.UserPassPairs) != 2 { - t.Errorf("用户密码对数量 = %d, want 2", len(result.Config.Credentials.UserPassPairs)) - } - }) - - t.Run("用户密码对-空用户名", func(t *testing.T) { - pairsFile := createTestFile(t, `admin:admin123 -:emptyuser -root:toor`) - - input := &CredentialInput{ - UserPassFile: pairsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 空用户名的行应该被跳过 - if len(result.Config.Credentials.UserPassPairs) != 2 { - t.Errorf("用户密码对数量 = %d, want 2", len(result.Config.Credentials.UserPassPairs)) - } - }) - - t.Run("密码中包含冒号", func(t *testing.T) { - pairsFile := createTestFile(t, `admin:pass:word:123 -root:simple`) - - input := &CredentialInput{ - UserPassFile: pairsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if len(result.Config.Credentials.UserPassPairs) != 2 { - t.Errorf("用户密码对数量 = %d, want 2", len(result.Config.Credentials.UserPassPairs)) - } - - // 验证密码中的冒号被正确保留 - if result.Config.Credentials.UserPassPairs[0].Password != "pass:word:123" { - t.Errorf("密码 = %s, want pass:word:123", result.Config.Credentials.UserPassPairs[0].Password) - } - }) - - t.Run("不存在的文件", func(t *testing.T) { - input := &CredentialInput{ - UsersFile: "/nonexistent/users.txt", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("Parse不应返回错误: %v", err) - return - } - - // 应该有文件错误 - if result.Success { - t.Error("解析不应成功(文件不存在)") - } - - if len(result.Errors) == 0 { - t.Error("应该有错误记录") - } - }) -} - -// ============================================================================= -// removeDuplicateStrings 测试 -// ============================================================================= - -func TestCredentialParser_RemoveDuplicateStrings(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewCredentialParser(fileReader, nil) - - input := []string{"a", "b", "a", "c", "b", "d"} - result := parser.removeDuplicateStrings(input) - - expected := []string{"a", "b", "c", "d"} - - if len(result) != len(expected) { - t.Errorf("结果数量 = %d, want %d", len(result), len(expected)) - } - - // 检查所有元素都存在(顺序可能不同) - resultMap := make(map[string]bool) - for _, item := range result { - resultMap[item] = true - } - - for _, item := range expected { - if !resultMap[item] { - t.Errorf("缺少元素: %s", item) - } - } -} - -// ============================================================================= -// SSH和Domain字段测试 -// ============================================================================= - -func TestCredentialParser_SSHAndDomain(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewCredentialParser(fileReader, nil) - - input := &CredentialInput{ - SSHKeyPath: "/path/to/ssh/key", - Domain: "example.com", - Username: "admin", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if result.Config.Credentials.SSHKeyPath != "/path/to/ssh/key" { - t.Errorf("SSHKeyPath = %s, want /path/to/ssh/key", result.Config.Credentials.SSHKeyPath) - } - - if result.Config.Credentials.Domain != "example.com" { - t.Errorf("Domain = %s, want example.com", result.Config.Credentials.Domain) - } -} diff --git a/common/parsers/file.go b/common/parsers/file.go deleted file mode 100644 index 2995fb0..0000000 --- a/common/parsers/file.go +++ /dev/null @@ -1,311 +0,0 @@ -package parsers - -import ( - "bufio" - "context" - "fmt" - "os" - "strings" - "sync" - "time" - - "github.com/shadow1ng/fscan/common/i18n" -) - -// FileReader 高性能文件读取器 -type FileReader struct { - mu sync.RWMutex - cache map[string]*FileResult // 文件缓存 - maxCacheSize int // 最大缓存大小 - enableCache bool // 是否启用缓存 - maxFileSize int64 // 最大文件大小 - timeout time.Duration // 读取超时 - enableValidation bool // 是否启用内容验证 -} - -// FileResult 文件读取结果 -type FileResult struct { - Lines []string `json:"lines"` - Source *FileSource `json:"source"` - ReadTime time.Duration `json:"read_time"` - ValidLines int `json:"valid_lines"` - Errors []error `json:"errors,omitempty"` - Cached bool `json:"cached"` -} - -// NewFileReader 创建文件读取器 -func NewFileReader(options *FileReaderOptions) *FileReader { - if options == nil { - options = DefaultFileReaderOptions() - } - - return &FileReader{ - cache: make(map[string]*FileResult), - maxCacheSize: options.MaxCacheSize, - enableCache: options.EnableCache, - maxFileSize: options.MaxFileSize, - timeout: options.Timeout, - enableValidation: options.EnableValidation, - } -} - -// FileReaderOptions 文件读取器选项 -type FileReaderOptions struct { - MaxCacheSize int // 最大缓存文件数 - EnableCache bool // 启用文件缓存 - MaxFileSize int64 // 最大文件大小(字节) - Timeout time.Duration // 读取超时 - EnableValidation bool // 启用内容验证 - TrimSpace bool // 自动清理空白字符 - SkipEmpty bool // 跳过空行 - SkipComments bool // 跳过注释行(#开头) -} - -// DefaultFileReaderOptions 默认文件读取器选项 -func DefaultFileReaderOptions() *FileReaderOptions { - return &FileReaderOptions{ - MaxCacheSize: DefaultMaxCacheSize, - EnableCache: DefaultEnableCache, - MaxFileSize: DefaultFileReaderMaxFileSize, - Timeout: DefaultFileReaderTimeout, - EnableValidation: DefaultFileReaderEnableValidation, - TrimSpace: DefaultTrimSpace, - SkipEmpty: DefaultSkipEmpty, - SkipComments: DefaultSkipComments, - } -} - -// ReadFile 读取文件内容 -func (fr *FileReader) ReadFile(filename string, options ...*FileReaderOptions) (*FileResult, error) { - if filename == "" { - return nil, NewParseError("FILE_ERROR", "文件名为空", filename, 0, ErrEmptyInput) - } - - // 检查缓存 - if fr.enableCache { - if result := fr.getFromCache(filename); result != nil { - result.Cached = true - return result, nil - } - } - - // 合并选项 - opts := fr.mergeOptions(options...) - - // 创建带超时的上下文 - 使用合并后的超时配置 - ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) - defer cancel() - - // 异步读取文件 - resultChan := make(chan *FileResult, 1) - errorChan := make(chan error, 1) - - go func() { - result, err := fr.readFileSync(filename, opts) - if err != nil { - errorChan <- err - } else { - resultChan <- result - } - }() - - // 等待结果或超时 - select { - case result := <-resultChan: - // 添加到缓存 - if fr.enableCache { - fr.addToCache(filename, result) - } - return result, nil - case err := <-errorChan: - return nil, err - case <-ctx.Done(): - return nil, NewParseError(ErrorTypeTimeout, "文件读取超时", filename, 0, ctx.Err()) - } -} - -// ============================================================================================= -// 已删除的死代码(未使用):ReadFiles 并发读取多个文件的方法 -// ============================================================================================= - -// readFileSync 同步读取文件 -func (fr *FileReader) readFileSync(filename string, options *FileReaderOptions) (*FileResult, error) { - startTime := time.Now() - - // 检查文件 - fileInfo, err := os.Stat(filename) - if err != nil { - return nil, NewParseError("FILE_ERROR", "文件不存在或无法访问", filename, 0, err) - } - - // 检查文件大小 - 使用传入的配置选项 - if fileInfo.Size() > options.MaxFileSize { - return nil, NewParseError("FILE_ERROR", - fmt.Sprintf("文件过大: %d bytes, 最大限制: %d bytes", fileInfo.Size(), options.MaxFileSize), - filename, 0, nil) - } - - // 打开文件 - file, err := os.Open(filename) - if err != nil { - return nil, NewParseError("FILE_ERROR", "无法打开文件", filename, 0, err) - } - defer func() { _ = file.Close() }() // 只读文件,Close错误可安全忽略 - - // 创建结果 - result := &FileResult{ - Lines: make([]string, 0), - Source: &FileSource{ - Path: filename, - Size: fileInfo.Size(), - ModTime: fileInfo.ModTime(), - }, - } - - // 读取文件内容 - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - - lineNum := 0 - validLines := 0 - - for scanner.Scan() { - lineNum++ - line := scanner.Text() - - // 处理行内容 - if processedLine, valid := fr.processLine(line, options); valid { - result.Lines = append(result.Lines, processedLine) - validLines++ - } - } - - // 检查扫描错误 - if err := scanner.Err(); err != nil { - return nil, NewParseError(ErrorTypeReadError, i18n.GetText("parser_file_scan_failed"), filename, lineNum, err) - } - - // 更新统计信息 - result.Source.LineCount = lineNum - result.Source.ValidLines = validLines - result.ValidLines = validLines - result.ReadTime = time.Since(startTime) - - return result, nil -} - -// processLine 处理单行内容 -func (fr *FileReader) processLine(line string, options *FileReaderOptions) (string, bool) { - // 清理空白字符 - if options.TrimSpace { - line = strings.TrimSpace(line) - } - - // 跳过空行 - if options.SkipEmpty && line == "" { - return "", false - } - - // 跳过注释行 - if options.SkipComments && strings.HasPrefix(line, CommentPrefix) { - return "", false - } - - // 内容验证 - if options.EnableValidation && fr.enableValidation { - if !fr.validateLine(line) { - return "", false - } - } - - return line, true -} - -// validateLine 验证行内容 -func (fr *FileReader) validateLine(line string) bool { - // 基本验证:检查是否包含特殊字符或过长 - if len(line) > MaxLineLength { // 单行最大字符数 - return false - } - - // 检查是否包含控制字符 - for _, r := range line { - if r < MaxValidRune && r != TabRune && r != NewlineRune && r != CarriageReturnRune { // 排除tab、换行、回车 - return false - } - } - - return true -} - -// mergeOptions 合并选项 -func (fr *FileReader) mergeOptions(options ...*FileReaderOptions) *FileReaderOptions { - opts := DefaultFileReaderOptions() - if len(options) > 0 && options[0] != nil { - opts = options[0] - } - return opts -} - -// getFromCache 从缓存获取结果 -func (fr *FileReader) getFromCache(filename string) *FileResult { - fr.mu.RLock() - result, exists := fr.cache[filename] - if !exists { - fr.mu.RUnlock() - return nil - } - - // 检查文件是否有更新 - fileInfo, err := os.Stat(filename) - if err != nil { - fr.mu.RUnlock() - return result - } - - if fileInfo.ModTime().After(result.Source.ModTime) { - // 文件已更新,需要删除缓存 - 使用双重检查锁定模式 - fr.mu.RUnlock() // 释放读锁 - - fr.mu.Lock() // 获取写锁 - // 重新检查条件(双重检查,因为在锁切换期间状态可能改变) - if cachedResult, stillExists := fr.cache[filename]; stillExists { - if reCheckInfo, reCheckErr := os.Stat(filename); reCheckErr == nil { - if reCheckInfo.ModTime().After(cachedResult.Source.ModTime) { - delete(fr.cache, filename) - } - } - } - fr.mu.Unlock() // 释放写锁 - return nil - } - - fr.mu.RUnlock() - return result -} - -// addToCache 添加到缓存 -func (fr *FileReader) addToCache(filename string, result *FileResult) { - fr.mu.Lock() - defer fr.mu.Unlock() - - // 检查缓存大小 - if len(fr.cache) >= fr.maxCacheSize { - // 移除最旧的条目(简单的LRU策略) - var oldestFile string - var oldestTime time.Time - for file, res := range fr.cache { - if oldestFile == "" || res.Source.ModTime.Before(oldestTime) { - oldestFile = file - oldestTime = res.Source.ModTime - } - } - delete(fr.cache, oldestFile) - } - - fr.cache[filename] = result -} - -// ============================================================================================= -// 已删除的死代码(未使用):ClearCache 和 GetCacheStats 方法 -// ============================================================================================= diff --git a/common/parsers/ip_parser_bench_test.go b/common/parsers/ip_parser_bench_test.go deleted file mode 100644 index 32af57e..0000000 --- a/common/parsers/ip_parser_bench_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package parsers - -import ( - "testing" -) - -// BenchmarkParseIPCIDR24 测试 /24 网段解析性能 -func BenchmarkParseIPCIDR24(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = ParseIP("192.168.1.0/24", "") - } -} - -// BenchmarkParseIPCIDR16 测试 /16 网段解析性能 -func BenchmarkParseIPCIDR16(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = ParseIP("192.168.0.0/16", "") - } -} - -// BenchmarkParseIPRange 测试 IP 范围解析性能 -func BenchmarkParseIPRange(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = ParseIP("192.168.1.1-192.168.1.254", "") - } -} - -// BenchmarkParseIPSingle 测试单个 IP 解析性能 -func BenchmarkParseIPSingle(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = ParseIP("192.168.1.1", "") - } -} - -// BenchmarkParsePortRange 测试端口范围解析性能 -func BenchmarkParsePortRange(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ParsePort("1-65535") - } -} - -// BenchmarkParsePortList 测试端口列表解析性能 -func BenchmarkParsePortList(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ParsePort("22,80,443,3389,8080,8443,9000,9001,9002") - } -} - -// BenchmarkParsePortCommon 测试常用端口解析性能 -func BenchmarkParsePortCommon(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ParsePort("21,22,23,25,80,110,139,143,443,445,3306,3389,5432,6379,8080") - } -} diff --git a/common/parsers/network.go b/common/parsers/network.go deleted file mode 100644 index b29c8d1..0000000 --- a/common/parsers/network.go +++ /dev/null @@ -1,368 +0,0 @@ -package parsers - -import ( - "fmt" - "net/url" - "strconv" - "strings" - "sync" - "time" - - "github.com/shadow1ng/fscan/common/i18n" -) - -// NetworkParser 网络配置解析器 -type NetworkParser struct { - mu sync.RWMutex //nolint:unused // reserved for future thread safety - options *NetworkParserOptions -} - -// NetworkParserOptions 网络解析器选项 -type NetworkParserOptions struct { - ValidateProxies bool `json:"validate_proxies"` - AllowInsecure bool `json:"allow_insecure"` - DefaultTimeout time.Duration `json:"default_timeout"` - DefaultWebTimeout time.Duration `json:"default_web_timeout"` - DefaultUserAgent string `json:"default_user_agent"` -} - -// DefaultNetworkParserOptions 默认网络解析器选项 -func DefaultNetworkParserOptions() *NetworkParserOptions { - return &NetworkParserOptions{ - ValidateProxies: DefaultValidateProxies, - AllowInsecure: DefaultAllowInsecure, - DefaultTimeout: DefaultNetworkTimeout, - DefaultWebTimeout: DefaultWebTimeout, - DefaultUserAgent: DefaultUserAgent, - } -} - -// NewNetworkParser 创建网络配置解析器 -func NewNetworkParser(options *NetworkParserOptions) *NetworkParser { - if options == nil { - options = DefaultNetworkParserOptions() - } - - return &NetworkParser{ - options: options, - } -} - -// NetworkInput 网络配置输入参数 -type NetworkInput struct { - // 代理配置 - HTTPProxy string `json:"http_proxy"` - Socks5Proxy string `json:"socks5_proxy"` - - // 超时配置 - Timeout int64 `json:"timeout"` - WebTimeout int64 `json:"web_timeout"` - - // 网络选项 - DisablePing bool `json:"disable_ping"` - DNSLog bool `json:"dns_log"` - UserAgent string `json:"user_agent"` - Cookie string `json:"cookie"` -} - -// Parse 解析网络配置 -func (np *NetworkParser) Parse(input *NetworkInput, options *ParserOptions) (*ParseResult, error) { - if input == nil { - return nil, NewParseError("INPUT_ERROR", "网络配置输入为空", "", 0, ErrEmptyInput) - } - - startTime := time.Now() - result := &ParseResult{ - Config: &ParsedConfig{ - Network: &NetworkConfig{ - EnableDNSLog: input.DNSLog, - DisablePing: input.DisablePing, - }, - }, - Success: true, - } - - var errors []error - var warnings []string - - // 解析HTTP代理 - httpProxy, httpErrors, httpWarnings := np.parseHTTPProxy(input.HTTPProxy) - errors = append(errors, httpErrors...) - warnings = append(warnings, httpWarnings...) - - // 解析Socks5代理 - socks5Proxy, socks5Errors, socks5Warnings := np.parseSocks5Proxy(input.Socks5Proxy) - errors = append(errors, socks5Errors...) - warnings = append(warnings, socks5Warnings...) - - // 解析超时配置 - timeout, webTimeout, timeoutErrors, timeoutWarnings := np.parseTimeouts(input.Timeout, input.WebTimeout) - errors = append(errors, timeoutErrors...) - warnings = append(warnings, timeoutWarnings...) - - // 解析用户代理 - userAgent, uaErrors, uaWarnings := np.parseUserAgent(input.UserAgent) - errors = append(errors, uaErrors...) - warnings = append(warnings, uaWarnings...) - - // 解析Cookie - cookie, cookieErrors, cookieWarnings := np.parseCookie(input.Cookie) - errors = append(errors, cookieErrors...) - warnings = append(warnings, cookieWarnings...) - - // 检查代理冲突 - if httpProxy != "" && socks5Proxy != "" { - warnings = append(warnings, "同时配置了HTTP代理和Socks5代理,Socks5代理将被优先使用") - } - - // 更新配置 - result.Config.Network.HTTPProxy = httpProxy - result.Config.Network.Socks5Proxy = socks5Proxy - result.Config.Network.Timeout = timeout - result.Config.Network.WebTimeout = webTimeout - result.Config.Network.UserAgent = userAgent - result.Config.Network.Cookie = cookie - - // 设置结果状态 - result.Errors = errors - result.Warnings = warnings - result.ParseTime = time.Since(startTime) - result.Success = len(errors) == 0 - - return result, nil -} - -// parseHTTPProxy 解析HTTP代理配置 -func (np *NetworkParser) parseHTTPProxy(proxyStr string) (string, []error, []string) { - var errors []error - var warnings []string - - if proxyStr == "" { - return "", nil, nil - } - - // 处理简写形式 - normalizedProxy := np.normalizeHTTPProxy(proxyStr) - - // 验证代理URL - if np.options.ValidateProxies { - if err := np.validateProxyURL(normalizedProxy); err != nil { - errors = append(errors, NewParseError(ErrorTypeProxyError, err.Error(), "http_proxy", 0, err)) - return "", errors, warnings - } - } - - return normalizedProxy, errors, warnings -} - -// parseSocks5Proxy 解析Socks5代理配置 -func (np *NetworkParser) parseSocks5Proxy(proxyStr string) (string, []error, []string) { - var errors []error - var warnings []string - - if proxyStr == "" { - return "", nil, nil - } - - // 处理简写形式 - normalizedProxy := np.normalizeSocks5Proxy(proxyStr) - - // 验证代理URL - if np.options.ValidateProxies { - if err := np.validateProxyURL(normalizedProxy); err != nil { - errors = append(errors, NewParseError(ErrorTypeProxyError, err.Error(), "socks5_proxy", 0, err)) - return "", errors, warnings - } - } - - // 使用Socks5代理时建议禁用Ping - if normalizedProxy != "" { - warnings = append(warnings, "使用Socks5代理时建议禁用Ping检测") - } - - return normalizedProxy, errors, warnings -} - -// parseTimeouts 解析超时配置 -func (np *NetworkParser) parseTimeouts(timeout, webTimeout int64) (time.Duration, time.Duration, []error, []string) { //nolint:unparam - var errors []error - var warnings []string - - // 处理普通超时 - finalTimeout := np.options.DefaultTimeout - if timeout > 0 { - if timeout > MaxTimeoutSeconds { - warnings = append(warnings, "超时时间过长,建议不超过300秒") - } - finalTimeout = time.Duration(timeout) * time.Second - } - - // 处理Web超时 - finalWebTimeout := np.options.DefaultWebTimeout - if webTimeout > 0 { - if webTimeout > MaxWebTimeoutSeconds { - warnings = append(warnings, "Web超时时间过长,建议不超过120秒") - } - finalWebTimeout = time.Duration(webTimeout) * time.Second - } - - // 验证超时配置合理性:只有在Web超时显著大于普通超时时才警告 - // Web超时适当大于普通超时是合理的,因为Web请求包含更多步骤 - if finalWebTimeout > finalTimeout*2 { - warnings = append(warnings, i18n.GetText("config_web_timeout_warning")) - } - - return finalTimeout, finalWebTimeout, errors, warnings -} - -// parseUserAgent 解析用户代理 -func (np *NetworkParser) parseUserAgent(userAgent string) (string, []error, []string) { - var errors []error - var warnings []string - - if userAgent == "" { - return np.options.DefaultUserAgent, errors, warnings - } - - // 基本格式验证 - if len(userAgent) > MaxUserAgentLength { - errors = append(errors, NewParseError(ErrorTypeUserAgentError, "用户代理字符串过长", "user_agent", 0, nil)) - return "", errors, warnings - } - - // 检查是否包含特殊字符 - if strings.ContainsAny(userAgent, InvalidUserAgentChars) { - errors = append(errors, NewParseError(ErrorTypeUserAgentError, "用户代理包含非法字符", "user_agent", 0, nil)) - return "", errors, warnings - } - - // 检查是否为常见浏览器用户代理 - if !np.isValidUserAgent(userAgent) { - warnings = append(warnings, "用户代理格式可能不被目标服务器识别") - } - - return userAgent, errors, warnings -} - -// parseCookie 解析Cookie -func (np *NetworkParser) parseCookie(cookie string) (string, []error, []string) { - var errors []error - var warnings []string - - if cookie == "" { - return "", errors, warnings - } - - // 基本格式验证 - if len(cookie) > MaxCookieLength { // HTTP Cookie长度限制 - errors = append(errors, NewParseError(ErrorTypeCookieError, "Cookie字符串过长", "cookie", 0, nil)) - return "", errors, warnings - } - - // 检查Cookie格式 - if !np.isValidCookie(cookie) { - warnings = append(warnings, "Cookie格式可能不正确") - } - - return cookie, errors, warnings -} - -// normalizeHTTPProxy 规范化HTTP代理URL -func (np *NetworkParser) normalizeHTTPProxy(proxy string) string { - switch strings.ToLower(proxy) { - case ProxyShortcut1: - return ProxyShortcutHTTP - case ProxyShortcut2: - return ProxyShortcutSOCKS5 - default: - // 如果没有协议前缀,默认使用HTTP - if !strings.Contains(proxy, ProtocolPrefix) { - if strings.Contains(proxy, ":") { - return HTTPPrefix + proxy - } - return HTTPPrefix + "127.0.0.1:" + proxy - } - return proxy - } -} - -// normalizeSocks5Proxy 规范化Socks5代理URL -func (np *NetworkParser) normalizeSocks5Proxy(proxy string) string { - // 如果没有协议前缀,添加SOCKS5协议 - if !strings.HasPrefix(proxy, SOCKS5Prefix) { - if strings.Contains(proxy, ":") { - return SOCKS5Prefix + proxy - } - return SOCKS5Prefix + "127.0.0.1:" + proxy - } - return proxy -} - -// validateProxyURL 验证代理URL格式 -func (np *NetworkParser) validateProxyURL(proxyURL string) error { - if proxyURL == "" { - return nil - } - - parsedURL, err := url.Parse(proxyURL) - if err != nil { - return fmt.Errorf("代理URL格式无效: %w", err) - } - - // 检查协议 - switch parsedURL.Scheme { - case ProtocolHTTP, ProtocolHTTPS, ProtocolSOCKS5: - // 支持的协议 - default: - return fmt.Errorf("不支持的代理协议: %s", parsedURL.Scheme) - } - - // 检查主机名 - if parsedURL.Hostname() == "" { - return fmt.Errorf("代理主机名为空") - } - - // 检查端口 - portStr := parsedURL.Port() - if portStr != "" { - port, err := strconv.Atoi(portStr) - if err != nil { - return fmt.Errorf("代理端口号无效: %s", portStr) - } - if port < 1 || port > 65535 { - return fmt.Errorf("代理端口号超出范围: %d", port) - } - } - - // 安全检查 - if !np.options.AllowInsecure && parsedURL.Scheme == ProtocolHTTP { - return fmt.Errorf("不允许使用不安全的HTTP代理") - } - - return nil -} - -// isValidUserAgent 检查用户代理是否有效 -func (np *NetworkParser) isValidUserAgent(userAgent string) bool { - // 检查是否包含常见的浏览器标识 - commonBrowsers := GetCommonBrowsers() - - userAgentLower := strings.ToLower(userAgent) - for _, browser := range commonBrowsers { - if strings.Contains(userAgentLower, strings.ToLower(browser)) { - return true - } - } - - return false -} - -// isValidCookie 检查Cookie格式是否有效 -func (np *NetworkParser) isValidCookie(cookie string) bool { - // 基本Cookie格式检查 (name=value; name2=value2) - return CompiledCookieRegex.MatchString(strings.TrimSpace(cookie)) -} - -// ============================================================================================= -// 已删除的死代码(未使用):Validate 和 GetStatistics 方法 -// ============================================================================================= diff --git a/common/parsers/network_test.go b/common/parsers/network_test.go deleted file mode 100644 index fe9a538..0000000 --- a/common/parsers/network_test.go +++ /dev/null @@ -1,720 +0,0 @@ -package parsers - -import ( - "strings" - "testing" - "time" -) - -// ============================================================================= -// NetworkParser 构造函数测试 -// ============================================================================= - -func TestNewNetworkParser(t *testing.T) { - tests := []struct { - name string - options *NetworkParserOptions - wantNil bool - }{ - { - name: "使用默认选项", - options: nil, - wantNil: false, - }, - { - name: "使用自定义选项", - options: &NetworkParserOptions{ - ValidateProxies: false, - AllowInsecure: true, - }, - wantNil: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewNetworkParser(tt.options) - - if tt.wantNil && parser != nil { - t.Error("期望parser为nil,但不是") - } - if !tt.wantNil && parser == nil { - t.Error("期望parser不为nil,但是nil") - } - - if parser != nil && parser.options == nil { - t.Error("parser.options为nil") - } - }) - } -} - -// ============================================================================= -// Parse 主函数测试 -// ============================================================================= - -func TestNetworkParser_Parse(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - input *NetworkInput - wantSuccess bool - wantError bool - }{ - { - name: "空输入", - input: nil, - wantSuccess: false, - wantError: true, - }, - { - name: "完整HTTPS代理配置", - input: &NetworkInput{ - HTTPProxy: "https://127.0.0.1:8443", - }, - wantSuccess: true, - }, - { - name: "Socks5代理配置", - input: &NetworkInput{ - Socks5Proxy: "socks5://127.0.0.1:1080", - }, - wantSuccess: true, - }, - { - name: "自定义超时", - input: &NetworkInput{ - Timeout: 60, - WebTimeout: 30, - }, - wantSuccess: true, - }, - { - name: "自定义User-Agent", - input: &NetworkInput{ - UserAgent: "Custom-Agent/1.0", - }, - wantSuccess: true, - }, - { - name: "Cookie配置", - input: &NetworkInput{ - Cookie: "session=abc123; token=xyz789", - }, - wantSuccess: true, - }, - { - name: "禁用Ping", - input: &NetworkInput{ - DisablePing: true, - }, - wantSuccess: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parser.Parse(tt.input, nil) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if result == nil { - t.Fatal("result为nil") - } - - if result.Success != tt.wantSuccess { - t.Errorf("Success = %v, want %v (errors: %v)", result.Success, tt.wantSuccess, result.Errors) - } - }) - } -} - -// ============================================================================= -// normalizeHttpProxy 测试 -// ============================================================================= - -func TestNetworkParser_NormalizeHttpProxy(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - input string - expected string - }{ - { - name: "快捷方式1", - input: "1", - expected: "http://127.0.0.1:8080", - }, - { - name: "快捷方式2", - input: "2", - expected: "socks5://127.0.0.1:1080", - }, - { - name: "只有IP和端口", - input: "192.168.1.1:8080", - expected: "http://192.168.1.1:8080", - }, - { - name: "只有端口号", - input: "8080", - expected: "http://127.0.0.1:8080", - }, - { - name: "完整HTTP URL", - input: "http://proxy.example.com:8080", - expected: "http://proxy.example.com:8080", - }, - { - name: "完整HTTPS URL", - input: "https://proxy.example.com:8443", - expected: "https://proxy.example.com:8443", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parser.normalizeHTTPProxy(tt.input) - - if result != tt.expected { - t.Errorf("normalizeHTTPProxy(%s) = %s, want %s", tt.input, result, tt.expected) - } - }) - } -} - -// ============================================================================= -// normalizeSocks5Proxy 测试 -// ============================================================================= - -func TestNetworkParser_NormalizeSocks5Proxy(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - input string - expected string - }{ - { - name: "IP和端口", - input: "192.168.1.1:1080", - expected: "socks5://192.168.1.1:1080", - }, - { - name: "只有端口号", - input: "1080", - expected: "socks5://127.0.0.1:1080", - }, - { - name: "已有socks5前缀", - input: "socks5://proxy.example.com:1080", - expected: "socks5://proxy.example.com:1080", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parser.normalizeSocks5Proxy(tt.input) - - if result != tt.expected { - t.Errorf("normalizeSocks5Proxy(%s) = %s, want %s", tt.input, result, tt.expected) - } - }) - } -} - -// ============================================================================= -// validateProxyURL 测试 -// ============================================================================= - -func TestNetworkParser_ValidateProxyURL(t *testing.T) { - // 使用AllowInsecure选项测试HTTP代理 - parser := NewNetworkParser(&NetworkParserOptions{ - ValidateProxies: true, - AllowInsecure: true, - }) - - tests := []struct { - name string - proxyURL string - wantError bool - }{ - { - name: "有效HTTP代理(AllowInsecure=true)", - proxyURL: "http://127.0.0.1:8080", - wantError: false, - }, - { - name: "有效HTTPS代理", - proxyURL: "https://proxy.example.com:8443", - wantError: false, - }, - { - name: "有效Socks5代理", - proxyURL: "socks5://127.0.0.1:1080", - wantError: false, - }, - { - name: "空URL", - proxyURL: "", - wantError: false, // 空URL被认为是有效的(无代理) - }, - { - name: "不支持的协议", - proxyURL: "ftp://proxy.example.com:21", - wantError: true, - }, - { - name: "缺少主机名", - proxyURL: "http://:8080", - wantError: true, - }, - { - name: "无效端口号", - proxyURL: "http://127.0.0.1:99999", - wantError: true, - }, - { - name: "端口号为0", - proxyURL: "http://127.0.0.1:0", - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := parser.validateProxyURL(tt.proxyURL) - - if tt.wantError && err == nil { - t.Error("期望错误,但没有错误") - } - if !tt.wantError && err != nil { - t.Errorf("意外错误: %v", err) - } - }) - } -} - -// ============================================================================= -// validateProxyURL 不允许不安全代理测试 -// ============================================================================= - -func TestNetworkParser_ValidateProxyURL_Insecure(t *testing.T) { - parser := NewNetworkParser(&NetworkParserOptions{ - ValidateProxies: true, - AllowInsecure: false, - }) - - err := parser.validateProxyURL("http://proxy.example.com:8080") - - if err == nil { - t.Error("期望错误(不允许HTTP代理),但没有错误") - } -} - -// ============================================================================= -// parseTimeouts 测试 -// ============================================================================= - -func TestNetworkParser_ParseTimeouts(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - timeout int64 - webTimeout int64 - wantTimeout time.Duration - wantWebTimeout time.Duration - wantWarnings int - }{ - { - name: "使用默认超时", - timeout: 0, - webTimeout: 0, - wantTimeout: DefaultNetworkTimeout, - wantWebTimeout: DefaultWebTimeout, - wantWarnings: 0, - }, - { - name: "自定义超时", - timeout: 60, - webTimeout: 30, - wantTimeout: 60 * time.Second, - wantWebTimeout: 30 * time.Second, - wantWarnings: 0, - }, - { - name: "超时过长(警告)", - timeout: 400, - webTimeout: 200, - wantTimeout: 400 * time.Second, - wantWebTimeout: 200 * time.Second, - wantWarnings: 2, - }, - { - name: "Web超时远大于普通超时(警告)", - timeout: 10, - webTimeout: 100, - wantTimeout: 10 * time.Second, - wantWebTimeout: 100 * time.Second, - wantWarnings: 1, // 只有Web超时远大于普通超时警告 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - timeout, webTimeout, _, warnings := parser.parseTimeouts(tt.timeout, tt.webTimeout) - - if timeout != tt.wantTimeout { - t.Errorf("timeout = %v, want %v", timeout, tt.wantTimeout) - } - - if webTimeout != tt.wantWebTimeout { - t.Errorf("webTimeout = %v, want %v", webTimeout, tt.wantWebTimeout) - } - - if len(warnings) != tt.wantWarnings { - t.Errorf("警告数量 = %d, want %d (warnings: %v)", len(warnings), tt.wantWarnings, warnings) - } - }) - } -} - -// ============================================================================= -// parseUserAgent 测试 -// ============================================================================= - -func TestNetworkParser_ParseUserAgent(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - userAgent string - wantUA string - wantError bool - }{ - { - name: "使用默认User-Agent", - userAgent: "", - wantUA: DefaultUserAgent, - wantError: false, - }, - { - name: "自定义User-Agent", - userAgent: "Custom-Bot/1.0", - wantError: false, - }, - { - name: "过长的User-Agent", - userAgent: strings.Repeat("a", 600), - wantError: true, - }, - { - name: "包含非法字符的User-Agent", - userAgent: "Agent\nWith\nNewlines", - wantError: true, - }, - { - name: "包含制表符的User-Agent", - userAgent: "Agent\tWith\tTabs", - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ua, errors, _ := parser.parseUserAgent(tt.userAgent) - - if tt.wantError { - if len(errors) == 0 { - t.Error("期望错误,但没有错误") - } - return - } - - if len(errors) > 0 { - t.Errorf("意外错误: %v", errors) - return - } - - if tt.userAgent == "" && ua != tt.wantUA { - t.Errorf("使用默认UA失败: got %s, want %s", ua, tt.wantUA) - } - }) - } -} - -// ============================================================================= -// parseCookie 测试 -// ============================================================================= - -func TestNetworkParser_ParseCookie(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - cookie string - wantError bool - }{ - { - name: "空Cookie", - cookie: "", - wantError: false, - }, - { - name: "单个Cookie", - cookie: "session=abc123", - wantError: false, - }, - { - name: "多个Cookie", - cookie: "session=abc123; token=xyz789; user=admin", - wantError: false, - }, - { - name: "过长的Cookie", - cookie: strings.Repeat("a", 5000), - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, errors, _ := parser.parseCookie(tt.cookie) - - if tt.wantError && len(errors) == 0 { - t.Error("期望错误,但没有错误") - } - if !tt.wantError && len(errors) > 0 { - t.Errorf("意外错误: %v", errors) - } - }) - } -} - -// ============================================================================= -// isValidUserAgent 测试 -// ============================================================================= - -func TestNetworkParser_IsValidUserAgent(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - userAgent string - wantValid bool - }{ - { - name: "包含Mozilla", - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", - wantValid: true, - }, - { - name: "包含Chrome", - userAgent: "Chrome/104.0.0.0", - wantValid: true, - }, - { - name: "包含Safari", - userAgent: "Safari/537.36", - wantValid: true, - }, - { - name: "包含Firefox", - userAgent: "Firefox/100.0", - wantValid: true, - }, - { - name: "自定义Agent(不在列表中)", - userAgent: "CustomBot/1.0", - wantValid: false, - }, - { - name: "空User-Agent", - userAgent: "", - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - valid := parser.isValidUserAgent(tt.userAgent) - - if valid != tt.wantValid { - t.Errorf("isValidUserAgent(%s) = %v, want %v", tt.userAgent, valid, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// isValidCookie 测试 -// ============================================================================= - -func TestNetworkParser_IsValidCookie(t *testing.T) { - parser := NewNetworkParser(nil) - - tests := []struct { - name string - cookie string - wantValid bool - }{ - { - name: "有效的简单Cookie", - cookie: "session=abc123", - wantValid: true, - }, - { - name: "有效的多Cookie", - cookie: "session=abc123; token=xyz789", - wantValid: true, - }, - { - name: "带空格的Cookie", - cookie: "session=abc123; token=xyz789", - wantValid: true, - }, - { - name: "无值Cookie", - cookie: "session=; token=xyz", - wantValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - valid := parser.isValidCookie(tt.cookie) - - if valid != tt.wantValid { - t.Errorf("isValidCookie(%s) = %v, want %v", tt.cookie, valid, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// 代理冲突警告测试 -// ============================================================================= - -func TestNetworkParser_ProxyConflictWarning(t *testing.T) { - parser := NewNetworkParser(&NetworkParserOptions{ - AllowInsecure: true, // 允许HTTP代理以测试冲突警告 - }) - - input := &NetworkInput{ - HTTPProxy: "http://127.0.0.1:8080", - Socks5Proxy: "socks5://127.0.0.1:1080", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该有警告(同时配置了两种代理+Socks5建议) - if len(result.Warnings) < 2 { - t.Errorf("期望至少有2个警告,但只有 %d 个", len(result.Warnings)) - } -} - -// ============================================================================= -// Socks5代理建议测试 -// ============================================================================= - -func TestNetworkParser_Socks5ProxyHint(t *testing.T) { - parser := NewNetworkParser(nil) - - input := &NetworkInput{ - Socks5Proxy: "socks5://127.0.0.1:1080", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 应该有建议禁用Ping的警告 - foundPingHint := false - for _, warning := range result.Warnings { - if strings.Contains(warning, "Ping") || strings.Contains(warning, "ping") { - foundPingHint = true - break - } - } - - if !foundPingHint { - t.Errorf("未找到Ping建议警告,warnings: %v", result.Warnings) - } -} - -// ============================================================================= -// 完整配置测试 -// ============================================================================= - -func TestNetworkParser_FullConfiguration(t *testing.T) { - parser := NewNetworkParser(nil) - - input := &NetworkInput{ - HTTPProxy: "https://proxy.example.com:8443", - Timeout: 60, - WebTimeout: 30, - DisablePing: true, - DNSLog: true, - UserAgent: "Mozilla/5.0", - Cookie: "session=test123; token=abc", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - config := result.Config.Network - - if config.HTTPProxy != "https://proxy.example.com:8443" { - t.Errorf("HTTPProxy = %s, want https://proxy.example.com:8443", config.HTTPProxy) - } - - if config.Timeout != 60*time.Second { - t.Errorf("Timeout = %v, want 60s", config.Timeout) - } - - if config.WebTimeout != 30*time.Second { - t.Errorf("WebTimeout = %v, want 30s", config.WebTimeout) - } - - if !config.DisablePing { - t.Error("DisablePing应为true") - } - - if !config.EnableDNSLog { - t.Error("EnableDNSLog应为true") - } - - if config.UserAgent != "Mozilla/5.0" { - t.Errorf("UserAgent = %s, want Mozilla/5.0", config.UserAgent) - } - - if config.Cookie != "session=test123; token=abc" { - t.Errorf("Cookie = %s, want session=test123; token=abc", config.Cookie) - } -} diff --git a/common/parsers/parse.go b/common/parsers/parse.go deleted file mode 100644 index 83046b3..0000000 --- a/common/parsers/parse.go +++ /dev/null @@ -1,260 +0,0 @@ -package parsers - -import ( - "bufio" - "fmt" - "os" - "sort" - "strconv" - "strings" - - "github.com/shadow1ng/fscan/common/config" -) - -/* -Simple.go - 简化版本的解析器函数 - -这个文件提供了简化但功能完整的解析函数,用于替代复杂的解析器架构。 -保持与现有代码的接口兼容性,但大幅简化实现逻辑。 -*/ - -// ============================================================================= -// 简化的IP/主机解析函数 -// ============================================================================= - -// ParseIP 解析各种格式的IP地址 -// 支持单个IP、IP范围、CIDR和文件输入 -func ParseIP(host string, filename string, nohosts ...string) ([]string, error) { - var hosts []string - - // 如果提供了文件名,从文件读取主机列表 - if filename != "" { - fileHosts, fileErr := readHostsFromFile(filename) - if fileErr != nil { - return nil, fmt.Errorf("读取主机文件失败: %w", fileErr) - } - hosts = append(hosts, fileHosts...) - } - - // 解析主机参数 - if host != "" { - hostList, hostErr := parseHostString(host) - if hostErr != nil { - return nil, fmt.Errorf("解析主机失败: %w", hostErr) - } - hosts = append(hosts, hostList...) - } - - // 处理排除主机 - if len(nohosts) > 0 && nohosts[0] != "" { - excludeList, excludeErr := parseHostString(nohosts[0]) - if excludeErr != nil { - return nil, fmt.Errorf("解析排除主机失败: %w", excludeErr) - } - hosts = excludeHosts(hosts, excludeList) - } - - // 去重和排序 - hosts = removeDuplicates(hosts) - sort.Strings(hosts) - - if len(hosts) == 0 { - return nil, fmt.Errorf("没有找到有效的主机") - } - - return hosts, nil -} - -// ============================================================================= -// 简化的端口解析函数 -// ============================================================================= - -// ParsePort 解析端口配置字符串为端口号列表 -// 保持与 ParsePort 的接口兼容性 -func ParsePort(ports string) []int { - if ports == "" { - return nil - } - - var result []int - - // 处理预定义端口组 - ports = expandPortGroups(ports) - - // 按逗号分割 - for _, portStr := range strings.Split(ports, ",") { - portStr = strings.TrimSpace(portStr) - if portStr == "" { - continue - } - - // 处理端口范围 (如 1-100) - if strings.Contains(portStr, "-") { - rangePorts := parsePortRange(portStr) - result = append(result, rangePorts...) - } else { - // 单个端口 - if port, err := strconv.Atoi(portStr); err == nil { - if port >= MinPort && port <= MaxPort { - result = append(result, port) - } - } - } - } - - // 去重和排序 - result = removeDuplicatePorts(result) - sort.Ints(result) - - return result -} - -// 已移除未使用的 ParsePortsFromString 方法 - -// ============================================================================= -// 辅助函数 -// ============================================================================= - -// readHostsFromFile 从文件读取主机列表 -func readHostsFromFile(filename string) ([]string, error) { - file, err := os.Open(filename) - if err != nil { - return nil, err - } - defer func() { _ = file.Close() }() // 只读文件,Close错误可安全忽略 - - var hosts []string - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line != "" && !strings.HasPrefix(line, CommentPrefix) { - hosts = append(hosts, line) - } - } - - return hosts, scanner.Err() -} - -// parseHostString 解析主机字符串 -func parseHostString(host string) ([]string, error) { - var hosts []string - - // 按逗号分割多个主机 - for _, h := range strings.Split(host, ",") { - h = strings.TrimSpace(h) - if h == "" { - continue - } - - // 检查是否为CIDR格式 - if strings.Contains(h, "/") { - cidrHosts, err := parseIPCIDR(h, SimpleMaxHosts) - if err != nil { - return nil, fmt.Errorf("解析CIDR %s 失败: %w", h, err) - } - hosts = append(hosts, cidrHosts...) - } else if strings.Contains(h, "-") && !strings.Contains(h, ":") { - // IP范围格式 (如 192.168.1.1-10) - rangeHosts, err := parseIPRangeString(h, SimpleMaxHosts) - if err != nil { - return nil, fmt.Errorf("解析IP范围 %s 失败: %w", h, err) - } - hosts = append(hosts, rangeHosts...) - } else { - // 单个主机 - hosts = append(hosts, h) - } - } - - return hosts, nil -} - -// parsePortRange 解析端口范围 -func parsePortRange(rangeStr string) []int { - parts := strings.Split(rangeStr, "-") - if len(parts) != 2 { - return nil - } - - start, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) - end, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) - - if err1 != nil || err2 != nil || start < MinPort || end > MaxPort || start > end { - return nil - } - - var ports []int - for i := start; i <= end; i++ { - ports = append(ports, i) - } - - return ports -} - -// expandPortGroups 展开端口组 -func expandPortGroups(ports string) string { - // 使用预定义的端口组 - portGroups := config.GetPortGroups() - - result := ports - for group, portList := range portGroups { - result = strings.ReplaceAll(result, group, portList) - } - - return result -} - -// excludeHosts 排除指定的主机 -func excludeHosts(hosts, excludeList []string) []string { - if len(excludeList) == 0 { - return hosts - } - - excludeMap := make(map[string]struct{}) - for _, exclude := range excludeList { - excludeMap[exclude] = struct{}{} - } - - var result []string - for _, host := range hosts { - if _, found := excludeMap[host]; !found { - result = append(result, host) - } - } - - return result -} - -// removeDuplicates 去除字符串重复项 -func removeDuplicates(slice []string) []string { - keys := make(map[string]struct{}) - var result []string - - for _, item := range slice { - if _, found := keys[item]; !found { - keys[item] = struct{}{} - result = append(result, item) - } - } - - return result -} - -// removeDuplicatePorts 去除端口重复项 -func removeDuplicatePorts(slice []int) []int { - if len(slice) == 0 { - return nil - } - - keys := make(map[int]struct{}, len(slice)) - result := make([]int, 0, len(slice)) - - for _, item := range slice { - if _, found := keys[item]; !found { - keys[item] = struct{}{} - result = append(result, item) - } - } - - return result -} diff --git a/common/parsers/parsers.go b/common/parsers/parsers.go new file mode 100644 index 0000000..537144b --- /dev/null +++ b/common/parsers/parsers.go @@ -0,0 +1,488 @@ +package parsers + +import ( + "bufio" + "encoding/hex" + "fmt" + "net" + "os" + "sort" + "strconv" + "strings" + + "github.com/shadow1ng/fscan/common/config" +) + +/* +parsers.go - 核心解析函数 + +保留的核心功能: +- ParseIP() - IP地址/CIDR/范围解析 +- ParsePort() - 端口解析 +- ReadLinesFromFile() - 文件读取 +- ParseUserPassFile() - 用户密码对解析 +- ParseHashFile() - 哈希文件解析 +*/ + +// ============================================================================= +// IP/主机解析 +// ============================================================================= + +// ParseIP 解析各种格式的IP地址 +// 支持单个IP、IP范围、CIDR和文件输入 +func ParseIP(host string, filename string, nohosts ...string) ([]string, error) { + var hosts []string + + // 从文件读取主机列表 + if filename != "" { + fileHosts, err := ReadLinesFromFile(filename) + if err != nil { + return nil, fmt.Errorf("读取主机文件失败: %w", err) + } + for _, h := range fileHosts { + parsed, err := parseHostString(h) + if err != nil { + continue // 跳过无效行 + } + hosts = append(hosts, parsed...) + } + } + + // 解析主机参数 + if host != "" { + hostList, err := parseHostString(host) + if err != nil { + return nil, fmt.Errorf("解析主机失败: %w", err) + } + hosts = append(hosts, hostList...) + } + + // 处理排除主机 + if len(nohosts) > 0 && nohosts[0] != "" { + excludeList, err := parseHostString(nohosts[0]) + if err != nil { + return nil, fmt.Errorf("解析排除主机失败: %w", err) + } + hosts = excludeFromList(hosts, excludeList) + } + + // 去重和排序 + hosts = removeDuplicateStrings(hosts) + sort.Strings(hosts) + + if len(hosts) == 0 { + return nil, fmt.Errorf("没有找到有效的主机") + } + + return hosts, nil +} + +// parseHostString 解析主机字符串 +func parseHostString(host string) ([]string, error) { + var hosts []string + + for _, h := range strings.Split(host, ",") { + h = strings.TrimSpace(h) + if h == "" { + continue + } + + switch { + case h == "192": + cidrHosts, err := parseIPCIDR("192.168.0.0/16", SimpleMaxHosts) + if err != nil { + return nil, err + } + hosts = append(hosts, cidrHosts...) + case h == "172": + cidrHosts, err := parseIPCIDR("172.16.0.0/12", SimpleMaxHosts) + if err != nil { + return nil, err + } + hosts = append(hosts, cidrHosts...) + case h == "10": + cidrHosts, err := parseIPCIDR("10.0.0.0/8", SimpleMaxHosts) + if err != nil { + return nil, err + } + hosts = append(hosts, cidrHosts...) + case strings.Contains(h, "/"): + cidrHosts, err := parseIPCIDR(h, SimpleMaxHosts) + if err != nil { + return nil, fmt.Errorf("CIDR解析失败 %s: %w", h, err) + } + hosts = append(hosts, cidrHosts...) + case strings.Contains(h, "-") && !strings.Contains(h, ":"): + rangeHosts, err := parseIPRangeString(h, SimpleMaxHosts) + if err != nil { + return nil, fmt.Errorf("IP范围解析失败 %s: %w", h, err) + } + hosts = append(hosts, rangeHosts...) + default: + hosts = append(hosts, h) + } + } + + return hosts, nil +} + +// ============================================================================= +// 端口解析 +// ============================================================================= + +// ParsePort 解析端口配置字符串为端口号列表 +func ParsePort(ports string) []int { + if ports == "" { + return nil + } + + var result []int + + // 展开端口组 + ports = expandPortGroups(ports) + + for _, portStr := range strings.Split(ports, ",") { + portStr = strings.TrimSpace(portStr) + if portStr == "" { + continue + } + + if strings.Contains(portStr, "-") { + rangePorts := parsePortRange(portStr) + result = append(result, rangePorts...) + } else { + if port, err := strconv.Atoi(portStr); err == nil { + if port >= MinPort && port <= MaxPort { + result = append(result, port) + } + } + } + } + + result = removeDuplicatePorts(result) + sort.Ints(result) + + return result +} + +// parsePortRange 解析端口范围 +func parsePortRange(rangeStr string) []int { + parts := strings.Split(rangeStr, "-") + if len(parts) != 2 { + return nil + } + + start, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) + end, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) + + if err1 != nil || err2 != nil || start < MinPort || end > MaxPort || start > end { + return nil + } + + ports := make([]int, 0, end-start+1) + for i := start; i <= end; i++ { + ports = append(ports, i) + } + + return ports +} + +// expandPortGroups 展开端口组 +func expandPortGroups(ports string) string { + portGroups := config.GetPortGroups() + result := ports + for group, portList := range portGroups { + result = strings.ReplaceAll(result, group, portList) + } + return result +} + +// ============================================================================= +// 文件读取 +// ============================================================================= + +// ReadLinesFromFile 从文件读取非空非注释行 +func ReadLinesFromFile(filename string) ([]string, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" && !strings.HasPrefix(line, "#") { + lines = append(lines, line) + } + } + + return lines, scanner.Err() +} + +// ============================================================================= +// 凭据解析 +// ============================================================================= + +// ParseUserPassFile 解析用户名:密码文件 +func ParseUserPassFile(filename string) ([]config.CredentialPair, error) { + lines, err := ReadLinesFromFile(filename) + if err != nil { + return nil, err + } + + var pairs []config.CredentialPair + for _, line := range lines { + idx := strings.Index(line, ":") + if idx == -1 { + continue + } + + user := strings.TrimSpace(line[:idx]) + pass := line[idx+1:] // 密码不trim,可能包含空格 + + if user == "" { + continue + } + + pairs = append(pairs, config.CredentialPair{ + Username: user, + Password: pass, + }) + } + + return pairs, nil +} + +// ParseHashFile 解析哈希文件 +func ParseHashFile(filename string) ([]string, [][]byte, error) { + lines, err := ReadLinesFromFile(filename) + if err != nil { + return nil, nil, err + } + + var hashValues []string + var hashBytes [][]byte + + for _, line := range lines { + line = strings.TrimSpace(line) + if len(line) != 32 { // MD5长度 + continue + } + if !CompiledHashRegex.MatchString(line) { + continue + } + + hashValues = append(hashValues, line) + if hashByte, err := hex.DecodeString(line); err == nil { + hashBytes = append(hashBytes, hashByte) + } + } + + return hashValues, hashBytes, nil +} + +// ============================================================================= +// 内部辅助函数 +// ============================================================================= + +// parseIPCIDR 解析CIDR网段 +func parseIPCIDR(cidr string, maxTargets int) ([]string, error) { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + var ips []string + ip := make(net.IP, len(ipNet.IP)) + copy(ip, ipNet.IP) + + count := 0 + for ipNet.Contains(ip) { + ips = append(ips, ip.String()) + count++ + if count >= maxTargets { + break + } + incrementIP(ip) + } + + // 移除网络地址和广播地址 + if len(ips) > 2 { + ips = ips[1 : len(ips)-1] + } + + return ips, nil +} + +// parseIPRangeString 解析IP范围字符串 +func parseIPRangeString(rangeStr string, maxTargets int) ([]string, error) { + parts := strings.Split(rangeStr, "-") + if len(parts) != 2 { + return nil, fmt.Errorf("无效的IP范围格式: %s", rangeStr) + } + + startIPStr := strings.TrimSpace(parts[0]) + endIPStr := strings.TrimSpace(parts[1]) + + startIP := net.ParseIP(startIPStr) + if startIP == nil { + return nil, fmt.Errorf("无效的起始IP地址: %s", startIPStr) + } + + // 处理简写格式 (如: 192.168.1.1-100) + if len(endIPStr) < 4 || !strings.Contains(endIPStr, ".") { + return parseIPShortRange(startIPStr, endIPStr) + } + + // 处理完整格式 (如: 192.168.1.1-192.168.1.100) + endIP := net.ParseIP(endIPStr) + if endIP == nil { + return nil, fmt.Errorf("无效的结束IP地址: %s", endIPStr) + } + + return parseIPFullRange(startIP, endIP, maxTargets) +} + +// parseIPShortRange 解析短格式IP范围 +func parseIPShortRange(startIPStr, endSuffix string) ([]string, error) { + endNum, err := strconv.Atoi(endSuffix) + if err != nil || endNum > 255 { + return nil, fmt.Errorf("无效的IP范围结束值: %s", endSuffix) + } + + ipParts := strings.Split(startIPStr, ".") + if len(ipParts) != 4 { + return nil, fmt.Errorf("无效的IP地址格式: %s", startIPStr) + } + + prefixIP := strings.Join(ipParts[0:3], ".") + startNum, err := strconv.Atoi(ipParts[3]) + if err != nil || startNum > endNum { + return nil, fmt.Errorf("无效的IP范围: %s-%s", startIPStr, endSuffix) + } + + var allIP []string + for i := startNum; i <= endNum; i++ { + allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i)) + } + + return allIP, nil +} + +// parseIPFullRange 解析完整格式的IP范围 +func parseIPFullRange(startIP, endIP net.IP, maxTargets int) ([]string, error) { + start4 := startIP.To4() + end4 := endIP.To4() + if start4 == nil || end4 == nil { + return nil, fmt.Errorf("仅支持IPv4地址范围") + } + + startInt := (int(start4[0]) << 24) | (int(start4[1]) << 16) | (int(start4[2]) << 8) | int(start4[3]) + endInt := (int(end4[0]) << 24) | (int(end4[1]) << 16) | (int(end4[2]) << 8) | int(end4[3]) + + if startInt > endInt { + return nil, fmt.Errorf("起始IP大于结束IP") + } + + var ips []string + current := make(net.IP, len(start4)) + copy(current, start4) + + count := 0 + for { + ips = append(ips, current.String()) + count++ + + if current.Equal(end4) || count >= maxTargets { + break + } + incrementIP(current) + } + + return ips, nil +} + +// incrementIP 计算下一个IP地址 +func incrementIP(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +// excludeFromList 从列表中排除指定项 +func excludeFromList(hosts, excludeList []string) []string { + if len(excludeList) == 0 { + return hosts + } + + excludeMap := make(map[string]struct{}, len(excludeList)) + for _, e := range excludeList { + excludeMap[e] = struct{}{} + } + + result := make([]string, 0, len(hosts)) + for _, h := range hosts { + if _, found := excludeMap[h]; !found { + result = append(result, h) + } + } + + return result +} + +// removeDuplicateStrings 去除字符串重复项 +func removeDuplicateStrings(slice []string) []string { + if len(slice) == 0 { + return nil + } + + seen := make(map[string]struct{}, len(slice)) + result := make([]string, 0, len(slice)) + + for _, item := range slice { + if _, found := seen[item]; !found { + seen[item] = struct{}{} + result = append(result, item) + } + } + + return result +} + +// removeDuplicatePorts 去除端口重复项 +func removeDuplicatePorts(slice []int) []int { + if len(slice) == 0 { + return nil + } + + seen := make(map[int]struct{}, len(slice)) + result := make([]int, 0, len(slice)) + + for _, item := range slice { + if _, found := seen[item]; !found { + seen[item] = struct{}{} + result = append(result, item) + } + } + + return result +} + +// ============================================================================= +// 导出别名(供测试使用) +// ============================================================================= + +// excludeHosts 是 excludeFromList 的别名(供测试兼容) +func excludeHosts(hosts, excludeList []string) []string { + return excludeFromList(hosts, excludeList) +} + +// removeDuplicates 是 removeDuplicateStrings 的别名(供测试兼容) +func removeDuplicates(slice []string) []string { + return removeDuplicateStrings(slice) +} diff --git a/common/parsers/target.go b/common/parsers/target.go deleted file mode 100644 index aed8f5b..0000000 --- a/common/parsers/target.go +++ /dev/null @@ -1,1002 +0,0 @@ -package parsers - -import ( - "fmt" - "net" - "net/url" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "github.com/shadow1ng/fscan/common/config" -) - -// TargetParser 目标解析器 -type TargetParser struct { - fileReader *FileReader - mu sync.RWMutex //nolint:unused // reserved for future thread safety - ipRegex *regexp.Regexp - portRegex *regexp.Regexp - urlRegex *regexp.Regexp - options *TargetParserOptions -} - -// TargetParserOptions 目标解析器选项 -type TargetParserOptions struct { - MaxTargets int `json:"max_targets"` - MaxPortRange int `json:"max_port_range"` - AllowPrivateIPs bool `json:"allow_private_ips"` - AllowLoopback bool `json:"allow_loopback"` - ValidateURLs bool `json:"validate_urls"` - ResolveDomains bool `json:"resolve_domains"` -} - -// DefaultTargetParserOptions 默认目标解析器选项 -func DefaultTargetParserOptions() *TargetParserOptions { - return &TargetParserOptions{ - MaxTargets: DefaultTargetMaxTargets, - MaxPortRange: DefaultMaxPortRange, - AllowPrivateIPs: DefaultAllowPrivateIPs, - AllowLoopback: DefaultAllowLoopback, - ValidateURLs: DefaultValidateURLs, - ResolveDomains: DefaultResolveDomains, - } -} - -// NewTargetParser 创建目标解析器 -func NewTargetParser(fileReader *FileReader, options *TargetParserOptions) *TargetParser { - if options == nil { - options = DefaultTargetParserOptions() - } - - // 使用预编译的正则表达式 - ipRegex := CompiledIPv4Regex - portRegex := CompiledPortRegex - urlRegex := CompiledURLRegex - - return &TargetParser{ - fileReader: fileReader, - ipRegex: ipRegex, - portRegex: portRegex, - urlRegex: urlRegex, - options: options, - } -} - -// TargetInput 目标输入参数 -type TargetInput struct { - // 主机相关 - Host string `json:"host"` - HostsFile string `json:"hosts_file"` - ExcludeHosts string `json:"exclude_hosts"` - ExcludeHostsFile string `json:"exclude_hosts_file"` - - // 端口相关 - Ports string `json:"ports"` - PortsFile string `json:"ports_file"` - AddPorts string `json:"add_ports"` - ExcludePorts string `json:"exclude_ports"` - - // URL相关 - TargetURL string `json:"target_url"` - URLsFile string `json:"urls_file"` - - // 主机端口组合 - HostPort []string `json:"host_port"` - - // 模式标识 - LocalMode bool `json:"local_mode"` -} - -// Parse 解析目标配置 -func (tp *TargetParser) Parse(input *TargetInput, options *ParserOptions) (*ParseResult, error) { - if input == nil { - return nil, NewParseError(ErrorTypeInputError, "目标输入为空", "", 0, ErrEmptyInput) - } - - startTime := time.Now() - result := &ParseResult{ - Config: &ParsedConfig{ - Targets: &TargetConfig{ - LocalMode: input.LocalMode, - }, - }, - Success: true, - } - - var errors []error - var warnings []string - - // 解析主机 - hosts, hostErrors, hostWarnings := tp.parseHosts(input) - errors = append(errors, hostErrors...) - warnings = append(warnings, hostWarnings...) - - // 解析URL - urls, urlErrors, urlWarnings := tp.parseURLs(input) - errors = append(errors, urlErrors...) - warnings = append(warnings, urlWarnings...) - - // 解析端口 - ports, portErrors, portWarnings := tp.parsePorts(input) - errors = append(errors, portErrors...) - warnings = append(warnings, portWarnings...) - - // 解析排除端口 - excludePorts, excludeErrors, excludeWarnings := tp.parseExcludePorts(input) - errors = append(errors, excludeErrors...) - warnings = append(warnings, excludeWarnings...) - - // 解析主机端口组合 - hostPorts, hpErrors, hpWarnings := tp.parseHostPorts(input) - errors = append(errors, hpErrors...) - warnings = append(warnings, hpWarnings...) - - // 更新配置 - result.Config.Targets.Hosts = hosts - result.Config.Targets.URLs = urls - - // 如果存在明确的host:port组合,则清空端口列表避免双重扫描 - if len(hostPorts) > 0 { - result.Config.Targets.Ports = nil // 清空默认端口,只扫描指定的host:port - } else { - result.Config.Targets.Ports = ports - } - - result.Config.Targets.ExcludePorts = excludePorts - result.Config.Targets.HostPorts = hostPorts - - // 设置结果状态 - result.Errors = errors - result.Warnings = warnings - result.ParseTime = time.Since(startTime) - result.Success = len(errors) == 0 - - return result, nil -} - -// parseHosts 解析主机 -func (tp *TargetParser) parseHosts(input *TargetInput) ([]string, []error, []string) { - var hosts []string - var errors []error - var warnings []string - - // 解析命令行主机 - if input.Host != "" { - // 检查是否为host:port格式,直接添加到HostPort避免双重处理 - if strings.Contains(input.Host, ":") { - if _, portStr, err := net.SplitHostPort(input.Host); err == nil { - if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort { - // 这是有效的host:port格式,直接添加到HostPort - input.HostPort = append(input.HostPort, input.Host) - // 清空Ports字段,避免解析默认端口 - input.Ports = "" - // 不添加到hosts,避免双重处理 - } else { - // 端口无效,作为普通主机处理 - hostList, parseErr := tp.parseHostList(input.Host) - if parseErr != nil { - errors = append(errors, NewParseError(ErrorTypeHostError, parseErr.Error(), "command line", 0, parseErr)) - } else { - hosts = append(hosts, hostList...) - } - } - } else { - // 不是有效的host:port格式,作为普通主机处理 - hostList, parseErr := tp.parseHostList(input.Host) - if parseErr != nil { - errors = append(errors, NewParseError(ErrorTypeHostError, parseErr.Error(), "command line", 0, parseErr)) - } else { - hosts = append(hosts, hostList...) - } - } - } else { - // 普通主机,正常处理 - hostList, err := tp.parseHostList(input.Host) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeHostError, err.Error(), "command line", 0, err)) - } else { - hosts = append(hosts, hostList...) - } - } - } - - // 从文件读取主机 - if input.HostsFile != "" { - fileResult, err := tp.fileReader.ReadFile(input.HostsFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取主机文件失败", input.HostsFile, 0, err)) - } else { - for i, line := range fileResult.Lines { - hostList, err := tp.parseHostList(line) - if err != nil { - warnings = append(warnings, fmt.Sprintf("主机文件第%d行解析失败: %s", i+1, err.Error())) - } else { - hosts = append(hosts, hostList...) - } - } - } - } - - // 处理排除主机 - var excludeList []string - - // 从命令行参数读取排除主机 - if input.ExcludeHosts != "" { - cmdExclude, err := tp.parseHostList(input.ExcludeHosts) - if err != nil { - warnings = append(warnings, fmt.Sprintf("排除主机解析失败: %s", err.Error())) - } else { - excludeList = append(excludeList, cmdExclude...) - } - } - - // 从文件读取排除主机 - if input.ExcludeHostsFile != "" { - fileResult, err := tp.fileReader.ReadFile(input.ExcludeHostsFile) - if err != nil { - warnings = append(warnings, fmt.Sprintf("读取排除主机文件失败: %s", err.Error())) - } else { - for i, line := range fileResult.Lines { - fileExclude, err := tp.parseHostList(line) - if err != nil { - warnings = append(warnings, fmt.Sprintf("排除主机文件第%d行解析失败: %s", i+1, err.Error())) - } else { - excludeList = append(excludeList, fileExclude...) - } - } - } - } - - // 应用排除列表 - if len(excludeList) > 0 { - hosts = tp.excludeHosts(hosts, excludeList) - } - - // 去重和验证,同时分离host:port格式 - hosts = tp.removeDuplicateStrings(hosts) - validHosts := make([]string, 0, len(hosts)) - hostPorts := make([]string, 0) - - for _, host := range hosts { - // 检查是否为host:port格式 - if strings.Contains(host, ":") { - if h, portStr, err := net.SplitHostPort(host); err == nil { - // 验证端口号 - if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort { - // 验证主机部分 - if valid, hostErr := tp.validateHost(h); valid { - // 这是有效的host:port组合,添加到hostPorts - hostPorts = append(hostPorts, host) - continue - } else if hostErr != nil { - warnings = append(warnings, fmt.Sprintf("无效主机端口组合: %s - %s", host, hostErr.Error())) - continue - } - } - } - } - - // 作为普通主机验证 - if valid, err := tp.validateHost(host); valid { - validHosts = append(validHosts, host) - } else if err != nil { - warnings = append(warnings, fmt.Sprintf("无效主机: %s - %s", host, err.Error())) - } - } - - // 将找到的hostPorts合并到输入结果中(通过修改input结构) - if len(hostPorts) > 0 { - input.HostPort = append(input.HostPort, hostPorts...) - } - - // 检查目标数量限制 - if len(validHosts) > tp.options.MaxTargets { - warnings = append(warnings, fmt.Sprintf("主机数量超过限制,截取前%d个", tp.options.MaxTargets)) - validHosts = validHosts[:tp.options.MaxTargets] - } - - return validHosts, errors, warnings -} - -// parseURLs 解析URL -func (tp *TargetParser) parseURLs(input *TargetInput) ([]string, []error, []string) { - var urls []string - var errors []error - var warnings []string - - // 解析命令行URL - if input.TargetURL != "" { - urlList := strings.Split(input.TargetURL, ",") - for _, rawURL := range urlList { - rawURL = strings.TrimSpace(rawURL) - if rawURL != "" { - normalizedURL := tp.normalizeURL(rawURL) - if valid, err := tp.validateURL(normalizedURL); valid { - urls = append(urls, normalizedURL) - } else { - warnings = append(warnings, fmt.Sprintf("无效URL: %s - %s", rawURL, err.Error())) - } - } - } - } - - // 从文件读取URL - if input.URLsFile != "" { - fileResult, err := tp.fileReader.ReadFile(input.URLsFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取URL文件失败", input.URLsFile, 0, err)) - } else { - for i, line := range fileResult.Lines { - normalizedURL := tp.normalizeURL(line) - if valid, err := tp.validateURL(normalizedURL); valid { - urls = append(urls, normalizedURL) - } else { - warnings = append(warnings, fmt.Sprintf("URL文件第%d行无效: %s", i+1, err.Error())) - } - } - } - } - - // 去重 - urls = tp.removeDuplicateStrings(urls) - - return urls, errors, warnings -} - -// normalizeURL 规范化URL,自动补全协议头 -func (tp *TargetParser) normalizeURL(rawURL string) string { - rawURL = strings.TrimSpace(rawURL) - if rawURL == "" { - return rawURL - } - // 如果没有协议头,自动添加 http:// - if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { - return "http://" + rawURL - } - return rawURL -} - -// parsePorts 解析端口 -func (tp *TargetParser) parsePorts(input *TargetInput) ([]int, []error, []string) { - var ports []int - var errors []error - var warnings []string - - // 解析命令行端口 - if input.Ports != "" { - portList, err := tp.parsePortList(input.Ports) - if err != nil { - errors = append(errors, NewParseError(ErrorTypePortError, err.Error(), "command line", 0, err)) - } else { - ports = append(ports, portList...) - } - } - - // 从文件读取端口 - if input.PortsFile != "" { - fileResult, err := tp.fileReader.ReadFile(input.PortsFile) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeFileError, "读取端口文件失败", input.PortsFile, 0, err)) - } else { - for i, line := range fileResult.Lines { - portList, err := tp.parsePortList(line) - if err != nil { - warnings = append(warnings, fmt.Sprintf("端口文件第%d行解析失败: %s", i+1, err.Error())) - } else { - ports = append(ports, portList...) - } - } - } - } - - // 处理额外端口 - if input.AddPorts != "" { - addPortList, err := tp.parsePortList(input.AddPorts) - if err != nil { - warnings = append(warnings, fmt.Sprintf("额外端口解析失败: %s", err.Error())) - } else { - ports = append(ports, addPortList...) - } - } - - // 去重和排序 - ports = tp.removeDuplicatePorts(ports) - - return ports, errors, warnings -} - -// parseExcludePorts 解析排除端口 -func (tp *TargetParser) parseExcludePorts(input *TargetInput) ([]int, []error, []string) { //nolint:unparam - var excludePorts []int - var errors []error - var warnings []string - - if input.ExcludePorts != "" { - portList, err := tp.parsePortList(input.ExcludePorts) - if err != nil { - errors = append(errors, NewParseError(ErrorTypeExcludePortError, err.Error(), "command line", 0, err)) - } else { - excludePorts = portList - } - } - - return excludePorts, errors, warnings -} - -// parseHostPorts 解析主机端口组合 -func (tp *TargetParser) parseHostPorts(input *TargetInput) ([]string, []error, []string) { //nolint:unparam - var hostPorts []string - var errors []error - var warnings []string - - for _, hp := range input.HostPort { - if hp != "" { - if valid, err := tp.validateHostPort(hp); valid { - hostPorts = append(hostPorts, hp) - } else { - warnings = append(warnings, fmt.Sprintf("无效主机端口组合: %s - %s", hp, err.Error())) - } - } - } - - return hostPorts, errors, warnings -} - -// parseHostList 解析主机列表 -func (tp *TargetParser) parseHostList(hostStr string) ([]string, error) { - if hostStr == "" { - return nil, nil - } - - var hosts []string - hostItems := strings.Split(hostStr, ",") - - for _, item := range hostItems { - item = strings.TrimSpace(item) - if item == "" { - continue - } - - // 检查各种IP格式 - switch { - case item == PrivateNetwork192: - // 常用内网段简写 - cidrHosts, err := tp.parseCIDR(PrivateNetwork192CIDR) - if err != nil { - return nil, fmt.Errorf("192网段解析失败: %w", err) - } - hosts = append(hosts, cidrHosts...) - case item == PrivateNetwork172: - // 常用内网段简写 - cidrHosts, err := tp.parseCIDR(PrivateNetwork172CIDR) - if err != nil { - return nil, fmt.Errorf("172网段解析失败: %w", err) - } - hosts = append(hosts, cidrHosts...) - case item == PrivateNetwork10: - // 常用内网段简写 - cidrHosts, err := tp.parseCIDR(PrivateNetwork10CIDR) - if err != nil { - return nil, fmt.Errorf("10网段解析失败: %w", err) - } - hosts = append(hosts, cidrHosts...) - case strings.HasSuffix(item, "/8"): - // 处理/8网段(使用采样方式) - sampledHosts := tp.parseSubnet8(item) - hosts = append(hosts, sampledHosts...) - case strings.Contains(item, "/"): - // CIDR表示法 - cidrHosts, err := tp.parseCIDR(item) - if err != nil { - return nil, fmt.Errorf("CIDR解析失败 %s: %w", item, err) - } - hosts = append(hosts, cidrHosts...) - case strings.Contains(item, "-"): - // IP范围表示法 - rangeHosts, err := tp.parseIPRange(item) - if err != nil { - return nil, fmt.Errorf("IP范围解析失败 %s: %w", item, err) - } - hosts = append(hosts, rangeHosts...) - default: - // 检查是否为host:port格式 - if strings.Contains(item, ":") { - if _, portStr, err := net.SplitHostPort(item); err == nil { - // 验证端口号 - if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort { - // 这是有效的host:port格式,但在这里仍然作为主机处理 - // 在后续的processHostPorts函数中会被正确处理 - hosts = append(hosts, item) - } else { - // 端口无效,作为普通主机处理 - hosts = append(hosts, item) - } - } else { - // 不是有效的host:port格式,作为普通主机处理 - hosts = append(hosts, item) - } - } else { - // 单个IP或域名 - hosts = append(hosts, item) - } - } - } - - return hosts, nil -} - -// parsePortList 解析端口列表,支持预定义端口组 -func (tp *TargetParser) parsePortList(portStr string) ([]int, error) { - if portStr == "" { - return nil, nil - } - - // 检查是否为预定义端口组 - portStr = tp.expandPortGroups(portStr) - - var ports []int - portItems := strings.Split(portStr, ",") - - for _, item := range portItems { - item = strings.TrimSpace(item) - if item == "" { - continue - } - - if strings.Contains(item, "-") { - // 端口范围 - rangePorts, err := tp.parsePortRange(item) - if err != nil { - return nil, fmt.Errorf("端口范围解析失败 %s: %w", item, err) - } - - // 检查范围大小 - if len(rangePorts) > tp.options.MaxPortRange { - return nil, fmt.Errorf("端口范围过大: %d, 最大允许: %d", len(rangePorts), tp.options.MaxPortRange) - } - - ports = append(ports, rangePorts...) - } else { - // 单个端口 - port, err := strconv.Atoi(item) - if err != nil { - return nil, fmt.Errorf("无效端口号: %s", item) - } - - if port < MinPort || port > MaxPort { - return nil, fmt.Errorf("端口号超出范围: %d", port) - } - - ports = append(ports, port) - } - } - - return ports, nil -} - -// expandPortGroups 展开预定义端口组 -func (tp *TargetParser) expandPortGroups(portStr string) string { - // 使用预定义的端口组 - portGroups := config.GetPortGroups() - - if expandedPorts, exists := portGroups[portStr]; exists { - return expandedPorts - } - return portStr -} - -// parseCIDR 解析CIDR网段 -func (tp *TargetParser) parseCIDR(cidr string) ([]string, error) { - return parseIPCIDR(cidr, tp.options.MaxTargets) -} - -// parseIPRange 解析IP范围,支持简写格式 -func (tp *TargetParser) parseIPRange(rangeStr string) ([]string, error) { - return parseIPRangeString(rangeStr, tp.options.MaxTargets) -} - -// parseSubnet8 解析/8网段的IP地址,生成采样IP列表 -func (tp *TargetParser) parseSubnet8(subnet string) []string { - // 去除CIDR后缀获取基础IP - baseIP := subnet[:len(subnet)-2] - if net.ParseIP(baseIP) == nil { - return nil - } - - // 获取/8网段的第一段 - firstOctet := strings.Split(baseIP, ".")[0] - var sampleIPs []string - - // 对常用网段进行更全面的扫描 - commonSecondOctets := GetCommonSecondOctets() - - // 对于每个选定的第二段,采样部分第三段 - for _, secondOctet := range commonSecondOctets { - for thirdOctet := 0; thirdOctet < 256; thirdOctet += Subnet8ThirdOctetStep { - // 添加常见的网关和服务器IP - sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, DefaultGatewayLastOctet)) // 默认网关 - sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, RouterSwitchLastOctet)) // 通常用于路由器/交换机 - - // 随机采样不同范围的主机IP - fourthOctet := tp.randomInt(SamplingMinHost, SamplingMaxHost) - sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, fourthOctet)) - } - } - - // 对其他二级网段进行稀疏采样 - for secondOctet := 0; secondOctet < 256; secondOctet += Subnet8SamplingStep { - for thirdOctet := 0; thirdOctet < 256; thirdOctet += Subnet8SamplingStep { - // 对于采样的网段,取几个代表性IP - sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, DefaultGatewayLastOctet)) - sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, tp.randomInt(SamplingMinHost, SamplingMaxHost))) - } - } - - // 限制采样数量 - if len(sampleIPs) > tp.options.MaxTargets { - sampleIPs = sampleIPs[:tp.options.MaxTargets] - } - - return sampleIPs -} - -// randomInt 生成指定范围内的随机整数 -func (tp *TargetParser) randomInt(min, max int) int { - if min >= max || min < 0 || max <= 0 { - return max - } - return min + (max-min)/2 // 简化版本,避免依赖rand -} - -// parsePortRange 解析端口范围 -func (tp *TargetParser) parsePortRange(rangeStr string) ([]int, error) { - parts := strings.Split(rangeStr, "-") - if len(parts) != 2 { - return nil, fmt.Errorf("无效的端口范围格式") - } - - startPort, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) - endPort, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) - - if err1 != nil || err2 != nil { - return nil, fmt.Errorf("无效的端口号") - } - - if startPort > endPort { - startPort, endPort = endPort, startPort - } - - if startPort < MinPort || endPort > MaxPort { - return nil, fmt.Errorf("端口号超出范围") - } - - var ports []int - for port := startPort; port <= endPort; port++ { - ports = append(ports, port) - } - - return ports, nil -} - -// validateHost 验证主机地址 -func (tp *TargetParser) validateHost(host string) (bool, error) { - if host == "" { - return false, fmt.Errorf("主机地址为空") - } - - // 检查是否为host:port格式 - if strings.Contains(host, ":") { - // 可能是host:port格式,尝试分离 - if h, portStr, err := net.SplitHostPort(host); err == nil { - // 验证端口号 - if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort { - // 递归验证主机部分(不包含端口) - return tp.validateHost(h) - } - } - // 如果不是有效的host:port格式,继续按普通主机地址处理 - } - - // 检查是否为IP地址 - if ip := net.ParseIP(host); ip != nil { - return tp.validateIP(ip) - } - - // 检查是否为域名 - if tp.isValidDomain(host) { - return true, nil - } - - return false, fmt.Errorf("无效的主机地址格式") -} - -// validateIP 验证IP地址 -func (tp *TargetParser) validateIP(ip net.IP) (bool, error) { - if ip == nil { - return false, fmt.Errorf("IP地址为空") - } - - // 检查是否为私有IP - if !tp.options.AllowPrivateIPs && tp.isPrivateIP(ip) { - return false, fmt.Errorf("不允许私有IP地址") - } - - // 检查是否为回环地址 - if !tp.options.AllowLoopback && ip.IsLoopback() { - return false, fmt.Errorf("不允许回环地址") - } - - return true, nil -} - -// validateURL 验证URL -func (tp *TargetParser) validateURL(rawURL string) (bool, error) { - if rawURL == "" { - return false, fmt.Errorf("URL为空") - } - - if !tp.options.ValidateURLs { - return true, nil - } - - if !tp.urlRegex.MatchString(rawURL) { - return false, fmt.Errorf("URL格式无效") - } - - // 进一步验证URL格式 - _, err := url.Parse(rawURL) - if err != nil { - return false, fmt.Errorf("URL解析失败: %w", err) - } - - return true, nil -} - -// validateHostPort 验证主机端口组合 -func (tp *TargetParser) validateHostPort(hostPort string) (bool, error) { - parts := strings.Split(hostPort, ":") - if len(parts) != 2 { - return false, fmt.Errorf("主机端口格式无效,应为 host:port") - } - - host := strings.TrimSpace(parts[0]) - portStr := strings.TrimSpace(parts[1]) - - // 验证主机 - if valid, err := tp.validateHost(host); !valid { - return false, fmt.Errorf("主机无效: %w", err) - } - - // 验证端口 - port, err := strconv.Atoi(portStr) - if err != nil { - return false, fmt.Errorf("端口号无效: %s", portStr) - } - - if port < MinPort || port > MaxPort { - return false, fmt.Errorf("端口号超出范围: %d", port) - } - - return true, nil -} - -// isPrivateIP 检查是否为私有IP -func (tp *TargetParser) isPrivateIP(ip net.IP) bool { - if ip4 := ip.To4(); ip4 != nil { - // 10.0.0.0/8 - if ip4[0] == 10 { - return true - } - // 172.16.0.0/12 - if ip4[0] == 172 && ip4[1] >= Private172StartSecondOctet && ip4[1] <= Private172EndSecondOctet { - return true - } - // 192.168.0.0/16 - if ip4[0] == 192 && ip4[1] == Private192SecondOctet { - return true - } - } - return false -} - -// isValidDomain 检查是否为有效域名 -func (tp *TargetParser) isValidDomain(domain string) bool { - return CompiledDomainRegex.MatchString(domain) && len(domain) <= MaxDomainLength -} - -// excludeHosts 排除指定主机 -func (tp *TargetParser) excludeHosts(hosts, excludeList []string) []string { - excludeMap := make(map[string]struct{}) - for _, exclude := range excludeList { - excludeMap[exclude] = struct{}{} - } - - var result []string - for _, host := range hosts { - if _, excluded := excludeMap[host]; !excluded { - result = append(result, host) - } - } - - return result -} - -// removeDuplicateStrings 去重字符串切片 -func (tp *TargetParser) removeDuplicateStrings(slice []string) []string { - seen := make(map[string]struct{}) - var result []string - - for _, item := range slice { - if _, exists := seen[item]; !exists { - seen[item] = struct{}{} - result = append(result, item) - } - } - - return result -} - -// removeDuplicatePorts 去重端口切片 -func (tp *TargetParser) removeDuplicatePorts(slice []int) []int { - seen := make(map[int]struct{}) - var result []int - - for _, item := range slice { - if _, exists := seen[item]; !exists { - seen[item] = struct{}{} - result = append(result, item) - } - } - - return result -} - -// ============================================================================= -// 包级共享辅助函数(供 simple.go 和 target_parser.go 共用) -// ============================================================================= - -// incrementIP 计算下一个IP地址(统一的包级函数) -func incrementIP(ip net.IP) { - for j := len(ip) - 1; j >= 0; j-- { - ip[j]++ - if ip[j] > 0 { - break - } - } -} - -// parseIPCIDR 解析CIDR网段(包级函数) -func parseIPCIDR(cidr string, maxTargets int) ([]string, error) { - _, ipNet, err := net.ParseCIDR(cidr) - if err != nil { - return nil, err - } - - var ips []string - ip := make(net.IP, len(ipNet.IP)) - copy(ip, ipNet.IP) - - count := 0 - for ipNet.Contains(ip) { - ips = append(ips, ip.String()) - count++ - - // 防止生成过多IP - if count >= maxTargets { - break - } - - incrementIP(ip) - } - - // 移除网络地址和广播地址 - if len(ips) > 2 { - ips = ips[1 : len(ips)-1] - } - - return ips, nil -} - -// parseIPRangeString 解析IP范围字符串(包级函数) -func parseIPRangeString(rangeStr string, maxTargets int) ([]string, error) { - parts := strings.Split(rangeStr, "-") - if len(parts) != 2 { - return nil, fmt.Errorf("无效的IP范围格式: %s", rangeStr) - } - - startIPStr := strings.TrimSpace(parts[0]) - endIPStr := strings.TrimSpace(parts[1]) - - // 验证起始IP - startIP := net.ParseIP(startIPStr) - if startIP == nil { - return nil, fmt.Errorf("无效的起始IP地址: %s", startIPStr) - } - - // 处理简写格式 (如: 192.168.1.1-100) - if len(endIPStr) < 4 || !strings.Contains(endIPStr, ".") { - return parseIPShortRange(startIPStr, endIPStr) - } - - // 处理完整格式 (如: 192.168.1.1-192.168.1.100) - endIP := net.ParseIP(endIPStr) - if endIP == nil { - return nil, fmt.Errorf("无效的结束IP地址: %s", endIPStr) - } - - return parseIPFullRange(startIP, endIP, maxTargets) -} - -// parseIPShortRange 解析短格式IP范围(包级函数) -func parseIPShortRange(startIPStr, endSuffix string) ([]string, error) { - // 将结束段转换为数字 - endNum, err := strconv.Atoi(endSuffix) - if err != nil || endNum > MaxIPv4OctetValue { - return nil, fmt.Errorf("无效的IP范围结束值: %s", endSuffix) - } - - // 分解起始IP - ipParts := strings.Split(startIPStr, ".") - if len(ipParts) != IPv4OctetCount { - return nil, fmt.Errorf("无效的IP地址格式: %s", startIPStr) - } - - // 获取前缀和起始IP的最后一部分 - prefixIP := strings.Join(ipParts[0:3], ".") - startNum, err := strconv.Atoi(ipParts[3]) - if err != nil || startNum > endNum { - return nil, fmt.Errorf("无效的IP范围: %s-%s", startIPStr, endSuffix) - } - - // 生成IP范围 - var allIP []string - for i := startNum; i <= endNum; i++ { - allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i)) - } - - return allIP, nil -} - -// parseIPFullRange 解析完整格式的IP范围(包级函数) -func parseIPFullRange(startIP, endIP net.IP, maxTargets int) ([]string, error) { - // 转换为IPv4 - start4 := startIP.To4() - end4 := endIP.To4() - if start4 == nil || end4 == nil { - return nil, fmt.Errorf("仅支持IPv4地址范围") - } - - // 计算IP地址的整数表示 - startInt := (int(start4[0]) << IPFirstOctetShift) | (int(start4[1]) << IPSecondOctetShift) | (int(start4[2]) << IPThirdOctetShift) | int(start4[3]) - endInt := (int(end4[0]) << IPFirstOctetShift) | (int(end4[1]) << IPSecondOctetShift) | (int(end4[2]) << IPThirdOctetShift) | int(end4[3]) - - if startInt > endInt { - return nil, fmt.Errorf("起始IP大于结束IP") - } - - // 生成IP列表 - var ips []string - current := make(net.IP, len(start4)) - copy(current, start4) - - count := 0 - for { - ips = append(ips, current.String()) - count++ - - if current.Equal(end4) || count >= maxTargets { - break - } - - incrementIP(current) - } - - return ips, nil -} - -// ============================================================================================= -// 已删除的死代码(未使用):Validate 和 GetStatistics 方法 -// ============================================================================================= diff --git a/common/parsers/target_test.go b/common/parsers/target_test.go deleted file mode 100644 index 9fb4e11..0000000 --- a/common/parsers/target_test.go +++ /dev/null @@ -1,1322 +0,0 @@ -package parsers - -import ( - "net" - "os" - "path/filepath" - "testing" -) - -// ============================================================================= -// 测试辅助函数 -// ============================================================================= - -// createTestFile 创建临时测试文件 -func createTestFile(t *testing.T, content string) string { - t.Helper() - - tmpDir := t.TempDir() - tmpFile := filepath.Join(tmpDir, "test.txt") - - if err := os.WriteFile(tmpFile, []byte(content), 0600); err != nil { - t.Fatalf("创建测试文件失败: %v", err) - } - - return tmpFile -} - -// ============================================================================= -// TargetParser 构造函数测试 -// ============================================================================= - -func TestNewTargetParser(t *testing.T) { - tests := []struct { - name string - options *TargetParserOptions - wantNil bool - }{ - { - name: "使用默认选项", - options: nil, - wantNil: false, - }, - { - name: "使用自定义选项", - options: &TargetParserOptions{ - MaxTargets: 5000, - MaxPortRange: 500, - AllowPrivateIPs: false, - }, - wantNil: false, - }, - } - - fileReader := NewFileReader(nil) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewTargetParser(fileReader, tt.options) - - if tt.wantNil && parser != nil { - t.Error("期望parser为nil,但不是") - } - if !tt.wantNil && parser == nil { - t.Error("期望parser不为nil,但是nil") - } - - if parser != nil { - if parser.options == nil { - t.Error("parser.options为nil") - } - if parser.ipRegex == nil { - t.Error("parser.ipRegex为nil") - } - } - }) - } -} - -// ============================================================================= -// Parse 主函数测试 -// ============================================================================= - -func TestTargetParser_Parse(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - input *TargetInput - wantSuccess bool - wantHosts int - wantPorts int - wantURLs int - wantError bool - }{ - { - name: "空输入", - input: nil, - wantSuccess: false, - wantError: true, - }, - { - name: "单个IP", - input: &TargetInput{ - Host: "192.168.1.1", - }, - wantSuccess: true, - wantHosts: 1, - }, - { - name: "多个IP(逗号分隔)", - input: &TargetInput{ - Host: "192.168.1.1,192.168.1.2,192.168.1.3", - }, - wantSuccess: true, - wantHosts: 3, - }, - { - name: "单个端口", - input: &TargetInput{ - Ports: "80", - }, - wantSuccess: true, - wantPorts: 1, - }, - { - name: "端口范围", - input: &TargetInput{ - Ports: "80-85", - }, - wantSuccess: true, - wantPorts: 6, - }, - { - name: "URL输入", - input: &TargetInput{ - TargetURL: "http://example.com", - }, - wantSuccess: true, - wantURLs: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parser.Parse(tt.input, nil) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if result == nil { - t.Fatal("result为nil") - } - - if result.Success != tt.wantSuccess { - t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess) - } - - if tt.wantHosts > 0 && len(result.Config.Targets.Hosts) != tt.wantHosts { - t.Errorf("Hosts数量 = %d, want %d", len(result.Config.Targets.Hosts), tt.wantHosts) - } - - if tt.wantPorts > 0 && len(result.Config.Targets.Ports) != tt.wantPorts { - t.Errorf("Ports数量 = %d, want %d", len(result.Config.Targets.Ports), tt.wantPorts) - } - - if tt.wantURLs > 0 && len(result.Config.Targets.URLs) != tt.wantURLs { - t.Errorf("URLs数量 = %d, want %d", len(result.Config.Targets.URLs), tt.wantURLs) - } - }) - } -} - -// ============================================================================= -// parseHostList 测试 - CIDR解析 -// ============================================================================= - -func TestTargetParser_ParseHostList_CIDR(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - input string - wantCount int - wantError bool - }{ - { - name: "标准/24 CIDR", - input: "192.168.1.0/24", - wantCount: 254, // 排除网络地址和广播地址 - wantError: false, - }, - { - name: "标准/30 CIDR", - input: "192.168.1.0/30", - wantCount: 2, // 4个地址减去网络和广播 - wantError: false, - }, - { - name: "192网段简写", - input: "192", - wantCount: 65534, // /16网段 - wantError: false, - }, - { - name: "172网段简写", - input: "172", - wantCount: 1048574, // /12网段(被MaxTargets限制) - wantError: false, - }, - { - name: "10网段简写", - input: "10", - wantCount: 16777214, // /8网段(被MaxTargets限制) - wantError: false, - }, - { - name: "无效CIDR", - input: "192.168.1.0/33", - wantCount: 0, - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hosts, err := parser.parseHostList(tt.input) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 由于MaxTargets限制,实际数量可能小于预期 - if len(hosts) > parser.options.MaxTargets { - t.Errorf("主机数量 %d 超过MaxTargets %d", len(hosts), parser.options.MaxTargets) - } - - // 对于小网段,检查准确数量 - if tt.wantCount < parser.options.MaxTargets && len(hosts) != tt.wantCount { - t.Errorf("主机数量 = %d, want %d", len(hosts), tt.wantCount) - } - }) - } -} - -// ============================================================================= -// parseHostList 测试 - IP范围解析 -// ============================================================================= - -func TestTargetParser_ParseHostList_IPRange(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - input string - wantCount int - wantFirst string - wantLast string - wantError bool - }{ - { - name: "简写范围(同网段)", - input: "192.168.1.1-10", - wantCount: 10, - wantFirst: "192.168.1.1", - wantLast: "192.168.1.10", - wantError: false, - }, - { - name: "简写范围(单个IP)", - input: "192.168.1.5-5", - wantCount: 1, - wantFirst: "192.168.1.5", - wantLast: "192.168.1.5", - wantError: false, - }, - { - name: "完整范围", - input: "192.168.1.1-192.168.1.5", - wantCount: 5, - wantFirst: "192.168.1.1", - wantLast: "192.168.1.5", - wantError: false, - }, - { - name: "反向范围(错误:起始大于结束)", - input: "192.168.1.10-192.168.1.5", - wantCount: 0, - wantError: true, - }, - { - name: "无效范围(格式错误)", - input: "192.168.1.1-300", - wantCount: 0, - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hosts, err := parser.parseHostList(tt.input) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if len(hosts) != tt.wantCount { - t.Errorf("主机数量 = %d, want %d", len(hosts), tt.wantCount) - } - - if len(hosts) > 0 { - if hosts[0] != tt.wantFirst { - t.Errorf("第一个主机 = %s, want %s", hosts[0], tt.wantFirst) - } - if hosts[len(hosts)-1] != tt.wantLast { - t.Errorf("最后一个主机 = %s, want %s", hosts[len(hosts)-1], tt.wantLast) - } - } - }) - } -} - -// ============================================================================= -// parsePortList 测试 -// ============================================================================= - -func TestTargetParser_ParsePortList(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - input string - wantPorts []int - wantError bool - }{ - { - name: "单个端口", - input: "80", - wantPorts: []int{80}, - wantError: false, - }, - { - name: "多个端口(逗号分隔)", - input: "80,443,8080", - wantPorts: []int{80, 443, 8080}, - wantError: false, - }, - { - name: "端口范围", - input: "8000-8005", - wantPorts: []int{8000, 8001, 8002, 8003, 8004, 8005}, - wantError: false, - }, - { - name: "混合格式", - input: "80,443,8000-8002", - wantPorts: []int{80, 443, 8000, 8001, 8002}, - wantError: false, - }, - { - name: "无效端口号", - input: "99999", - wantPorts: nil, - wantError: true, - }, - { - name: "无效端口范围", - input: "abc-def", - wantPorts: nil, - wantError: true, - }, - { - name: "端口范围过大", - input: "1-70000", - wantPorts: nil, - wantError: true, // 超过MaxPortRange限制(65535) - }, - { - name: "空输入", - input: "", - wantPorts: nil, - wantError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ports, err := parser.parsePortList(tt.input) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if len(ports) != len(tt.wantPorts) { - t.Errorf("端口数量 = %d, want %d", len(ports), len(tt.wantPorts)) - return - } - - for i, port := range ports { - if port != tt.wantPorts[i] { - t.Errorf("端口[%d] = %d, want %d", i, port, tt.wantPorts[i]) - } - } - }) - } -} - -// ============================================================================= -// validateHost 测试 -// ============================================================================= - -func TestTargetParser_ValidateHost(t *testing.T) { - fileReader := NewFileReader(nil) - - tests := []struct { - name string - host string - options *TargetParserOptions - wantValid bool - }{ - { - name: "有效IPv4地址", - host: "192.168.1.1", - options: nil, - wantValid: true, - }, - { - name: "有效域名", - host: "example.com", - options: nil, - wantValid: true, - }, - { - name: "有效子域名", - host: "www.example.com", - options: nil, - wantValid: true, - }, - { - name: "空主机", - host: "", - options: nil, - wantValid: false, - }, - { - name: "私有IP(不允许)", - host: "192.168.1.1", - options: &TargetParserOptions{ - AllowPrivateIPs: false, - }, - wantValid: false, - }, - { - name: "回环地址(不允许)", - host: "127.0.0.1", - options: &TargetParserOptions{ - AllowLoopback: false, - }, - wantValid: false, - }, - { - name: "回环地址(允许)", - host: "127.0.0.1", - options: &TargetParserOptions{ - AllowLoopback: true, - }, - wantValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewTargetParser(fileReader, tt.options) - - valid, err := parser.validateHost(tt.host) - - if valid != tt.wantValid { - t.Errorf("validateHost() = %v (err: %v), want %v", valid, err, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// validateURL 测试 -// ============================================================================= - -func TestTargetParser_ValidateURL(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - url string - wantValid bool - }{ - { - name: "有效HTTP URL", - url: "http://example.com", - wantValid: true, - }, - { - name: "有效HTTPS URL", - url: "https://example.com", - wantValid: true, - }, - { - name: "带路径的URL", - url: "http://example.com/path/to/resource", - wantValid: true, - }, - { - name: "带端口的URL", - url: "http://example.com:8080", - wantValid: true, - }, - { - name: "带查询参数的URL", - url: "http://example.com?key=value", - wantValid: true, - }, - { - name: "空URL", - url: "", - wantValid: false, - }, - { - name: "无协议的URL", - url: "example.com", - wantValid: false, - }, - { - name: "FTP协议(不支持)", - url: "ftp://example.com", - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - valid, err := parser.validateURL(tt.url) - - if valid != tt.wantValid { - t.Errorf("validateURL() = %v (err: %v), want %v", valid, err, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// validateHostPort 测试 -// ============================================================================= - -func TestTargetParser_ValidateHostPort(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - hostPort string - wantValid bool - }{ - { - name: "有效IP:端口", - hostPort: "192.168.1.1:80", - wantValid: true, - }, - { - name: "有效域名:端口", - hostPort: "example.com:443", - wantValid: true, - }, - { - name: "缺少端口", - hostPort: "192.168.1.1", - wantValid: false, - }, - { - name: "无效端口号", - hostPort: "192.168.1.1:99999", - wantValid: false, - }, - { - name: "端口号为0", - hostPort: "192.168.1.1:0", - wantValid: false, - }, - { - name: "空字符串", - hostPort: "", - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - valid, err := parser.validateHostPort(tt.hostPort) - - if valid != tt.wantValid { - t.Errorf("validateHostPort() = %v (err: %v), want %v", valid, err, tt.wantValid) - } - }) - } -} - -// ============================================================================= -// isPrivateIP 测试 -// ============================================================================= - -func TestTargetParser_IsPrivateIP(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - ip string - wantPrivate bool - }{ - { - name: "10.x.x.x网段", - ip: "10.0.0.1", - wantPrivate: true, - }, - { - name: "172.16.x.x网段(起始)", - ip: "172.16.0.1", - wantPrivate: true, - }, - { - name: "172.31.x.x网段(结束)", - ip: "172.31.255.254", - wantPrivate: true, - }, - { - name: "172.15.x.x(不在范围内)", - ip: "172.15.0.1", - wantPrivate: false, - }, - { - name: "172.32.x.x(不在范围内)", - ip: "172.32.0.1", - wantPrivate: false, - }, - { - name: "192.168.x.x网段", - ip: "192.168.1.1", - wantPrivate: true, - }, - { - name: "公网IP", - ip: "8.8.8.8", - wantPrivate: false, - }, - { - name: "公网IP(1.1.1.1)", - ip: "1.1.1.1", - wantPrivate: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ip := net.ParseIP(tt.ip) - if ip == nil { - t.Fatalf("无效IP地址: %s", tt.ip) - } - - isPrivate := parser.isPrivateIP(ip) - - if isPrivate != tt.wantPrivate { - t.Errorf("isPrivateIP(%s) = %v, want %v", tt.ip, isPrivate, tt.wantPrivate) - } - }) - } -} - -// ============================================================================= -// 文件读取测试 -// ============================================================================= - -func TestTargetParser_ParseFromFile(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - fileContent string - wantHosts int - }{ - { - name: "主机列表文件", - fileContent: `192.168.1.1 -192.168.1.2 -192.168.1.3`, - wantHosts: 3, - }, - { - name: "带注释的主机列表", - fileContent: `# 这是注释 -192.168.1.1 -# 另一个注释 -192.168.1.2`, - wantHosts: 2, - }, - { - name: "带空行的主机列表", - fileContent: `192.168.1.1 - -192.168.1.2 - -192.168.1.3`, - wantHosts: 3, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpFile := createTestFile(t, tt.fileContent) - - input := &TargetInput{ - HostsFile: tmpFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - if len(result.Config.Targets.Hosts) != tt.wantHosts { - t.Errorf("主机数量 = %d, want %d", len(result.Config.Targets.Hosts), tt.wantHosts) - } - }) - } - - // 端口文件测试 - t.Run("端口列表文件", func(t *testing.T) { - portsFile := createTestFile(t, `80 -443 -8080 -# 注释行 -8443`) - - input := &TargetInput{ - PortsFile: portsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - // 应解析出4个端口(注释被跳过) - if len(result.Config.Targets.Ports) != 4 { - t.Errorf("端口数量 = %d, want 4", len(result.Config.Targets.Ports)) - } - }) - - // URL文件测试 - t.Run("URL列表文件", func(t *testing.T) { - urlsFile := createTestFile(t, `http://example1.com -https://example2.com -# 注释 -http://example3.com:8080 -`) - - input := &TargetInput{ - URLsFile: urlsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - // 应解析出3个URL - if len(result.Config.Targets.URLs) != 3 { - t.Errorf("URL数量 = %d, want 3", len(result.Config.Targets.URLs)) - } - }) - - // 排除主机文件测试 - t.Run("排除主机列表", func(t *testing.T) { - hostsFile := createTestFile(t, "192.168.1.1\n192.168.1.2\n192.168.1.3\n192.168.1.4\n192.168.1.5") - excludeFile := createTestFile(t, "192.168.1.2\n192.168.1.4") - - input := &TargetInput{ - HostsFile: hostsFile, - ExcludeHostsFile: excludeFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 5个主机 - 2个排除 = 3个 - if len(result.Config.Targets.Hosts) != 3 { - t.Errorf("主机数量 = %d, want 3", len(result.Config.Targets.Hosts)) - } - }) - - // 混合输入:命令行+文件 - t.Run("混合输入(命令行+文件)", func(t *testing.T) { - hostsFile := createTestFile(t, "192.168.1.10\n192.168.1.11") - portsFile := createTestFile(t, "8080\n8443") - - input := &TargetInput{ - Host: "192.168.1.1,192.168.1.2", - HostsFile: hostsFile, - Ports: "80,443", - PortsFile: portsFile, - AddPorts: "9000,9001", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 主机: 2(命令行) + 2(文件) = 4 - if len(result.Config.Targets.Hosts) != 4 { - t.Errorf("主机数量 = %d, want 4", len(result.Config.Targets.Hosts)) - } - - // 端口: 2(命令行) + 2(文件) + 2(AddPorts) = 6 - if len(result.Config.Targets.Ports) != 6 { - t.Errorf("端口数量 = %d, want 6", len(result.Config.Targets.Ports)) - } - }) - - // 文件中的无效行测试 - t.Run("文件包含无效行(应产生警告)", func(t *testing.T) { - hostsFile := createTestFile(t, `192.168.1.1 -invalid!!!host!!!format -192.168.1.2 -999.999.999.999 -192.168.1.3`) - - input := &TargetInput{ - HostsFile: hostsFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // 999.999.999.999 应该被过滤掉,因为是无效IP - // invalid!!!host!!!format 可能被识别为域名(取决于域名验证规则) - // 至少应该有3个有效主机 - if len(result.Config.Targets.Hosts) < 3 { - t.Errorf("主机数量 = %d, 应该至少有3个", len(result.Config.Targets.Hosts)) - } - }) -} - -// ============================================================================= -// excludeHosts 测试 -// ============================================================================= - -func TestTargetParser_ExcludeHosts(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - hosts := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"} - excludeList := []string{"192.168.1.2", "192.168.1.4"} - - result := parser.excludeHosts(hosts, excludeList) - - expected := []string{"192.168.1.1", "192.168.1.3"} - - if len(result) != len(expected) { - t.Errorf("结果数量 = %d, want %d", len(result), len(expected)) - } - - for i, host := range result { - if host != expected[i] { - t.Errorf("主机[%d] = %s, want %s", i, host, expected[i]) - } - } -} - -// ============================================================================= -// removeDuplicateStrings 测试 -// ============================================================================= - -func TestTargetParser_RemoveDuplicateStrings(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - input := []string{"a", "b", "a", "c", "b", "d"} - expected := []string{"a", "b", "c", "d"} - - result := parser.removeDuplicateStrings(input) - - if len(result) != len(expected) { - t.Errorf("结果数量 = %d, want %d", len(result), len(expected)) - } - - // 检查所有预期元素都存在(顺序可能不同) - resultMap := make(map[string]bool) - for _, item := range result { - resultMap[item] = true - } - - for _, item := range expected { - if !resultMap[item] { - t.Errorf("缺少元素: %s", item) - } - } -} - -// ============================================================================= -// removeDuplicatePorts 测试 -// ============================================================================= - -func TestTargetParser_RemoveDuplicatePorts(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - input := []int{80, 443, 80, 8080, 443, 22} - expected := []int{80, 443, 8080, 22} - - result := parser.removeDuplicatePorts(input) - - if len(result) != len(expected) { - t.Errorf("结果数量 = %d, want %d", len(result), len(expected)) - } - - // 检查所有预期元素都存在 - resultMap := make(map[int]bool) - for _, port := range result { - resultMap[port] = true - } - - for _, port := range expected { - if !resultMap[port] { - t.Errorf("缺少端口: %d", port) - } - } -} - -// ============================================================================= -// parseSubnet8 测试(/8网段采样) -// ============================================================================= - -func TestTargetParser_ParseSubnet8(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - input string - wantError bool - }{ - { - name: "/8网段采样", - input: "10.0.0.0/8", - wantError: false, - }, - { - name: "/8网段采样(另一个例子)", - input: "172.0.0.0/8", - wantError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hosts, err := parser.parseHostList(tt.input) - - if tt.wantError { - if err == nil { - t.Error("期望错误,但没有错误") - } - return - } - - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - // /8网段会被限制到MaxTargets - if len(hosts) == 0 { - t.Error("采样结果为空") - } - - if len(hosts) > parser.options.MaxTargets { - t.Errorf("主机数量 %d 超过MaxTargets %d", len(hosts), parser.options.MaxTargets) - } - - // 验证生成的IP格式正确 - for i, host := range hosts { - if net.ParseIP(host) == nil { - t.Errorf("无效IP地址[%d]: %s", i, host) - break - } - } - }) - } -} - -// ============================================================================= -// host:port 组合测试 -// ============================================================================= - -func TestTargetParser_HostPortCombination(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - tests := []struct { - name string - input *TargetInput - wantHostPorts int - wantHosts int - wantPorts int - }{ - { - name: "单个host:port", - input: &TargetInput{ - Host: "192.168.1.1:80", - }, - wantHostPorts: 1, - wantHosts: 0, // host:port不会被添加到hosts - wantPorts: 0, // Ports应该为空,只扫描指定的host:port - }, - { - name: "多个host:port", - input: &TargetInput{ - HostPort: []string{"192.168.1.1:80", "192.168.1.2:443"}, - }, - wantHostPorts: 2, - wantHosts: 0, - wantPorts: 0, - }, - { - name: "混合host:port和普通host", - input: &TargetInput{ - Host: "192.168.1.1:80,192.168.1.2", - }, - wantHostPorts: 1, - wantHosts: 1, // 192.168.1.2 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parser.Parse(tt.input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - if len(result.Config.Targets.HostPorts) != tt.wantHostPorts { - t.Errorf("HostPorts数量 = %d, want %d", len(result.Config.Targets.HostPorts), tt.wantHostPorts) - } - - if tt.wantHosts > 0 && len(result.Config.Targets.Hosts) != tt.wantHosts { - t.Errorf("Hosts数量 = %d, want %d", len(result.Config.Targets.Hosts), tt.wantHosts) - } - }) - } -} - -// ============================================================================= -// 边界条件测试 -// ============================================================================= - -func TestTargetParser_EdgeCases(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - t.Run("空字符串输入", func(t *testing.T) { - hosts, err := parser.parseHostList("") - if err != nil { - t.Errorf("意外错误: %v", err) - } - if len(hosts) != 0 { - t.Errorf("空输入应返回空列表,得到 %d 个主机", len(hosts)) - } - }) - - t.Run("只有逗号的输入", func(t *testing.T) { - hosts, err := parser.parseHostList(",,,") - if err != nil { - t.Errorf("意外错误: %v", err) - } - if len(hosts) != 0 { - t.Errorf("只有逗号的输入应返回空列表,得到 %d 个主机", len(hosts)) - } - }) - - t.Run("带空格的输入", func(t *testing.T) { - hosts, err := parser.parseHostList(" 192.168.1.1 , 192.168.1.2 ") - if err != nil { - t.Errorf("意外错误: %v", err) - } - if len(hosts) != 2 { - t.Errorf("应该解析出2个主机,得到 %d 个", len(hosts)) - } - }) - - t.Run("端口号边界值", func(t *testing.T) { - tests := []struct { - port string - wantError bool - }{ - {"1", false}, // 最小端口 - {"65535", false}, // 最大端口 - {"0", true}, // 无效端口 - {"65536", true}, // 超出范围 - {"-1", true}, // 负数 - } - - for _, tt := range tests { - t.Run("端口:"+tt.port, func(t *testing.T) { - _, err := parser.parsePortList(tt.port) - if tt.wantError && err == nil { - t.Error("期望错误,但没有错误") - } - if !tt.wantError && err != nil { - t.Errorf("意外错误: %v", err) - } - }) - } - }) - - t.Run("域名验证", func(t *testing.T) { - tests := []struct { - domain string - wantValid bool - }{ - {"example.com", true}, - {"sub.example.com", true}, - {"example-with-dash.com", true}, - {"123.com", true}, - {"-invalid.com", false}, // 以连字符开头 - {"invalid-.com", false}, // 以连字符结尾 - {"too..many.dots.com", false}, - } - - for _, tt := range tests { - t.Run(tt.domain, func(t *testing.T) { - valid := parser.isValidDomain(tt.domain) - if valid != tt.wantValid { - t.Errorf("isValidDomain(%s) = %v, want %v", tt.domain, valid, tt.wantValid) - } - }) - } - }) -} - -// ============================================================================= -// 文件解析错误处理测试 -// ============================================================================= - -func TestTargetParser_FileErrorHandling(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - t.Run("不存在的文件", func(t *testing.T) { - input := &TargetInput{ - HostsFile: "/nonexistent/file/path.txt", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("Parse不应返回错误: %v", err) - return - } - - if result.Success { - t.Error("解析不应成功(文件不存在)") - } - - if len(result.Errors) == 0 { - t.Error("应该有错误记录") - } - }) - - t.Run("空文件", func(t *testing.T) { - tmpFile := createTestFile(t, "") - - input := &TargetInput{ - HostsFile: tmpFile, - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Errorf("意外错误: %v", err) - return - } - - if len(result.Config.Targets.Hosts) != 0 { - t.Errorf("空文件应解析出0个主机,得到 %d 个", len(result.Config.Targets.Hosts)) - } - }) -} - -// ============================================================================= -// 复杂场景集成测试 -// ============================================================================= - -func TestTargetParser_ComplexScenarios(t *testing.T) { - fileReader := NewFileReader(nil) - parser := NewTargetParser(fileReader, nil) - - t.Run("混合输入(IP+CIDR+范围+域名)", func(t *testing.T) { - input := &TargetInput{ - Host: "192.168.1.1,192.168.2.0/30,192.168.3.1-5,example.com", - Ports: "80,443,8000-8002", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Fatalf("意外错误: %v", err) - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - // 验证主机数量: 1(单IP) + 2(CIDR) + 5(范围) + 1(域名) = 9 - expectedHosts := 9 - if len(result.Config.Targets.Hosts) != expectedHosts { - t.Errorf("主机数量 = %d, want %d", len(result.Config.Targets.Hosts), expectedHosts) - } - - // 验证端口数量: 2(单端口) + 3(范围) = 5 - expectedPorts := 5 - if len(result.Config.Targets.Ports) != expectedPorts { - t.Errorf("端口数量 = %d, want %d", len(result.Config.Targets.Ports), expectedPorts) - } - }) - - t.Run("带排除的扫描", func(t *testing.T) { - input := &TargetInput{ - Host: "192.168.1.1-10", - ExcludeHosts: "192.168.1.5,192.168.1.6", - Ports: "1-100", - ExcludePorts: "22,23,24", - } - - result, err := parser.Parse(input, nil) - if err != nil { - t.Fatalf("意外错误: %v", err) - } - - if !result.Success { - t.Errorf("解析失败,错误: %v", result.Errors) - } - - // 验证排除后的主机数量: 10 - 2 = 8 - if len(result.Config.Targets.Hosts) != 8 { - t.Errorf("主机数量 = %d, want 8", len(result.Config.Targets.Hosts)) - } - - // 验证排除端口列表 - if len(result.Config.Targets.ExcludePorts) != 3 { - t.Errorf("排除端口数量 = %d, want 3", len(result.Config.Targets.ExcludePorts)) - } - }) -} diff --git a/common/parsers/types.go b/common/parsers/types.go deleted file mode 100644 index 6d8cf8e..0000000 --- a/common/parsers/types.go +++ /dev/null @@ -1,140 +0,0 @@ -package parsers - -import ( - "errors" - "fmt" - "time" - - "github.com/shadow1ng/fscan/common/config" - "github.com/shadow1ng/fscan/common/i18n" -) - -// ParsedConfig 解析后的完整配置 -type ParsedConfig struct { - Targets *TargetConfig `json:"targets"` - Credentials *CredentialConfig `json:"credentials"` - Network *NetworkConfig `json:"network"` - Validation *ValidationConfig `json:"validation"` -} - -// TargetConfig 目标配置 -type TargetConfig struct { - Hosts []string `json:"hosts"` - URLs []string `json:"urls"` - Ports []int `json:"ports"` - ExcludePorts []int `json:"exclude_ports"` - HostPorts []string `json:"host_ports"` - LocalMode bool `json:"local_mode"` -} - -// CredentialConfig 认证配置 -type CredentialConfig struct { - Usernames []string `json:"usernames"` - Passwords []string `json:"passwords"` - UserPassPairs []config.CredentialPair `json:"user_pass_pairs,omitempty"` // 精确的用户密码对 - HashValues []string `json:"hash_values"` - HashBytes [][]byte `json:"hash_bytes,omitempty"` - SSHKeyPath string `json:"ssh_key_path"` - Domain string `json:"domain"` -} - -// NetworkConfig 网络配置 -type NetworkConfig struct { - HTTPProxy string `json:"http_proxy"` - Socks5Proxy string `json:"socks5_proxy"` - Timeout time.Duration `json:"timeout"` - WebTimeout time.Duration `json:"web_timeout"` - DisablePing bool `json:"disable_ping"` - EnableDNSLog bool `json:"enable_dns_log"` - UserAgent string `json:"user_agent"` - Cookie string `json:"cookie"` -} - -// ValidationConfig 验证配置 -type ValidationConfig struct { - ScanMode string `json:"scan_mode"` - ConflictChecked bool `json:"conflict_checked"` - Errors []error `json:"errors,omitempty"` - Warnings []string `json:"warnings,omitempty"` -} - -// ParseResult 解析结果 -type ParseResult struct { - Config *ParsedConfig `json:"config"` - Success bool `json:"success"` - Errors []error `json:"errors,omitempty"` - Warnings []string `json:"warnings,omitempty"` - ParseTime time.Duration `json:"parse_time"` -} - -// 预定义错误类型 -var ( - ErrEmptyInput = errors.New(i18n.GetText("parser_empty_input")) -) - -// ParserOptions 解析器选项 -type ParserOptions struct { - EnableConcurrency bool // 启用并发解析 - MaxWorkers int // 最大工作协程数 - Timeout time.Duration // 解析超时时间 - EnableValidation bool // 启用详细验证 - IgnoreErrors bool // 忽略非致命错误 - FileMaxSize int64 // 文件最大大小限制 - MaxTargets int // 最大目标数量限制 -} - -// DefaultParserOptions 返回默认解析器选项 -func DefaultParserOptions() *ParserOptions { - return &ParserOptions{ - EnableConcurrency: DefaultEnableConcurrency, - MaxWorkers: DefaultMaxWorkers, - Timeout: DefaultTimeout, - EnableValidation: DefaultEnableValidation, - IgnoreErrors: DefaultIgnoreErrors, - FileMaxSize: DefaultFileMaxSize, - MaxTargets: DefaultMaxTargets, - } -} - -// Parser 解析器接口 -type Parser interface { - Parse(options *ParserOptions) (*ParseResult, error) - Validate() error -} - -// FileSource 文件源 -type FileSource struct { - Path string `json:"path"` - Size int64 `json:"size"` - ModTime time.Time `json:"mod_time"` - LineCount int `json:"line_count"` - ValidLines int `json:"valid_lines"` -} - -// ParseError 解析错误,包含详细上下文 -type ParseError struct { - Type string `json:"type"` - Message string `json:"message"` - Source string `json:"source"` - Line int `json:"line,omitempty"` - Context string `json:"context,omitempty"` - Original error `json:"original,omitempty"` -} - -func (e *ParseError) Error() string { - if e.Line > 0 { - return fmt.Sprintf("%s:%d - %s: %s", e.Source, e.Line, e.Type, e.Message) - } - return fmt.Sprintf("%s - %s: %s", e.Source, e.Type, e.Message) -} - -// NewParseError 创建解析错误 -func NewParseError(errType, message, source string, line int, original error) *ParseError { - return &ParseError{ - Type: errType, - Message: message, - Source: source, - Line: line, - Original: original, - } -} diff --git a/common/parsers/validation.go b/common/parsers/validation.go deleted file mode 100644 index 97d43fb..0000000 --- a/common/parsers/validation.go +++ /dev/null @@ -1,288 +0,0 @@ -package parsers - -import ( - "fmt" - "sync" - "time" -) - -// ValidationParser 参数验证解析器 -type ValidationParser struct { - mu sync.RWMutex //nolint:unused // reserved for future thread safety - options *ValidationParserOptions -} - -// ValidationParserOptions 验证解析器选项 -type ValidationParserOptions struct { - StrictMode bool `json:"strict_mode"` // 严格模式 - AllowEmpty bool `json:"allow_empty"` // 允许空配置 - CheckConflicts bool `json:"check_conflicts"` // 检查参数冲突 - ValidateTargets bool `json:"validate_targets"` // 验证目标有效性 - ValidateNetwork bool `json:"validate_network"` // 验证网络配置 - MaxErrorCount int `json:"max_error_count"` // 最大错误数量 -} - -// DefaultValidationParserOptions 默认验证解析器选项 -func DefaultValidationParserOptions() *ValidationParserOptions { - return &ValidationParserOptions{ - StrictMode: DefaultStrictMode, - AllowEmpty: DefaultAllowEmpty, - CheckConflicts: DefaultCheckConflicts, - ValidateTargets: DefaultValidateTargets, - ValidateNetwork: DefaultValidateNetwork, - MaxErrorCount: DefaultMaxErrorCount, - } -} - -// NewValidationParser 创建验证解析器 -func NewValidationParser(options *ValidationParserOptions) *ValidationParser { - if options == nil { - options = DefaultValidationParserOptions() - } - - return &ValidationParser{ - options: options, - } -} - -// ValidationInput 验证输入参数 -type ValidationInput struct { - // 扫描模式 - ScanMode string `json:"scan_mode"` - LocalMode bool `json:"local_mode"` - - // 目标配置 - HasHosts bool `json:"has_hosts"` - HasURLs bool `json:"has_urls"` - HasPorts bool `json:"has_ports"` - - // 网络配置 - HasProxy bool `json:"has_proxy"` - DisablePing bool `json:"disable_ping"` - - // 凭据配置 - HasCredentials bool `json:"has_credentials"` - - // 特殊模式 - PocScan bool `json:"poc_scan"` - BruteScan bool `json:"brute_scan"` - LocalScan bool `json:"local_scan"` -} - -// ConflictRule 冲突规则 -type ConflictRule struct { - Name string `json:"name"` - Description string `json:"description"` - Fields []string `json:"fields"` - Severity string `json:"severity"` // error, warning, info -} - -// ValidationRule 验证规则 -type ValidationRule struct { - Name string `json:"name"` - Description string `json:"description"` - Validator func(input *ValidationInput) error `json:"-"` - Severity string `json:"severity"` -} - -// Parse 执行参数验证 -func (vp *ValidationParser) Parse(input *ValidationInput, config *ParsedConfig, options *ParserOptions) (*ParseResult, error) { - if input == nil { - return nil, NewParseError(ErrorTypeInputError, "验证输入为空", "", 0, ErrEmptyInput) - } - - startTime := time.Now() - result := &ParseResult{ - Config: &ParsedConfig{ - Validation: &ValidationConfig{ - ScanMode: input.ScanMode, - ConflictChecked: true, - }, - }, - Success: true, - } - - var errors []error - var warnings []string - - // 基础验证 - basicErrors, basicWarnings := vp.validateBasic(input) - errors = append(errors, basicErrors...) - warnings = append(warnings, basicWarnings...) - - // 冲突检查 - if vp.options.CheckConflicts { - conflictErrors, conflictWarnings := vp.checkConflicts(input) - errors = append(errors, conflictErrors...) - warnings = append(warnings, conflictWarnings...) - } - - // 逻辑验证 - logicErrors, logicWarnings := vp.validateLogic(input, config) - errors = append(errors, logicErrors...) - warnings = append(warnings, logicWarnings...) - - // 性能建议 - performanceWarnings := vp.checkPerformance(input, config) - warnings = append(warnings, performanceWarnings...) - - // 检查错误数量限制 - if len(errors) > vp.options.MaxErrorCount { - errors = errors[:vp.options.MaxErrorCount] - warnings = append(warnings, fmt.Sprintf("错误数量过多,仅显示前%d个", vp.options.MaxErrorCount)) - } - - // 更新结果 - result.Config.Validation.Errors = errors - result.Config.Validation.Warnings = warnings - result.Errors = errors - result.Warnings = warnings - result.ParseTime = time.Since(startTime) - result.Success = len(errors) == 0 - - return result, nil -} - -// validateBasic 基础验证 -func (vp *ValidationParser) validateBasic(input *ValidationInput) ([]error, []string) { - var errors []error - var warnings []string - - // 检查是否有任何目标 - if !input.HasHosts && !input.HasURLs && !input.LocalMode { - if !vp.options.AllowEmpty { - errors = append(errors, NewParseError("VALIDATION_ERROR", "未指定任何扫描目标", "basic", 0, nil)) - } else { - warnings = append(warnings, "未指定扫描目标,将使用默认配置") - } - } - - // 检查扫描模式 - if input.ScanMode != "" { - if err := vp.validateScanMode(input.ScanMode); err != nil { - if vp.options.StrictMode { - errors = append(errors, err) - } else { - warnings = append(warnings, err.Error()) - } - } - } - - return errors, warnings -} - -// checkConflicts 检查参数冲突 -func (vp *ValidationParser) checkConflicts(input *ValidationInput) ([]error, []string) { - var errors []error - var warnings []string - - // 定义冲突规则 (预留用于扩展) - _ = []ConflictRule{ - { - Name: "multiple_scan_modes", - Description: "不能同时使用多种扫描模式", - Fields: []string{"hosts", "urls", "local_mode"}, - Severity: "error", - }, - { - Name: "proxy_with_ping", - Description: "使用代理时建议禁用Ping检测", - Fields: []string{"proxy", "ping"}, - Severity: "warning", - }, - } - - // 检查扫描模式冲突 - scanModes := 0 - if input.HasHosts { - scanModes++ - } - if input.HasURLs { - scanModes++ - } - if input.LocalMode { - scanModes++ - } - - if scanModes > 1 { - errors = append(errors, NewParseError("CONFLICT_ERROR", - "不能同时指定多种扫描模式(主机扫描、URL扫描、本地模式)", "validation", 0, nil)) - } - - // 检查代理和Ping冲突 - if input.HasProxy && !input.DisablePing { - warnings = append(warnings, "代理模式下Ping检测可能失效") - } - - return errors, warnings -} - -// validateLogic 逻辑验证 -func (vp *ValidationParser) validateLogic(input *ValidationInput, config *ParsedConfig) ([]error, []string) { - var errors []error - var warnings []string - - // 验证目标配置逻辑 - if vp.options.ValidateTargets && config != nil && config.Targets != nil { - // 检查排除端口配置 - if len(config.Targets.ExcludePorts) > 0 && len(config.Targets.Ports) == 0 { - warnings = append(warnings, "排除端口无效") - } - } - - return errors, warnings -} - -// checkPerformance 性能检查 -func (vp *ValidationParser) checkPerformance(input *ValidationInput, config *ParsedConfig) []string { - var warnings []string - - if config == nil { - return warnings - } - - // 检查目标数量 - if config.Targets != nil { - totalTargets := len(config.Targets.Hosts) * len(config.Targets.Ports) - if totalTargets > MaxTargetsThreshold { - warnings = append(warnings, fmt.Sprintf("大量目标(%d),可能耗时较长", totalTargets)) - } - - // 检查端口范围 - if len(config.Targets.Ports) > PortCountWarningThreshold { - warnings = append(warnings, "端口数量过多") - } - } - - // 检查超时配置 - if config.Network != nil { - if config.Network.Timeout < MinTimeoutThreshold { - warnings = append(warnings, "超时过短") - } - if config.Network.Timeout > MaxTimeoutThreshold { - warnings = append(warnings, "超时过长") - } - } - - return warnings -} - -// validateScanMode 验证扫描模式 -func (vp *ValidationParser) validateScanMode(scanMode string) error { //nolint:unparam - validModes := []string{"all", "icmp"} - - // 检查是否为预定义模式 - for _, mode := range validModes { - if scanMode == mode { - return nil - } - } - - // 允许插件名称作为扫描模式,实际插件验证在运行时进行 - // 这里不做严格验证,避免维护两套插件列表 - return nil -} - -// ============================================================================================= -// 已删除的死代码(未使用):Validate 和 GetStatistics 方法 -// ============================================================================================= diff --git a/common/parsers/validation_test.go b/common/parsers/validation_test.go deleted file mode 100644 index 8da644a..0000000 --- a/common/parsers/validation_test.go +++ /dev/null @@ -1,620 +0,0 @@ -package parsers - -import ( - "strings" - "testing" -) - -/* -validation_test.go - 参数验证解析器测试 - -测试目标:ValidationParser核心验证逻辑 -价值:验证逻辑错误会导致: - - 用户无法启动扫描(false positive错误) - - 错误配置未被发现(false negative漏检) - - 性能问题未预警(大规模扫描超时) - -"验证是用户的第一道防线。验证太严=拒绝合法输入, -验证太松=允许错误配置。必须精确测试每个规则。" -*/ - -// ============================================================================= -// ValidationParser - 构造函数测试 -// ============================================================================= - -// TestNewValidationParser_DefaultOptions 测试默认选项 -// -// 验证:nil选项时使用默认配置 -func TestNewValidationParser_DefaultOptions(t *testing.T) { - parser := NewValidationParser(nil) - - if parser == nil { - t.Fatal("NewValidationParser(nil)应该返回有效parser") - } - - if parser.options == nil { - t.Error("options不应为nil(应使用默认配置)") - } - - // 验证默认值合理性 - if parser.options.MaxErrorCount <= 0 { - t.Error("MaxErrorCount应该大于0") - } - - t.Logf("✓ 默认选项测试通过(MaxErrorCount=%d)", parser.options.MaxErrorCount) -} - -// TestNewValidationParser_CustomOptions 测试自定义选项 -func TestNewValidationParser_CustomOptions(t *testing.T) { - options := &ValidationParserOptions{ - StrictMode: true, - AllowEmpty: false, - CheckConflicts: true, - ValidateTargets: true, - ValidateNetwork: true, - MaxErrorCount: 10, - } - - parser := NewValidationParser(options) - - if parser.options.StrictMode != true { - t.Error("StrictMode应该为true") - } - - if parser.options.MaxErrorCount != 10 { - t.Error("MaxErrorCount应该为10") - } - - t.Logf("✓ 自定义选项测试通过") -} - -// ============================================================================= -// ValidationParser - 基础验证测试 -// ============================================================================= - -// TestValidationParser_Parse_NoTargets 测试无目标验证 -// -// 验证:无目标时返回错误(AllowEmpty=false) -func TestValidationParser_Parse_NoTargets(t *testing.T) { - parser := NewValidationParser(&ValidationParserOptions{ - AllowEmpty: false, - MaxErrorCount: 10, - }) - - input := &ValidationInput{ - ScanMode: "all", - HasHosts: false, - HasURLs: false, - LocalMode: false, - } - - result, err := parser.Parse(input, nil, nil) - - if err != nil { - t.Fatalf("Parse不应返回err: %v", err) - } - - if result.Success { - t.Error("无目标时Success应该为false") - } - - if len(result.Errors) == 0 { - t.Error("无目标时应该有错误") - } - - // 验证错误消息 - hasTargetError := false - for _, e := range result.Errors { - if strings.Contains(e.Error(), "目标") { - hasTargetError = true - break - } - } - if !hasTargetError { - t.Error("应该包含目标相关的错误消息") - } - - t.Logf("✓ 无目标验证测试通过(错误数=%d)", len(result.Errors)) -} - -// TestValidationParser_Parse_AllowEmpty 测试允许空配置 -func TestValidationParser_Parse_AllowEmpty(t *testing.T) { - parser := NewValidationParser(&ValidationParserOptions{ - AllowEmpty: true, - MaxErrorCount: 10, - }) - - input := &ValidationInput{ - ScanMode: "", - HasHosts: false, - HasURLs: false, - LocalMode: false, - } - - result, err := parser.Parse(input, nil, nil) - - if err != nil { - t.Fatalf("Parse不应返回err: %v", err) - } - - // AllowEmpty=true时,无目标只警告不报错 - if !result.Success { - t.Error("AllowEmpty=true时Success应该为true") - } - - if len(result.Warnings) == 0 { - t.Error("应该有警告") - } - - t.Logf("✓ AllowEmpty测试通过(警告数=%d)", len(result.Warnings)) -} - -// TestValidationParser_Parse_ValidScanModes 测试有效扫描模式 -func TestValidationParser_Parse_ValidScanModes(t *testing.T) { - parser := NewValidationParser(nil) - - validModes := []string{"all", "icmp", "ssh", "mysql", ""} - - for _, mode := range validModes { - t.Run(mode, func(t *testing.T) { - input := &ValidationInput{ - ScanMode: mode, - HasHosts: true, - } - - result, err := parser.Parse(input, nil, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - if !result.Success { - t.Errorf("模式%q应该有效,但Success=false,错误: %v", mode, result.Errors) - } - - t.Logf("✓ 模式%q验证通过", mode) - }) - } -} - -// ============================================================================= -// ValidationParser - 冲突检测测试 -// ============================================================================= - -// TestValidationParser_Parse_ConflictMultipleScanModes 测试多种扫描模式冲突 -// -// 验证:同时指定多种扫描模式时报错 -func TestValidationParser_Parse_ConflictMultipleScanModes(t *testing.T) { - parser := NewValidationParser(&ValidationParserOptions{ - CheckConflicts: true, - MaxErrorCount: 10, - }) - - tests := []struct { - name string - input *ValidationInput - hasConflict bool - }{ - { - name: "主机+URL冲突", - input: &ValidationInput{ - HasHosts: true, - HasURLs: true, - }, - hasConflict: true, - }, - { - name: "主机+本地模式冲突", - input: &ValidationInput{ - HasHosts: true, - LocalMode: true, - }, - hasConflict: true, - }, - { - name: "URL+本地模式冲突", - input: &ValidationInput{ - HasURLs: true, - LocalMode: true, - }, - hasConflict: true, - }, - { - name: "三种模式同时冲突", - input: &ValidationInput{ - HasHosts: true, - HasURLs: true, - LocalMode: true, - }, - hasConflict: true, - }, - { - name: "仅主机-无冲突", - input: &ValidationInput{ - HasHosts: true, - }, - hasConflict: false, - }, - { - name: "仅URL-无冲突", - input: &ValidationInput{ - HasURLs: true, - }, - hasConflict: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parser.Parse(tt.input, nil, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - if tt.hasConflict { - if result.Success { - t.Error("有冲突时Success应该为false") - } - if len(result.Errors) == 0 { - t.Error("应该有冲突错误") - } - - // 验证错误消息包含"扫描模式" - hasConflictError := false - for _, e := range result.Errors { - if strings.Contains(e.Error(), "扫描模式") { - hasConflictError = true - break - } - } - if !hasConflictError { - t.Error("应该包含扫描模式冲突的错误") - } - } else { - if !result.Success { - t.Errorf("无冲突时Success应该为true,错误: %v", result.Errors) - } - } - - t.Logf("✓ %s 测试通过", tt.name) - }) - } -} - -// TestValidationParser_Parse_ProxyPingWarning 测试代理+Ping警告 -// -// 验证:代理模式下未禁用Ping时给出警告 -func TestValidationParser_Parse_ProxyPingWarning(t *testing.T) { - parser := NewValidationParser(&ValidationParserOptions{ - CheckConflicts: true, - MaxErrorCount: 10, - }) - - tests := []struct { - name string - hasProxy bool - disablePing bool - wantWarning bool - }{ - { - name: "代理+Ping启用-有警告", - hasProxy: true, - disablePing: false, - wantWarning: true, - }, - { - name: "代理+Ping禁用-无警告", - hasProxy: true, - disablePing: true, - wantWarning: false, - }, - { - name: "无代理+Ping启用-无警告", - hasProxy: false, - disablePing: false, - wantWarning: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - input := &ValidationInput{ - HasHosts: true, - HasProxy: tt.hasProxy, - DisablePing: tt.disablePing, - } - - result, err := parser.Parse(input, nil, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - hasPingWarning := false - for _, w := range result.Warnings { - if strings.Contains(w, "Ping") || strings.Contains(w, "代理") { - hasPingWarning = true - break - } - } - - if tt.wantWarning && !hasPingWarning { - t.Error("应该有代理Ping警告") - } - - if !tt.wantWarning && hasPingWarning { - t.Errorf("不应该有警告,实际警告: %v", result.Warnings) - } - - t.Logf("✓ %s 测试通过", tt.name) - }) - } -} - -// ============================================================================= -// ValidationParser - 逻辑验证测试 -// ============================================================================= - -// TestValidationParser_Parse_ExcludePortsLogic 测试排除端口逻辑 -// -// 验证:排除端口但未指定端口时给出警告 -func TestValidationParser_Parse_ExcludePortsLogic(t *testing.T) { - parser := NewValidationParser(&ValidationParserOptions{ - ValidateTargets: true, - MaxErrorCount: 10, - }) - - config := &ParsedConfig{ - Targets: &TargetConfig{ - Hosts: []string{"192.168.1.1"}, - Ports: []int{}, // 无端口 - ExcludePorts: []int{80, 443, 8080}, // 但有排除端口 - }, - } - - input := &ValidationInput{ - HasHosts: true, - HasPorts: false, - } - - result, err := parser.Parse(input, config, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - // 应该有警告 - hasExcludeWarning := false - for _, w := range result.Warnings { - if strings.Contains(w, "排除端口") { - hasExcludeWarning = true - break - } - } - - if !hasExcludeWarning { - t.Errorf("排除端口逻辑错误时应该有警告,实际警告: %v", result.Warnings) - } - - t.Logf("✓ 排除端口逻辑测试通过") -} - -// ============================================================================= -// ValidationParser - 性能检查测试 -// ============================================================================= - -// TestValidationParser_Parse_PerformanceLargeTargets 测试大量目标警告 -// -// 验证:目标数量过多时给出性能警告 -func TestValidationParser_Parse_PerformanceLargeTargets(t *testing.T) { - parser := NewValidationParser(nil) - - // 创建大量目标:1000个主机 x 1000个端口 = 100万目标 - hosts := make([]string, 1000) - for i := 0; i < 1000; i++ { - hosts[i] = "192.168.1.1" - } - - ports := make([]int, 1000) - for i := 0; i < 1000; i++ { - ports[i] = i + 1 - } - - config := &ParsedConfig{ - Targets: &TargetConfig{ - Hosts: hosts, - Ports: ports, - }, - } - - input := &ValidationInput{ - HasHosts: true, - HasPorts: true, - } - - result, err := parser.Parse(input, config, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - // 应该有性能警告 - hasPerformanceWarning := false - for _, w := range result.Warnings { - if strings.Contains(w, "大量目标") || strings.Contains(w, "耗时") { - hasPerformanceWarning = true - break - } - } - - if !hasPerformanceWarning { - t.Errorf("大量目标时应该有性能警告,实际警告: %v", result.Warnings) - } - - t.Logf("✓ 大量目标性能警告测试通过") -} - -// TestValidationParser_Parse_PerformanceManyPorts 测试端口数量警告 -func TestValidationParser_Parse_PerformanceManyPorts(t *testing.T) { - parser := NewValidationParser(nil) - - // 创建大量端口 - ports := make([]int, 10000) - for i := 0; i < 10000; i++ { - ports[i] = i + 1 - } - - config := &ParsedConfig{ - Targets: &TargetConfig{ - Hosts: []string{"192.168.1.1"}, - Ports: ports, - }, - } - - input := &ValidationInput{ - HasHosts: true, - HasPorts: true, - } - - result, err := parser.Parse(input, config, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - // 应该有端口数量警告 - hasPortWarning := false - for _, w := range result.Warnings { - if strings.Contains(w, "端口") { - hasPortWarning = true - break - } - } - - if !hasPortWarning { - t.Errorf("大量端口时应该有警告,实际警告: %v", result.Warnings) - } - - t.Logf("✓ 端口数量警告测试通过") -} - -// ============================================================================= -// ValidationParser - 错误数量限制测试 -// ============================================================================= - -// TestValidationParser_Parse_MaxErrorCount 测试错误数量限制 -// -// 验证:错误超过MaxErrorCount时截断 -func TestValidationParser_Parse_MaxErrorCount(t *testing.T) { - parser := NewValidationParser(&ValidationParserOptions{ - MaxErrorCount: 3, - CheckConflicts: true, - }) - - // 创建多个错误:多种扫描模式冲突 - input := &ValidationInput{ - HasHosts: true, - HasURLs: true, - LocalMode: true, - HasProxy: true, - // 这会产生至少1个冲突错误 - } - - result, err := parser.Parse(input, nil, nil) - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - if len(result.Errors) > 3 { - t.Errorf("错误数量应该被限制为%d,实际%d", 3, len(result.Errors)) - } - - // 注意:如果实际错误数<=MaxErrorCount,不会有截断警告 - // 这是正常行为,不算失败 - if len(result.Errors) <= 3 { - t.Logf("✓ 错误数量限制测试通过(限制=%d,实际=%d,无需截断)", 3, len(result.Errors)) - } else { - // 只有超过限制才需要截断警告 - hasTruncateWarning := false - for _, w := range result.Warnings { - if strings.Contains(w, "仅显示") || strings.Contains(w, "过多") { - hasTruncateWarning = true - break - } - } - if !hasTruncateWarning { - t.Error("超过限制时应该有错误截断警告") - } - t.Logf("✓ 错误数量限制测试通过(限制=%d,截断后=%d)", 3, len(result.Errors)) - } -} - -// ============================================================================= -// ValidationParser - nil输入测试 -// ============================================================================= - -// TestValidationParser_Parse_NilInput 测试nil输入 -// -// 验证:nil输入时返回错误 -func TestValidationParser_Parse_NilInput(t *testing.T) { - parser := NewValidationParser(nil) - - result, err := parser.Parse(nil, nil, nil) - - if err == nil { - t.Error("nil输入应该返回错误") - } - - if result != nil { - t.Error("nil输入时result应该为nil") - } - - if !strings.Contains(err.Error(), "空") { - t.Errorf("错误消息应该提示空输入,实际: %v", err) - } - - t.Logf("✓ nil输入测试通过") -} - -// ============================================================================= -// ValidationParser - 成功场景测试 -// ============================================================================= - -// TestValidationParser_Parse_Success 测试正常验证通过 -func TestValidationParser_Parse_Success(t *testing.T) { - parser := NewValidationParser(nil) - - input := &ValidationInput{ - ScanMode: "all", - HasHosts: true, - HasPorts: true, - DisablePing: false, - } - - config := &ParsedConfig{ - Targets: &TargetConfig{ - Hosts: []string{"192.168.1.1"}, - Ports: []int{80, 443}, - }, - } - - result, err := parser.Parse(input, config, nil) - - if err != nil { - t.Fatalf("Parse失败: %v", err) - } - - if !result.Success { - t.Errorf("正常配置应该Success=true,错误: %v", result.Errors) - } - - if len(result.Errors) > 0 { - t.Errorf("正常配置不应有错误: %v", result.Errors) - } - - // ParseTime可能为0(如果验证非常快) - if result.ParseTime < 0 { - t.Error("ParseTime不应该为负数") - } - - if result.Config == nil || result.Config.Validation == nil { - t.Error("result.Config.Validation不应为nil") - } - - t.Logf("✓ 成功场景测试通过(耗时=%v)", result.ParseTime) -}