sing-box/outbound/provider.go
2022-09-12 14:02:08 +08:00

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
}