github.com/sagernet/sing-box@v1.9.0-rc.20/common/urltest/urltest.go (about)

     1  package urltest
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"net/http"
     7  	"net/url"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/sagernet/sing/common"
    12  	M "github.com/sagernet/sing/common/metadata"
    13  	N "github.com/sagernet/sing/common/network"
    14  )
    15  
    16  type History struct {
    17  	Time  time.Time `json:"time"`
    18  	Delay uint16    `json:"delay"`
    19  }
    20  
    21  type HistoryStorage struct {
    22  	access       sync.RWMutex
    23  	delayHistory map[string]*History
    24  	updateHook   chan<- struct{}
    25  }
    26  
    27  func NewHistoryStorage() *HistoryStorage {
    28  	return &HistoryStorage{
    29  		delayHistory: make(map[string]*History),
    30  	}
    31  }
    32  
    33  func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
    34  	s.updateHook = hook
    35  }
    36  
    37  func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
    38  	if s == nil {
    39  		return nil
    40  	}
    41  	s.access.RLock()
    42  	defer s.access.RUnlock()
    43  	return s.delayHistory[tag]
    44  }
    45  
    46  func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
    47  	s.access.Lock()
    48  	delete(s.delayHistory, tag)
    49  	s.access.Unlock()
    50  	s.notifyUpdated()
    51  }
    52  
    53  func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
    54  	s.access.Lock()
    55  	s.delayHistory[tag] = history
    56  	s.access.Unlock()
    57  	s.notifyUpdated()
    58  }
    59  
    60  func (s *HistoryStorage) notifyUpdated() {
    61  	updateHook := s.updateHook
    62  	if updateHook != nil {
    63  		select {
    64  		case updateHook <- struct{}{}:
    65  		default:
    66  		}
    67  	}
    68  }
    69  
    70  func (s *HistoryStorage) Close() error {
    71  	s.updateHook = nil
    72  	return nil
    73  }
    74  
    75  func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
    76  	if link == "" {
    77  		link = "https://www.gstatic.com/generate_204"
    78  	}
    79  	linkURL, err := url.Parse(link)
    80  	if err != nil {
    81  		return
    82  	}
    83  	hostname := linkURL.Hostname()
    84  	port := linkURL.Port()
    85  	if port == "" {
    86  		switch linkURL.Scheme {
    87  		case "http":
    88  			port = "80"
    89  		case "https":
    90  			port = "443"
    91  		}
    92  	}
    93  
    94  	start := time.Now()
    95  	instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port))
    96  	if err != nil {
    97  		return
    98  	}
    99  	defer instance.Close()
   100  	if earlyConn, isEarlyConn := common.Cast[N.EarlyConn](instance); isEarlyConn && earlyConn.NeedHandshake() {
   101  		start = time.Now()
   102  	}
   103  	req, err := http.NewRequest(http.MethodHead, link, nil)
   104  	if err != nil {
   105  		return
   106  	}
   107  	client := http.Client{
   108  		Transport: &http.Transport{
   109  			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
   110  				return instance, nil
   111  			},
   112  		},
   113  		CheckRedirect: func(req *http.Request, via []*http.Request) error {
   114  			return http.ErrUseLastResponse
   115  		},
   116  	}
   117  	defer client.CloseIdleConnections()
   118  	resp, err := client.Do(req.WithContext(ctx))
   119  	if err != nil {
   120  		return
   121  	}
   122  	resp.Body.Close()
   123  	t = uint16(time.Since(start) / time.Millisecond)
   124  	return
   125  }