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