mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 20:51:52 +08:00
feat: v2.1.0 核心重构与功能增强
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
constants.go - 解析器系统常量定义
|
||||
|
||||
统一管理common/parsers包中的所有常量,便于查看和编辑。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 默认解析器选项常量 (从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 = 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迁移)
|
||||
// =============================================================================
|
||||
|
||||
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 = 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
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 错误类型常量
|
||||
// =============================================================================
|
||||
|
||||
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 *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)
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
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.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 方法
|
||||
// =============================================================================================
|
||||
@@ -0,0 +1,766 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
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 方法
|
||||
// =============================================================================================
|
||||
@@ -0,0 +1,61 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
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 方法
|
||||
// =============================================================================================
|
||||
@@ -0,0 +1,720 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,845 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
parse_test.go - 简化解析器测试
|
||||
|
||||
测试目标:ParseIP和ParsePort两个核心解析函数
|
||||
价值:解析错误会导致:
|
||||
- 错误的扫描目标(用户扫描了错误的主机)
|
||||
- 错误的端口范围(遗漏关键服务)
|
||||
- 性能问题(重复目标导致浪费)
|
||||
|
||||
"解析器是扫描器的入口。解析错误=整个扫描就是错的。
|
||||
端口范围解析bug会让用户遗漏漏洞。这是真实问题。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ParsePort - 端口解析测试
|
||||
// =============================================================================
|
||||
|
||||
// TestParsePort_Empty 测试空字符串
|
||||
//
|
||||
// 验证:空输入返回nil而不是空切片
|
||||
//
|
||||
// empty slice表示'有数据但是空的'。这个区别很重要。"
|
||||
func TestParsePort_Empty(t *testing.T) {
|
||||
result := ParsePort("")
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("ParsePort(\"\") = %v, want nil", result)
|
||||
}
|
||||
|
||||
t.Logf("✓ 空字符串正确返回nil")
|
||||
}
|
||||
|
||||
// TestParsePort_SinglePort 测试单个端口
|
||||
func TestParsePort_SinglePort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{"HTTP", "80", []int{80}},
|
||||
{"HTTPS", "443", []int{443}},
|
||||
{"SSH", "22", []int{22}},
|
||||
{"MinPort", "1", []int{1}},
|
||||
{"MaxPort", "65535", []int{65535}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) → %v", tt.input, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_MultiplePorts 测试多个端口
|
||||
func TestParsePort_MultiplePorts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"Web端口",
|
||||
"80,443,8080",
|
||||
[]int{80, 443, 8080},
|
||||
},
|
||||
{
|
||||
"数据库端口",
|
||||
"3306,5432,27017",
|
||||
[]int{3306, 5432, 27017},
|
||||
},
|
||||
{
|
||||
"带空格",
|
||||
" 80 , 443 , 8080 ",
|
||||
[]int{80, 443, 8080},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) → %v", tt.input, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_PortRange 测试端口范围
|
||||
//
|
||||
// 验证:范围解析正确,包含起始和结束端口
|
||||
//
|
||||
// 1-5应该是[1,2,3,4,5]还是[1,2,3,4]?搞错了就是bug。"
|
||||
func TestParsePort_PortRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"小范围",
|
||||
"1-5",
|
||||
[]int{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
"HTTP备用端口",
|
||||
"8000-8003",
|
||||
[]int{8000, 8001, 8002, 8003},
|
||||
},
|
||||
{
|
||||
"单端口范围",
|
||||
"80-80",
|
||||
[]int{80},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) → %v", tt.input, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_MixedFormat 测试混合格式
|
||||
func TestParsePort_MixedFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"端口+范围",
|
||||
"80,100-102,443",
|
||||
[]int{80, 100, 101, 102, 443},
|
||||
},
|
||||
{
|
||||
"多个范围",
|
||||
"1-3,10-12",
|
||||
[]int{1, 2, 3, 10, 11, 12},
|
||||
},
|
||||
{
|
||||
"复杂混合",
|
||||
"22,80-82,443,8000-8001",
|
||||
[]int{22, 80, 81, 82, 443, 8000, 8001},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) → %v", tt.input, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_InvalidRange 测试无效范围
|
||||
//
|
||||
// 验证:无效范围被正确过滤
|
||||
func TestParsePort_InvalidRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"反向范围",
|
||||
"100-50",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"超出上限起始",
|
||||
"65536-65540",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"低于下限",
|
||||
"0-5",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"无效格式",
|
||||
"80-90-100",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"非数字",
|
||||
"abc-xyz",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) 正确拒绝无效范围", tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_OutOfRange 测试超出范围的端口
|
||||
func TestParsePort_OutOfRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"端口0",
|
||||
"0",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"端口65536",
|
||||
"65536",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"混合有效和无效",
|
||||
"0,80,443,65536",
|
||||
[]int{80, 443},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) 正确过滤无效端口", tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_Deduplicate 测试去重
|
||||
//
|
||||
// 验证:重复端口被去重,结果已排序
|
||||
func TestParsePort_Deduplicate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"简单重复",
|
||||
"80,80,80",
|
||||
[]int{80},
|
||||
},
|
||||
{
|
||||
"多个重复",
|
||||
"80,443,80,22,443",
|
||||
[]int{22, 80, 443},
|
||||
},
|
||||
{
|
||||
"范围重复",
|
||||
"1-3,2-4",
|
||||
[]int{1, 2, 3, 4},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
// 验证已排序
|
||||
if !sort.IntsAreSorted(result) {
|
||||
t.Errorf("ParsePort(%q) 结果未排序: %v", tt.input, result)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) 正确去重并排序 → %v", tt.input, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_Sorted 测试排序
|
||||
func TestParsePort_Sorted(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"乱序端口", "8080,22,443,80"},
|
||||
{"乱序范围", "1000-1002,80-82"},
|
||||
{"混合乱序", "443,100-102,22,80"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !sort.IntsAreSorted(result) {
|
||||
t.Errorf("ParsePort(%q) 结果未排序: %v", tt.input, result)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) 结果已排序: %v", tt.input, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_PortGroups 测试端口组展开
|
||||
//
|
||||
// 验证:预定义端口组被正确展开
|
||||
func TestParsePort_PortGroups(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
shouldContain []int
|
||||
shouldNotBeNil bool
|
||||
}{
|
||||
{
|
||||
"web组",
|
||||
"web",
|
||||
[]int{80, 443, 8080, 8443},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"all组",
|
||||
"all",
|
||||
[]int{1, 100, 1000, 10000, 65535},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if tt.shouldNotBeNil && result == nil {
|
||||
t.Errorf("ParsePort(%q) = nil, want non-nil", tt.input)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证包含特定端口
|
||||
resultMap := make(map[int]bool)
|
||||
for _, port := range result {
|
||||
resultMap[port] = true
|
||||
}
|
||||
|
||||
for _, port := range tt.shouldContain {
|
||||
if !resultMap[port] {
|
||||
t.Errorf("ParsePort(%q) 应该包含端口 %d,但不包含", tt.input, port)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) 正确展开端口组(%d个端口)", tt.input, len(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePort_WhitespaceHandling 测试空格处理
|
||||
func TestParsePort_WhitespaceHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"端口前后空格",
|
||||
" 80 , 443 ",
|
||||
[]int{80, 443},
|
||||
},
|
||||
{
|
||||
"范围中的空格",
|
||||
" 1 - 3 ",
|
||||
[]int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
"混合空格",
|
||||
" 80 , 100 - 102 , 443 ",
|
||||
[]int{80, 100, 101, 102, 443},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParsePort(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParsePort(%q) 正确处理空格", tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ParseIP - IP解析测试
|
||||
// =============================================================================
|
||||
|
||||
// TestParseIP_SingleIP 测试单个IP
|
||||
func TestParseIP_SingleIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"IPv4",
|
||||
"192.168.1.1",
|
||||
[]string{"192.168.1.1"},
|
||||
},
|
||||
{
|
||||
"域名",
|
||||
"example.com",
|
||||
[]string{"example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseIP(tt.host, "", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP(%q) error = %v", tt.host, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParseIP(%q) = %v, want %v", tt.host, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParseIP(%q) → %v", tt.host, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_MultipleIPs 测试多个IP
|
||||
func TestParseIP_MultipleIPs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"两个IP",
|
||||
"192.168.1.1,192.168.1.2",
|
||||
[]string{"192.168.1.1", "192.168.1.2"},
|
||||
},
|
||||
{
|
||||
"三个IP带空格",
|
||||
" 192.168.1.1 , 192.168.1.2 , 192.168.1.3 ",
|
||||
[]string{"192.168.1.1", "192.168.1.2", "192.168.1.3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseIP(tt.host, "", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP(%q) error = %v", tt.host, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParseIP(%q) = %v, want %v", tt.host, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParseIP(%q) → %d个IP", tt.host, len(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_CIDR 测试CIDR格式
|
||||
//
|
||||
// 验证:CIDR被正确展开为IP列表
|
||||
func TestParseIP_CIDR(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cidr string
|
||||
expectCount int
|
||||
}{
|
||||
{
|
||||
"/30网络",
|
||||
"192.168.1.0/30",
|
||||
2, // .1, .2 (排除网络地址和广播地址)
|
||||
},
|
||||
{
|
||||
"/29网络",
|
||||
"10.0.0.0/29",
|
||||
6, // .1-.6
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseIP(tt.cidr, "", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP(%q) error = %v", tt.cidr, err)
|
||||
}
|
||||
|
||||
if len(result) != tt.expectCount {
|
||||
t.Errorf("ParseIP(%q) 返回%d个IP,期望%d个",
|
||||
tt.cidr, len(result), tt.expectCount)
|
||||
}
|
||||
|
||||
// 验证已排序
|
||||
if !sort.StringsAreSorted(result) {
|
||||
t.Errorf("ParseIP(%q) 结果未排序", tt.cidr)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParseIP(%q) → %d个IP", tt.cidr, len(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_IPRange 测试IP范围
|
||||
func TestParseIP_IPRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rangeStr string
|
||||
expectCount int
|
||||
}{
|
||||
{
|
||||
"小范围",
|
||||
"192.168.1.1-3",
|
||||
3,
|
||||
},
|
||||
{
|
||||
"单IP范围",
|
||||
"192.168.1.1-1",
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseIP(tt.rangeStr, "", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP(%q) error = %v", tt.rangeStr, err)
|
||||
}
|
||||
|
||||
if len(result) != tt.expectCount {
|
||||
t.Errorf("ParseIP(%q) 返回%d个IP,期望%d个",
|
||||
tt.rangeStr, len(result), tt.expectCount)
|
||||
}
|
||||
|
||||
t.Logf("✓ ParseIP(%q) → %d个IP", tt.rangeStr, len(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_FromFile 测试从文件读取
|
||||
//
|
||||
// 验证:文件中的IP列表被正确读取
|
||||
func TestParseIP_FromFile(t *testing.T) {
|
||||
// 创建临时文件
|
||||
tmpDir := t.TempDir()
|
||||
hostFile := filepath.Join(tmpDir, "hosts.txt")
|
||||
|
||||
content := `# 这是注释
|
||||
192.168.1.1
|
||||
192.168.1.2
|
||||
|
||||
# 空行会被忽略
|
||||
192.168.1.3
|
||||
`
|
||||
|
||||
if err := os.WriteFile(hostFile, []byte(content), 0600); err != nil {
|
||||
t.Fatalf("创建测试文件失败: %v", err)
|
||||
}
|
||||
|
||||
result, err := ParseIP("", hostFile, "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP(file=%q) error = %v", hostFile, err)
|
||||
}
|
||||
|
||||
expected := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("ParseIP(file) = %v, want %v", result, expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ 从文件读取%d个IP(正确过滤注释和空行)", len(result))
|
||||
}
|
||||
|
||||
// TestParseIP_FileNotFound 测试文件不存在
|
||||
func TestParseIP_FileNotFound(t *testing.T) {
|
||||
_, err := ParseIP("", "nonexistent_file_12345.txt", "")
|
||||
|
||||
if err == nil {
|
||||
t.Error("ParseIP(不存在的文件) 应该返回错误")
|
||||
}
|
||||
|
||||
t.Logf("✓ 文件不存在时正确返回错误: %v", err)
|
||||
}
|
||||
|
||||
// TestParseIP_Exclude 测试排除主机
|
||||
//
|
||||
// 验证:排除列表中的主机被正确过滤
|
||||
func TestParseIP_Exclude(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hosts string
|
||||
exclude string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"排除单个",
|
||||
"192.168.1.1,192.168.1.2,192.168.1.3",
|
||||
"192.168.1.2",
|
||||
[]string{"192.168.1.1", "192.168.1.3"},
|
||||
},
|
||||
{
|
||||
"排除多个",
|
||||
"192.168.1.1,192.168.1.2,192.168.1.3",
|
||||
"192.168.1.1,192.168.1.3",
|
||||
[]string{"192.168.1.2"},
|
||||
},
|
||||
{
|
||||
"排除不存在的",
|
||||
"192.168.1.1,192.168.1.2",
|
||||
"192.168.1.100",
|
||||
[]string{"192.168.1.1", "192.168.1.2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseIP(tt.hosts, "", tt.exclude)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP error = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("ParseIP(hosts=%q, exclude=%q) = %v, want %v",
|
||||
tt.hosts, tt.exclude, result, tt.expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ 正确排除指定主机: %d → %d",
|
||||
len(tt.expected)+len(result)-len(tt.expected), len(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_Deduplicate 测试去重
|
||||
func TestParseIP_Deduplicate(t *testing.T) {
|
||||
result, err := ParseIP("192.168.1.1,192.168.1.1,192.168.1.2,192.168.1.2", "", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{"192.168.1.1", "192.168.1.2"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("ParseIP(重复IP) = %v, want %v", result, expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ 正确去重: 4个输入 → %d个输出", len(result))
|
||||
}
|
||||
|
||||
// TestParseIP_Sorted 测试排序
|
||||
func TestParseIP_Sorted(t *testing.T) {
|
||||
result, err := ParseIP("192.168.1.3,192.168.1.1,192.168.1.2", "", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP error = %v", err)
|
||||
}
|
||||
|
||||
if !sort.StringsAreSorted(result) {
|
||||
t.Errorf("ParseIP 结果未排序: %v", result)
|
||||
}
|
||||
|
||||
t.Logf("✓ 结果已排序: %v", result)
|
||||
}
|
||||
|
||||
// TestParseIP_NoHosts 测试无有效主机
|
||||
func TestParseIP_NoHosts(t *testing.T) {
|
||||
_, err := ParseIP("", "", "")
|
||||
|
||||
if err == nil {
|
||||
t.Error("ParseIP(空输入) 应该返回错误")
|
||||
}
|
||||
|
||||
if err.Error() != "没有找到有效的主机" {
|
||||
t.Errorf("错误信息不匹配: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("✓ 无有效主机时正确返回错误")
|
||||
}
|
||||
|
||||
// TestParseIP_MixedSources 测试混合来源
|
||||
func TestParseIP_MixedSources(t *testing.T) {
|
||||
// 创建临时文件
|
||||
tmpDir := t.TempDir()
|
||||
hostFile := filepath.Join(tmpDir, "hosts.txt")
|
||||
|
||||
if err := os.WriteFile(hostFile, []byte("192.168.1.1\n192.168.1.2\n"), 0600); err != nil {
|
||||
t.Fatalf("创建测试文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 命令行 + 文件
|
||||
result, err := ParseIP("192.168.1.3,192.168.1.4", hostFile, "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("ParseIP(混合来源) = %v, want %v", result, expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ 正确合并多个来源: %d个IP", len(result))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助函数测试
|
||||
// =============================================================================
|
||||
|
||||
// TestParsePortRange 测试端口范围解析
|
||||
func TestParsePortRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{"正常范围", "1-5", []int{1, 2, 3, 4, 5}},
|
||||
{"单端口", "80-80", []int{80}},
|
||||
{"反向范围", "5-1", nil},
|
||||
{"超出范围", "65535-65540", nil},
|
||||
{"格式错误", "1-2-3", nil},
|
||||
{"非数字", "a-b", nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parsePortRange(tt.input)
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("parsePortRange(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExcludeHosts 测试排除主机
|
||||
func TestExcludeHosts(t *testing.T) {
|
||||
hosts := []string{"host1", "host2", "host3", "host4"}
|
||||
exclude := []string{"host2", "host4"}
|
||||
|
||||
result := excludeHosts(hosts, exclude)
|
||||
expected := []string{"host1", "host3"}
|
||||
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("excludeHosts = %v, want %v", result, expected)
|
||||
}
|
||||
|
||||
t.Logf("✓ excludeHosts: %d → %d", len(hosts), len(result))
|
||||
}
|
||||
|
||||
// TestExcludeHosts_EmptyExclude 测试空排除列表
|
||||
func TestExcludeHosts_EmptyExclude(t *testing.T) {
|
||||
hosts := []string{"host1", "host2"}
|
||||
result := excludeHosts(hosts, []string{})
|
||||
|
||||
if !reflect.DeepEqual(result, hosts) {
|
||||
t.Errorf("excludeHosts(空排除列表) 应该返回原列表")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveDuplicates 测试去重
|
||||
func TestRemoveDuplicates(t *testing.T) {
|
||||
input := []string{"a", "b", "a", "c", "b", "d"}
|
||||
result := removeDuplicates(input)
|
||||
|
||||
// 验证无重复
|
||||
seen := make(map[string]bool)
|
||||
for _, item := range result {
|
||||
if seen[item] {
|
||||
t.Errorf("removeDuplicates 结果包含重复项: %s", item)
|
||||
}
|
||||
seen[item] = true
|
||||
}
|
||||
|
||||
// 验证长度
|
||||
if len(result) != 4 {
|
||||
t.Errorf("removeDuplicates 返回%d项,期望4项", len(result))
|
||||
}
|
||||
|
||||
t.Logf("✓ removeDuplicates: %d → %d", len(input), len(result))
|
||||
}
|
||||
|
||||
// TestRemoveDuplicatePorts 测试端口去重
|
||||
func TestRemoveDuplicatePorts(t *testing.T) {
|
||||
input := []int{80, 443, 80, 22, 443, 8080}
|
||||
result := removeDuplicatePorts(input)
|
||||
|
||||
// 验证无重复
|
||||
seen := make(map[int]bool)
|
||||
for _, port := range result {
|
||||
if seen[port] {
|
||||
t.Errorf("removeDuplicatePorts 结果包含重复项: %d", port)
|
||||
}
|
||||
seen[port] = true
|
||||
}
|
||||
|
||||
// 验证长度
|
||||
if len(result) != 4 {
|
||||
t.Errorf("removeDuplicatePorts 返回%d项,期望4项", len(result))
|
||||
}
|
||||
|
||||
t.Logf("✓ removeDuplicatePorts: %d → %d", len(input), len(result))
|
||||
}
|
||||
@@ -0,0 +1,987 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
// TargetParser 目标解析器
|
||||
type TargetParser struct {
|
||||
fileReader *FileReader
|
||||
mu sync.RWMutex //nolint:unused // reserved for future thread safety
|
||||
ipRegex *regexp.Regexp
|
||||
portRegex *regexp.Regexp
|
||||
urlRegex *regexp.Regexp
|
||||
options *TargetParserOptions
|
||||
}
|
||||
|
||||
// TargetParserOptions 目标解析器选项
|
||||
type TargetParserOptions struct {
|
||||
MaxTargets int `json:"max_targets"`
|
||||
MaxPortRange int `json:"max_port_range"`
|
||||
AllowPrivateIPs bool `json:"allow_private_ips"`
|
||||
AllowLoopback bool `json:"allow_loopback"`
|
||||
ValidateURLs bool `json:"validate_urls"`
|
||||
ResolveDomains bool `json:"resolve_domains"`
|
||||
}
|
||||
|
||||
// DefaultTargetParserOptions 默认目标解析器选项
|
||||
func DefaultTargetParserOptions() *TargetParserOptions {
|
||||
return &TargetParserOptions{
|
||||
MaxTargets: DefaultTargetMaxTargets,
|
||||
MaxPortRange: DefaultMaxPortRange,
|
||||
AllowPrivateIPs: DefaultAllowPrivateIPs,
|
||||
AllowLoopback: DefaultAllowLoopback,
|
||||
ValidateURLs: DefaultValidateURLs,
|
||||
ResolveDomains: DefaultResolveDomains,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTargetParser 创建目标解析器
|
||||
func NewTargetParser(fileReader *FileReader, options *TargetParserOptions) *TargetParser {
|
||||
if options == nil {
|
||||
options = DefaultTargetParserOptions()
|
||||
}
|
||||
|
||||
// 使用预编译的正则表达式
|
||||
ipRegex := CompiledIPv4Regex
|
||||
portRegex := CompiledPortRegex
|
||||
urlRegex := CompiledURLRegex
|
||||
|
||||
return &TargetParser{
|
||||
fileReader: fileReader,
|
||||
ipRegex: ipRegex,
|
||||
portRegex: portRegex,
|
||||
urlRegex: urlRegex,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// TargetInput 目标输入参数
|
||||
type TargetInput struct {
|
||||
// 主机相关
|
||||
Host string `json:"host"`
|
||||
HostsFile string `json:"hosts_file"`
|
||||
ExcludeHosts string `json:"exclude_hosts"`
|
||||
ExcludeHostsFile string `json:"exclude_hosts_file"`
|
||||
|
||||
// 端口相关
|
||||
Ports string `json:"ports"`
|
||||
PortsFile string `json:"ports_file"`
|
||||
AddPorts string `json:"add_ports"`
|
||||
ExcludePorts string `json:"exclude_ports"`
|
||||
|
||||
// URL相关
|
||||
TargetURL string `json:"target_url"`
|
||||
URLsFile string `json:"urls_file"`
|
||||
|
||||
// 主机端口组合
|
||||
HostPort []string `json:"host_port"`
|
||||
|
||||
// 模式标识
|
||||
LocalMode bool `json:"local_mode"`
|
||||
}
|
||||
|
||||
// Parse 解析目标配置
|
||||
func (tp *TargetParser) Parse(input *TargetInput, options *ParserOptions) (*ParseResult, error) {
|
||||
if input == nil {
|
||||
return nil, NewParseError(ErrorTypeInputError, "目标输入为空", "", 0, ErrEmptyInput)
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
result := &ParseResult{
|
||||
Config: &ParsedConfig{
|
||||
Targets: &TargetConfig{
|
||||
LocalMode: input.LocalMode,
|
||||
},
|
||||
},
|
||||
Success: true,
|
||||
}
|
||||
|
||||
var errors []error
|
||||
var warnings []string
|
||||
|
||||
// 解析主机
|
||||
hosts, hostErrors, hostWarnings := tp.parseHosts(input)
|
||||
errors = append(errors, hostErrors...)
|
||||
warnings = append(warnings, hostWarnings...)
|
||||
|
||||
// 解析URL
|
||||
urls, urlErrors, urlWarnings := tp.parseURLs(input)
|
||||
errors = append(errors, urlErrors...)
|
||||
warnings = append(warnings, urlWarnings...)
|
||||
|
||||
// 解析端口
|
||||
ports, portErrors, portWarnings := tp.parsePorts(input)
|
||||
errors = append(errors, portErrors...)
|
||||
warnings = append(warnings, portWarnings...)
|
||||
|
||||
// 解析排除端口
|
||||
excludePorts, excludeErrors, excludeWarnings := tp.parseExcludePorts(input)
|
||||
errors = append(errors, excludeErrors...)
|
||||
warnings = append(warnings, excludeWarnings...)
|
||||
|
||||
// 解析主机端口组合
|
||||
hostPorts, hpErrors, hpWarnings := tp.parseHostPorts(input)
|
||||
errors = append(errors, hpErrors...)
|
||||
warnings = append(warnings, hpWarnings...)
|
||||
|
||||
// 更新配置
|
||||
result.Config.Targets.Hosts = hosts
|
||||
result.Config.Targets.URLs = urls
|
||||
|
||||
// 如果存在明确的host:port组合,则清空端口列表避免双重扫描
|
||||
if len(hostPorts) > 0 {
|
||||
result.Config.Targets.Ports = nil // 清空默认端口,只扫描指定的host:port
|
||||
} else {
|
||||
result.Config.Targets.Ports = ports
|
||||
}
|
||||
|
||||
result.Config.Targets.ExcludePorts = excludePorts
|
||||
result.Config.Targets.HostPorts = hostPorts
|
||||
|
||||
// 设置结果状态
|
||||
result.Errors = errors
|
||||
result.Warnings = warnings
|
||||
result.ParseTime = time.Since(startTime)
|
||||
result.Success = len(errors) == 0
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseHosts 解析主机
|
||||
func (tp *TargetParser) parseHosts(input *TargetInput) ([]string, []error, []string) {
|
||||
var hosts []string
|
||||
var errors []error
|
||||
var warnings []string
|
||||
|
||||
// 解析命令行主机
|
||||
if input.Host != "" {
|
||||
// 检查是否为host:port格式,直接添加到HostPort避免双重处理
|
||||
if strings.Contains(input.Host, ":") {
|
||||
if _, portStr, err := net.SplitHostPort(input.Host); err == nil {
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
||||
// 这是有效的host:port格式,直接添加到HostPort
|
||||
input.HostPort = append(input.HostPort, input.Host)
|
||||
// 清空Ports字段,避免解析默认端口
|
||||
input.Ports = ""
|
||||
// 不添加到hosts,避免双重处理
|
||||
} else {
|
||||
// 端口无效,作为普通主机处理
|
||||
hostList, parseErr := tp.parseHostList(input.Host)
|
||||
if parseErr != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeHostError, parseErr.Error(), "command line", 0, parseErr))
|
||||
} else {
|
||||
hosts = append(hosts, hostList...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不是有效的host:port格式,作为普通主机处理
|
||||
hostList, parseErr := tp.parseHostList(input.Host)
|
||||
if parseErr != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeHostError, parseErr.Error(), "command line", 0, parseErr))
|
||||
} else {
|
||||
hosts = append(hosts, hostList...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 普通主机,正常处理
|
||||
hostList, err := tp.parseHostList(input.Host)
|
||||
if err != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeHostError, err.Error(), "command line", 0, err))
|
||||
} else {
|
||||
hosts = append(hosts, hostList...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取主机
|
||||
if input.HostsFile != "" {
|
||||
fileResult, err := tp.fileReader.ReadFile(input.HostsFile)
|
||||
if err != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeFileError, "读取主机文件失败", input.HostsFile, 0, err))
|
||||
} else {
|
||||
for i, line := range fileResult.Lines {
|
||||
hostList, err := tp.parseHostList(line)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("主机文件第%d行解析失败: %s", i+1, err.Error()))
|
||||
} else {
|
||||
hosts = append(hosts, hostList...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理排除主机
|
||||
var excludeList []string
|
||||
|
||||
// 从命令行参数读取排除主机
|
||||
if input.ExcludeHosts != "" {
|
||||
cmdExclude, err := tp.parseHostList(input.ExcludeHosts)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("排除主机解析失败: %s", err.Error()))
|
||||
} else {
|
||||
excludeList = append(excludeList, cmdExclude...)
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取排除主机
|
||||
if input.ExcludeHostsFile != "" {
|
||||
fileResult, err := tp.fileReader.ReadFile(input.ExcludeHostsFile)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("读取排除主机文件失败: %s", err.Error()))
|
||||
} else {
|
||||
for i, line := range fileResult.Lines {
|
||||
fileExclude, err := tp.parseHostList(line)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("排除主机文件第%d行解析失败: %s", i+1, err.Error()))
|
||||
} else {
|
||||
excludeList = append(excludeList, fileExclude...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用排除列表
|
||||
if len(excludeList) > 0 {
|
||||
hosts = tp.excludeHosts(hosts, excludeList)
|
||||
}
|
||||
|
||||
// 去重和验证,同时分离host:port格式
|
||||
hosts = tp.removeDuplicateStrings(hosts)
|
||||
validHosts := make([]string, 0, len(hosts))
|
||||
hostPorts := make([]string, 0)
|
||||
|
||||
for _, host := range hosts {
|
||||
// 检查是否为host:port格式
|
||||
if strings.Contains(host, ":") {
|
||||
if h, portStr, err := net.SplitHostPort(host); err == nil {
|
||||
// 验证端口号
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
||||
// 验证主机部分
|
||||
if valid, hostErr := tp.validateHost(h); valid {
|
||||
// 这是有效的host:port组合,添加到hostPorts
|
||||
hostPorts = append(hostPorts, host)
|
||||
continue
|
||||
} else if hostErr != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("无效主机端口组合: %s - %s", host, hostErr.Error()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 作为普通主机验证
|
||||
if valid, err := tp.validateHost(host); valid {
|
||||
validHosts = append(validHosts, host)
|
||||
} else if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("无效主机: %s - %s", host, err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// 将找到的hostPorts合并到输入结果中(通过修改input结构)
|
||||
if len(hostPorts) > 0 {
|
||||
input.HostPort = append(input.HostPort, hostPorts...)
|
||||
}
|
||||
|
||||
// 检查目标数量限制
|
||||
if len(validHosts) > tp.options.MaxTargets {
|
||||
warnings = append(warnings, fmt.Sprintf("主机数量超过限制,截取前%d个", tp.options.MaxTargets))
|
||||
validHosts = validHosts[:tp.options.MaxTargets]
|
||||
}
|
||||
|
||||
return validHosts, errors, warnings
|
||||
}
|
||||
|
||||
// parseURLs 解析URL
|
||||
func (tp *TargetParser) parseURLs(input *TargetInput) ([]string, []error, []string) {
|
||||
var urls []string
|
||||
var errors []error
|
||||
var warnings []string
|
||||
|
||||
// 解析命令行URL
|
||||
if input.TargetURL != "" {
|
||||
urlList := strings.Split(input.TargetURL, ",")
|
||||
for _, rawURL := range urlList {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL != "" {
|
||||
if valid, err := tp.validateURL(rawURL); valid {
|
||||
urls = append(urls, rawURL)
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("无效URL: %s - %s", rawURL, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取URL
|
||||
if input.URLsFile != "" {
|
||||
fileResult, err := tp.fileReader.ReadFile(input.URLsFile)
|
||||
if err != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeFileError, "读取URL文件失败", input.URLsFile, 0, err))
|
||||
} else {
|
||||
for i, line := range fileResult.Lines {
|
||||
if valid, err := tp.validateURL(line); valid {
|
||||
urls = append(urls, line)
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("URL文件第%d行无效: %s", i+1, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重
|
||||
urls = tp.removeDuplicateStrings(urls)
|
||||
|
||||
return urls, errors, warnings
|
||||
}
|
||||
|
||||
// parsePorts 解析端口
|
||||
func (tp *TargetParser) parsePorts(input *TargetInput) ([]int, []error, []string) {
|
||||
var ports []int
|
||||
var errors []error
|
||||
var warnings []string
|
||||
|
||||
// 解析命令行端口
|
||||
if input.Ports != "" {
|
||||
portList, err := tp.parsePortList(input.Ports)
|
||||
if err != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypePortError, err.Error(), "command line", 0, err))
|
||||
} else {
|
||||
ports = append(ports, portList...)
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取端口
|
||||
if input.PortsFile != "" {
|
||||
fileResult, err := tp.fileReader.ReadFile(input.PortsFile)
|
||||
if err != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeFileError, "读取端口文件失败", input.PortsFile, 0, err))
|
||||
} else {
|
||||
for i, line := range fileResult.Lines {
|
||||
portList, err := tp.parsePortList(line)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("端口文件第%d行解析失败: %s", i+1, err.Error()))
|
||||
} else {
|
||||
ports = append(ports, portList...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理额外端口
|
||||
if input.AddPorts != "" {
|
||||
addPortList, err := tp.parsePortList(input.AddPorts)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("额外端口解析失败: %s", err.Error()))
|
||||
} else {
|
||||
ports = append(ports, addPortList...)
|
||||
}
|
||||
}
|
||||
|
||||
// 去重和排序
|
||||
ports = tp.removeDuplicatePorts(ports)
|
||||
|
||||
return ports, errors, warnings
|
||||
}
|
||||
|
||||
// parseExcludePorts 解析排除端口
|
||||
func (tp *TargetParser) parseExcludePorts(input *TargetInput) ([]int, []error, []string) { //nolint:unparam
|
||||
var excludePorts []int
|
||||
var errors []error
|
||||
var warnings []string
|
||||
|
||||
if input.ExcludePorts != "" {
|
||||
portList, err := tp.parsePortList(input.ExcludePorts)
|
||||
if err != nil {
|
||||
errors = append(errors, NewParseError(ErrorTypeExcludePortError, err.Error(), "command line", 0, err))
|
||||
} else {
|
||||
excludePorts = portList
|
||||
}
|
||||
}
|
||||
|
||||
return excludePorts, errors, warnings
|
||||
}
|
||||
|
||||
// parseHostPorts 解析主机端口组合
|
||||
func (tp *TargetParser) parseHostPorts(input *TargetInput) ([]string, []error, []string) { //nolint:unparam
|
||||
var hostPorts []string
|
||||
var errors []error
|
||||
var warnings []string
|
||||
|
||||
for _, hp := range input.HostPort {
|
||||
if hp != "" {
|
||||
if valid, err := tp.validateHostPort(hp); valid {
|
||||
hostPorts = append(hostPorts, hp)
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("无效主机端口组合: %s - %s", hp, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hostPorts, errors, warnings
|
||||
}
|
||||
|
||||
// parseHostList 解析主机列表
|
||||
func (tp *TargetParser) parseHostList(hostStr string) ([]string, error) {
|
||||
if hostStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var hosts []string
|
||||
hostItems := strings.Split(hostStr, ",")
|
||||
|
||||
for _, item := range hostItems {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查各种IP格式
|
||||
switch {
|
||||
case item == PrivateNetwork192:
|
||||
// 常用内网段简写
|
||||
cidrHosts, err := tp.parseCIDR(PrivateNetwork192CIDR)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("192网段解析失败: %w", err)
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case item == PrivateNetwork172:
|
||||
// 常用内网段简写
|
||||
cidrHosts, err := tp.parseCIDR(PrivateNetwork172CIDR)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("172网段解析失败: %w", err)
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case item == PrivateNetwork10:
|
||||
// 常用内网段简写
|
||||
cidrHosts, err := tp.parseCIDR(PrivateNetwork10CIDR)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("10网段解析失败: %w", err)
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case strings.HasSuffix(item, "/8"):
|
||||
// 处理/8网段(使用采样方式)
|
||||
sampledHosts := tp.parseSubnet8(item)
|
||||
hosts = append(hosts, sampledHosts...)
|
||||
case strings.Contains(item, "/"):
|
||||
// CIDR表示法
|
||||
cidrHosts, err := tp.parseCIDR(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CIDR解析失败 %s: %w", item, err)
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case strings.Contains(item, "-"):
|
||||
// IP范围表示法
|
||||
rangeHosts, err := tp.parseIPRange(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("IP范围解析失败 %s: %w", item, err)
|
||||
}
|
||||
hosts = append(hosts, rangeHosts...)
|
||||
default:
|
||||
// 检查是否为host:port格式
|
||||
if strings.Contains(item, ":") {
|
||||
if _, portStr, err := net.SplitHostPort(item); err == nil {
|
||||
// 验证端口号
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
||||
// 这是有效的host:port格式,但在这里仍然作为主机处理
|
||||
// 在后续的processHostPorts函数中会被正确处理
|
||||
hosts = append(hosts, item)
|
||||
} else {
|
||||
// 端口无效,作为普通主机处理
|
||||
hosts = append(hosts, item)
|
||||
}
|
||||
} else {
|
||||
// 不是有效的host:port格式,作为普通主机处理
|
||||
hosts = append(hosts, item)
|
||||
}
|
||||
} else {
|
||||
// 单个IP或域名
|
||||
hosts = append(hosts, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// parsePortList 解析端口列表,支持预定义端口组
|
||||
func (tp *TargetParser) parsePortList(portStr string) ([]int, error) {
|
||||
if portStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 检查是否为预定义端口组
|
||||
portStr = tp.expandPortGroups(portStr)
|
||||
|
||||
var ports []int
|
||||
portItems := strings.Split(portStr, ",")
|
||||
|
||||
for _, item := range portItems {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(item, "-") {
|
||||
// 端口范围
|
||||
rangePorts, err := tp.parsePortRange(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("端口范围解析失败 %s: %w", item, err)
|
||||
}
|
||||
|
||||
// 检查范围大小
|
||||
if len(rangePorts) > tp.options.MaxPortRange {
|
||||
return nil, fmt.Errorf("端口范围过大: %d, 最大允许: %d", len(rangePorts), tp.options.MaxPortRange)
|
||||
}
|
||||
|
||||
ports = append(ports, rangePorts...)
|
||||
} else {
|
||||
// 单个端口
|
||||
port, err := strconv.Atoi(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效端口号: %s", item)
|
||||
}
|
||||
|
||||
if port < MinPort || port > MaxPort {
|
||||
return nil, fmt.Errorf("端口号超出范围: %d", port)
|
||||
}
|
||||
|
||||
ports = append(ports, port)
|
||||
}
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// expandPortGroups 展开预定义端口组
|
||||
func (tp *TargetParser) expandPortGroups(portStr string) string {
|
||||
// 使用预定义的端口组
|
||||
portGroups := config.GetPortGroups()
|
||||
|
||||
if expandedPorts, exists := portGroups[portStr]; exists {
|
||||
return expandedPorts
|
||||
}
|
||||
return portStr
|
||||
}
|
||||
|
||||
// parseCIDR 解析CIDR网段
|
||||
func (tp *TargetParser) parseCIDR(cidr string) ([]string, error) {
|
||||
return parseIPCIDR(cidr, tp.options.MaxTargets)
|
||||
}
|
||||
|
||||
// parseIPRange 解析IP范围,支持简写格式
|
||||
func (tp *TargetParser) parseIPRange(rangeStr string) ([]string, error) {
|
||||
return parseIPRangeString(rangeStr, tp.options.MaxTargets)
|
||||
}
|
||||
|
||||
// parseSubnet8 解析/8网段的IP地址,生成采样IP列表
|
||||
func (tp *TargetParser) parseSubnet8(subnet string) []string {
|
||||
// 去除CIDR后缀获取基础IP
|
||||
baseIP := subnet[:len(subnet)-2]
|
||||
if net.ParseIP(baseIP) == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取/8网段的第一段
|
||||
firstOctet := strings.Split(baseIP, ".")[0]
|
||||
var sampleIPs []string
|
||||
|
||||
// 对常用网段进行更全面的扫描
|
||||
commonSecondOctets := GetCommonSecondOctets()
|
||||
|
||||
// 对于每个选定的第二段,采样部分第三段
|
||||
for _, secondOctet := range commonSecondOctets {
|
||||
for thirdOctet := 0; thirdOctet < 256; thirdOctet += Subnet8ThirdOctetStep {
|
||||
// 添加常见的网关和服务器IP
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, DefaultGatewayLastOctet)) // 默认网关
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, RouterSwitchLastOctet)) // 通常用于路由器/交换机
|
||||
|
||||
// 随机采样不同范围的主机IP
|
||||
fourthOctet := tp.randomInt(SamplingMinHost, SamplingMaxHost)
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, fourthOctet))
|
||||
}
|
||||
}
|
||||
|
||||
// 对其他二级网段进行稀疏采样
|
||||
for secondOctet := 0; secondOctet < 256; secondOctet += Subnet8SamplingStep {
|
||||
for thirdOctet := 0; thirdOctet < 256; thirdOctet += Subnet8SamplingStep {
|
||||
// 对于采样的网段,取几个代表性IP
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, DefaultGatewayLastOctet))
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, tp.randomInt(SamplingMinHost, SamplingMaxHost)))
|
||||
}
|
||||
}
|
||||
|
||||
// 限制采样数量
|
||||
if len(sampleIPs) > tp.options.MaxTargets {
|
||||
sampleIPs = sampleIPs[:tp.options.MaxTargets]
|
||||
}
|
||||
|
||||
return sampleIPs
|
||||
}
|
||||
|
||||
// randomInt 生成指定范围内的随机整数
|
||||
func (tp *TargetParser) randomInt(min, max int) int {
|
||||
if min >= max || min < 0 || max <= 0 {
|
||||
return max
|
||||
}
|
||||
return min + (max-min)/2 // 简化版本,避免依赖rand
|
||||
}
|
||||
|
||||
// parsePortRange 解析端口范围
|
||||
func (tp *TargetParser) parsePortRange(rangeStr string) ([]int, error) {
|
||||
parts := strings.Split(rangeStr, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("无效的端口范围格式")
|
||||
}
|
||||
|
||||
startPort, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
endPort, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
return nil, fmt.Errorf("无效的端口号")
|
||||
}
|
||||
|
||||
if startPort > endPort {
|
||||
startPort, endPort = endPort, startPort
|
||||
}
|
||||
|
||||
if startPort < MinPort || endPort > MaxPort {
|
||||
return nil, fmt.Errorf("端口号超出范围")
|
||||
}
|
||||
|
||||
var ports []int
|
||||
for port := startPort; port <= endPort; port++ {
|
||||
ports = append(ports, port)
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// validateHost 验证主机地址
|
||||
func (tp *TargetParser) validateHost(host string) (bool, error) {
|
||||
if host == "" {
|
||||
return false, fmt.Errorf("主机地址为空")
|
||||
}
|
||||
|
||||
// 检查是否为host:port格式
|
||||
if strings.Contains(host, ":") {
|
||||
// 可能是host:port格式,尝试分离
|
||||
if h, portStr, err := net.SplitHostPort(host); err == nil {
|
||||
// 验证端口号
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
||||
// 递归验证主机部分(不包含端口)
|
||||
return tp.validateHost(h)
|
||||
}
|
||||
}
|
||||
// 如果不是有效的host:port格式,继续按普通主机地址处理
|
||||
}
|
||||
|
||||
// 检查是否为IP地址
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return tp.validateIP(ip)
|
||||
}
|
||||
|
||||
// 检查是否为域名
|
||||
if tp.isValidDomain(host) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("无效的主机地址格式")
|
||||
}
|
||||
|
||||
// validateIP 验证IP地址
|
||||
func (tp *TargetParser) validateIP(ip net.IP) (bool, error) {
|
||||
if ip == nil {
|
||||
return false, fmt.Errorf("IP地址为空")
|
||||
}
|
||||
|
||||
// 检查是否为私有IP
|
||||
if !tp.options.AllowPrivateIPs && tp.isPrivateIP(ip) {
|
||||
return false, fmt.Errorf("不允许私有IP地址")
|
||||
}
|
||||
|
||||
// 检查是否为回环地址
|
||||
if !tp.options.AllowLoopback && ip.IsLoopback() {
|
||||
return false, fmt.Errorf("不允许回环地址")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validateURL 验证URL
|
||||
func (tp *TargetParser) validateURL(rawURL string) (bool, error) {
|
||||
if rawURL == "" {
|
||||
return false, fmt.Errorf("URL为空")
|
||||
}
|
||||
|
||||
if !tp.options.ValidateURLs {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !tp.urlRegex.MatchString(rawURL) {
|
||||
return false, fmt.Errorf("URL格式无效")
|
||||
}
|
||||
|
||||
// 进一步验证URL格式
|
||||
_, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("URL解析失败: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validateHostPort 验证主机端口组合
|
||||
func (tp *TargetParser) validateHostPort(hostPort string) (bool, error) {
|
||||
parts := strings.Split(hostPort, ":")
|
||||
if len(parts) != 2 {
|
||||
return false, fmt.Errorf("主机端口格式无效,应为 host:port")
|
||||
}
|
||||
|
||||
host := strings.TrimSpace(parts[0])
|
||||
portStr := strings.TrimSpace(parts[1])
|
||||
|
||||
// 验证主机
|
||||
if valid, err := tp.validateHost(host); !valid {
|
||||
return false, fmt.Errorf("主机无效: %w", err)
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("端口号无效: %s", portStr)
|
||||
}
|
||||
|
||||
if port < MinPort || port > MaxPort {
|
||||
return false, fmt.Errorf("端口号超出范围: %d", port)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// isPrivateIP 检查是否为私有IP
|
||||
func (tp *TargetParser) isPrivateIP(ip net.IP) bool {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
// 10.0.0.0/8
|
||||
if ip4[0] == 10 {
|
||||
return true
|
||||
}
|
||||
// 172.16.0.0/12
|
||||
if ip4[0] == 172 && ip4[1] >= Private172StartSecondOctet && ip4[1] <= Private172EndSecondOctet {
|
||||
return true
|
||||
}
|
||||
// 192.168.0.0/16
|
||||
if ip4[0] == 192 && ip4[1] == Private192SecondOctet {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidDomain 检查是否为有效域名
|
||||
func (tp *TargetParser) isValidDomain(domain string) bool {
|
||||
return CompiledDomainRegex.MatchString(domain) && len(domain) <= MaxDomainLength
|
||||
}
|
||||
|
||||
// excludeHosts 排除指定主机
|
||||
func (tp *TargetParser) excludeHosts(hosts, excludeList []string) []string {
|
||||
excludeMap := make(map[string]struct{})
|
||||
for _, exclude := range excludeList {
|
||||
excludeMap[exclude] = struct{}{}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, host := range hosts {
|
||||
if _, excluded := excludeMap[host]; !excluded {
|
||||
result = append(result, host)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removeDuplicateStrings 去重字符串切片
|
||||
func (tp *TargetParser) removeDuplicateStrings(slice []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var result []string
|
||||
|
||||
for _, item := range slice {
|
||||
if _, exists := seen[item]; !exists {
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removeDuplicatePorts 去重端口切片
|
||||
func (tp *TargetParser) removeDuplicatePorts(slice []int) []int {
|
||||
seen := make(map[int]struct{})
|
||||
var result []int
|
||||
|
||||
for _, item := range slice {
|
||||
if _, exists := seen[item]; !exists {
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 包级共享辅助函数(供 simple.go 和 target_parser.go 共用)
|
||||
// =============================================================================
|
||||
|
||||
// incrementIP 计算下一个IP地址(统一的包级函数)
|
||||
func incrementIP(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseIPCIDR 解析CIDR网段(包级函数)
|
||||
func parseIPCIDR(cidr string, maxTargets int) ([]string, error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []string
|
||||
ip := make(net.IP, len(ipNet.IP))
|
||||
copy(ip, ipNet.IP)
|
||||
|
||||
count := 0
|
||||
for ipNet.Contains(ip) {
|
||||
ips = append(ips, ip.String())
|
||||
count++
|
||||
|
||||
// 防止生成过多IP
|
||||
if count >= maxTargets {
|
||||
break
|
||||
}
|
||||
|
||||
incrementIP(ip)
|
||||
}
|
||||
|
||||
// 移除网络地址和广播地址
|
||||
if len(ips) > 2 {
|
||||
ips = ips[1 : len(ips)-1]
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// parseIPRangeString 解析IP范围字符串(包级函数)
|
||||
func parseIPRangeString(rangeStr string, maxTargets int) ([]string, error) {
|
||||
parts := strings.Split(rangeStr, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("无效的IP范围格式: %s", rangeStr)
|
||||
}
|
||||
|
||||
startIPStr := strings.TrimSpace(parts[0])
|
||||
endIPStr := strings.TrimSpace(parts[1])
|
||||
|
||||
// 验证起始IP
|
||||
startIP := net.ParseIP(startIPStr)
|
||||
if startIP == nil {
|
||||
return nil, fmt.Errorf("无效的起始IP地址: %s", startIPStr)
|
||||
}
|
||||
|
||||
// 处理简写格式 (如: 192.168.1.1-100)
|
||||
if len(endIPStr) < 4 || !strings.Contains(endIPStr, ".") {
|
||||
return parseIPShortRange(startIPStr, endIPStr)
|
||||
}
|
||||
|
||||
// 处理完整格式 (如: 192.168.1.1-192.168.1.100)
|
||||
endIP := net.ParseIP(endIPStr)
|
||||
if endIP == nil {
|
||||
return nil, fmt.Errorf("无效的结束IP地址: %s", endIPStr)
|
||||
}
|
||||
|
||||
return parseIPFullRange(startIP, endIP, maxTargets)
|
||||
}
|
||||
|
||||
// parseIPShortRange 解析短格式IP范围(包级函数)
|
||||
func parseIPShortRange(startIPStr, endSuffix string) ([]string, error) {
|
||||
// 将结束段转换为数字
|
||||
endNum, err := strconv.Atoi(endSuffix)
|
||||
if err != nil || endNum > MaxIPv4OctetValue {
|
||||
return nil, fmt.Errorf("无效的IP范围结束值: %s", endSuffix)
|
||||
}
|
||||
|
||||
// 分解起始IP
|
||||
ipParts := strings.Split(startIPStr, ".")
|
||||
if len(ipParts) != IPv4OctetCount {
|
||||
return nil, fmt.Errorf("无效的IP地址格式: %s", startIPStr)
|
||||
}
|
||||
|
||||
// 获取前缀和起始IP的最后一部分
|
||||
prefixIP := strings.Join(ipParts[0:3], ".")
|
||||
startNum, err := strconv.Atoi(ipParts[3])
|
||||
if err != nil || startNum > endNum {
|
||||
return nil, fmt.Errorf("无效的IP范围: %s-%s", startIPStr, endSuffix)
|
||||
}
|
||||
|
||||
// 生成IP范围
|
||||
var allIP []string
|
||||
for i := startNum; i <= endNum; i++ {
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i))
|
||||
}
|
||||
|
||||
return allIP, nil
|
||||
}
|
||||
|
||||
// parseIPFullRange 解析完整格式的IP范围(包级函数)
|
||||
func parseIPFullRange(startIP, endIP net.IP, maxTargets int) ([]string, error) {
|
||||
// 转换为IPv4
|
||||
start4 := startIP.To4()
|
||||
end4 := endIP.To4()
|
||||
if start4 == nil || end4 == nil {
|
||||
return nil, fmt.Errorf("仅支持IPv4地址范围")
|
||||
}
|
||||
|
||||
// 计算IP地址的整数表示
|
||||
startInt := (int(start4[0]) << IPFirstOctetShift) | (int(start4[1]) << IPSecondOctetShift) | (int(start4[2]) << IPThirdOctetShift) | int(start4[3])
|
||||
endInt := (int(end4[0]) << IPFirstOctetShift) | (int(end4[1]) << IPSecondOctetShift) | (int(end4[2]) << IPThirdOctetShift) | int(end4[3])
|
||||
|
||||
if startInt > endInt {
|
||||
return nil, fmt.Errorf("起始IP大于结束IP")
|
||||
}
|
||||
|
||||
// 生成IP列表
|
||||
var ips []string
|
||||
current := make(net.IP, len(start4))
|
||||
copy(current, start4)
|
||||
|
||||
count := 0
|
||||
for {
|
||||
ips = append(ips, current.String())
|
||||
count++
|
||||
|
||||
if current.Equal(end4) || count >= maxTargets {
|
||||
break
|
||||
}
|
||||
|
||||
incrementIP(current)
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// =============================================================================================
|
||||
// 已删除的死代码(未使用):Validate 和 GetStatistics 方法
|
||||
// =============================================================================================
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
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 方法
|
||||
// =============================================================================================
|
||||
@@ -0,0 +1,620 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user