174 lines
4.1 KiB
Go
174 lines
4.1 KiB
Go
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)<title[^>]*>([^<]+)</title>`)
|
|
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)<link[^>]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']+)["']`,
|
|
`(?i)<link[^>]*href=["']([^"']+)["'][^>]*rel=["'](?:shortcut )?icon["']`,
|
|
`(?i)<link[^>]*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
|
|
}
|