mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
fetch links
This commit is contained in:
parent
fd9184bbde
commit
1f55082c0c
1
box.go
1
box.go
@ -170,6 +170,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||||
|
logFactory,
|
||||||
serviceOptions)
|
serviceOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse outbound[", i, "]")
|
return nil, E.Cause(err, "parse outbound[", i, "]")
|
||||||
|
@ -10,13 +10,13 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Service) (adapter.BoxService, error) {
|
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, logFactory log.Factory, options option.Service) (adapter.BoxService, error) {
|
||||||
if options.Type == "" {
|
if options.Type == "" {
|
||||||
return nil, E.New("missing service type")
|
return nil, E.New("missing service type")
|
||||||
}
|
}
|
||||||
switch options.Type {
|
switch options.Type {
|
||||||
case C.ServiceSubscription:
|
case C.ServiceSubscription:
|
||||||
return NewSubscription(router, logger, options)
|
return NewSubscription(ctx, router, logger, logFactory, options)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown service type: ", options.Type)
|
return nil, E.New("unknown service type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,24 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/link"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/outbound"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.BoxService = (*Subscription)(nil)
|
var _ adapter.BoxService = (*Subscription)(nil)
|
||||||
@ -12,23 +26,195 @@ var _ adapter.BoxService = (*Subscription)(nil)
|
|||||||
// Subscription is a service that subscribes to remote servers for outbounds.
|
// Subscription is a service that subscribes to remote servers for outbounds.
|
||||||
type Subscription struct {
|
type Subscription struct {
|
||||||
myServiceAdapter
|
myServiceAdapter
|
||||||
|
|
||||||
|
parentCtx context.Context
|
||||||
|
logFactory log.Factory
|
||||||
|
options option.SubscriptionServiceOptions
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSubscription(router adapter.Router, logger log.ContextLogger, options option.Service) (*Subscription, error) {
|
// NewSubscription creates a new subscription service.
|
||||||
|
func NewSubscription(ctx context.Context, router adapter.Router, logger log.ContextLogger, logFactory log.Factory, options option.Service) (*Subscription, error) {
|
||||||
|
ctx2, cancel := context.WithCancel(ctx)
|
||||||
return &Subscription{
|
return &Subscription{
|
||||||
myServiceAdapter{
|
myServiceAdapter: myServiceAdapter{
|
||||||
router: router,
|
router: router,
|
||||||
serviceType: C.ServiceSubscription,
|
serviceType: C.ServiceSubscription,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
tag: options.Tag,
|
tag: options.Tag,
|
||||||
},
|
},
|
||||||
|
options: options.SubscriptionOptions,
|
||||||
|
parentCtx: ctx,
|
||||||
|
logFactory: logFactory,
|
||||||
|
ctx: ctx2,
|
||||||
|
cancel: cancel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start starts the service.
|
||||||
func (s *Subscription) Start() error {
|
func (s *Subscription) Start() error {
|
||||||
panic("implement me")
|
go s.fetchLoop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the service.
|
||||||
func (s *Subscription) Close() error {
|
func (s *Subscription) Close() error {
|
||||||
panic("implement me")
|
if s.cancel != nil {
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) fetchLoop() {
|
||||||
|
ticker := time.NewTicker(time.Duration(s.options.Interval))
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
if err := s.fetch(); err != nil {
|
||||||
|
s.logger.Error("fetch subscription: ", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
break
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := s.fetch(); err != nil {
|
||||||
|
s.logger.Error("fetch subscription: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) fetch() error {
|
||||||
|
client, err := s.client()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, provider := range s.options.Providers {
|
||||||
|
var tag string
|
||||||
|
if provider.Tag != "" {
|
||||||
|
tag = provider.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
links, err := s.fetchProvider(client, provider)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("fetch provider [", tag, "]: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.logger.Info(len(links), " links found from provider [", tag, "]")
|
||||||
|
for _, link := range links {
|
||||||
|
// TODO: filter links
|
||||||
|
opt := link.Options()
|
||||||
|
s.applyOptions(opt, &provider)
|
||||||
|
outbound, err := outbound.New(
|
||||||
|
s.parentCtx,
|
||||||
|
s.router,
|
||||||
|
s.logFactory.NewLogger(F.ToString("outbound/", opt.Type, "[", opt.Tag, "]")),
|
||||||
|
*opt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("create outbound [", opt.Tag, "]: ", err)
|
||||||
|
}
|
||||||
|
s.router.AddOutbound(outbound)
|
||||||
|
s.logger.Info("created outbound [", opt.Tag, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) applyOptions(options *option.Outbound, provider *option.SubscriptionProviderOptions) {
|
||||||
|
options.Tag = s.tag + provider.Tag + options.Tag
|
||||||
|
// TODO: implement me
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) fetchProvider(client *http.Client, provider option.SubscriptionProviderOptions) ([]link.Link, error) {
|
||||||
|
req, err := http.NewRequestWithContext(s.ctx, http.MethodGet, provider.URL, nil)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, E.New("unexpected status code: ", resp.StatusCode)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// try undecoded
|
||||||
|
content := string(body)
|
||||||
|
links, err := linksFromContent(content)
|
||||||
|
if len(links) > 0 {
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("some links failed", err)
|
||||||
|
}
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
// try decoded
|
||||||
|
decoded, err := base64Decode(content)
|
||||||
|
links, err = linksFromContent(string(decoded))
|
||||||
|
if len(links) > 0 {
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("some links failed", err)
|
||||||
|
}
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
return nil, E.New("no links found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func linksFromContent(content string) ([]link.Link, error) {
|
||||||
|
links := make([]link.Link, 0)
|
||||||
|
errs := make([]error, 0)
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
link, err := link.Parse(line)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
return links, E.Errors(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64Decode(b64 string) ([]byte, error) {
|
||||||
|
b64 = strings.TrimSpace(b64)
|
||||||
|
stdb64 := b64
|
||||||
|
if pad := len(b64) % 4; pad != 0 {
|
||||||
|
stdb64 += strings.Repeat("=", 4-pad)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := base64.StdEncoding.DecodeString(stdb64)
|
||||||
|
if err != nil {
|
||||||
|
return base64.URLEncoding.DecodeString(b64)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) client() (*http.Client, error) {
|
||||||
|
var detour adapter.Outbound
|
||||||
|
if s.options.DownloadDetour != "" {
|
||||||
|
outbound, loaded := s.router.Outbound(s.options.DownloadDetour)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("detour outbound not found: ", s.options.DownloadDetour)
|
||||||
|
}
|
||||||
|
detour = outbound
|
||||||
|
} else {
|
||||||
|
detour = s.router.DefaultOutbound(N.NetworkTCP)
|
||||||
|
}
|
||||||
|
return &http.Client{
|
||||||
|
Timeout: time.Second * 30,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user