package utils import ( "crypto/tls" "fmt" "io" "net" "net/http" "net/url" "regexp" "strings" "time" ) // HTTPClient HTTP客户端工具 type HTTPClient struct { client *http.Client } // NewHTTPClient 创建HTTP客户端 func NewHTTPClient(timeout time.Duration) *HTTPClient { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: timeout, DialContext: (&net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, } return &HTTPClient{ client: &http.Client{ Timeout: timeout, Transport: transport, 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 } // CheckWebsiteStatus 轻量级状态检测(不读取页面内容) func (c *HTTPClient) CheckWebsiteStatus(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", "*/*") req.Header.Set("Connection", "close") 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 以便连接正确释放 io.Copy(io.Discard, io.LimitReader(resp.Body, 4096)) return result } // 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)) 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 "" } 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) } } 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 }