mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 04:31:52 +08:00
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
1323 lines
30 KiB
Go
1323 lines
30 KiB
Go
package parsers
|
||
|
||
import (
|
||
"net"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
// =============================================================================
|
||
// 测试辅助函数
|
||
// =============================================================================
|
||
|
||
// createTestFile 创建临时测试文件
|
||
func createTestFile(t *testing.T, content string) string {
|
||
t.Helper()
|
||
|
||
tmpDir := t.TempDir()
|
||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||
|
||
if err := os.WriteFile(tmpFile, []byte(content), 0600); err != nil {
|
||
t.Fatalf("创建测试文件失败: %v", err)
|
||
}
|
||
|
||
return tmpFile
|
||
}
|
||
|
||
// =============================================================================
|
||
// TargetParser 构造函数测试
|
||
// =============================================================================
|
||
|
||
func TestNewTargetParser(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
options *TargetParserOptions
|
||
wantNil bool
|
||
}{
|
||
{
|
||
name: "使用默认选项",
|
||
options: nil,
|
||
wantNil: false,
|
||
},
|
||
{
|
||
name: "使用自定义选项",
|
||
options: &TargetParserOptions{
|
||
MaxTargets: 5000,
|
||
MaxPortRange: 500,
|
||
AllowPrivateIPs: false,
|
||
},
|
||
wantNil: false,
|
||
},
|
||
}
|
||
|
||
fileReader := NewFileReader(nil)
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
parser := NewTargetParser(fileReader, tt.options)
|
||
|
||
if tt.wantNil && parser != nil {
|
||
t.Error("期望parser为nil,但不是")
|
||
}
|
||
if !tt.wantNil && parser == nil {
|
||
t.Error("期望parser不为nil,但是nil")
|
||
}
|
||
|
||
if parser != nil {
|
||
if parser.options == nil {
|
||
t.Error("parser.options为nil")
|
||
}
|
||
if parser.ipRegex == nil {
|
||
t.Error("parser.ipRegex为nil")
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// Parse 主函数测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_Parse(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
input *TargetInput
|
||
wantSuccess bool
|
||
wantHosts int
|
||
wantPorts int
|
||
wantURLs int
|
||
wantError bool
|
||
}{
|
||
{
|
||
name: "空输入",
|
||
input: nil,
|
||
wantSuccess: false,
|
||
wantError: true,
|
||
},
|
||
{
|
||
name: "单个IP",
|
||
input: &TargetInput{
|
||
Host: "192.168.1.1",
|
||
},
|
||
wantSuccess: true,
|
||
wantHosts: 1,
|
||
},
|
||
{
|
||
name: "多个IP(逗号分隔)",
|
||
input: &TargetInput{
|
||
Host: "192.168.1.1,192.168.1.2,192.168.1.3",
|
||
},
|
||
wantSuccess: true,
|
||
wantHosts: 3,
|
||
},
|
||
{
|
||
name: "单个端口",
|
||
input: &TargetInput{
|
||
Ports: "80",
|
||
},
|
||
wantSuccess: true,
|
||
wantPorts: 1,
|
||
},
|
||
{
|
||
name: "端口范围",
|
||
input: &TargetInput{
|
||
Ports: "80-85",
|
||
},
|
||
wantSuccess: true,
|
||
wantPorts: 6,
|
||
},
|
||
{
|
||
name: "URL输入",
|
||
input: &TargetInput{
|
||
TargetURL: "http://example.com",
|
||
},
|
||
wantSuccess: true,
|
||
wantURLs: 1,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result, err := parser.Parse(tt.input, nil)
|
||
|
||
if tt.wantError {
|
||
if err == nil {
|
||
t.Error("期望错误,但没有错误")
|
||
}
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if result == nil {
|
||
t.Fatal("result为nil")
|
||
}
|
||
|
||
if result.Success != tt.wantSuccess {
|
||
t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
|
||
}
|
||
|
||
if tt.wantHosts > 0 && len(result.Config.Targets.Hosts) != tt.wantHosts {
|
||
t.Errorf("Hosts数量 = %d, want %d", len(result.Config.Targets.Hosts), tt.wantHosts)
|
||
}
|
||
|
||
if tt.wantPorts > 0 && len(result.Config.Targets.Ports) != tt.wantPorts {
|
||
t.Errorf("Ports数量 = %d, want %d", len(result.Config.Targets.Ports), tt.wantPorts)
|
||
}
|
||
|
||
if tt.wantURLs > 0 && len(result.Config.Targets.URLs) != tt.wantURLs {
|
||
t.Errorf("URLs数量 = %d, want %d", len(result.Config.Targets.URLs), tt.wantURLs)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// parseHostList 测试 - CIDR解析
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ParseHostList_CIDR(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
input string
|
||
wantCount int
|
||
wantError bool
|
||
}{
|
||
{
|
||
name: "标准/24 CIDR",
|
||
input: "192.168.1.0/24",
|
||
wantCount: 254, // 排除网络地址和广播地址
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "标准/30 CIDR",
|
||
input: "192.168.1.0/30",
|
||
wantCount: 2, // 4个地址减去网络和广播
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "192网段简写",
|
||
input: "192",
|
||
wantCount: 65534, // /16网段
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "172网段简写",
|
||
input: "172",
|
||
wantCount: 1048574, // /12网段(被MaxTargets限制)
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "10网段简写",
|
||
input: "10",
|
||
wantCount: 16777214, // /8网段(被MaxTargets限制)
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "无效CIDR",
|
||
input: "192.168.1.0/33",
|
||
wantCount: 0,
|
||
wantError: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hosts, err := parser.parseHostList(tt.input)
|
||
|
||
if tt.wantError {
|
||
if err == nil {
|
||
t.Error("期望错误,但没有错误")
|
||
}
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
// 由于MaxTargets限制,实际数量可能小于预期
|
||
if len(hosts) > parser.options.MaxTargets {
|
||
t.Errorf("主机数量 %d 超过MaxTargets %d", len(hosts), parser.options.MaxTargets)
|
||
}
|
||
|
||
// 对于小网段,检查准确数量
|
||
if tt.wantCount < parser.options.MaxTargets && len(hosts) != tt.wantCount {
|
||
t.Errorf("主机数量 = %d, want %d", len(hosts), tt.wantCount)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// parseHostList 测试 - IP范围解析
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ParseHostList_IPRange(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
input string
|
||
wantCount int
|
||
wantFirst string
|
||
wantLast string
|
||
wantError bool
|
||
}{
|
||
{
|
||
name: "简写范围(同网段)",
|
||
input: "192.168.1.1-10",
|
||
wantCount: 10,
|
||
wantFirst: "192.168.1.1",
|
||
wantLast: "192.168.1.10",
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "简写范围(单个IP)",
|
||
input: "192.168.1.5-5",
|
||
wantCount: 1,
|
||
wantFirst: "192.168.1.5",
|
||
wantLast: "192.168.1.5",
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "完整范围",
|
||
input: "192.168.1.1-192.168.1.5",
|
||
wantCount: 5,
|
||
wantFirst: "192.168.1.1",
|
||
wantLast: "192.168.1.5",
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "反向范围(错误:起始大于结束)",
|
||
input: "192.168.1.10-192.168.1.5",
|
||
wantCount: 0,
|
||
wantError: true,
|
||
},
|
||
{
|
||
name: "无效范围(格式错误)",
|
||
input: "192.168.1.1-300",
|
||
wantCount: 0,
|
||
wantError: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hosts, err := parser.parseHostList(tt.input)
|
||
|
||
if tt.wantError {
|
||
if err == nil {
|
||
t.Error("期望错误,但没有错误")
|
||
}
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(hosts) != tt.wantCount {
|
||
t.Errorf("主机数量 = %d, want %d", len(hosts), tt.wantCount)
|
||
}
|
||
|
||
if len(hosts) > 0 {
|
||
if hosts[0] != tt.wantFirst {
|
||
t.Errorf("第一个主机 = %s, want %s", hosts[0], tt.wantFirst)
|
||
}
|
||
if hosts[len(hosts)-1] != tt.wantLast {
|
||
t.Errorf("最后一个主机 = %s, want %s", hosts[len(hosts)-1], tt.wantLast)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// parsePortList 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ParsePortList(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
input string
|
||
wantPorts []int
|
||
wantError bool
|
||
}{
|
||
{
|
||
name: "单个端口",
|
||
input: "80",
|
||
wantPorts: []int{80},
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "多个端口(逗号分隔)",
|
||
input: "80,443,8080",
|
||
wantPorts: []int{80, 443, 8080},
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "端口范围",
|
||
input: "8000-8005",
|
||
wantPorts: []int{8000, 8001, 8002, 8003, 8004, 8005},
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "混合格式",
|
||
input: "80,443,8000-8002",
|
||
wantPorts: []int{80, 443, 8000, 8001, 8002},
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "无效端口号",
|
||
input: "99999",
|
||
wantPorts: nil,
|
||
wantError: true,
|
||
},
|
||
{
|
||
name: "无效端口范围",
|
||
input: "abc-def",
|
||
wantPorts: nil,
|
||
wantError: true,
|
||
},
|
||
{
|
||
name: "端口范围过大",
|
||
input: "1-70000",
|
||
wantPorts: nil,
|
||
wantError: true, // 超过MaxPortRange限制(65535)
|
||
},
|
||
{
|
||
name: "空输入",
|
||
input: "",
|
||
wantPorts: nil,
|
||
wantError: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ports, err := parser.parsePortList(tt.input)
|
||
|
||
if tt.wantError {
|
||
if err == nil {
|
||
t.Error("期望错误,但没有错误")
|
||
}
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(ports) != len(tt.wantPorts) {
|
||
t.Errorf("端口数量 = %d, want %d", len(ports), len(tt.wantPorts))
|
||
return
|
||
}
|
||
|
||
for i, port := range ports {
|
||
if port != tt.wantPorts[i] {
|
||
t.Errorf("端口[%d] = %d, want %d", i, port, tt.wantPorts[i])
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// validateHost 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ValidateHost(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
host string
|
||
options *TargetParserOptions
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "有效IPv4地址",
|
||
host: "192.168.1.1",
|
||
options: nil,
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "有效域名",
|
||
host: "example.com",
|
||
options: nil,
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "有效子域名",
|
||
host: "www.example.com",
|
||
options: nil,
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "空主机",
|
||
host: "",
|
||
options: nil,
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "私有IP(不允许)",
|
||
host: "192.168.1.1",
|
||
options: &TargetParserOptions{
|
||
AllowPrivateIPs: false,
|
||
},
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "回环地址(不允许)",
|
||
host: "127.0.0.1",
|
||
options: &TargetParserOptions{
|
||
AllowLoopback: false,
|
||
},
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "回环地址(允许)",
|
||
host: "127.0.0.1",
|
||
options: &TargetParserOptions{
|
||
AllowLoopback: true,
|
||
},
|
||
wantValid: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
parser := NewTargetParser(fileReader, tt.options)
|
||
|
||
valid, err := parser.validateHost(tt.host)
|
||
|
||
if valid != tt.wantValid {
|
||
t.Errorf("validateHost() = %v (err: %v), want %v", valid, err, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// validateURL 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ValidateURL(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
url string
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "有效HTTP URL",
|
||
url: "http://example.com",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "有效HTTPS URL",
|
||
url: "https://example.com",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "带路径的URL",
|
||
url: "http://example.com/path/to/resource",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "带端口的URL",
|
||
url: "http://example.com:8080",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "带查询参数的URL",
|
||
url: "http://example.com?key=value",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "空URL",
|
||
url: "",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "无协议的URL",
|
||
url: "example.com",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "FTP协议(不支持)",
|
||
url: "ftp://example.com",
|
||
wantValid: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
valid, err := parser.validateURL(tt.url)
|
||
|
||
if valid != tt.wantValid {
|
||
t.Errorf("validateURL() = %v (err: %v), want %v", valid, err, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// validateHostPort 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ValidateHostPort(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
hostPort string
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "有效IP:端口",
|
||
hostPort: "192.168.1.1:80",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "有效域名:端口",
|
||
hostPort: "example.com:443",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "缺少端口",
|
||
hostPort: "192.168.1.1",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "无效端口号",
|
||
hostPort: "192.168.1.1:99999",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "端口号为0",
|
||
hostPort: "192.168.1.1:0",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "空字符串",
|
||
hostPort: "",
|
||
wantValid: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
valid, err := parser.validateHostPort(tt.hostPort)
|
||
|
||
if valid != tt.wantValid {
|
||
t.Errorf("validateHostPort() = %v (err: %v), want %v", valid, err, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// isPrivateIP 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_IsPrivateIP(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
ip string
|
||
wantPrivate bool
|
||
}{
|
||
{
|
||
name: "10.x.x.x网段",
|
||
ip: "10.0.0.1",
|
||
wantPrivate: true,
|
||
},
|
||
{
|
||
name: "172.16.x.x网段(起始)",
|
||
ip: "172.16.0.1",
|
||
wantPrivate: true,
|
||
},
|
||
{
|
||
name: "172.31.x.x网段(结束)",
|
||
ip: "172.31.255.254",
|
||
wantPrivate: true,
|
||
},
|
||
{
|
||
name: "172.15.x.x(不在范围内)",
|
||
ip: "172.15.0.1",
|
||
wantPrivate: false,
|
||
},
|
||
{
|
||
name: "172.32.x.x(不在范围内)",
|
||
ip: "172.32.0.1",
|
||
wantPrivate: false,
|
||
},
|
||
{
|
||
name: "192.168.x.x网段",
|
||
ip: "192.168.1.1",
|
||
wantPrivate: true,
|
||
},
|
||
{
|
||
name: "公网IP",
|
||
ip: "8.8.8.8",
|
||
wantPrivate: false,
|
||
},
|
||
{
|
||
name: "公网IP(1.1.1.1)",
|
||
ip: "1.1.1.1",
|
||
wantPrivate: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ip := net.ParseIP(tt.ip)
|
||
if ip == nil {
|
||
t.Fatalf("无效IP地址: %s", tt.ip)
|
||
}
|
||
|
||
isPrivate := parser.isPrivateIP(ip)
|
||
|
||
if isPrivate != tt.wantPrivate {
|
||
t.Errorf("isPrivateIP(%s) = %v, want %v", tt.ip, isPrivate, tt.wantPrivate)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// 文件读取测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ParseFromFile(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
fileContent string
|
||
wantHosts int
|
||
}{
|
||
{
|
||
name: "主机列表文件",
|
||
fileContent: `192.168.1.1
|
||
192.168.1.2
|
||
192.168.1.3`,
|
||
wantHosts: 3,
|
||
},
|
||
{
|
||
name: "带注释的主机列表",
|
||
fileContent: `# 这是注释
|
||
192.168.1.1
|
||
# 另一个注释
|
||
192.168.1.2`,
|
||
wantHosts: 2,
|
||
},
|
||
{
|
||
name: "带空行的主机列表",
|
||
fileContent: `192.168.1.1
|
||
|
||
192.168.1.2
|
||
|
||
192.168.1.3`,
|
||
wantHosts: 3,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
tmpFile := createTestFile(t, tt.fileContent)
|
||
|
||
input := &TargetInput{
|
||
HostsFile: tmpFile,
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if !result.Success {
|
||
t.Errorf("解析失败,错误: %v", result.Errors)
|
||
}
|
||
|
||
if len(result.Config.Targets.Hosts) != tt.wantHosts {
|
||
t.Errorf("主机数量 = %d, want %d", len(result.Config.Targets.Hosts), tt.wantHosts)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 端口文件测试
|
||
t.Run("端口列表文件", func(t *testing.T) {
|
||
portsFile := createTestFile(t, `80
|
||
443
|
||
8080
|
||
# 注释行
|
||
8443`)
|
||
|
||
input := &TargetInput{
|
||
PortsFile: portsFile,
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if !result.Success {
|
||
t.Errorf("解析失败,错误: %v", result.Errors)
|
||
}
|
||
|
||
// 应解析出4个端口(注释被跳过)
|
||
if len(result.Config.Targets.Ports) != 4 {
|
||
t.Errorf("端口数量 = %d, want 4", len(result.Config.Targets.Ports))
|
||
}
|
||
})
|
||
|
||
// URL文件测试
|
||
t.Run("URL列表文件", func(t *testing.T) {
|
||
urlsFile := createTestFile(t, `http://example1.com
|
||
https://example2.com
|
||
# 注释
|
||
http://example3.com:8080
|
||
`)
|
||
|
||
input := &TargetInput{
|
||
URLsFile: urlsFile,
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if !result.Success {
|
||
t.Errorf("解析失败,错误: %v", result.Errors)
|
||
}
|
||
|
||
// 应解析出3个URL
|
||
if len(result.Config.Targets.URLs) != 3 {
|
||
t.Errorf("URL数量 = %d, want 3", len(result.Config.Targets.URLs))
|
||
}
|
||
})
|
||
|
||
// 排除主机文件测试
|
||
t.Run("排除主机列表", func(t *testing.T) {
|
||
hostsFile := createTestFile(t, "192.168.1.1\n192.168.1.2\n192.168.1.3\n192.168.1.4\n192.168.1.5")
|
||
excludeFile := createTestFile(t, "192.168.1.2\n192.168.1.4")
|
||
|
||
input := &TargetInput{
|
||
HostsFile: hostsFile,
|
||
ExcludeHostsFile: excludeFile,
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
// 5个主机 - 2个排除 = 3个
|
||
if len(result.Config.Targets.Hosts) != 3 {
|
||
t.Errorf("主机数量 = %d, want 3", len(result.Config.Targets.Hosts))
|
||
}
|
||
})
|
||
|
||
// 混合输入:命令行+文件
|
||
t.Run("混合输入(命令行+文件)", func(t *testing.T) {
|
||
hostsFile := createTestFile(t, "192.168.1.10\n192.168.1.11")
|
||
portsFile := createTestFile(t, "8080\n8443")
|
||
|
||
input := &TargetInput{
|
||
Host: "192.168.1.1,192.168.1.2",
|
||
HostsFile: hostsFile,
|
||
Ports: "80,443",
|
||
PortsFile: portsFile,
|
||
AddPorts: "9000,9001",
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
// 主机: 2(命令行) + 2(文件) = 4
|
||
if len(result.Config.Targets.Hosts) != 4 {
|
||
t.Errorf("主机数量 = %d, want 4", len(result.Config.Targets.Hosts))
|
||
}
|
||
|
||
// 端口: 2(命令行) + 2(文件) + 2(AddPorts) = 6
|
||
if len(result.Config.Targets.Ports) != 6 {
|
||
t.Errorf("端口数量 = %d, want 6", len(result.Config.Targets.Ports))
|
||
}
|
||
})
|
||
|
||
// 文件中的无效行测试
|
||
t.Run("文件包含无效行(应产生警告)", func(t *testing.T) {
|
||
hostsFile := createTestFile(t, `192.168.1.1
|
||
invalid!!!host!!!format
|
||
192.168.1.2
|
||
999.999.999.999
|
||
192.168.1.3`)
|
||
|
||
input := &TargetInput{
|
||
HostsFile: hostsFile,
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
// 999.999.999.999 应该被过滤掉,因为是无效IP
|
||
// invalid!!!host!!!format 可能被识别为域名(取决于域名验证规则)
|
||
// 至少应该有3个有效主机
|
||
if len(result.Config.Targets.Hosts) < 3 {
|
||
t.Errorf("主机数量 = %d, 应该至少有3个", len(result.Config.Targets.Hosts))
|
||
}
|
||
})
|
||
}
|
||
|
||
// =============================================================================
|
||
// excludeHosts 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ExcludeHosts(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
hosts := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}
|
||
excludeList := []string{"192.168.1.2", "192.168.1.4"}
|
||
|
||
result := parser.excludeHosts(hosts, excludeList)
|
||
|
||
expected := []string{"192.168.1.1", "192.168.1.3"}
|
||
|
||
if len(result) != len(expected) {
|
||
t.Errorf("结果数量 = %d, want %d", len(result), len(expected))
|
||
}
|
||
|
||
for i, host := range result {
|
||
if host != expected[i] {
|
||
t.Errorf("主机[%d] = %s, want %s", i, host, expected[i])
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// removeDuplicateStrings 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_RemoveDuplicateStrings(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
input := []string{"a", "b", "a", "c", "b", "d"}
|
||
expected := []string{"a", "b", "c", "d"}
|
||
|
||
result := parser.removeDuplicateStrings(input)
|
||
|
||
if len(result) != len(expected) {
|
||
t.Errorf("结果数量 = %d, want %d", len(result), len(expected))
|
||
}
|
||
|
||
// 检查所有预期元素都存在(顺序可能不同)
|
||
resultMap := make(map[string]bool)
|
||
for _, item := range result {
|
||
resultMap[item] = true
|
||
}
|
||
|
||
for _, item := range expected {
|
||
if !resultMap[item] {
|
||
t.Errorf("缺少元素: %s", item)
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// removeDuplicatePorts 测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_RemoveDuplicatePorts(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
input := []int{80, 443, 80, 8080, 443, 22}
|
||
expected := []int{80, 443, 8080, 22}
|
||
|
||
result := parser.removeDuplicatePorts(input)
|
||
|
||
if len(result) != len(expected) {
|
||
t.Errorf("结果数量 = %d, want %d", len(result), len(expected))
|
||
}
|
||
|
||
// 检查所有预期元素都存在
|
||
resultMap := make(map[int]bool)
|
||
for _, port := range result {
|
||
resultMap[port] = true
|
||
}
|
||
|
||
for _, port := range expected {
|
||
if !resultMap[port] {
|
||
t.Errorf("缺少端口: %d", port)
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// parseSubnet8 测试(/8网段采样)
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ParseSubnet8(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
input string
|
||
wantError bool
|
||
}{
|
||
{
|
||
name: "/8网段采样",
|
||
input: "10.0.0.0/8",
|
||
wantError: false,
|
||
},
|
||
{
|
||
name: "/8网段采样(另一个例子)",
|
||
input: "172.0.0.0/8",
|
||
wantError: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hosts, err := parser.parseHostList(tt.input)
|
||
|
||
if tt.wantError {
|
||
if err == nil {
|
||
t.Error("期望错误,但没有错误")
|
||
}
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
// /8网段会被限制到MaxTargets
|
||
if len(hosts) == 0 {
|
||
t.Error("采样结果为空")
|
||
}
|
||
|
||
if len(hosts) > parser.options.MaxTargets {
|
||
t.Errorf("主机数量 %d 超过MaxTargets %d", len(hosts), parser.options.MaxTargets)
|
||
}
|
||
|
||
// 验证生成的IP格式正确
|
||
for i, host := range hosts {
|
||
if net.ParseIP(host) == nil {
|
||
t.Errorf("无效IP地址[%d]: %s", i, host)
|
||
break
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// host:port 组合测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_HostPortCombination(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
input *TargetInput
|
||
wantHostPorts int
|
||
wantHosts int
|
||
wantPorts int
|
||
}{
|
||
{
|
||
name: "单个host:port",
|
||
input: &TargetInput{
|
||
Host: "192.168.1.1:80",
|
||
},
|
||
wantHostPorts: 1,
|
||
wantHosts: 0, // host:port不会被添加到hosts
|
||
wantPorts: 0, // Ports应该为空,只扫描指定的host:port
|
||
},
|
||
{
|
||
name: "多个host:port",
|
||
input: &TargetInput{
|
||
HostPort: []string{"192.168.1.1:80", "192.168.1.2:443"},
|
||
},
|
||
wantHostPorts: 2,
|
||
wantHosts: 0,
|
||
wantPorts: 0,
|
||
},
|
||
{
|
||
name: "混合host:port和普通host",
|
||
input: &TargetInput{
|
||
Host: "192.168.1.1:80,192.168.1.2",
|
||
},
|
||
wantHostPorts: 1,
|
||
wantHosts: 1, // 192.168.1.2
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result, err := parser.Parse(tt.input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if !result.Success {
|
||
t.Errorf("解析失败,错误: %v", result.Errors)
|
||
}
|
||
|
||
if len(result.Config.Targets.HostPorts) != tt.wantHostPorts {
|
||
t.Errorf("HostPorts数量 = %d, want %d", len(result.Config.Targets.HostPorts), tt.wantHostPorts)
|
||
}
|
||
|
||
if tt.wantHosts > 0 && len(result.Config.Targets.Hosts) != tt.wantHosts {
|
||
t.Errorf("Hosts数量 = %d, want %d", len(result.Config.Targets.Hosts), tt.wantHosts)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// 边界条件测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_EdgeCases(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
t.Run("空字符串输入", func(t *testing.T) {
|
||
hosts, err := parser.parseHostList("")
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
}
|
||
if len(hosts) != 0 {
|
||
t.Errorf("空输入应返回空列表,得到 %d 个主机", len(hosts))
|
||
}
|
||
})
|
||
|
||
t.Run("只有逗号的输入", func(t *testing.T) {
|
||
hosts, err := parser.parseHostList(",,,")
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
}
|
||
if len(hosts) != 0 {
|
||
t.Errorf("只有逗号的输入应返回空列表,得到 %d 个主机", len(hosts))
|
||
}
|
||
})
|
||
|
||
t.Run("带空格的输入", func(t *testing.T) {
|
||
hosts, err := parser.parseHostList(" 192.168.1.1 , 192.168.1.2 ")
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
}
|
||
if len(hosts) != 2 {
|
||
t.Errorf("应该解析出2个主机,得到 %d 个", len(hosts))
|
||
}
|
||
})
|
||
|
||
t.Run("端口号边界值", func(t *testing.T) {
|
||
tests := []struct {
|
||
port string
|
||
wantError bool
|
||
}{
|
||
{"1", false}, // 最小端口
|
||
{"65535", false}, // 最大端口
|
||
{"0", true}, // 无效端口
|
||
{"65536", true}, // 超出范围
|
||
{"-1", true}, // 负数
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run("端口:"+tt.port, func(t *testing.T) {
|
||
_, err := parser.parsePortList(tt.port)
|
||
if tt.wantError && err == nil {
|
||
t.Error("期望错误,但没有错误")
|
||
}
|
||
if !tt.wantError && err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
|
||
t.Run("域名验证", func(t *testing.T) {
|
||
tests := []struct {
|
||
domain string
|
||
wantValid bool
|
||
}{
|
||
{"example.com", true},
|
||
{"sub.example.com", true},
|
||
{"example-with-dash.com", true},
|
||
{"123.com", true},
|
||
{"-invalid.com", false}, // 以连字符开头
|
||
{"invalid-.com", false}, // 以连字符结尾
|
||
{"too..many.dots.com", false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.domain, func(t *testing.T) {
|
||
valid := parser.isValidDomain(tt.domain)
|
||
if valid != tt.wantValid {
|
||
t.Errorf("isValidDomain(%s) = %v, want %v", tt.domain, valid, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
// =============================================================================
|
||
// 文件解析错误处理测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_FileErrorHandling(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
t.Run("不存在的文件", func(t *testing.T) {
|
||
input := &TargetInput{
|
||
HostsFile: "/nonexistent/file/path.txt",
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("Parse不应返回错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if result.Success {
|
||
t.Error("解析不应成功(文件不存在)")
|
||
}
|
||
|
||
if len(result.Errors) == 0 {
|
||
t.Error("应该有错误记录")
|
||
}
|
||
})
|
||
|
||
t.Run("空文件", func(t *testing.T) {
|
||
tmpFile := createTestFile(t, "")
|
||
|
||
input := &TargetInput{
|
||
HostsFile: tmpFile,
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Errorf("意外错误: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(result.Config.Targets.Hosts) != 0 {
|
||
t.Errorf("空文件应解析出0个主机,得到 %d 个", len(result.Config.Targets.Hosts))
|
||
}
|
||
})
|
||
}
|
||
|
||
// =============================================================================
|
||
// 复杂场景集成测试
|
||
// =============================================================================
|
||
|
||
func TestTargetParser_ComplexScenarios(t *testing.T) {
|
||
fileReader := NewFileReader(nil)
|
||
parser := NewTargetParser(fileReader, nil)
|
||
|
||
t.Run("混合输入(IP+CIDR+范围+域名)", func(t *testing.T) {
|
||
input := &TargetInput{
|
||
Host: "192.168.1.1,192.168.2.0/30,192.168.3.1-5,example.com",
|
||
Ports: "80,443,8000-8002",
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Fatalf("意外错误: %v", err)
|
||
}
|
||
|
||
if !result.Success {
|
||
t.Errorf("解析失败,错误: %v", result.Errors)
|
||
}
|
||
|
||
// 验证主机数量: 1(单IP) + 2(CIDR) + 5(范围) + 1(域名) = 9
|
||
expectedHosts := 9
|
||
if len(result.Config.Targets.Hosts) != expectedHosts {
|
||
t.Errorf("主机数量 = %d, want %d", len(result.Config.Targets.Hosts), expectedHosts)
|
||
}
|
||
|
||
// 验证端口数量: 2(单端口) + 3(范围) = 5
|
||
expectedPorts := 5
|
||
if len(result.Config.Targets.Ports) != expectedPorts {
|
||
t.Errorf("端口数量 = %d, want %d", len(result.Config.Targets.Ports), expectedPorts)
|
||
}
|
||
})
|
||
|
||
t.Run("带排除的扫描", func(t *testing.T) {
|
||
input := &TargetInput{
|
||
Host: "192.168.1.1-10",
|
||
ExcludeHosts: "192.168.1.5,192.168.1.6",
|
||
Ports: "1-100",
|
||
ExcludePorts: "22,23,24",
|
||
}
|
||
|
||
result, err := parser.Parse(input, nil)
|
||
if err != nil {
|
||
t.Fatalf("意外错误: %v", err)
|
||
}
|
||
|
||
if !result.Success {
|
||
t.Errorf("解析失败,错误: %v", result.Errors)
|
||
}
|
||
|
||
// 验证排除后的主机数量: 10 - 2 = 8
|
||
if len(result.Config.Targets.Hosts) != 8 {
|
||
t.Errorf("主机数量 = %d, want 8", len(result.Config.Targets.Hosts))
|
||
}
|
||
|
||
// 验证排除端口列表
|
||
if len(result.Config.Targets.ExcludePorts) != 3 {
|
||
t.Errorf("排除端口数量 = %d, want 3", len(result.Config.Targets.ExcludePorts))
|
||
}
|
||
})
|
||
}
|