mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-26 21:21:53 +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 - 统一初始化入口
|
initialize.go - 统一初始化入口
|
||||||
|
|
||||||
将分散的初始化步骤整合为单一入口,简化 main.go。
|
简化后的流程:
|
||||||
|
命令行 → FlagVars → BuildConfig() → Config + State
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// InitResult 初始化结果
|
// InitResult 初始化结果
|
||||||
@@ -18,60 +19,22 @@ type InitResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 统一初始化函数
|
// Initialize 统一初始化函数
|
||||||
// 封装 Parse → InitGlobalConfigAndState → InitOutput 流程
|
// 封装 BuildConfig → InitOutput 流程
|
||||||
// 返回可直接使用的 Config 和 State 对象
|
|
||||||
func Initialize(info *HostInfo) (*InitResult, error) {
|
func Initialize(info *HostInfo) (*InitResult, error) {
|
||||||
// 初始化日志系统
|
// 1. 初始化日志系统
|
||||||
InitLogger()
|
InitLogger()
|
||||||
|
|
||||||
// 解析和验证参数(会更新 globalConfig 的凭据信息)
|
// 2. 从 FlagVars 构建 Config 和 State
|
||||||
if err := Parse(info); err != nil {
|
cfg, state, err := BuildConfig(GetFlagVars(), info)
|
||||||
return nil, fmt.Errorf("参数解析失败: %w", err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("配置构建失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Parse 更新过的凭据信息
|
// 3. 设置全局实例
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置全局实例
|
|
||||||
SetGlobalConfig(cfg)
|
SetGlobalConfig(cfg)
|
||||||
SetGlobalState(state)
|
SetGlobalState(state)
|
||||||
|
|
||||||
// 初始化输出系统
|
// 4. 初始化输出系统
|
||||||
if err := InitOutput(); err != nil {
|
if err := InitOutput(); err != nil {
|
||||||
return nil, fmt.Errorf("输出初始化失败: %w", err)
|
return nil, fmt.Errorf("输出初始化失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -120,7 +83,6 @@ func ValidateExclusiveParams(info *HostInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup 清理资源
|
// Cleanup 清理资源
|
||||||
// 应该在程序退出前调用
|
|
||||||
func Cleanup() error {
|
func Cleanup() error {
|
||||||
return CloseOutput()
|
return CloseOutput()
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-368
@@ -1,364 +1,22 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common/i18n"
|
|
||||||
"github.com/shadow1ng/fscan/common/logging"
|
"github.com/shadow1ng/fscan/common/logging"
|
||||||
"github.com/shadow1ng/fscan/common/parsers"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParsedConfiguration 解析后的完整配置(兼容旧代码)
|
/*
|
||||||
type ParsedConfiguration struct {
|
parse.go - 解析相关工具函数
|
||||||
*parsers.ParsedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parser 主解析器
|
重构后只保留:
|
||||||
type Parser struct {
|
- RemoveDuplicate - 字符串去重
|
||||||
mu sync.RWMutex
|
- applyLogLevel - 日志级别应用
|
||||||
fileReader *parsers.FileReader
|
- 辅助函数
|
||||||
credentialParser *parsers.CredentialParser
|
*/
|
||||||
targetParser *parsers.TargetParser
|
|
||||||
networkParser *parsers.NetworkParser
|
|
||||||
validationParser *parsers.ValidationParser
|
|
||||||
options *parsers.ParserOptions
|
|
||||||
initialized bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewParser 创建新的解析器实例
|
// RemoveDuplicate 去重函数
|
||||||
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 去重函数 - 恢复原始高效实现
|
|
||||||
func RemoveDuplicate(old []string) []string {
|
func RemoveDuplicate(old []string) []string {
|
||||||
if len(old) <= 1 {
|
if len(old) <= 1 {
|
||||||
return old
|
return old
|
||||||
@@ -377,8 +35,6 @@ func RemoveDuplicate(old []string) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数
|
|
||||||
|
|
||||||
// joinStrings 连接字符串切片
|
// joinStrings 连接字符串切片
|
||||||
func joinStrings(slice []string, sep string) string {
|
func joinStrings(slice []string, sep string) string {
|
||||||
return strings.Join(slice, sep)
|
return strings.Join(slice, sep)
|
||||||
@@ -396,14 +52,8 @@ func joinInts(slice []int, sep string) string {
|
|||||||
return strings.Join(strs, sep)
|
return strings.Join(strs, sep)
|
||||||
}
|
}
|
||||||
|
|
||||||
// showParseSummary 显示解析结果摘要(已精简,不再输出冗余信息)
|
// logLevelMap 日志级别字符串到级别的映射
|
||||||
func showParseSummary(config *parsers.ParsedConfig) {
|
|
||||||
// 不再输出开局配置信息,减少干扰
|
|
||||||
}
|
|
||||||
|
|
||||||
// logLevelMap 日志级别字符串到级别的映射(支持新旧格式)
|
|
||||||
var logLevelMap = map[string]logging.LogLevel{
|
var logLevelMap = map[string]logging.LogLevel{
|
||||||
// 新格式(小写)
|
|
||||||
LogLevelAll: logging.LevelAll,
|
LogLevelAll: logging.LevelAll,
|
||||||
LogLevelError: logging.LevelError,
|
LogLevelError: logging.LevelError,
|
||||||
LogLevelBase: logging.LevelBase,
|
LogLevelBase: logging.LevelBase,
|
||||||
@@ -426,16 +76,14 @@ func applyLogLevel() {
|
|||||||
fv := GetFlagVars()
|
fv := GetFlagVars()
|
||||||
logLevel := fv.LogLevel
|
logLevel := fv.LogLevel
|
||||||
if logLevel == "" {
|
if logLevel == "" {
|
||||||
return // 使用默认级别
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找日志级别
|
|
||||||
level, ok := logLevelMap[logLevel]
|
level, ok := logLevelMap[logLevel]
|
||||||
if !ok {
|
if !ok {
|
||||||
return // 无效的级别,保持默认
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新全局日志管理器的级别
|
|
||||||
if globalLogger != nil {
|
if globalLogger != nil {
|
||||||
config := &logging.LoggerConfig{
|
config := &logging.LoggerConfig{
|
||||||
Level: level,
|
Level: level,
|
||||||
@@ -447,12 +95,7 @@ func applyLogLevel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newLogger := logging.NewLogger(config)
|
newLogger := logging.NewLogger(config)
|
||||||
|
|
||||||
// 设置协调输出函数,使用LogWithProgress
|
|
||||||
newLogger.SetCoordinatedOutput(LogWithProgress)
|
newLogger.SetCoordinatedOutput(LogWithProgress)
|
||||||
|
|
||||||
// 更新全局日志管理器
|
|
||||||
globalLogger = newLogger
|
globalLogger = newLogger
|
||||||
// status变量已移除,如需获取状态请直接调用newLogger.GetScanStatus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-305
@@ -2,346 +2,52 @@ package parsers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
constants.go - 解析器系统常量定义
|
constants.go - 核心解析器常量
|
||||||
|
|
||||||
统一管理common/parsers包中的所有常量,便于查看和编辑。
|
精简后只保留 parsers.go 所需的常量。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 默认解析器选项常量 (从Types.go迁移)
|
// 端口常量
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultEnableConcurrency 解析器默认启用并发
|
// MinPort 最小端口号
|
||||||
DefaultEnableConcurrency = true
|
|
||||||
// DefaultMaxWorkers 默认最大工作线程数
|
|
||||||
DefaultMaxWorkers = 4
|
|
||||||
// DefaultTimeout 默认超时时间
|
|
||||||
DefaultTimeout = 30 * time.Second
|
|
||||||
// DefaultEnableValidation 默认启用验证
|
|
||||||
DefaultEnableValidation = true
|
|
||||||
// DefaultIgnoreErrors 默认不忽略错误
|
|
||||||
DefaultIgnoreErrors = false
|
|
||||||
// DefaultFileMaxSize 默认文件最大大小100MB
|
|
||||||
DefaultFileMaxSize = 100 * 1024 * 1024
|
|
||||||
// DefaultMaxTargets 默认最大目标数量10K
|
|
||||||
DefaultMaxTargets = 10000
|
|
||||||
)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 文件读取器常量 (从FileReader.go迁移)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultMaxCacheSize 默认最大缓存大小
|
|
||||||
DefaultMaxCacheSize = 10
|
|
||||||
// DefaultEnableCache 默认启用缓存
|
|
||||||
DefaultEnableCache = true
|
|
||||||
// DefaultFileReaderMaxFileSize 文件读取器默认最大文件大小50MB
|
|
||||||
DefaultFileReaderMaxFileSize = 50 * 1024 * 1024
|
|
||||||
// DefaultFileReaderTimeout 文件读取器默认超时时间
|
|
||||||
DefaultFileReaderTimeout = 30 * time.Second
|
|
||||||
// DefaultFileReaderEnableValidation 文件读取器默认启用验证
|
|
||||||
DefaultFileReaderEnableValidation = true
|
|
||||||
// DefaultTrimSpace 默认去除空格
|
|
||||||
DefaultTrimSpace = true
|
|
||||||
// DefaultSkipEmpty 默认跳过空行
|
|
||||||
DefaultSkipEmpty = true
|
|
||||||
// DefaultSkipComments 默认跳过注释
|
|
||||||
DefaultSkipComments = true
|
|
||||||
|
|
||||||
// MaxLineLength 单行最大字符数
|
|
||||||
MaxLineLength = 1000
|
|
||||||
// MaxValidRune 最小有效字符ASCII值
|
|
||||||
MaxValidRune = 32
|
|
||||||
// TabRune Tab字符
|
|
||||||
TabRune = 9
|
|
||||||
// NewlineRune 换行符
|
|
||||||
NewlineRune = 10
|
|
||||||
// CarriageReturnRune 回车符
|
|
||||||
CarriageReturnRune = 13
|
|
||||||
// CommentPrefix 注释前缀
|
|
||||||
CommentPrefix = "#"
|
|
||||||
)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 凭据解析器常量 (从CredentialParser.go迁移)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultMaxUsernameLength 凭据验证限制 - 默认最大用户名长度
|
|
||||||
DefaultMaxUsernameLength = 64
|
|
||||||
// DefaultMaxPasswordLength 默认最大密码长度
|
|
||||||
DefaultMaxPasswordLength = 128
|
|
||||||
// DefaultAllowEmptyPasswords 默认允许空密码
|
|
||||||
DefaultAllowEmptyPasswords = true
|
|
||||||
// DefaultValidateHashes 默认验证哈希
|
|
||||||
DefaultValidateHashes = true
|
|
||||||
// DefaultDeduplicateUsers 默认去重用户
|
|
||||||
DefaultDeduplicateUsers = true
|
|
||||||
// DefaultDeduplicatePasswords 默认去重密码
|
|
||||||
DefaultDeduplicatePasswords = true
|
|
||||||
|
|
||||||
// HashRegexPattern MD5哈希正则表达式
|
|
||||||
HashRegexPattern = `^[a-fA-F0-9]{32}$`
|
|
||||||
// HashValidationLength 有效哈希长度
|
|
||||||
HashValidationLength = 32
|
|
||||||
// InvalidUsernameChars 无效用户名字符
|
|
||||||
InvalidUsernameChars = "\r\n\t"
|
|
||||||
)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 网络解析器常量 (从NetworkParser.go迁移)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultValidateProxies 网络配置默认值 - 默认验证代理
|
|
||||||
DefaultValidateProxies = true
|
|
||||||
// DefaultAllowInsecure 默认不允许不安全连接
|
|
||||||
DefaultAllowInsecure = false
|
|
||||||
// DefaultNetworkTimeout 默认网络超时时间
|
|
||||||
DefaultNetworkTimeout = 30 * time.Second
|
|
||||||
// DefaultWebTimeout 默认Web超时时间
|
|
||||||
DefaultWebTimeout = 10 * time.Second
|
|
||||||
// DefaultUserAgent 默认用户代理字符串
|
|
||||||
DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
|
||||||
|
|
||||||
// MaxTimeoutSeconds 超时限制 - 最大超时5分钟
|
|
||||||
MaxTimeoutSeconds = 300
|
|
||||||
// MaxWebTimeoutSeconds 最大Web超时2分钟
|
|
||||||
MaxWebTimeoutSeconds = 120
|
|
||||||
|
|
||||||
// MaxUserAgentLength 字符串长度限制 - 最大用户代理长度
|
|
||||||
MaxUserAgentLength = 512
|
|
||||||
// MaxCookieLength 最大Cookie长度
|
|
||||||
MaxCookieLength = 4096
|
|
||||||
|
|
||||||
// ProxyShortcut1 代理快捷配置 - 快捷方式1
|
|
||||||
ProxyShortcut1 = "1"
|
|
||||||
// ProxyShortcut2 快捷方式2
|
|
||||||
ProxyShortcut2 = "2"
|
|
||||||
// ProxyShortcutHTTP 快捷方式HTTP代理地址
|
|
||||||
ProxyShortcutHTTP = "http://127.0.0.1:8080"
|
|
||||||
// ProxyShortcutSOCKS5 快捷方式SOCKS5代理地址
|
|
||||||
ProxyShortcutSOCKS5 = "socks5://127.0.0.1:1080"
|
|
||||||
|
|
||||||
// ProtocolHTTP 协议支持 - HTTP协议
|
|
||||||
ProtocolHTTP = "http"
|
|
||||||
// ProtocolHTTPS HTTPS协议
|
|
||||||
ProtocolHTTPS = "https"
|
|
||||||
// ProtocolSOCKS5 SOCKS5协议
|
|
||||||
ProtocolSOCKS5 = "socks5"
|
|
||||||
// ProtocolPrefix 协议前缀分隔符
|
|
||||||
ProtocolPrefix = "://"
|
|
||||||
// SOCKS5Prefix SOCKS5协议前缀
|
|
||||||
SOCKS5Prefix = "socks5://"
|
|
||||||
// HTTPPrefix HTTP协议前缀
|
|
||||||
HTTPPrefix = "http://"
|
|
||||||
|
|
||||||
// MinPort 端口范围 - 最小端口号
|
|
||||||
MinPort = 1
|
MinPort = 1
|
||||||
// MaxPort 最大端口号
|
// MaxPort 最大端口号
|
||||||
MaxPort = 65535
|
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 (
|
const (
|
||||||
// DefaultTargetMaxTargets 目标解析器默认配置 - 默认最大目标数量
|
// SimpleMaxHosts 最大主机数量限制
|
||||||
DefaultTargetMaxTargets = 10000
|
|
||||||
// DefaultMaxPortRange 默认最大端口范围(支持全端口扫描)
|
|
||||||
DefaultMaxPortRange = 65535
|
|
||||||
// DefaultAllowPrivateIPs 默认允许私有IP
|
|
||||||
DefaultAllowPrivateIPs = true
|
|
||||||
// DefaultAllowLoopback 默认允许回环地址
|
|
||||||
DefaultAllowLoopback = true
|
|
||||||
// DefaultValidateURLs 默认验证URL
|
|
||||||
DefaultValidateURLs = true
|
|
||||||
// DefaultResolveDomains 默认解析域名
|
|
||||||
DefaultResolveDomains = false
|
|
||||||
|
|
||||||
// IPv4RegexPattern 正则表达式模式 - IPv4地址正则
|
|
||||||
IPv4RegexPattern = `^(\d{1,3}\.){3}\d{1,3}$`
|
|
||||||
// PortRangeRegexPattern 端口范围正则
|
|
||||||
PortRangeRegexPattern = `^(\d+)(-(\d+))?$`
|
|
||||||
// URLValidationRegexPattern URL验证正则
|
|
||||||
URLValidationRegexPattern = `^https?://[^\s]+$`
|
|
||||||
// DomainRegexPattern 域名正则
|
|
||||||
DomainRegexPattern = `^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`
|
|
||||||
// CookieRegexPattern Cookie正则
|
|
||||||
CookieRegexPattern = `^[^=;\s]+(=[^;\s]*)?(\s*;\s*[^=;\s]+(=[^;\s]*)?)*$`
|
|
||||||
|
|
||||||
// MaxIPv4OctetValue IP地址限制 - IPv4八位组最大值
|
|
||||||
MaxIPv4OctetValue = 255
|
|
||||||
// IPv4OctetCount IPv4八位组数量
|
|
||||||
IPv4OctetCount = 4
|
|
||||||
// MaxDomainLength 域名最大长度
|
|
||||||
MaxDomainLength = 253
|
|
||||||
|
|
||||||
// PrivateNetwork192 CIDR网段简写 - 192私有网络前缀
|
|
||||||
PrivateNetwork192 = "192"
|
|
||||||
// PrivateNetwork172 172私有网络前缀
|
|
||||||
PrivateNetwork172 = "172"
|
|
||||||
// PrivateNetwork10 10私有网络前缀
|
|
||||||
PrivateNetwork10 = "10"
|
|
||||||
// PrivateNetwork192CIDR 192私有网络CIDR
|
|
||||||
PrivateNetwork192CIDR = "192.168.0.0/16"
|
|
||||||
// PrivateNetwork172CIDR 172私有网络CIDR
|
|
||||||
PrivateNetwork172CIDR = "172.16.0.0/12"
|
|
||||||
// PrivateNetwork10CIDR 10私有网络CIDR
|
|
||||||
PrivateNetwork10CIDR = "10.0.0.0/8"
|
|
||||||
|
|
||||||
// Private172StartSecondOctet 私有网络范围 - 172网段起始第二段
|
|
||||||
Private172StartSecondOctet = 16
|
|
||||||
// Private172EndSecondOctet 172网段结束第二段
|
|
||||||
Private172EndSecondOctet = 31
|
|
||||||
// Private192SecondOctet 192网段第二段
|
|
||||||
Private192SecondOctet = 168
|
|
||||||
|
|
||||||
// Subnet8SamplingStep /8网段采样配置 - 采样步长
|
|
||||||
Subnet8SamplingStep = 32
|
|
||||||
// Subnet8ThirdOctetStep 第三段步长
|
|
||||||
Subnet8ThirdOctetStep = 10
|
|
||||||
|
|
||||||
// IPFirstOctetShift IP地址计算位移 - 第一段位移
|
|
||||||
IPFirstOctetShift = 24
|
|
||||||
// IPSecondOctetShift 第二段位移
|
|
||||||
IPSecondOctetShift = 16
|
|
||||||
// IPThirdOctetShift 第三段位移
|
|
||||||
IPThirdOctetShift = 8
|
|
||||||
// IPOctetMask 八位组掩码
|
|
||||||
IPOctetMask = 0xFF
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetCommonSecondOctets 获取常用第二段IP
|
|
||||||
func GetCommonSecondOctets() []int {
|
|
||||||
return []int{0, 1, 2, 10, 100, 200, 254}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 简化解析器常量 (从Simple.go迁移)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SimpleMaxHosts 端口和主机限制 - 最大主机数量
|
|
||||||
SimpleMaxHosts = 10000
|
SimpleMaxHosts = 10000
|
||||||
|
|
||||||
// DefaultGatewayLastOctet 网段简写展开 - 默认网关最后一段
|
|
||||||
DefaultGatewayLastOctet = 1
|
|
||||||
// RouterSwitchLastOctet 路由器/交换机最后一段
|
|
||||||
RouterSwitchLastOctet = 254
|
|
||||||
// SamplingMinHost 采样最小主机号
|
|
||||||
SamplingMinHost = 2
|
|
||||||
// SamplingMaxHost 采样最大主机号
|
|
||||||
SamplingMaxHost = 253
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 端口组定义已迁移到 common/config/constants.go
|
|
||||||
// 此处保留引用函数以保持向后兼容
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 验证解析器常量 (从ValidationParser.go迁移)
|
// 哈希验证常量
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultMaxErrorCount 验证解析器默认配置 - 默认最大错误数
|
// HashRegexPattern MD5哈希正则表达式(32位十六进制)
|
||||||
DefaultMaxErrorCount = 100
|
HashRegexPattern = `^[a-fA-F0-9]{32}$`
|
||||||
// DefaultStrictMode 默认严格模式
|
|
||||||
DefaultStrictMode = false
|
|
||||||
// DefaultAllowEmpty 默认允许空值
|
|
||||||
DefaultAllowEmpty = true
|
|
||||||
// DefaultCheckConflicts 默认检查冲突
|
|
||||||
DefaultCheckConflicts = true
|
|
||||||
// DefaultValidateTargets 默认验证目标
|
|
||||||
DefaultValidateTargets = true
|
|
||||||
// DefaultValidateNetwork 默认验证网络配置
|
|
||||||
DefaultValidateNetwork = true
|
|
||||||
|
|
||||||
// MaxTargetsThreshold 性能警告阈值 - 最大目标数量阈值
|
|
||||||
MaxTargetsThreshold = 100000
|
|
||||||
// PortCountWarningThreshold 端口数量警告阈值(超过此值时警告)
|
|
||||||
PortCountWarningThreshold = 5000
|
|
||||||
// MinTimeoutThreshold 最小超时阈值
|
|
||||||
MinTimeoutThreshold = 1 * time.Second
|
|
||||||
// MaxTimeoutThreshold 最大超时阈值
|
|
||||||
MaxTimeoutThreshold = 60 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 错误类型常量
|
// 预编译正则表达式
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrorTypeInputError 解析错误类型 - 输入错误
|
|
||||||
ErrorTypeInputError = "INPUT_ERROR"
|
|
||||||
// ErrorTypeFileError 文件错误
|
|
||||||
ErrorTypeFileError = "FILE_ERROR"
|
|
||||||
// ErrorTypeTimeout 超时错误
|
|
||||||
ErrorTypeTimeout = "TIMEOUT"
|
|
||||||
// ErrorTypeReadError 读取错误
|
|
||||||
ErrorTypeReadError = "READ_ERROR"
|
|
||||||
// ErrorTypeUsernameError 用户名错误
|
|
||||||
ErrorTypeUsernameError = "USERNAME_ERROR"
|
|
||||||
// ErrorTypePasswordError 密码错误
|
|
||||||
ErrorTypePasswordError = "PASSWORD_ERROR"
|
|
||||||
// ErrorTypeHashError 哈希错误
|
|
||||||
ErrorTypeHashError = "HASH_ERROR"
|
|
||||||
// ErrorTypeProxyError 代理错误
|
|
||||||
ErrorTypeProxyError = "PROXY_ERROR"
|
|
||||||
// ErrorTypeUserAgentError 用户代理错误
|
|
||||||
ErrorTypeUserAgentError = "USERAGENT_ERROR"
|
|
||||||
// ErrorTypeCookieError Cookie错误
|
|
||||||
ErrorTypeCookieError = "COOKIE_ERROR"
|
|
||||||
// ErrorTypeHostError 主机错误
|
|
||||||
ErrorTypeHostError = "HOST_ERROR"
|
|
||||||
// ErrorTypePortError 端口错误
|
|
||||||
ErrorTypePortError = "PORT_ERROR"
|
|
||||||
// ErrorTypeExcludePortError 排除端口错误
|
|
||||||
ErrorTypeExcludePortError = "EXCLUDE_PORT_ERROR"
|
|
||||||
)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 编译时正则表达式
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// CompiledHashRegex 预编译的正则表达式,提高性能 - MD5哈希正则
|
// CompiledHashRegex 预编译的MD5哈希正则
|
||||||
CompiledHashRegex *regexp.Regexp
|
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() {
|
func init() {
|
||||||
CompiledHashRegex = regexp.MustCompile(HashRegexPattern)
|
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