mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-04-04 20:37:37 +03:00
224 lines
5.8 KiB
Go
224 lines
5.8 KiB
Go
package script
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/option"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
F "github.com/sagernet/sing/common/format"
|
|
"github.com/sagernet/sing/common/logger"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
"github.com/sagernet/sing/common/ntp"
|
|
"github.com/sagernet/sing/service"
|
|
"github.com/sagernet/sing/service/pause"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
var _ Source = (*RemoteSource)(nil)
|
|
|
|
type RemoteSource struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
logger logger.Logger
|
|
outbound adapter.OutboundManager
|
|
options option.Script
|
|
updateInterval time.Duration
|
|
dialer N.Dialer
|
|
program *goja.Program
|
|
lastUpdated time.Time
|
|
lastEtag string
|
|
updateTicker *time.Ticker
|
|
cacheFile adapter.CacheFile
|
|
pauseManager pause.Manager
|
|
}
|
|
|
|
func NewRemoteSource(ctx context.Context, logger logger.Logger, options option.Script) (*RemoteSource, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
var updateInterval time.Duration
|
|
if options.RemoteOptions.UpdateInterval > 0 {
|
|
updateInterval = time.Duration(options.RemoteOptions.UpdateInterval)
|
|
} else {
|
|
updateInterval = 24 * time.Hour
|
|
}
|
|
return &RemoteSource{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
logger: logger,
|
|
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
|
options: options,
|
|
updateInterval: updateInterval,
|
|
pauseManager: service.FromContext[pause.Manager](ctx),
|
|
}, nil
|
|
}
|
|
|
|
func (s *RemoteSource) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error {
|
|
s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
|
|
var dialer N.Dialer
|
|
if s.options.RemoteOptions.DownloadDetour != "" {
|
|
outbound, loaded := s.outbound.Outbound(s.options.RemoteOptions.DownloadDetour)
|
|
if !loaded {
|
|
return E.New("download detour not found: ", s.options.RemoteOptions.DownloadDetour)
|
|
}
|
|
dialer = outbound
|
|
} else {
|
|
dialer = s.outbound.Default()
|
|
}
|
|
s.dialer = dialer
|
|
if s.cacheFile != nil {
|
|
if savedSet := s.cacheFile.LoadScript(s.options.Tag); savedSet != nil {
|
|
err := s.loadBytes(savedSet.Content)
|
|
if err != nil {
|
|
return E.Cause(err, "restore cached rule-set")
|
|
}
|
|
s.lastUpdated = savedSet.LastUpdated
|
|
s.lastEtag = savedSet.LastEtag
|
|
}
|
|
}
|
|
if s.lastUpdated.IsZero() {
|
|
err := s.fetchOnce(ctx, startContext)
|
|
if err != nil {
|
|
return E.Cause(err, "initial rule-set: ", s.options.Tag)
|
|
}
|
|
}
|
|
s.updateTicker = time.NewTicker(s.updateInterval)
|
|
return nil
|
|
}
|
|
|
|
func (s *RemoteSource) PostStart() error {
|
|
go s.loopUpdate()
|
|
return nil
|
|
}
|
|
|
|
func (s *RemoteSource) Program() *goja.Program {
|
|
return s.program
|
|
}
|
|
|
|
func (s *RemoteSource) loadBytes(content []byte) error {
|
|
program, err := goja.Compile(F.ToString("script:", s.options.Tag), string(content), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.program = program
|
|
return nil
|
|
}
|
|
|
|
func (s *RemoteSource) loopUpdate() {
|
|
if time.Since(s.lastUpdated) > s.updateInterval {
|
|
err := s.fetchOnce(s.ctx, nil)
|
|
if err != nil {
|
|
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
|
}
|
|
}
|
|
for {
|
|
runtime.GC()
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case <-s.updateTicker.C:
|
|
s.pauseManager.WaitActive()
|
|
err := s.fetchOnce(s.ctx, nil)
|
|
if err != nil {
|
|
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *RemoteSource) fetchOnce(ctx context.Context, startContext *adapter.HTTPStartContext) error {
|
|
s.logger.Debug("updating script ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
|
|
var httpClient *http.Client
|
|
if startContext != nil {
|
|
httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer)
|
|
} else {
|
|
httpClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
ForceAttemptHTTP2: true,
|
|
TLSHandshakeTimeout: C.TCPTimeout,
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
},
|
|
TLSClientConfig: &tls.Config{
|
|
Time: ntp.TimeFuncFromContext(s.ctx),
|
|
RootCAs: adapter.RootPoolFromContext(s.ctx),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s.lastEtag != "" {
|
|
request.Header.Set("If-None-Match", s.lastEtag)
|
|
}
|
|
response, err := httpClient.Do(request.WithContext(ctx))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch response.StatusCode {
|
|
case http.StatusOK:
|
|
case http.StatusNotModified:
|
|
s.lastUpdated = time.Now()
|
|
if s.cacheFile != nil {
|
|
savedRuleSet := s.cacheFile.LoadScript(s.options.Tag)
|
|
if savedRuleSet != nil {
|
|
savedRuleSet.LastUpdated = s.lastUpdated
|
|
err = s.cacheFile.SaveScript(s.options.Tag, savedRuleSet)
|
|
if err != nil {
|
|
s.logger.Error("save script updated time: ", err)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
s.logger.Info("update script ", s.options.Tag, ": not modified")
|
|
return nil
|
|
default:
|
|
return E.New("unexpected status: ", response.Status)
|
|
}
|
|
content, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
response.Body.Close()
|
|
return err
|
|
}
|
|
err = s.loadBytes(content)
|
|
if err != nil {
|
|
response.Body.Close()
|
|
return err
|
|
}
|
|
response.Body.Close()
|
|
eTagHeader := response.Header.Get("Etag")
|
|
if eTagHeader != "" {
|
|
s.lastEtag = eTagHeader
|
|
}
|
|
s.lastUpdated = time.Now()
|
|
if s.cacheFile != nil {
|
|
err = s.cacheFile.SaveScript(s.options.Tag, &adapter.SavedBinary{
|
|
LastUpdated: s.lastUpdated,
|
|
Content: content,
|
|
LastEtag: s.lastEtag,
|
|
})
|
|
if err != nil {
|
|
s.logger.Error("save script cache: ", err)
|
|
}
|
|
}
|
|
s.logger.Info("updated script ", s.options.Tag)
|
|
return nil
|
|
}
|
|
|
|
func (s *RemoteSource) Close() error {
|
|
if s.updateTicker != nil {
|
|
s.updateTicker.Stop()
|
|
}
|
|
s.cancel()
|
|
return nil
|
|
}
|