proxy-detector/pkg/health-check/health.go

140 lines
3.7 KiB
Go
Raw Normal View History

package healthcheck
import (
"context"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"sync"
"time"
2024-12-11 11:13:06 +08:00
"gitea.timerzz.com/timerzz/proxy-detector/config"
"gitea.timerzz.com/timerzz/proxy-detector/log"
"gitea.timerzz.com/timerzz/proxy-detector/pkg/proxy/structs"
"gitea.timerzz.com/timerzz/proxy-detector/pkg/worker"
C "github.com/metacubex/mihomo/constant"
)
// Return 0 for error
func HealthCheck(ctx context.Context, proxy C.Proxy) (err error) {
return HTTPGetViaProxy(ctx, proxy, "https://www.coachoutlet.com/api/get-shop/bags/view-all")
}
func CleanBadProxies(ctx context.Context, proxies []structs.Proxy) (newProxies []structs.Proxy) {
newProxies = make([]structs.Proxy, 0, len(proxies))
var m sync.Mutex
var wg sync.WaitGroup
for _, p := range proxies {
var proxy = p
wg.Add(1)
err := worker.Pool.Submit(func() {
defer wg.Done()
err := HealthCheck(ctx, proxy.ClashProxy())
if err == nil {
m.Lock()
newProxies = append(newProxies, proxy)
m.Unlock()
}
})
if err != nil {
log.Errorln("添加并发任务失败: ", err)
}
}
wg.Wait()
return newProxies
}
func HTTPGetViaProxy(ctx context.Context, clashProxy C.Proxy, url string) error {
ctx, cancel := context.WithTimeout(ctx, time.Duration(config.Config.HealthCheckTimeout)*time.Second)
defer cancel()
addr, err := urlToMetadata(url)
if err != nil {
return err
}
conn, err := clashProxy.DialContext(ctx, &addr) // 建立到proxy server的connection对Proxy的类别做了自适应相当于泛型
if err != nil {
return err
}
defer conn.Close()
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return err
}
req = req.WithContext(ctx)
transport := &http.Transport{
// Note: Dial specifies the dial function for creating unencrypted TCP connections.
// When httpClient sets this transport, it will use the tcp/udp connection returned from
// function Dial instead of default tcp/udp connection. It's the key to set custom proxy for http transport
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
req.Header = http.Header{
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"},
"sec-fetch-site": []string{"same-origin"},
"upgrade-insecure-requests": []string{"1"},
"sec-fetch-dest": []string{"empty"},
"sec-fetch-mode": []string{"navigate"},
"Accept": []string{"*/*"},
"Accept-Encoding": []string{"gzip, deflate, br"},
"Connection": []string{"keep-alive"},
}
client := http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("%d %s for proxy %s %s", resp.StatusCode, resp.Status, clashProxy.Name(), clashProxy.Addr())
}
resp.Body.Close()
return nil
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: uint16(uintPort),
}
return
}