Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 76 additions & 3 deletions adapter/adapter.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package adapter

import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/url"
"strings"
"time"

"github.com/andybalholm/brotli"
"github.com/dlclark/regexp2"

"github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/queue"
"github.com/metacubex/mihomo/common/utils"
Expand Down Expand Up @@ -163,7 +168,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {

// URLTest get the delay for the specified URL
// implements C.Proxy
func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (t uint16, err error) {
func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], options ...C.HealthCheckOption) (t uint16, err error) {
var satisfied bool

defer func() {
Expand Down Expand Up @@ -215,11 +220,29 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
_ = instance.Close()
}()

req, err := http.NewRequest(http.MethodHead, url, nil)
option := C.HealthCheckOption{ExpectedStatus: expectedStatus}.WithDefault()
if len(options) != 0 {
option = options[0].WithDefault()
}

method := http.MethodHead
if option.Method == C.HealthCheckMethodGet {
method = http.MethodGet
}

req, err := http.NewRequest(method, url, nil)
if err != nil {
return
}
req = req.WithContext(ctx)
for key, values := range option.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
if method == http.MethodGet && req.Header.Get("Accept-Encoding") == "" {
req.Header.Set("Accept-Encoding", "gzip, br")
}

tlsConfig, err := ca.GetTLSConfig(ca.Option{})
if err != nil {
Expand All @@ -230,6 +253,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil
},
DisableCompression: true,
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
Expand All @@ -254,7 +278,11 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
return
}

satisfied, err = healthCheckResponseSatisfied(resp, option)
_ = resp.Body.Close()
if err != nil {
return
}

if unifiedDelay {
second := time.Now()
Expand All @@ -263,7 +291,11 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
secondResp, ignoredErr = client.Do(req)
if ignoredErr == nil {
resp = secondResp
satisfied, err = healthCheckResponseSatisfied(resp, option)
_ = resp.Body.Close()
if err != nil {
return
}
start = second
} else {
if strings.HasPrefix(url, "http://") {
Expand All @@ -273,11 +305,52 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
}
}

satisfied = resp != nil && (expectedStatus == nil || expectedStatus.Check(uint16(resp.StatusCode)))
t = uint16(time.Since(start) / time.Millisecond)
return
}

func healthCheckResponseSatisfied(resp *http.Response, option C.HealthCheckOption) (bool, error) {
if resp == nil {
return false, nil
}
if option.ExpectedStatus != nil && !option.ExpectedStatus.Check(uint16(resp.StatusCode)) {
return false, nil
}
if option.Method != C.HealthCheckMethodGet || option.ExpectedBodyMatch == "" {
return true, nil
}

body, err := readHealthCheckBody(resp)
if err != nil {
return false, err
}
matcher, err := regexp2.Compile(option.ExpectedBodyMatch, regexp2.None)
if err != nil {
return false, err
}
matched, err := matcher.MatchString(string(body))
if err != nil {
return false, err
}
return matched, nil
}

func readHealthCheckBody(resp *http.Response) ([]byte, error) {
reader := resp.Body
switch strings.ToLower(strings.TrimSpace(resp.Header.Get("Content-Encoding"))) {
case "gzip":
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return nil, err
}
defer gzipReader.Close()
reader = gzipReader
case "br":
reader = io.NopCloser(brotli.NewReader(resp.Body))
}
return io.ReadAll(reader)
}

func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{
ProxyAdapter: adapter,
Expand Down
20 changes: 10 additions & 10 deletions adapter/outboundgroup/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import (

"github.com/metacubex/mihomo/common/callback"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
C "github.com/metacubex/mihomo/constant"
P "github.com/metacubex/mihomo/constant/provider"
)

type Fallback struct {
*GroupBase
disableUDP bool
testUrl string
selected string
expectedStatus string
disableUDP bool
testUrl string
selected string
expectedStatus string
healthCheckOption C.HealthCheckOption
}

func (f *Fallback) Now() string {
Expand Down Expand Up @@ -137,8 +137,7 @@ func (f *Fallback) Set(name string) error {
if !p.AliveForTestUrl(f.testUrl) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel()
expectedStatus, _ := utils.NewUnsignedRanges[uint16](f.expectedStatus)
_, _ = p.URLTest(ctx, f.testUrl, expectedStatus)
_, _ = p.URLTest(ctx, f.testUrl, f.healthCheckOption.ExpectedStatus, f.healthCheckOption)
}

return nil
Expand Down Expand Up @@ -170,8 +169,9 @@ func NewFallback(option *GroupCommonOption, providers []P.ProxyProvider) *Fallba
MaxFailedTimes: option.MaxFailedTimes,
Providers: providers,
}),
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
healthCheckOption: option.HealthCheckOption(),
}
}
8 changes: 6 additions & 2 deletions adapter/outboundgroup/groupbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,16 +228,20 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
return proxies
}

func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], options ...C.HealthCheckOption) (map[string]uint16, error) {
var wg sync.WaitGroup
var lock sync.Mutex
mp := map[string]uint16{}
healthCheckOption := C.HealthCheckOption{ExpectedStatus: expectedStatus}.WithDefault()
if len(options) != 0 {
healthCheckOption = options[0].WithDefault()
}
proxies := gb.GetProxies(false)
for _, proxy := range proxies {
proxy := proxy
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, url, expectedStatus)
delay, err := proxy.URLTest(ctx, url, healthCheckOption.ExpectedStatus, healthCheckOption)
if err == nil {
lock.Lock()
mp[proxy.Name()] = delay
Expand Down
60 changes: 37 additions & 23 deletions adapter/outboundgroup/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,33 @@ var (
)

type GroupCommonOption struct {
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
TestTimeout int `group:"timeout,omitempty"`
MaxFailedTimes int `group:"max-failed-times,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
ExpectedStatus string `group:"expected-status,omitempty"`
IncludeAll bool `group:"include-all,omitempty"`
IncludeAllProxies bool `group:"include-all-proxies,omitempty"`
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
Hidden bool `group:"hidden,omitempty"`
Icon string `group:"icon,omitempty"`
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
TestTimeout int `group:"timeout,omitempty"`
MaxFailedTimes int `group:"max-failed-times,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
ExpectedStatus string `group:"expected-status,omitempty"`
CheckMethod string `group:"check-method,omitempty"`
HTTPHeaders any `group:"http-headers,omitempty"`
ExpectedBodyMatch string `group:"expected-body-match,omitempty"`
HealthCheck C.HealthCheckOption `group:"-"`
IncludeAll bool `group:"include-all,omitempty"`
IncludeAllProxies bool `group:"include-all-proxies,omitempty"`
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
Hidden bool `group:"hidden,omitempty"`
Icon string `group:"icon,omitempty"`
}

func (o *GroupCommonOption) HealthCheckOption() C.HealthCheckOption {
return o.HealthCheck.WithDefault()
}

func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
Expand Down Expand Up @@ -110,12 +118,18 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
healthCheckOption, err := C.NewHealthCheckOption(expectedStatus, groupOption.CheckMethod, groupOption.HTTPHeaders, groupOption.ExpectedBodyMatch)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}

status := strings.TrimSpace(groupOption.ExpectedStatus)
if status == "" {
status = "*"
}
groupOption.ExpectedStatus = status
groupOption.CheckMethod = healthCheckOption.Method
groupOption.HealthCheck = healthCheckOption

if len(groupOption.Use) != 0 {
PDs, err := getProviders(providersMap, groupOption.Use)
Expand All @@ -135,7 +149,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
groupOption.URL = C.DefaultTestURL
}
} else {
addTestUrlToProviders(PDs, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval))
addTestUrlToProviders(PDs, groupOption.URL, healthCheckOption, groupOption.Filter, uint(groupOption.Interval))
}
providers = append(providers, PDs...)
}
Expand All @@ -161,7 +175,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
}
}

hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus)
hc := provider.NewHealthCheckWithOption(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, healthCheckOption)

pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
Expand Down Expand Up @@ -221,12 +235,12 @@ func getProviders(mapping map[string]P.ProxyProvider, list []string) ([]P.ProxyP
return ps, nil
}

func addTestUrlToProviders(providers []P.ProxyProvider, url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
func addTestUrlToProviders(providers []P.ProxyProvider, url string, option C.HealthCheckOption, filter string, interval uint) {
if len(providers) == 0 || len(url) == 0 {
return
}

for _, pd := range providers {
pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval)
pd.RegisterHealthCheckTask(url, option, filter, interval)
}
}
4 changes: 2 additions & 2 deletions adapter/outboundgroup/urltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ func (u *URLTest) Proxies() []C.Proxy {
return u.GetProxies(false)
}

func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus)
func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], options ...C.HealthCheckOption) (map[string]uint16, error) {
return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus, options...)
}

func parseURLTestOption(config map[string]any) []urlTestOption {
Expand Down
2 changes: 1 addition & 1 deletion adapter/outboundgroup/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type ProxyGroup interface {
Hidden() bool
Icon() string

URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error)
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], options ...C.HealthCheckOption) (mp map[string]uint16, err error)
}

var _ ProxyGroup = (*Fallback)(nil)
Expand Down
Loading