fscan/plugins/web/webtitle.go
ZacharyZcR 43ddb3630d feat: 完善本地插件控制机制和参数验证
- 实现本地插件严格单个指定控制,拒绝多插件分隔符
- 修复本地插件自动调用问题,避免不必要的插件实例创建
- 添加-local与-h/-u参数的互斥性检查
- 优化插件存在性检查,使用pluginExists()替代plugins.Get()
- 完善统一插件系统的端口信息管理
- 增强Web插件的协议智能检测功能

主要变更:
* 本地插件现在只能通过-local参数明确指定单个插件运行
* 插件适用性检查不再创建不必要的插件实例,提升性能
* 本地扫描与网络扫描参数完全隔离,避免配置冲突
2025-08-26 19:34:14 +08:00

156 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package web
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/shadow1ng/fscan/common"
)
// WebTitlePlugin Web标题获取插件
type WebTitlePlugin struct {
name string
}
// NewWebTitlePlugin 创建WebTitle插件
func NewWebTitlePlugin() *WebTitlePlugin {
return &WebTitlePlugin{
name: "webtitle",
}
}
// GetName 实现Plugin接口
func (p *WebTitlePlugin) GetName() string {
return p.name
}
// GetPorts 实现Plugin接口 - Web插件不需要预定义端口
func (p *WebTitlePlugin) GetPorts() []int {
return []int{}
}
// Scan 执行WebTitle扫描
func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo) *WebScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
title, status, server, err := p.getWebTitle(ctx, info)
if err != nil {
return &WebScanResult{
Success: false,
Error: err,
}
}
msg := fmt.Sprintf("WebTitle %s", target)
if title != "" {
msg += fmt.Sprintf(" [%s]", title)
}
if status != 0 {
msg += fmt.Sprintf(" %d", status)
}
if server != "" {
msg += fmt.Sprintf(" %s", server)
}
common.LogSuccess(msg)
return &WebScanResult{
Success: true,
Title: title,
Status: status,
Server: server,
}
}
func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo) (string, int, string, error) {
// 智能协议检测
protocol := p.detectProtocol(info)
url := fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
client := &http.Client{
Timeout: time.Duration(common.WebTimeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: true,
},
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", 0, "", err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
resp, err := client.Do(req)
if err != nil {
return "", 0, "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", resp.StatusCode, resp.Header.Get("Server"), err
}
title := p.extractTitle(string(body))
return title, resp.StatusCode, resp.Header.Get("Server"), nil
}
// detectProtocol 智能检测HTTP/HTTPS协议
func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo) string {
port := info.Ports
// 已知的HTTPS端口
httpsPorts := []string{"443", "8443", "9443"}
for _, httpsPort := range httpsPorts {
if port == httpsPort {
return "https"
}
}
// 常见HTTP端口优先使用HTTP
httpPorts := []string{"80", "8080", "8000", "8888", "8090", "3000", "5000", "9000"}
for _, httpPort := range httpPorts {
if port == httpPort {
return "http"
}
}
// 对于其他端口优先尝试HTTP
return "http"
}
func (p *WebTitlePlugin) extractTitle(html string) string {
titleRe := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
matches := titleRe.FindStringSubmatch(html)
if len(matches) > 1 {
title := strings.TrimSpace(matches[1])
title = regexp.MustCompile(`\s+`).ReplaceAllString(title, " ")
if len(title) > 100 {
title = title[:100] + "..."
}
if utf8.ValidString(title) {
return title
}
}
return ""
}
func init() {
RegisterWebPlugin("webtitle", func() WebPlugin {
return NewWebTitlePlugin()
})
}