package utils import ( "crypto/tls" "fmt" "io" "net/http" "net/url" "regexp" "strings" "time" ) // HTTPClient HTTP客户端工具 type HTTPClient struct { client *http.Client } // NewHTTPClient 创建HTTP客户端 func NewHTTPClient(timeout time.Duration) *HTTPClient { return &HTTPClient{ client: &http.Client{ Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 10 { return fmt.Errorf("too many redirects") } return nil }, }, } } // CheckResult 检查结果 type CheckResult struct { StatusCode int Latency time.Duration Title string Favicon string Error error } // CheckWebsite 检查网站 func (c *HTTPClient) CheckWebsite(targetURL string) CheckResult { result := CheckResult{} start := time.Now() req, err := http.NewRequest("GET", targetURL, nil) if err != nil { result.Error = err return result } req.Header.Set("User-Agent", "MengYaPing/1.0 (Website Monitor)") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") resp, err := c.client.Do(req) if err != nil { result.Error = err result.Latency = time.Since(start) return result } defer resp.Body.Close() result.Latency = time.Since(start) result.StatusCode = resp.StatusCode // 读取响应体获取标题 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100)) // 限制100KB if err == nil { result.Title = extractTitle(string(body)) result.Favicon = extractFavicon(string(body), targetURL) } return result } // extractTitle 提取网页标题 func extractTitle(html string) string { re := regexp.MustCompile(`(?i)]*>([^<]+)`) matches := re.FindStringSubmatch(html) if len(matches) > 1 { return strings.TrimSpace(matches[1]) } return "" } // extractFavicon 提取Favicon func extractFavicon(html string, baseURL string) string { parsedURL, err := url.Parse(baseURL) if err != nil { return "" } // 尝试从HTML中提取favicon链接 patterns := []string{ `(?i)]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']+)["']`, `(?i)]*href=["']([^"']+)["'][^>]*rel=["'](?:shortcut )?icon["']`, `(?i)]*rel=["']apple-touch-icon["'][^>]*href=["']([^"']+)["']`, } for _, pattern := range patterns { re := regexp.MustCompile(pattern) matches := re.FindStringSubmatch(html) if len(matches) > 1 { faviconURL := matches[1] return resolveURL(parsedURL, faviconURL) } } // 默认返回 /favicon.ico return fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host) } // resolveURL 解析相对URL func resolveURL(base *url.URL, ref string) string { refURL, err := url.Parse(ref) if err != nil { return ref } return base.ResolveReference(refURL).String() } // IsSuccessStatus 判断是否为成功状态码 func IsSuccessStatus(statusCode int) bool { return statusCode >= 200 && statusCode < 400 }