mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-09 03:54:15 +08:00
181 lines
4.6 KiB
Go
181 lines
4.6 KiB
Go
package outbound
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dlclark/regexp2"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
)
|
|
|
|
type Provider struct {
|
|
url string
|
|
filter *regexp2.Regexp
|
|
interval time.Duration
|
|
logger log.ContextLogger
|
|
ctx context.Context
|
|
router adapter.Router
|
|
}
|
|
|
|
type providerAdapter struct {
|
|
providers []Provider
|
|
}
|
|
|
|
type CachedProvider struct {
|
|
Outbounds []adapter.Outbound
|
|
LastUpdate time.Time
|
|
Lock *sync.Mutex
|
|
}
|
|
|
|
type ProviderResolver interface {
|
|
GetOutbounds([]byte, context.Context, adapter.Router, log.ContextLogger) []adapter.Outbound
|
|
}
|
|
|
|
var (
|
|
providerResolvers = make(map[string]ProviderResolver)
|
|
cachedProviders = make(map[string]*CachedProvider, 0)
|
|
compatibleProxy adapter.Outbound
|
|
)
|
|
|
|
func NewProvider(url, filterStr string, interval int, ctx context.Context, router adapter.Router,
|
|
logger log.ContextLogger) (Provider, error) {
|
|
if interval == 0 {
|
|
interval = 3600 * 24
|
|
}
|
|
filter, err := regexp2.Compile(filterStr, 0)
|
|
if err != nil {
|
|
return Provider{}, E.New("cannot parse provider regex filter")
|
|
}
|
|
return Provider{
|
|
url: url,
|
|
filter: filter,
|
|
interval: time.Duration(interval) * time.Second,
|
|
ctx: ctx,
|
|
router: router,
|
|
logger: logger},
|
|
nil
|
|
}
|
|
|
|
func (p *Provider) GetOutbounds() ([]string, map[string]adapter.Outbound) {
|
|
tags := make([]string, 0)
|
|
outbounds := make(map[string]adapter.Outbound, 0)
|
|
allOutbounds := p.getAllOutbounds()
|
|
for _, outbound := range allOutbounds {
|
|
if ok, _ := p.filter.MatchString(outbound.Tag()); ok {
|
|
p.router.AddOutbound(outbound.Tag(), outbound)
|
|
tags = append(tags, outbound.Tag())
|
|
outbounds[outbound.Tag()] = outbound
|
|
}
|
|
}
|
|
return tags, outbounds
|
|
}
|
|
|
|
func (p *Provider) getAllOutbounds() (res []adapter.Outbound) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
res = make([]adapter.Outbound, 0)
|
|
p.logger.Warn("failed to get provider outbounds: ", r)
|
|
}
|
|
}()
|
|
if _, ok := cachedProviders[p.url]; !ok {
|
|
cachedProviders[p.url] = &CachedProvider{
|
|
Outbounds: make([]adapter.Outbound, 0),
|
|
LastUpdate: time.Time{},
|
|
Lock: &sync.Mutex{},
|
|
}
|
|
}
|
|
cachedProviders[p.url].Lock.Lock()
|
|
defer cachedProviders[p.url].Lock.Unlock()
|
|
if (cachedProviders[p.url].LastUpdate.Add(p.interval)).Before(time.Now()) {
|
|
outbounds := make([]adapter.Outbound, 0)
|
|
resp, err := http.DefaultClient.Get(p.url)
|
|
if err == nil {
|
|
body := resp.Body
|
|
defer body.Close()
|
|
content, _ := io.ReadAll(body)
|
|
for _, resolver := range providerResolvers {
|
|
if len(outbounds) == 0 {
|
|
outbounds = resolver.GetOutbounds(content, p.ctx, p.router, p.logger)
|
|
}
|
|
}
|
|
cachedProviders[p.url].SetOutbounds(outbounds)
|
|
}
|
|
}
|
|
cachedProviders[p.url].RefeshUpdateTime()
|
|
return cachedProviders[p.url].Outbounds
|
|
}
|
|
|
|
func (p *providerAdapter) NewUpdateFunc(tags *[]string, outbounds *map[string]adapter.Outbound, router adapter.Router, funcs []func()) func() {
|
|
res := func() {
|
|
for _, f := range funcs {
|
|
defer f()
|
|
}
|
|
for _, provider := range p.providers {
|
|
_, newOutbounds := provider.GetOutbounds()
|
|
for k, v := range newOutbounds {
|
|
if _, ok := (*outbounds)[k]; !ok {
|
|
*tags = append(*tags, k)
|
|
(*outbounds)[k] = v
|
|
}
|
|
}
|
|
}
|
|
if len(*tags) == 0 {
|
|
p.AddCompatibleProxy(tags, outbounds, router)
|
|
}
|
|
if len(*tags) > 2 {
|
|
if _, ok := (*outbounds)["compatible"]; ok {
|
|
delete(*outbounds, "compatible")
|
|
for i, tag := range *tags {
|
|
if tag == "compatible" {
|
|
*tags = append((*tags)[:i], (*tags)[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
go func() {
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
res()
|
|
}
|
|
}()
|
|
return res
|
|
}
|
|
|
|
func (p *providerAdapter) AddCompatibleProxy(tags *[]string, outbounds *map[string]adapter.Outbound, router adapter.Router) {
|
|
if len(*tags) == 0 {
|
|
*tags = append(*tags, "compatible")
|
|
(*outbounds)["compatible"] = compatibleProxy
|
|
router.AddOutbound("compatible", compatibleProxy)
|
|
}
|
|
}
|
|
|
|
func (p *CachedProvider) SetOutbounds(outbounds []adapter.Outbound) {
|
|
p.Outbounds = outbounds
|
|
}
|
|
|
|
func (p *CachedProvider) RefeshUpdateTime() {
|
|
p.LastUpdate = time.Now()
|
|
}
|
|
|
|
func InjectClashProviderResolver(name string, resolver ProviderResolver) {
|
|
providerResolvers[name] = resolver
|
|
}
|
|
|
|
func InitCompatibleProxy(router adapter.Router, logger log.ContextLogger, opts option.DirectOutboundOptions) adapter.Outbound {
|
|
var err error
|
|
compatibleProxy, err = NewDirect(router, logger, "compatible", opts)
|
|
if err != nil {
|
|
logger.Panic("cannot create compatible proxy")
|
|
}
|
|
return compatibleProxy
|
|
}
|