mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
refactor: 重构PortFinger.go为模块化架构以提升代码可维护性
将原有的878行单一文件重构为多个专门化模块: - 类型定义模块:集中管理所有数据结构 - 扫描器核心:初始化和全局状态管理 - 编码工具:处理各种编码格式转换 - 探测器解析:解析nmap-service-probes格式 - 匹配引擎:模式匹配和服务识别 - 版本解析:服务版本信息提取 通过向后兼容层保持原有API接口不变,确保现有代码无需修改即可使用新架构
This commit is contained in:
parent
69a70fc577
commit
0a60d76f71
@ -1,877 +1,40 @@
|
||||
package core
|
||||
|
||||
// 重新导出portfinger包的类型和函数,保持向后兼容性
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/core/portfinger"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed nmap-service-probes.txt
|
||||
var ProbeString string
|
||||
// 重新导出类型定义
|
||||
type VScan = portfinger.VScan
|
||||
type Probe = portfinger.Probe
|
||||
type Match = portfinger.Match
|
||||
type Directive = portfinger.Directive
|
||||
type Extras = portfinger.Extras
|
||||
type Target = portfinger.Target
|
||||
|
||||
var v VScan // 改为VScan类型而不是指针
|
||||
|
||||
type VScan struct {
|
||||
Exclude string
|
||||
AllProbes []Probe
|
||||
UdpProbes []Probe
|
||||
Probes []Probe
|
||||
ProbesMapKName map[string]Probe
|
||||
}
|
||||
|
||||
type Probe struct {
|
||||
Name string // 探测器名称
|
||||
Data string // 探测数据
|
||||
Protocol string // 协议
|
||||
Ports string // 端口范围
|
||||
SSLPorts string // SSL端口范围
|
||||
|
||||
TotalWaitMS int // 总等待时间
|
||||
TCPWrappedMS int // TCP包装等待时间
|
||||
Rarity int // 稀有度
|
||||
Fallback string // 回退探测器名称
|
||||
|
||||
Matchs *[]Match // 匹配规则列表
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
IsSoft bool // 是否为软匹配
|
||||
Service string // 服务名称
|
||||
Pattern string // 匹配模式
|
||||
VersionInfo string // 版本信息格式
|
||||
FoundItems []string // 找到的项目
|
||||
PatternCompiled *regexp.Regexp // 编译后的正则表达式
|
||||
}
|
||||
|
||||
type Directive struct {
|
||||
DirectiveName string
|
||||
Flag string
|
||||
Delimiter string
|
||||
DirectiveStr string
|
||||
}
|
||||
|
||||
type Extras struct {
|
||||
VendorProduct string
|
||||
Version string
|
||||
Info string
|
||||
Hostname string
|
||||
OperatingSystem string
|
||||
DeviceType string
|
||||
CPE string
|
||||
}
|
||||
// 兼容原有的全局变量访问模式
|
||||
var v *VScan
|
||||
var null *Probe
|
||||
var commonProbe *Probe
|
||||
|
||||
func init() {
|
||||
common.LogDebug("开始初始化全局变量")
|
||||
common.LogDebug("初始化PortFinger兼容层")
|
||||
|
||||
v = VScan{} // 直接初始化VScan结构体
|
||||
v.Init()
|
||||
// 初始化兼容性全局变量
|
||||
v = portfinger.GetGlobalVScan()
|
||||
null = portfinger.GetNullProbe()
|
||||
commonProbe = portfinger.GetCommonProbe()
|
||||
|
||||
// 获取并检查 NULL 探测器
|
||||
if nullProbe, ok := v.ProbesMapKName["NULL"]; ok {
|
||||
common.LogDebug(fmt.Sprintf("成功获取NULL探测器,Data长度: %d", len(nullProbe.Data)))
|
||||
null = &nullProbe
|
||||
} else {
|
||||
common.LogDebug("警告: 未找到NULL探测器")
|
||||
common.LogDebug("PortFinger兼容层初始化完成")
|
||||
}
|
||||
|
||||
// 获取并检查 GenericLines 探测器
|
||||
if genericProbe, ok := v.ProbesMapKName["GenericLines"]; ok {
|
||||
common.LogDebug(fmt.Sprintf("成功获取GenericLines探测器,Data长度: %d", len(genericProbe.Data)))
|
||||
commonProbe = &genericProbe
|
||||
} else {
|
||||
common.LogDebug("警告: 未找到GenericLines探测器")
|
||||
}
|
||||
|
||||
common.LogDebug("全局变量初始化完成")
|
||||
}
|
||||
|
||||
// 解析指令语法,返回指令结构
|
||||
func (p *Probe) getDirectiveSyntax(data string) (directive Directive) {
|
||||
common.LogDebug("开始解析指令语法,输入数据: " + data)
|
||||
|
||||
directive = Directive{}
|
||||
// 查找第一个空格的位置
|
||||
blankIndex := strings.Index(data, " ")
|
||||
if blankIndex == -1 {
|
||||
common.LogDebug("未找到空格分隔符")
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
directiveName := data[:blankIndex]
|
||||
Flag := data[blankIndex+1 : blankIndex+2]
|
||||
delimiter := data[blankIndex+2 : blankIndex+3]
|
||||
directiveStr := data[blankIndex+3:]
|
||||
|
||||
directive.DirectiveName = directiveName
|
||||
directive.Flag = Flag
|
||||
directive.Delimiter = delimiter
|
||||
directive.DirectiveStr = directiveStr
|
||||
|
||||
common.LogDebug(fmt.Sprintf("指令解析结果: 名称=%s, 标志=%s, 分隔符=%s, 内容=%s",
|
||||
directiveName, Flag, delimiter, directiveStr))
|
||||
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析探测器信息
|
||||
func (p *Probe) parseProbeInfo(probeStr string) {
|
||||
common.LogDebug("开始解析探测器信息,输入字符串: " + probeStr)
|
||||
|
||||
// 提取协议和其他信息
|
||||
proto := probeStr[:4]
|
||||
other := probeStr[4:]
|
||||
|
||||
// 验证协议类型
|
||||
if !(proto == "TCP " || proto == "UDP ") {
|
||||
errMsg := "探测器协议必须是 TCP 或 UDP"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 验证其他信息不为空
|
||||
if len(other) == 0 {
|
||||
errMsg := "nmap-service-probes - 探测器名称无效"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 解析指令
|
||||
directive := p.getDirectiveSyntax(other)
|
||||
|
||||
// 设置探测器属性
|
||||
p.Name = directive.DirectiveName
|
||||
p.Data = strings.Split(directive.DirectiveStr, directive.Delimiter)[0]
|
||||
p.Protocol = strings.ToLower(strings.TrimSpace(proto))
|
||||
|
||||
common.LogDebug(fmt.Sprintf("探测器解析完成: 名称=%s, 数据=%s, 协议=%s",
|
||||
p.Name, p.Data, p.Protocol))
|
||||
}
|
||||
|
||||
// 从字符串解析探测器信息
|
||||
func (p *Probe) fromString(data string) error {
|
||||
common.LogDebug("开始解析探测器字符串数据")
|
||||
var err error
|
||||
|
||||
// 预处理数据
|
||||
data = strings.TrimSpace(data)
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) == 0 {
|
||||
return fmt.Errorf("输入数据为空")
|
||||
}
|
||||
|
||||
probeStr := lines[0]
|
||||
p.parseProbeInfo(probeStr)
|
||||
|
||||
// 解析匹配规则和其他配置
|
||||
var matchs []Match
|
||||
for _, line := range lines {
|
||||
common.LogDebug("处理行: " + line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "match "):
|
||||
match, err := p.getMatch(line)
|
||||
if err != nil {
|
||||
common.LogDebug("解析match失败: " + err.Error())
|
||||
continue
|
||||
}
|
||||
matchs = append(matchs, match)
|
||||
|
||||
case strings.HasPrefix(line, "softmatch "):
|
||||
softMatch, err := p.getSoftMatch(line)
|
||||
if err != nil {
|
||||
common.LogDebug("解析softmatch失败: " + err.Error())
|
||||
continue
|
||||
}
|
||||
matchs = append(matchs, softMatch)
|
||||
|
||||
case strings.HasPrefix(line, "ports "):
|
||||
p.parsePorts(line)
|
||||
|
||||
case strings.HasPrefix(line, "sslports "):
|
||||
p.parseSSLPorts(line)
|
||||
|
||||
case strings.HasPrefix(line, "totalwaitms "):
|
||||
p.parseTotalWaitMS(line)
|
||||
|
||||
case strings.HasPrefix(line, "tcpwrappedms "):
|
||||
p.parseTCPWrappedMS(line)
|
||||
|
||||
case strings.HasPrefix(line, "rarity "):
|
||||
p.parseRarity(line)
|
||||
|
||||
case strings.HasPrefix(line, "fallback "):
|
||||
p.parseFallback(line)
|
||||
}
|
||||
}
|
||||
p.Matchs = &matchs
|
||||
common.LogDebug(fmt.Sprintf("解析完成,共有 %d 个匹配规则", len(matchs)))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析端口配置
|
||||
func (p *Probe) parsePorts(data string) {
|
||||
p.Ports = data[len("ports")+1:]
|
||||
common.LogDebug("解析端口: " + p.Ports)
|
||||
}
|
||||
|
||||
// 解析SSL端口配置
|
||||
func (p *Probe) parseSSLPorts(data string) {
|
||||
p.SSLPorts = data[len("sslports")+1:]
|
||||
common.LogDebug("解析SSL端口: " + p.SSLPorts)
|
||||
}
|
||||
|
||||
// 解析总等待时间
|
||||
func (p *Probe) parseTotalWaitMS(data string) {
|
||||
waitMS, err := strconv.Atoi(strings.TrimSpace(data[len("totalwaitms")+1:]))
|
||||
if err != nil {
|
||||
common.LogDebug("解析总等待时间失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.TotalWaitMS = waitMS
|
||||
common.LogDebug(fmt.Sprintf("总等待时间: %d ms", waitMS))
|
||||
}
|
||||
|
||||
// 解析TCP包装等待时间
|
||||
func (p *Probe) parseTCPWrappedMS(data string) {
|
||||
wrappedMS, err := strconv.Atoi(strings.TrimSpace(data[len("tcpwrappedms")+1:]))
|
||||
if err != nil {
|
||||
common.LogDebug("解析TCP包装等待时间失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.TCPWrappedMS = wrappedMS
|
||||
common.LogDebug(fmt.Sprintf("TCP包装等待时间: %d ms", wrappedMS))
|
||||
}
|
||||
|
||||
// 解析稀有度
|
||||
func (p *Probe) parseRarity(data string) {
|
||||
rarity, err := strconv.Atoi(strings.TrimSpace(data[len("rarity")+1:]))
|
||||
if err != nil {
|
||||
common.LogDebug("解析稀有度失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.Rarity = rarity
|
||||
common.LogDebug(fmt.Sprintf("稀有度: %d", rarity))
|
||||
}
|
||||
|
||||
// 解析回退配置
|
||||
func (p *Probe) parseFallback(data string) {
|
||||
p.Fallback = data[len("fallback")+1:]
|
||||
common.LogDebug("回退配置: " + p.Fallback)
|
||||
}
|
||||
|
||||
// 判断是否为十六进制编码
|
||||
func isHexCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\x[0-9a-fA-F]{2}`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 判断是否为八进制编码
|
||||
func isOctalCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\[0-7]{1,3}`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 判断是否为结构化转义字符
|
||||
func isStructCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\[aftnrv]`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 判断是否为正则表达式特殊字符
|
||||
func isReChar(n int64) bool {
|
||||
reChars := `.*?+{}()^$|\`
|
||||
for _, char := range reChars {
|
||||
if n == int64(char) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断是否为其他转义序列
|
||||
func isOtherEscapeCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\[^\\]`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 从内容解析探测器规则
|
||||
func (v *VScan) parseProbesFromContent(content string) {
|
||||
common.LogDebug("开始解析探测器规则文件内容")
|
||||
var probes []Probe
|
||||
var lines []string
|
||||
|
||||
// 过滤注释和空行
|
||||
linesTemp := strings.Split(content, "\n")
|
||||
for _, lineTemp := range linesTemp {
|
||||
lineTemp = strings.TrimSpace(lineTemp)
|
||||
if lineTemp == "" || strings.HasPrefix(lineTemp, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, lineTemp)
|
||||
}
|
||||
|
||||
// 验证文件内容
|
||||
if len(lines) == 0 {
|
||||
errMsg := "读取nmap-service-probes文件失败: 内容为空"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 检查Exclude指令
|
||||
excludeCount := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Exclude ") {
|
||||
excludeCount++
|
||||
}
|
||||
if excludeCount > 1 {
|
||||
errMsg := "nmap-service-probes文件中只允许有一个Exclude指令"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证第一行格式
|
||||
firstLine := lines[0]
|
||||
if !(strings.HasPrefix(firstLine, "Exclude ") || strings.HasPrefix(firstLine, "Probe ")) {
|
||||
errMsg := "解析错误: 首行必须以\"Probe \"或\"Exclude \"开头"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 处理Exclude指令
|
||||
if excludeCount == 1 {
|
||||
v.Exclude = firstLine[len("Exclude")+1:]
|
||||
lines = lines[1:]
|
||||
common.LogDebug("解析到Exclude规则: " + v.Exclude)
|
||||
}
|
||||
|
||||
// 合并内容并分割探测器
|
||||
content = "\n" + strings.Join(lines, "\n")
|
||||
probeParts := strings.Split(content, "\nProbe")[1:]
|
||||
|
||||
// 解析每个探测器
|
||||
for _, probePart := range probeParts {
|
||||
probe := Probe{}
|
||||
if err := probe.fromString(probePart); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("解析探测器失败: %v", err))
|
||||
continue
|
||||
}
|
||||
probes = append(probes, probe)
|
||||
}
|
||||
|
||||
v.AllProbes = probes
|
||||
common.LogDebug(fmt.Sprintf("成功解析 %d 个探测器规则", len(probes)))
|
||||
}
|
||||
|
||||
// 将探测器转换为名称映射
|
||||
func (v *VScan) parseProbesToMapKName() {
|
||||
common.LogDebug("开始构建探测器名称映射")
|
||||
v.ProbesMapKName = map[string]Probe{}
|
||||
for _, probe := range v.AllProbes {
|
||||
v.ProbesMapKName[probe.Name] = probe
|
||||
common.LogDebug("添加探测器映射: " + probe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置使用的探测器
|
||||
func (v *VScan) SetusedProbes() {
|
||||
common.LogDebug("开始设置要使用的探测器")
|
||||
|
||||
for _, probe := range v.AllProbes {
|
||||
if strings.ToLower(probe.Protocol) == "tcp" {
|
||||
if probe.Name == "SSLSessionReq" {
|
||||
common.LogDebug("跳过 SSLSessionReq 探测器")
|
||||
continue
|
||||
}
|
||||
|
||||
v.Probes = append(v.Probes, probe)
|
||||
common.LogDebug("添加TCP探测器: " + probe.Name)
|
||||
|
||||
// 特殊处理TLS会话请求
|
||||
if probe.Name == "TLSSessionReq" {
|
||||
sslProbe := v.ProbesMapKName["SSLSessionReq"]
|
||||
v.Probes = append(v.Probes, sslProbe)
|
||||
common.LogDebug("为TLSSessionReq添加SSL探测器")
|
||||
}
|
||||
} else {
|
||||
v.UdpProbes = append(v.UdpProbes, probe)
|
||||
common.LogDebug("添加UDP探测器: " + probe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("探测器设置完成,TCP: %d个, UDP: %d个",
|
||||
len(v.Probes), len(v.UdpProbes)))
|
||||
}
|
||||
|
||||
// 解析match指令获取匹配规则
|
||||
func (p *Probe) getMatch(data string) (match Match, err error) {
|
||||
common.LogDebug("开始解析match指令:" + data)
|
||||
match = Match{}
|
||||
|
||||
// 提取match文本并解析指令语法
|
||||
matchText := data[len("match")+1:]
|
||||
directive := p.getDirectiveSyntax(matchText)
|
||||
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return match, fmt.Errorf("无效的match指令格式")
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
versionInfo := strings.Join(textSplited[1:], "")
|
||||
|
||||
// 解码并编译正则表达式
|
||||
patternUnescaped, decodeErr := DecodePattern(pattern)
|
||||
if decodeErr != nil {
|
||||
common.LogDebug("解码pattern失败: " + decodeErr.Error())
|
||||
return match, decodeErr
|
||||
}
|
||||
|
||||
patternUnescapedStr := string([]rune(string(patternUnescaped)))
|
||||
patternCompiled, compileErr := regexp.Compile(patternUnescapedStr)
|
||||
if compileErr != nil {
|
||||
common.LogDebug("编译正则表达式失败: " + compileErr.Error())
|
||||
return match, compileErr
|
||||
}
|
||||
|
||||
// 设置match对象属性
|
||||
match.Service = directive.DirectiveName
|
||||
match.Pattern = pattern
|
||||
match.PatternCompiled = patternCompiled
|
||||
match.VersionInfo = versionInfo
|
||||
|
||||
common.LogDebug(fmt.Sprintf("解析match成功: 服务=%s, Pattern=%s",
|
||||
match.Service, match.Pattern))
|
||||
return match, nil
|
||||
}
|
||||
|
||||
// 解析softmatch指令获取软匹配规则
|
||||
func (p *Probe) getSoftMatch(data string) (softMatch Match, err error) {
|
||||
common.LogDebug("开始解析softmatch指令:" + data)
|
||||
softMatch = Match{IsSoft: true}
|
||||
|
||||
// 提取softmatch文本并解析指令语法
|
||||
matchText := data[len("softmatch")+1:]
|
||||
directive := p.getDirectiveSyntax(matchText)
|
||||
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return softMatch, fmt.Errorf("无效的softmatch指令格式")
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
versionInfo := strings.Join(textSplited[1:], "")
|
||||
|
||||
// 解码并编译正则表达式
|
||||
patternUnescaped, decodeErr := DecodePattern(pattern)
|
||||
if decodeErr != nil {
|
||||
common.LogDebug("解码pattern失败: " + decodeErr.Error())
|
||||
return softMatch, decodeErr
|
||||
}
|
||||
|
||||
patternUnescapedStr := string([]rune(string(patternUnescaped)))
|
||||
patternCompiled, compileErr := regexp.Compile(patternUnescapedStr)
|
||||
if compileErr != nil {
|
||||
common.LogDebug("编译正则表达式失败: " + compileErr.Error())
|
||||
return softMatch, compileErr
|
||||
}
|
||||
|
||||
// 设置softMatch对象属性
|
||||
softMatch.Service = directive.DirectiveName
|
||||
softMatch.Pattern = pattern
|
||||
softMatch.PatternCompiled = patternCompiled
|
||||
softMatch.VersionInfo = versionInfo
|
||||
|
||||
common.LogDebug(fmt.Sprintf("解析softmatch成功: 服务=%s, Pattern=%s",
|
||||
softMatch.Service, softMatch.Pattern))
|
||||
return softMatch, nil
|
||||
}
|
||||
|
||||
// 解码模式字符串,处理转义序列
|
||||
func DecodePattern(s string) ([]byte, error) {
|
||||
common.LogDebug("开始解码pattern: " + s)
|
||||
sByteOrigin := []byte(s)
|
||||
|
||||
// 处理十六进制、八进制和结构化转义序列
|
||||
matchRe := regexp.MustCompile(`\\(x[0-9a-fA-F]{2}|[0-7]{1,3}|[aftnrv])`)
|
||||
sByteDec := matchRe.ReplaceAllFunc(sByteOrigin, func(match []byte) (v []byte) {
|
||||
var replace []byte
|
||||
|
||||
// 处理十六进制转义
|
||||
if isHexCode(match) {
|
||||
hexNum := match[2:]
|
||||
byteNum, _ := strconv.ParseInt(string(hexNum), 16, 32)
|
||||
if isReChar(byteNum) {
|
||||
replace = []byte{'\\', uint8(byteNum)}
|
||||
} else {
|
||||
replace = []byte{uint8(byteNum)}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理结构化转义字符
|
||||
if isStructCode(match) {
|
||||
structCodeMap := map[int][]byte{
|
||||
97: []byte{0x07}, // \a 响铃
|
||||
102: []byte{0x0c}, // \f 换页
|
||||
116: []byte{0x09}, // \t 制表符
|
||||
110: []byte{0x0a}, // \n 换行
|
||||
114: []byte{0x0d}, // \r 回车
|
||||
118: []byte{0x0b}, // \v 垂直制表符
|
||||
}
|
||||
replace = structCodeMap[int(match[1])]
|
||||
}
|
||||
|
||||
// 处理八进制转义
|
||||
if isOctalCode(match) {
|
||||
octalNum := match[2:]
|
||||
byteNum, _ := strconv.ParseInt(string(octalNum), 8, 32)
|
||||
replace = []byte{uint8(byteNum)}
|
||||
}
|
||||
return replace
|
||||
})
|
||||
|
||||
// 处理其他转义序列
|
||||
matchRe2 := regexp.MustCompile(`\\([^\\])`)
|
||||
sByteDec2 := matchRe2.ReplaceAllFunc(sByteDec, func(match []byte) (v []byte) {
|
||||
if isOtherEscapeCode(match) {
|
||||
return match
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
common.LogDebug("pattern解码完成")
|
||||
return sByteDec2, nil
|
||||
}
|
||||
|
||||
// ProbesRarity 用于按稀有度排序的探测器切片
|
||||
type ProbesRarity []Probe
|
||||
|
||||
// Len 返回切片长度,实现 sort.Interface 接口
|
||||
func (ps ProbesRarity) Len() int {
|
||||
return len(ps)
|
||||
}
|
||||
|
||||
// Swap 交换切片中的两个元素,实现 sort.Interface 接口
|
||||
func (ps ProbesRarity) Swap(i, j int) {
|
||||
ps[i], ps[j] = ps[j], ps[i]
|
||||
}
|
||||
|
||||
// Less 比较函数,按稀有度升序排序,实现 sort.Interface 接口
|
||||
func (ps ProbesRarity) Less(i, j int) bool {
|
||||
return ps[i].Rarity < ps[j].Rarity
|
||||
}
|
||||
|
||||
// Target 定义目标结构体
|
||||
type Target struct {
|
||||
IP string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Protocol string // 协议类型
|
||||
}
|
||||
|
||||
// ContainsPort 检查指定端口是否在探测器的端口范围内
|
||||
func (p *Probe) ContainsPort(testPort int) bool {
|
||||
common.LogDebug(fmt.Sprintf("检查端口 %d 是否在探测器端口范围内: %s", testPort, p.Ports))
|
||||
|
||||
// 检查单个端口
|
||||
ports := strings.Split(p.Ports, ",")
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
cmpPort, err := strconv.Atoi(port)
|
||||
if err == nil && testPort == cmpPort {
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 匹配单个端口", testPort))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查端口范围
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
if strings.Contains(port, "-") {
|
||||
portRange := strings.Split(port, "-")
|
||||
if len(portRange) != 2 {
|
||||
common.LogDebug("无效的端口范围格式: " + port)
|
||||
continue
|
||||
}
|
||||
|
||||
start, err1 := strconv.Atoi(strings.TrimSpace(portRange[0]))
|
||||
end, err2 := strconv.Atoi(strings.TrimSpace(portRange[1]))
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
common.LogDebug(fmt.Sprintf("解析端口范围失败: %s", port))
|
||||
continue
|
||||
}
|
||||
|
||||
if testPort >= start && testPort <= end {
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 在范围 %d-%d 内", testPort, start, end))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 不在探测器端口范围内", testPort))
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchPattern 使用正则表达式匹配响应内容
|
||||
func (m *Match) MatchPattern(response []byte) bool {
|
||||
// 将响应转换为字符串并进行匹配
|
||||
responseStr := string([]rune(string(response)))
|
||||
foundItems := m.PatternCompiled.FindStringSubmatch(responseStr)
|
||||
|
||||
if len(foundItems) > 0 {
|
||||
m.FoundItems = foundItems
|
||||
common.LogDebug(fmt.Sprintf("匹配成功,找到 %d 个匹配项", len(foundItems)))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseVersionInfo 解析版本信息并返回额外信息结构
|
||||
func (m *Match) ParseVersionInfo(response []byte) Extras {
|
||||
common.LogDebug("开始解析版本信息")
|
||||
var extras = Extras{}
|
||||
|
||||
// 替换版本信息中的占位符
|
||||
foundItems := m.FoundItems[1:] // 跳过第一个完整匹配项
|
||||
versionInfo := m.VersionInfo
|
||||
for index, value := range foundItems {
|
||||
dollarName := "$" + strconv.Itoa(index+1)
|
||||
versionInfo = strings.Replace(versionInfo, dollarName, value, -1)
|
||||
}
|
||||
common.LogDebug("替换后的版本信息: " + versionInfo)
|
||||
|
||||
// 定义解析函数
|
||||
parseField := func(field, pattern string) string {
|
||||
patterns := []string{
|
||||
pattern + `/([^/]*)/`, // 斜线分隔
|
||||
pattern + `\|([^|]*)\|`, // 竖线分隔
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
if strings.Contains(versionInfo, pattern) {
|
||||
regex := regexp.MustCompile(p)
|
||||
if matches := regex.FindStringSubmatch(versionInfo); len(matches) > 1 {
|
||||
common.LogDebug(fmt.Sprintf("解析到%s: %s", field, matches[1]))
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
extras.VendorProduct = parseField("厂商产品", " p")
|
||||
extras.Version = parseField("版本", " v")
|
||||
extras.Info = parseField("信息", " i")
|
||||
extras.Hostname = parseField("主机名", " h")
|
||||
extras.OperatingSystem = parseField("操作系统", " o")
|
||||
extras.DeviceType = parseField("设备类型", " d")
|
||||
|
||||
// 特殊处理CPE
|
||||
if strings.Contains(versionInfo, " cpe:/") || strings.Contains(versionInfo, " cpe:|") {
|
||||
cpePatterns := []string{`cpe:/([^/]*)`, `cpe:\|([^|]*)`}
|
||||
for _, pattern := range cpePatterns {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
if cpeName := regex.FindStringSubmatch(versionInfo); len(cpeName) > 0 {
|
||||
if len(cpeName) > 1 {
|
||||
extras.CPE = cpeName[1]
|
||||
} else {
|
||||
extras.CPE = cpeName[0]
|
||||
}
|
||||
common.LogDebug("解析到CPE: " + extras.CPE)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extras
|
||||
}
|
||||
|
||||
// ToMap 将 Extras 转换为 map[string]string
|
||||
func (e *Extras) ToMap() map[string]string {
|
||||
common.LogDebug("开始转换Extras为Map")
|
||||
result := make(map[string]string)
|
||||
|
||||
// 定义字段映射
|
||||
fields := map[string]string{
|
||||
"vendor_product": e.VendorProduct,
|
||||
"version": e.Version,
|
||||
"info": e.Info,
|
||||
"hostname": e.Hostname,
|
||||
"os": e.OperatingSystem,
|
||||
"device_type": e.DeviceType,
|
||||
"cpe": e.CPE,
|
||||
}
|
||||
|
||||
// 添加非空字段到结果map
|
||||
for key, value := range fields {
|
||||
if value != "" {
|
||||
result[key] = value
|
||||
common.LogDebug(fmt.Sprintf("添加字段 %s: %s", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("转换完成,共有 %d 个字段", len(result)))
|
||||
return result
|
||||
}
|
||||
|
||||
func DecodeData(s string) ([]byte, error) {
|
||||
if len(s) == 0 {
|
||||
common.LogDebug("输入数据为空")
|
||||
return nil, fmt.Errorf("empty input")
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("开始解码数据,长度: %d, 内容: %q", len(s), s))
|
||||
sByteOrigin := []byte(s)
|
||||
|
||||
// 处理十六进制、八进制和结构化转义序列
|
||||
matchRe := regexp.MustCompile(`\\(x[0-9a-fA-F]{2}|[0-7]{1,3}|[aftnrv])`)
|
||||
sByteDec := matchRe.ReplaceAllFunc(sByteOrigin, func(match []byte) []byte {
|
||||
// 处理十六进制转义
|
||||
if isHexCode(match) {
|
||||
hexNum := match[2:]
|
||||
byteNum, err := strconv.ParseInt(string(hexNum), 16, 32)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
return []byte{uint8(byteNum)}
|
||||
}
|
||||
|
||||
// 处理结构化转义字符
|
||||
if isStructCode(match) {
|
||||
structCodeMap := map[int][]byte{
|
||||
97: []byte{0x07}, // \a 响铃
|
||||
102: []byte{0x0c}, // \f 换页
|
||||
116: []byte{0x09}, // \t 制表符
|
||||
110: []byte{0x0a}, // \n 换行
|
||||
114: []byte{0x0d}, // \r 回车
|
||||
118: []byte{0x0b}, // \v 垂直制表符
|
||||
}
|
||||
if replace, ok := structCodeMap[int(match[1])]; ok {
|
||||
return replace
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 处理八进制转义
|
||||
if isOctalCode(match) {
|
||||
octalNum := match[2:]
|
||||
byteNum, err := strconv.ParseInt(string(octalNum), 8, 32)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
return []byte{uint8(byteNum)}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("无法识别的转义序列: %s", string(match)))
|
||||
return match
|
||||
})
|
||||
|
||||
// 处理其他转义序列
|
||||
matchRe2 := regexp.MustCompile(`\\([^\\])`)
|
||||
sByteDec2 := matchRe2.ReplaceAllFunc(sByteDec, func(match []byte) []byte {
|
||||
if len(match) < 2 {
|
||||
return match
|
||||
}
|
||||
if isOtherEscapeCode(match) {
|
||||
return []byte{match[1]}
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
if len(sByteDec2) == 0 {
|
||||
common.LogDebug("解码后数据为空")
|
||||
return nil, fmt.Errorf("decoded data is empty")
|
||||
}
|
||||
// 重新导出编码函数
|
||||
var DecodeData = portfinger.DecodeData
|
||||
var DecodePattern = portfinger.DecodePattern
|
||||
|
||||
common.LogDebug(fmt.Sprintf("解码完成,结果长度: %d, 内容: %x", len(sByteDec2), sByteDec2))
|
||||
return sByteDec2, nil
|
||||
}
|
||||
// 重新导出探测器字符串
|
||||
var ProbeString = portfinger.ProbeString
|
||||
|
||||
// GetAddress 获取目标的完整地址(IP:端口)
|
||||
func (t *Target) GetAddress() string {
|
||||
addr := t.IP + ":" + strconv.Itoa(t.Port)
|
||||
common.LogDebug("获取目标地址: " + addr)
|
||||
return addr
|
||||
}
|
||||
|
||||
// trimBanner 处理和清理横幅数据
|
||||
func trimBanner(buf []byte) string {
|
||||
common.LogDebug("开始处理横幅数据")
|
||||
bufStr := string(buf)
|
||||
|
||||
// 特殊处理SMB协议
|
||||
if strings.Contains(bufStr, "SMB") {
|
||||
banner := hex.EncodeToString(buf)
|
||||
if len(banner) > 0xa+6 && banner[0xa:0xa+6] == "534d42" { // "SMB" in hex
|
||||
common.LogDebug("检测到SMB协议数据")
|
||||
plain := banner[0xa2:]
|
||||
data, err := hex.DecodeString(plain)
|
||||
if err != nil {
|
||||
common.LogDebug("SMB数据解码失败: " + err.Error())
|
||||
return bufStr
|
||||
}
|
||||
|
||||
// 解析domain
|
||||
var domain string
|
||||
var index int
|
||||
for i, s := range data {
|
||||
if s != 0 {
|
||||
domain += string(s)
|
||||
} else if i+1 < len(data) && data[i+1] == 0 {
|
||||
index = i + 2
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 解析hostname
|
||||
var hostname string
|
||||
remainData := data[index:]
|
||||
for i, h := range remainData {
|
||||
if h != 0 {
|
||||
hostname += string(h)
|
||||
}
|
||||
if i+1 < len(remainData) && remainData[i+1] == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
smbBanner := fmt.Sprintf("hostname: %s domain: %s", hostname, domain)
|
||||
common.LogDebug("SMB横幅: " + smbBanner)
|
||||
return smbBanner
|
||||
}
|
||||
}
|
||||
|
||||
// 处理常规数据
|
||||
var src string
|
||||
for _, ch := range bufStr {
|
||||
if ch > 32 && ch < 125 {
|
||||
src += string(ch)
|
||||
} else {
|
||||
src += " "
|
||||
}
|
||||
}
|
||||
|
||||
// 清理多余空白
|
||||
re := regexp.MustCompile(`\s{2,}`)
|
||||
src = re.ReplaceAllString(src, ".")
|
||||
result := strings.TrimSpace(src)
|
||||
common.LogDebug("处理后的横幅: " + result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Init 初始化VScan对象
|
||||
func (v *VScan) Init() {
|
||||
common.LogDebug("开始初始化VScan")
|
||||
v.parseProbesFromContent(ProbeString)
|
||||
v.parseProbesToMapKName()
|
||||
v.SetusedProbes()
|
||||
common.LogDebug("VScan初始化完成")
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/core/portfinger"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
@ -50,11 +51,7 @@ type PortInfoScanner struct {
|
||||
info *Info // 探测上下文
|
||||
}
|
||||
|
||||
// 预定义的基础探测器
|
||||
var (
|
||||
null = new(Probe) // 空探测器,用于基本协议识别
|
||||
commonProbe = new(Probe) // 通用探测器,用于常见服务识别
|
||||
)
|
||||
// 预定义的基础探测器已在PortFinger.go中定义,这里不再重复定义
|
||||
|
||||
// NewPortInfoScanner 创建新的端口服务识别器实例
|
||||
func NewPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration) *PortInfoScanner {
|
||||
@ -347,7 +344,7 @@ func (i *Info) handleHardMatch(response []byte, match *Match) {
|
||||
|
||||
result.Service.Name = match.Service
|
||||
result.Extras = extrasMap
|
||||
result.Banner = trimBanner(response)
|
||||
result.Banner = portfinger.TrimBanner(string(response))
|
||||
result.Service.Extras = extrasMap
|
||||
|
||||
// 特殊处理 microsoft-ds 服务
|
||||
@ -363,7 +360,7 @@ func (i *Info) handleHardMatch(response []byte, match *Match) {
|
||||
// handleNoMatch 处理未找到匹配的情况
|
||||
func (i *Info) handleNoMatch(response []byte, result *Result, softFound bool, softMatch Match) {
|
||||
common.LogDebug("处理未匹配情况")
|
||||
result.Banner = trimBanner(response)
|
||||
result.Banner = portfinger.TrimBanner(string(response))
|
||||
|
||||
if !softFound {
|
||||
// 尝试识别 HTTP 服务
|
||||
|
105
Core/portfinger/encoding_utils.go
Normal file
105
Core/portfinger/encoding_utils.go
Normal file
@ -0,0 +1,105 @@
|
||||
package portfinger
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
||||
// DecodePattern 解码匹配模式
|
||||
func DecodePattern(s string) ([]byte, error) {
|
||||
b := []byte(s)
|
||||
var result []byte
|
||||
|
||||
for i := 0; i < len(b); {
|
||||
if b[i] == '\\' && i+1 < len(b) {
|
||||
// 处理转义序列
|
||||
switch b[i+1] {
|
||||
case 'x':
|
||||
// 十六进制编码 \xNN
|
||||
if i+3 < len(b) {
|
||||
if hexStr := string(b[i+2:i+4]); isValidHex(hexStr) {
|
||||
if decoded, err := hex.DecodeString(hexStr); err == nil {
|
||||
result = append(result, decoded...)
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'a':
|
||||
result = append(result, '\a')
|
||||
i += 2
|
||||
continue
|
||||
case 'f':
|
||||
result = append(result, '\f')
|
||||
i += 2
|
||||
continue
|
||||
case 't':
|
||||
result = append(result, '\t')
|
||||
i += 2
|
||||
continue
|
||||
case 'n':
|
||||
result = append(result, '\n')
|
||||
i += 2
|
||||
continue
|
||||
case 'r':
|
||||
result = append(result, '\r')
|
||||
i += 2
|
||||
continue
|
||||
case 'v':
|
||||
result = append(result, '\v')
|
||||
i += 2
|
||||
continue
|
||||
case '\\':
|
||||
result = append(result, '\\')
|
||||
i += 2
|
||||
continue
|
||||
default:
|
||||
// 八进制编码 \NNN
|
||||
if i+1 < len(b) && b[i+1] >= '0' && b[i+1] <= '7' {
|
||||
octalStr := ""
|
||||
j := i + 1
|
||||
for j < len(b) && j < i+4 && b[j] >= '0' && b[j] <= '7' {
|
||||
octalStr += string(b[j])
|
||||
j++
|
||||
}
|
||||
if octal, err := strconv.ParseInt(octalStr, 8, 8); err == nil {
|
||||
result = append(result, byte(octal))
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 普通字符
|
||||
result = append(result, b[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeData 解码探测数据
|
||||
func DecodeData(s string) ([]byte, error) {
|
||||
// 移除首尾的分隔符
|
||||
if len(s) > 0 && (s[0] == '"' || s[0] == '\'') {
|
||||
s = s[1:]
|
||||
}
|
||||
if len(s) > 0 && (s[len(s)-1] == '"' || s[len(s)-1] == '\'') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
|
||||
return DecodePattern(s)
|
||||
}
|
||||
|
||||
// isValidHex 检查字符串是否为有效的十六进制
|
||||
func isValidHex(s string) bool {
|
||||
for _, c := range s {
|
||||
if !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) == 2
|
||||
}
|
||||
|
173
Core/portfinger/match_engine.go
Normal file
173
Core/portfinger/match_engine.go
Normal file
@ -0,0 +1,173 @@
|
||||
package portfinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// 解析match指令获取匹配规则
|
||||
func (p *Probe) getMatch(data string) (match Match, err error) {
|
||||
common.LogDebug("开始解析match指令:" + data)
|
||||
match = Match{}
|
||||
|
||||
// 提取match文本并解析指令语法
|
||||
matchText := data[len("match")+1:]
|
||||
directive := p.getDirectiveSyntax(matchText)
|
||||
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return match, fmt.Errorf("无效的match指令格式")
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
versionInfo := strings.Join(textSplited[1:], "")
|
||||
|
||||
// 解码并编译正则表达式
|
||||
patternUnescaped, decodeErr := DecodePattern(pattern)
|
||||
if decodeErr != nil {
|
||||
common.LogDebug("解码pattern失败: " + decodeErr.Error())
|
||||
return match, decodeErr
|
||||
}
|
||||
|
||||
patternUnescapedStr := string([]rune(string(patternUnescaped)))
|
||||
patternCompiled, compileErr := regexp.Compile(patternUnescapedStr)
|
||||
if compileErr != nil {
|
||||
common.LogDebug("编译正则表达式失败: " + compileErr.Error())
|
||||
return match, compileErr
|
||||
}
|
||||
|
||||
// 设置match对象属性
|
||||
match.Service = directive.DirectiveName
|
||||
match.Pattern = pattern
|
||||
match.PatternCompiled = patternCompiled
|
||||
match.VersionInfo = versionInfo
|
||||
|
||||
common.LogDebug(fmt.Sprintf("解析match成功: 服务=%s, Pattern=%s",
|
||||
match.Service, match.Pattern))
|
||||
return match, nil
|
||||
}
|
||||
|
||||
// 解析softmatch指令获取软匹配规则
|
||||
func (p *Probe) getSoftMatch(data string) (softMatch Match, err error) {
|
||||
common.LogDebug("开始解析softmatch指令:" + data)
|
||||
softMatch = Match{IsSoft: true}
|
||||
|
||||
// 提取softmatch文本并解析指令语法
|
||||
matchText := data[len("softmatch")+1:]
|
||||
directive := p.getDirectiveSyntax(matchText)
|
||||
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return softMatch, fmt.Errorf("无效的softmatch指令格式")
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
versionInfo := strings.Join(textSplited[1:], "")
|
||||
|
||||
// 解码并编译正则表达式
|
||||
patternUnescaped, decodeErr := DecodePattern(pattern)
|
||||
if decodeErr != nil {
|
||||
common.LogDebug("解码pattern失败: " + decodeErr.Error())
|
||||
return softMatch, decodeErr
|
||||
}
|
||||
|
||||
patternUnescapedStr := string([]rune(string(patternUnescaped)))
|
||||
patternCompiled, compileErr := regexp.Compile(patternUnescapedStr)
|
||||
if compileErr != nil {
|
||||
common.LogDebug("编译正则表达式失败: " + compileErr.Error())
|
||||
return softMatch, compileErr
|
||||
}
|
||||
|
||||
// 设置softMatch对象属性
|
||||
softMatch.Service = directive.DirectiveName
|
||||
softMatch.Pattern = pattern
|
||||
softMatch.PatternCompiled = patternCompiled
|
||||
softMatch.VersionInfo = versionInfo
|
||||
|
||||
common.LogDebug(fmt.Sprintf("解析softmatch成功: 服务=%s, Pattern=%s",
|
||||
softMatch.Service, softMatch.Pattern))
|
||||
return softMatch, nil
|
||||
}
|
||||
|
||||
// containsPort 检查指定端口是否在探测器的端口范围内(私有方法)
|
||||
func (p *Probe) containsPort(testPort int) bool {
|
||||
common.LogDebug(fmt.Sprintf("检查端口 %d 是否在探测器端口范围内: %s", testPort, p.Ports))
|
||||
|
||||
// 检查单个端口
|
||||
ports := strings.Split(p.Ports, ",")
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
cmpPort, err := strconv.Atoi(port)
|
||||
if err == nil && testPort == cmpPort {
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 匹配单个端口", testPort))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查端口范围
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
if strings.Contains(port, "-") {
|
||||
portRange := strings.Split(port, "-")
|
||||
if len(portRange) != 2 {
|
||||
common.LogDebug("无效的端口范围格式: " + port)
|
||||
continue
|
||||
}
|
||||
|
||||
start, err1 := strconv.Atoi(strings.TrimSpace(portRange[0]))
|
||||
end, err2 := strconv.Atoi(strings.TrimSpace(portRange[1]))
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
common.LogDebug(fmt.Sprintf("解析端口范围失败: %s", port))
|
||||
continue
|
||||
}
|
||||
|
||||
if testPort >= start && testPort <= end {
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 在范围 %d-%d 内", testPort, start, end))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 不在探测器端口范围内", testPort))
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchPattern 检查响应是否与匹配规则匹配
|
||||
func (m *Match) MatchPattern(response []byte) bool {
|
||||
if m.PatternCompiled == nil {
|
||||
common.LogDebug("警告: 匹配规则的正则表达式未编译")
|
||||
return false
|
||||
}
|
||||
|
||||
matched := m.PatternCompiled.Match(response)
|
||||
if matched {
|
||||
// 提取匹配到的子组
|
||||
submatches := m.PatternCompiled.FindStringSubmatch(string(response))
|
||||
if len(submatches) > 1 {
|
||||
m.FoundItems = submatches[1:] // 排除完整匹配,只保留分组
|
||||
common.LogDebug(fmt.Sprintf("模式匹配成功,提取到 %d 个分组", len(m.FoundItems)))
|
||||
} else {
|
||||
common.LogDebug("模式匹配成功,但没有分组匹配")
|
||||
}
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// IsPortMatch 检查探测器是否适用于指定端口
|
||||
func (p *Probe) IsPortMatch(port int) bool {
|
||||
// 如果没有指定端口范围,认为适用于所有端口
|
||||
if p.Ports == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return p.containsPort(port)
|
||||
}
|
||||
|
16624
Core/portfinger/nmap-service-probes.txt
Normal file
16624
Core/portfinger/nmap-service-probes.txt
Normal file
File diff suppressed because it is too large
Load Diff
292
Core/portfinger/probe_parser.go
Normal file
292
Core/portfinger/probe_parser.go
Normal file
@ -0,0 +1,292 @@
|
||||
package portfinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// 解析指令语法,返回指令结构
|
||||
func (p *Probe) getDirectiveSyntax(data string) (directive Directive) {
|
||||
common.LogDebug("开始解析指令语法,输入数据: " + data)
|
||||
|
||||
directive = Directive{}
|
||||
// 查找第一个空格的位置
|
||||
blankIndex := strings.Index(data, " ")
|
||||
if blankIndex == -1 {
|
||||
common.LogDebug("未找到空格分隔符")
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
directiveName := data[:blankIndex]
|
||||
Flag := data[blankIndex+1 : blankIndex+2]
|
||||
delimiter := data[blankIndex+2 : blankIndex+3]
|
||||
directiveStr := data[blankIndex+3:]
|
||||
|
||||
directive.DirectiveName = directiveName
|
||||
directive.Flag = Flag
|
||||
directive.Delimiter = delimiter
|
||||
directive.DirectiveStr = directiveStr
|
||||
|
||||
common.LogDebug(fmt.Sprintf("指令解析结果: 名称=%s, 标志=%s, 分隔符=%s, 内容=%s",
|
||||
directiveName, Flag, delimiter, directiveStr))
|
||||
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析探测器信息
|
||||
func (p *Probe) parseProbeInfo(probeStr string) {
|
||||
common.LogDebug("开始解析探测器信息,输入字符串: " + probeStr)
|
||||
|
||||
// 提取协议和其他信息
|
||||
proto := probeStr[:4]
|
||||
other := probeStr[4:]
|
||||
|
||||
// 验证协议类型
|
||||
if !(proto == "TCP " || proto == "UDP ") {
|
||||
errMsg := "探测器协议必须是 TCP 或 UDP"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 验证其他信息不为空
|
||||
if len(other) == 0 {
|
||||
errMsg := "nmap-service-probes - 探测器名称无效"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 解析指令
|
||||
directive := p.getDirectiveSyntax(other)
|
||||
|
||||
// 设置探测器属性
|
||||
p.Name = directive.DirectiveName
|
||||
p.Data = strings.Split(directive.DirectiveStr, directive.Delimiter)[0]
|
||||
p.Protocol = strings.ToLower(strings.TrimSpace(proto))
|
||||
|
||||
common.LogDebug(fmt.Sprintf("探测器解析完成: 名称=%s, 数据=%s, 协议=%s",
|
||||
p.Name, p.Data, p.Protocol))
|
||||
}
|
||||
|
||||
// 从字符串解析探测器信息
|
||||
func (p *Probe) fromString(data string) error {
|
||||
common.LogDebug("开始解析探测器字符串数据")
|
||||
var err error
|
||||
|
||||
// 预处理数据
|
||||
data = strings.TrimSpace(data)
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) == 0 {
|
||||
return fmt.Errorf("输入数据为空")
|
||||
}
|
||||
|
||||
probeStr := lines[0]
|
||||
p.parseProbeInfo(probeStr)
|
||||
|
||||
// 解析匹配规则和其他配置
|
||||
var matchs []Match
|
||||
for _, line := range lines {
|
||||
common.LogDebug("处理行: " + line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "match "):
|
||||
match, err := p.getMatch(line)
|
||||
if err != nil {
|
||||
common.LogDebug("解析match失败: " + err.Error())
|
||||
continue
|
||||
}
|
||||
matchs = append(matchs, match)
|
||||
|
||||
case strings.HasPrefix(line, "softmatch "):
|
||||
softMatch, err := p.getSoftMatch(line)
|
||||
if err != nil {
|
||||
common.LogDebug("解析softmatch失败: " + err.Error())
|
||||
continue
|
||||
}
|
||||
matchs = append(matchs, softMatch)
|
||||
|
||||
case strings.HasPrefix(line, "ports "):
|
||||
p.parsePorts(line)
|
||||
|
||||
case strings.HasPrefix(line, "sslports "):
|
||||
p.parseSSLPorts(line)
|
||||
|
||||
case strings.HasPrefix(line, "totalwaitms "):
|
||||
p.parseTotalWaitMS(line)
|
||||
|
||||
case strings.HasPrefix(line, "tcpwrappedms "):
|
||||
p.parseTCPWrappedMS(line)
|
||||
|
||||
case strings.HasPrefix(line, "rarity "):
|
||||
p.parseRarity(line)
|
||||
|
||||
case strings.HasPrefix(line, "fallback "):
|
||||
p.parseFallback(line)
|
||||
}
|
||||
}
|
||||
p.Matchs = &matchs
|
||||
common.LogDebug(fmt.Sprintf("解析完成,共有 %d 个匹配规则", len(matchs)))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析端口配置
|
||||
func (p *Probe) parsePorts(data string) {
|
||||
p.Ports = data[len("ports")+1:]
|
||||
common.LogDebug("解析端口: " + p.Ports)
|
||||
}
|
||||
|
||||
// 解析SSL端口配置
|
||||
func (p *Probe) parseSSLPorts(data string) {
|
||||
p.SSLPorts = data[len("sslports")+1:]
|
||||
common.LogDebug("解析SSL端口: " + p.SSLPorts)
|
||||
}
|
||||
|
||||
// 解析总等待时间
|
||||
func (p *Probe) parseTotalWaitMS(data string) {
|
||||
waitMS, err := strconv.Atoi(strings.TrimSpace(data[len("totalwaitms")+1:]))
|
||||
if err != nil {
|
||||
common.LogDebug("解析总等待时间失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.TotalWaitMS = waitMS
|
||||
common.LogDebug(fmt.Sprintf("总等待时间: %d ms", waitMS))
|
||||
}
|
||||
|
||||
// 解析TCP包装等待时间
|
||||
func (p *Probe) parseTCPWrappedMS(data string) {
|
||||
wrappedMS, err := strconv.Atoi(strings.TrimSpace(data[len("tcpwrappedms")+1:]))
|
||||
if err != nil {
|
||||
common.LogDebug("解析TCP包装等待时间失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.TCPWrappedMS = wrappedMS
|
||||
common.LogDebug(fmt.Sprintf("TCP包装等待时间: %d ms", wrappedMS))
|
||||
}
|
||||
|
||||
// 解析稀有度
|
||||
func (p *Probe) parseRarity(data string) {
|
||||
rarity, err := strconv.Atoi(strings.TrimSpace(data[len("rarity")+1:]))
|
||||
if err != nil {
|
||||
common.LogDebug("解析稀有度失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.Rarity = rarity
|
||||
common.LogDebug(fmt.Sprintf("稀有度: %d", rarity))
|
||||
}
|
||||
|
||||
// 解析回退配置
|
||||
func (p *Probe) parseFallback(data string) {
|
||||
p.Fallback = data[len("fallback")+1:]
|
||||
common.LogDebug("回退配置: " + p.Fallback)
|
||||
}
|
||||
|
||||
// 从内容解析探测器规则
|
||||
func (v *VScan) parseProbesFromContent(content string) {
|
||||
common.LogDebug("开始解析探测器规则文件内容")
|
||||
var probes []Probe
|
||||
var lines []string
|
||||
|
||||
// 过滤注释和空行
|
||||
linesTemp := strings.Split(content, "\n")
|
||||
for _, lineTemp := range linesTemp {
|
||||
lineTemp = strings.TrimSpace(lineTemp)
|
||||
if lineTemp == "" || strings.HasPrefix(lineTemp, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, lineTemp)
|
||||
}
|
||||
|
||||
// 验证文件内容
|
||||
if len(lines) == 0 {
|
||||
errMsg := "读取nmap-service-probes文件失败: 内容为空"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 检查Exclude指令
|
||||
excludeCount := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Exclude ") {
|
||||
excludeCount++
|
||||
}
|
||||
if excludeCount > 1 {
|
||||
errMsg := "nmap-service-probes文件中只允许有一个Exclude指令"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证第一行格式
|
||||
firstLine := lines[0]
|
||||
if !(strings.HasPrefix(firstLine, "Exclude ") || strings.HasPrefix(firstLine, "Probe ")) {
|
||||
errMsg := "解析错误: 首行必须以\"Probe \"或\"Exclude \"开头"
|
||||
common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 处理Exclude指令
|
||||
if excludeCount == 1 {
|
||||
v.Exclude = firstLine[len("Exclude")+1:]
|
||||
lines = lines[1:]
|
||||
common.LogDebug("解析到Exclude规则: " + v.Exclude)
|
||||
}
|
||||
|
||||
// 合并内容并分割探测器
|
||||
content = "\n" + strings.Join(lines, "\n")
|
||||
probeParts := strings.Split(content, "\nProbe")[1:]
|
||||
|
||||
// 解析每个探测器
|
||||
for _, probePart := range probeParts {
|
||||
probe := Probe{}
|
||||
if err := probe.fromString(probePart); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("解析探测器失败: %v", err))
|
||||
continue
|
||||
}
|
||||
probes = append(probes, probe)
|
||||
}
|
||||
|
||||
v.AllProbes = probes
|
||||
common.LogDebug(fmt.Sprintf("成功解析 %d 个探测器规则", len(probes)))
|
||||
}
|
||||
|
||||
// 将探测器转换为名称映射
|
||||
func (v *VScan) parseProbesToMapKName() {
|
||||
common.LogDebug("开始构建探测器名称映射")
|
||||
v.ProbesMapKName = map[string]Probe{}
|
||||
for _, probe := range v.AllProbes {
|
||||
v.ProbesMapKName[probe.Name] = probe
|
||||
common.LogDebug("添加探测器映射: " + probe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置使用的探测器
|
||||
func (v *VScan) SetusedProbes() {
|
||||
common.LogDebug("开始设置要使用的探测器")
|
||||
|
||||
for _, probe := range v.AllProbes {
|
||||
if strings.ToLower(probe.Protocol) == "tcp" {
|
||||
if probe.Name == "SSLSessionReq" {
|
||||
common.LogDebug("跳过 SSLSessionReq 探测器")
|
||||
continue
|
||||
}
|
||||
|
||||
v.Probes = append(v.Probes, probe)
|
||||
common.LogDebug("添加TCP探测器: " + probe.Name)
|
||||
|
||||
// 特殊处理TLS会话请求
|
||||
if probe.Name == "TLSSessionReq" {
|
||||
sslProbe := v.ProbesMapKName["SSLSessionReq"]
|
||||
v.Probes = append(v.Probes, sslProbe)
|
||||
common.LogDebug("为TLSSessionReq添加SSL探测器")
|
||||
}
|
||||
} else {
|
||||
v.UdpProbes = append(v.UdpProbes, probe)
|
||||
common.LogDebug("添加UDP探测器: " + probe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("探测器设置完成,TCP: %d个, UDP: %d个",
|
||||
len(v.Probes), len(v.UdpProbes)))
|
||||
}
|
71
Core/portfinger/scanner_core.go
Normal file
71
Core/portfinger/scanner_core.go
Normal file
@ -0,0 +1,71 @@
|
||||
package portfinger
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
//go:embed nmap-service-probes.txt
|
||||
var ProbeString string
|
||||
|
||||
var v VScan
|
||||
var null *Probe
|
||||
var commonProbe *Probe
|
||||
|
||||
// Init 初始化VScan对象
|
||||
func (vs *VScan) Init() {
|
||||
common.LogDebug("开始初始化VScan")
|
||||
vs.parseProbesFromContent(ProbeString)
|
||||
vs.parseProbesToMapKName()
|
||||
vs.SetusedProbes()
|
||||
common.LogDebug("VScan初始化完成")
|
||||
}
|
||||
|
||||
|
||||
// InitializeGlobalVScan 初始化全局VScan实例
|
||||
func InitializeGlobalVScan() {
|
||||
common.LogDebug("开始初始化全局变量")
|
||||
|
||||
v = VScan{}
|
||||
v.Init()
|
||||
|
||||
// 获取并检查 NULL 探测器
|
||||
if nullProbe, ok := v.ProbesMapKName["NULL"]; ok {
|
||||
common.LogDebug(fmt.Sprintf("成功获取NULL探测器,Data长度: %d", len(nullProbe.Data)))
|
||||
null = &nullProbe
|
||||
} else {
|
||||
common.LogDebug("警告: 未找到NULL探测器")
|
||||
}
|
||||
|
||||
// 获取并检查 GenericLines 探测器
|
||||
if genericProbe, ok := v.ProbesMapKName["GenericLines"]; ok {
|
||||
common.LogDebug(fmt.Sprintf("成功获取GenericLines探测器,Data长度: %d", len(genericProbe.Data)))
|
||||
commonProbe = &genericProbe
|
||||
} else {
|
||||
common.LogDebug("警告: 未找到GenericLines探测器")
|
||||
}
|
||||
|
||||
common.LogDebug("全局变量初始化完成")
|
||||
}
|
||||
|
||||
// GetGlobalVScan 获取全局VScan实例
|
||||
func GetGlobalVScan() *VScan {
|
||||
return &v
|
||||
}
|
||||
|
||||
// GetNullProbe 获取NULL探测器
|
||||
func GetNullProbe() *Probe {
|
||||
return null
|
||||
}
|
||||
|
||||
// GetCommonProbe 获取通用探测器
|
||||
func GetCommonProbe() *Probe {
|
||||
return commonProbe
|
||||
}
|
||||
|
||||
|
||||
func init() {
|
||||
InitializeGlobalVScan()
|
||||
}
|
67
Core/portfinger/types.go
Normal file
67
Core/portfinger/types.go
Normal file
@ -0,0 +1,67 @@
|
||||
package portfinger
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// VScan 主扫描器结构体
|
||||
type VScan struct {
|
||||
Exclude string
|
||||
AllProbes []Probe
|
||||
UdpProbes []Probe
|
||||
Probes []Probe
|
||||
ProbesMapKName map[string]Probe
|
||||
}
|
||||
|
||||
// Probe 探测器结构体
|
||||
type Probe struct {
|
||||
Name string // 探测器名称
|
||||
Data string // 探测数据
|
||||
Protocol string // 协议
|
||||
Ports string // 端口范围
|
||||
SSLPorts string // SSL端口范围
|
||||
|
||||
TotalWaitMS int // 总等待时间
|
||||
TCPWrappedMS int // TCP包装等待时间
|
||||
Rarity int // 稀有度
|
||||
Fallback string // 回退探测器名称
|
||||
|
||||
Matchs *[]Match // 匹配规则列表
|
||||
}
|
||||
|
||||
// Match 匹配规则结构体
|
||||
type Match struct {
|
||||
IsSoft bool // 是否为软匹配
|
||||
Service string // 服务名称
|
||||
Pattern string // 匹配模式
|
||||
VersionInfo string // 版本信息格式
|
||||
FoundItems []string // 找到的项目
|
||||
PatternCompiled *regexp.Regexp // 编译后的正则表达式
|
||||
}
|
||||
|
||||
// Directive 指令结构体
|
||||
type Directive struct {
|
||||
DirectiveName string
|
||||
Flag string
|
||||
Delimiter string
|
||||
DirectiveStr string
|
||||
}
|
||||
|
||||
// Extras 额外信息结构体
|
||||
type Extras struct {
|
||||
VendorProduct string
|
||||
Version string
|
||||
Info string
|
||||
Hostname string
|
||||
OperatingSystem string
|
||||
DeviceType string
|
||||
CPE string
|
||||
}
|
||||
|
||||
// Target 目标结构体
|
||||
type Target struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
131
Core/portfinger/version_parser.go
Normal file
131
Core/portfinger/version_parser.go
Normal file
@ -0,0 +1,131 @@
|
||||
package portfinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// ParseVersionInfo 解析版本信息并返回额外信息结构
|
||||
func (m *Match) ParseVersionInfo(response []byte) Extras {
|
||||
common.LogDebug("开始解析版本信息")
|
||||
var extras = Extras{}
|
||||
|
||||
// 确保有匹配项
|
||||
if len(m.FoundItems) == 0 {
|
||||
common.LogDebug("没有匹配项,无法解析版本信息")
|
||||
return extras
|
||||
}
|
||||
|
||||
// 替换版本信息中的占位符
|
||||
foundItems := m.FoundItems
|
||||
versionInfo := m.VersionInfo
|
||||
for index, value := range foundItems {
|
||||
dollarName := "$" + strconv.Itoa(index+1)
|
||||
versionInfo = strings.Replace(versionInfo, dollarName, value, -1)
|
||||
}
|
||||
common.LogDebug("替换后的版本信息: " + versionInfo)
|
||||
|
||||
// 定义解析函数
|
||||
parseField := func(field, pattern string) string {
|
||||
patterns := []string{
|
||||
pattern + `/([^/]*)/`, // 斜线分隔
|
||||
pattern + `\|([^|]*)\|`, // 竖线分隔
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
if strings.Contains(versionInfo, pattern) {
|
||||
regex := regexp.MustCompile(p)
|
||||
if matches := regex.FindStringSubmatch(versionInfo); len(matches) > 1 {
|
||||
common.LogDebug(fmt.Sprintf("解析到%s: %s", field, matches[1]))
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
extras.VendorProduct = parseField("厂商产品", " p")
|
||||
extras.Version = parseField("版本", " v")
|
||||
extras.Info = parseField("信息", " i")
|
||||
extras.Hostname = parseField("主机名", " h")
|
||||
extras.OperatingSystem = parseField("操作系统", " o")
|
||||
extras.DeviceType = parseField("设备类型", " d")
|
||||
|
||||
// 特殊处理CPE
|
||||
if strings.Contains(versionInfo, " cpe:/") || strings.Contains(versionInfo, " cpe:|") {
|
||||
cpePatterns := []string{`cpe:/([^/]*)`, `cpe:\|([^|]*)`}
|
||||
for _, pattern := range cpePatterns {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
if cpeName := regex.FindStringSubmatch(versionInfo); len(cpeName) > 0 {
|
||||
if len(cpeName) > 1 {
|
||||
extras.CPE = cpeName[1]
|
||||
} else {
|
||||
extras.CPE = cpeName[0]
|
||||
}
|
||||
common.LogDebug("解析到CPE: " + extras.CPE)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extras
|
||||
}
|
||||
|
||||
// ToMap 将 Extras 转换为 map[string]string
|
||||
func (e *Extras) ToMap() map[string]string {
|
||||
common.LogDebug("开始转换Extras为Map")
|
||||
result := make(map[string]string)
|
||||
|
||||
// 定义字段映射
|
||||
fields := map[string]string{
|
||||
"vendor_product": e.VendorProduct,
|
||||
"version": e.Version,
|
||||
"info": e.Info,
|
||||
"hostname": e.Hostname,
|
||||
"os": e.OperatingSystem,
|
||||
"device_type": e.DeviceType,
|
||||
"cpe": e.CPE,
|
||||
}
|
||||
|
||||
// 添加非空字段到结果map
|
||||
for key, value := range fields {
|
||||
if value != "" {
|
||||
result[key] = value
|
||||
common.LogDebug(fmt.Sprintf("添加字段 %s: %s", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("转换完成,共有 %d 个字段", len(result)))
|
||||
return result
|
||||
}
|
||||
|
||||
// TrimBanner 清理横幅数据,移除不可打印字符
|
||||
func TrimBanner(banner string) string {
|
||||
// 移除开头和结尾的空白字符
|
||||
banner = strings.TrimSpace(banner)
|
||||
|
||||
// 移除控制字符,但保留换行符和制表符
|
||||
var result strings.Builder
|
||||
for _, r := range banner {
|
||||
if r >= 32 && r <= 126 { // 可打印ASCII字符
|
||||
result.WriteRune(r)
|
||||
} else if r == '\n' || r == '\t' { // 保留换行符和制表符
|
||||
result.WriteRune(r)
|
||||
} else {
|
||||
result.WriteRune(' ') // 其他控制字符替换为空格
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩多个连续空格为单个空格
|
||||
resultStr := result.String()
|
||||
spaceRe := regexp.MustCompile(`\s+`)
|
||||
resultStr = spaceRe.ReplaceAllString(resultStr, " ")
|
||||
|
||||
return strings.TrimSpace(resultStr)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user