refactor: 精简parsers包,统一配置构建入口

- 删除冗余的中间层(XXXInput、XXXParser类)
- 新增 config_builder.go 统一配置构建
- parsers包从3000+行精简至~540行
- 保留核心函数:ParseIP、ParsePort、文件读取、凭据解析
This commit is contained in:
ZacharyZcR
2026-01-20 11:38:58 +08:00
parent 732cb6d719
commit e23d89dae2
17 changed files with 828 additions and 7001 deletions
+308
View File
@@ -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 中定义,这里不重复
+10 -48
View File
@@ -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()
}
+11 -368
View File
@@ -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()
}
}
+11 -305
View File
@@ -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)
}
-422
View File
@@ -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 方法
// =============================================================================================
-766
View File
@@ -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)
}
}
-311
View File
@@ -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 方法
// =============================================================================================
-61
View File
@@ -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")
}
}
-368
View File
@@ -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 方法
// =============================================================================================
-720
View File
@@ -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)
}
}
-260
View File
@@ -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
}
+488
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-140
View File
@@ -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,
}
}
-288
View File
@@ -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 方法
// =============================================================================================
-620
View File
@@ -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)
}