chore: sync local changes (2026-03-12)
This commit is contained in:
60
mengyaping-backend/utils/dns.go
Normal file
60
mengyaping-backend/utils/dns.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const dnsAPIBase = "https://cf-dns.smyhub.com/api/dns?domain="
|
||||
|
||||
type dnsResponse struct {
|
||||
Status string `json:"status"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
}
|
||||
|
||||
// ResolveDomainIPs 通过 DNS API 解析域名的 IPv4 + IPv6 地址
|
||||
func ResolveDomainIPs(rawURL string) ([]string, error) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostname := parsed.Hostname()
|
||||
if hostname == "" {
|
||||
return nil, fmt.Errorf("no hostname in URL")
|
||||
}
|
||||
|
||||
if net.ParseIP(hostname) != nil {
|
||||
return []string{hostname}, nil
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(dnsAPIBase + hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dnsResp dnsResponse
|
||||
if err := json.Unmarshal(body, &dnsResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dnsResp.Status != "success" {
|
||||
return nil, fmt.Errorf("DNS lookup failed for %s", hostname)
|
||||
}
|
||||
|
||||
ips := append(dnsResp.IPv4, dnsResp.IPv6...)
|
||||
return ips, nil
|
||||
}
|
||||
@@ -1,131 +1,173 @@
|
||||
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)<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 ""
|
||||
}
|
||||
|
||||
// 尝试从HTML中提取favicon链接
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回 /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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateID 生成唯一ID
|
||||
func GenerateID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
randomBytes := make([]byte, 4)
|
||||
rand.Read(randomBytes)
|
||||
return hex.EncodeToString([]byte{
|
||||
byte(timestamp >> 56),
|
||||
byte(timestamp >> 48),
|
||||
byte(timestamp >> 40),
|
||||
byte(timestamp >> 32),
|
||||
byte(timestamp >> 24),
|
||||
byte(timestamp >> 16),
|
||||
byte(timestamp >> 8),
|
||||
byte(timestamp),
|
||||
}) + hex.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// GenerateShortID 生成短ID
|
||||
func GenerateShortID() string {
|
||||
randomBytes := make([]byte, 6)
|
||||
rand.Read(randomBytes)
|
||||
return hex.EncodeToString(randomBytes)
|
||||
}
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateID 生成唯一ID
|
||||
func GenerateID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
randomBytes := make([]byte, 4)
|
||||
rand.Read(randomBytes)
|
||||
return hex.EncodeToString([]byte{
|
||||
byte(timestamp >> 56),
|
||||
byte(timestamp >> 48),
|
||||
byte(timestamp >> 40),
|
||||
byte(timestamp >> 32),
|
||||
byte(timestamp >> 24),
|
||||
byte(timestamp >> 16),
|
||||
byte(timestamp >> 8),
|
||||
byte(timestamp),
|
||||
}) + hex.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// GenerateShortID 生成短ID
|
||||
func GenerateShortID() string {
|
||||
randomBytes := make([]byte, 6)
|
||||
rand.Read(randomBytes)
|
||||
return hex.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user