package Plugins import ( "compress/gzip" "context" "crypto/tls" "fmt" "io" "net" "net/http" "regexp" "strings" "sync" "time" "unicode/utf8" "github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/WebScan" "github.com/shadow1ng/fscan/WebScan/lib" "golang.org/x/text/encoding/simplifiedchinese" ) // 常量定义 const ( maxTitleLength = 100 defaultProtocol = "http" httpsProtocol = "https" httpProtocol = "http" printerFingerPrint = "打印机" emptyTitle = "\"\"" noTitleText = "无标题" // HTTP相关常量 httpPort = "80" httpsPort = "443" contentEncoding = "Content-Encoding" gzipEncoding = "gzip" contentLength = "Content-Length" ) // 错误定义 var ( ErrNoTitle = fmt.Errorf("无法获取标题") ErrHTTPClientInit = fmt.Errorf("HTTP客户端未初始化") ErrReadRespBody = fmt.Errorf("读取响应内容失败") ) // 响应结果 type WebResponse struct { Url string StatusCode int Title string Length string Headers map[string]string RedirectUrl string Body []byte Error error } // 协议检测结果 type ProtocolResult struct { Protocol string Success bool } // WebTitle 获取Web标题和指纹信息 func WebTitle(info *Common.HostInfo) error { if info == nil { return fmt.Errorf("主机信息为空") } // 初始化Url if err := initializeUrl(info); err != nil { Common.LogError(fmt.Sprintf("初始化Url失败: %v", err)) return err } // 获取网站标题信息 checkData, err := fetchWebInfo(info) if err != nil { // 记录错误但继续处理可能获取的数据 Common.LogError(fmt.Sprintf("获取网站信息失败: %s %v", info.Url, err)) } // 分析指纹 if len(checkData) > 0 { info.Infostr = WebScan.InfoCheck(info.Url, &checkData) // 检查是否为打印机,避免意外打印 for _, v := range info.Infostr { if v == printerFingerPrint { Common.LogBase("检测到打印机,停止扫描") return nil } } } return err } // 初始化Url:根据主机和端口生成完整Url func initializeUrl(info *Common.HostInfo) error { if info.Url == "" { // 根据端口推断Url switch info.Ports { case httpPort: info.Url = fmt.Sprintf("%s://%s", httpProtocol, info.Host) case httpsPort: info.Url = fmt.Sprintf("%s://%s", httpsProtocol, info.Host) default: host := fmt.Sprintf("%s:%s", info.Host, info.Ports) protocol, err := detectProtocol(host, Common.Timeout) if err != nil { return fmt.Errorf("协议检测失败: %w", err) } info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports) } } else if !strings.Contains(info.Url, "://") { // 处理未指定协议的Url host := strings.Split(info.Url, "/")[0] protocol, err := detectProtocol(host, Common.Timeout) if err != nil { return fmt.Errorf("协议检测失败: %w", err) } info.Url = fmt.Sprintf("%s://%s", protocol, info.Url) } return nil } // 获取Web信息:标题、指纹等 func fetchWebInfo(info *Common.HostInfo) ([]WebScan.CheckDatas, error) { var checkData []WebScan.CheckDatas // 记录原始Url协议 originalUrl := info.Url isHTTPS := strings.HasPrefix(info.Url, "https://") // 第一次尝试访问Url resp, err := fetchUrlWithRetry(info, false, &checkData) // 处理不同的错误情况 if err != nil { // 如果是HTTPS并失败,尝试降级到HTTP if isHTTPS { info.Url = strings.Replace(info.Url, "https://", "http://", 1) resp, err = fetchUrlWithRetry(info, false, &checkData) // 如果HTTP也失败,恢复原始Url并返回错误 if err != nil { info.Url = originalUrl return checkData, err } } else { return checkData, err } } // 处理重定向 if resp != nil && resp.RedirectUrl != "" { info.Url = resp.RedirectUrl resp, err = fetchUrlWithRetry(info, true, &checkData) // 如果重定向后失败,尝试降级协议 if err != nil && strings.HasPrefix(info.Url, "https://") { info.Url = strings.Replace(info.Url, "https://", "http://", 1) resp, err = fetchUrlWithRetry(info, true, &checkData) } } // 处理需要升级到HTTPS的情况 if resp != nil && resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https://") { info.Url = strings.Replace(info.Url, "http://", "https://", 1) resp, err = fetchUrlWithRetry(info, false, &checkData) // 如果HTTPS升级失败,回退到HTTP if err != nil { info.Url = strings.Replace(info.Url, "https://", "http://", 1) resp, err = fetchUrlWithRetry(info, false, &checkData) } // 处理升级后的重定向 if resp != nil && resp.RedirectUrl != "" { info.Url = resp.RedirectUrl resp, err = fetchUrlWithRetry(info, true, &checkData) } } return checkData, err } // 尝试获取Url,支持重试 func fetchUrlWithRetry(info *Common.HostInfo, followRedirect bool, checkData *[]WebScan.CheckDatas) (*WebResponse, error) { // 获取页面内容 resp, err := fetchUrl(info.Url, followRedirect) if err != nil { return nil, err } // 保存检查数据 if resp.Body != nil && len(resp.Body) > 0 { headers := fmt.Sprintf("%v", resp.Headers) *checkData = append(*checkData, WebScan.CheckDatas{resp.Body, headers}) } // 保存扫描结果 if resp.StatusCode > 0 { saveWebResult(info, resp) } return resp, nil } // 抓取Url内容 func fetchUrl(targetUrl string, followRedirect bool) (*WebResponse, error) { // 创建HTTP请求 req, err := http.NewRequest("GET", targetUrl, nil) if err != nil { return nil, fmt.Errorf("创建HTTP请求失败: %w", err) } // 设置请求头 req.Header.Set("User-agent", Common.UserAgent) req.Header.Set("Accept", Common.Accept) req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") if Common.Cookie != "" { req.Header.Set("Cookie", Common.Cookie) } req.Header.Set("Connection", "close") // 选择HTTP客户端 var client *http.Client if followRedirect { client = lib.Client } else { client = lib.ClientNoRedirect } if client == nil { return nil, ErrHTTPClientInit } // 发送请求 resp, err := client.Do(req) if err != nil { // 特殊处理SSL/TLS相关错误 errMsg := strings.ToLower(err.Error()) if strings.Contains(errMsg, "tls") || strings.Contains(errMsg, "ssl") || strings.Contains(errMsg, "handshake") || strings.Contains(errMsg, "certificate") { return &WebResponse{Error: err}, nil } return nil, err } defer resp.Body.Close() // 准备响应结果 result := &WebResponse{ Url: req.URL.String(), StatusCode: resp.StatusCode, Headers: make(map[string]string), } // 提取响应头 for k, v := range resp.Header { if len(v) > 0 { result.Headers[k] = v[0] } } // 获取内容长度 result.Length = resp.Header.Get(contentLength) // 检查重定向 redirectUrl, err := resp.Location() if err == nil { result.RedirectUrl = redirectUrl.String() } // 读取响应内容 body, err := readResponseBody(resp) if err != nil { return result, fmt.Errorf("读取响应内容失败: %w", err) } result.Body = body // 提取标题 if !utf8.Valid(body) { body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body) } result.Title = extractTitle(body) if result.Length == "" { result.Length = fmt.Sprintf("%d", len(body)) } return result, nil } // 读取HTTP响应体内容 func readResponseBody(resp *http.Response) ([]byte, error) { var body []byte var reader io.Reader = resp.Body // 处理gzip压缩的响应 if resp.Header.Get(contentEncoding) == gzipEncoding { gr, err := gzip.NewReader(resp.Body) if err != nil { return nil, fmt.Errorf("创建gzip解压器失败: %w", err) } defer gr.Close() reader = gr } // 读取内容 body, err := io.ReadAll(reader) if err != nil { return nil, fmt.Errorf("读取响应内容失败: %w", err) } return body, nil } // 提取网页标题 func extractTitle(body []byte) string { // 使用正则表达式匹配title标签内容 re := regexp.MustCompile("(?ims)