package urltest import ( "context" "net" "net/http" "net/url" "sync" "time" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" ) type History struct { Time time.Time `json:"time"` Delay uint16 `json:"delay"` } type HistoryStorage struct { access sync.RWMutex delayHistory map[string]*History callbacks list.List[func()] } func NewHistoryStorage() *HistoryStorage { return &HistoryStorage{ delayHistory: make(map[string]*History), } } func (s *HistoryStorage) AddListener(listener func()) *list.Element[func()] { s.access.Lock() defer s.access.Unlock() return s.callbacks.PushBack(listener) } func (s *HistoryStorage) RemoveListener(element *list.Element[func()]) { s.access.Lock() defer s.access.Unlock() s.callbacks.Remove(element) } func (s *HistoryStorage) LoadURLTestHistory(tag string) *History { if s == nil { return nil } s.access.RLock() defer s.access.RUnlock() return s.delayHistory[tag] } func (s *HistoryStorage) DeleteURLTestHistory(tag string) { s.access.Lock() delete(s.delayHistory, tag) s.access.Unlock() s.notifyUpdated() } func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) { s.access.Lock() s.delayHistory[tag] = history s.access.Unlock() s.notifyUpdated() } func (s *HistoryStorage) notifyUpdated() { s.access.RLock() defer s.access.RUnlock() for element := s.callbacks.Front(); element != nil; element = element.Next() { element.Value() } } func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { if link == "" { link = "https://www.gstatic.com/generate_204" } linkURL, err := url.Parse(link) if err != nil { return } hostname := linkURL.Hostname() port := linkURL.Port() if port == "" { switch linkURL.Scheme { case "http": port = "80" case "https": port = "443" } } start := time.Now() instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port)) if err != nil { return } defer instance.Close() req, err := http.NewRequest(http.MethodHead, link, nil) if err != nil { return } req = req.WithContext(ctx) transport := &http.Transport{ Dial: func(string, string) (net.Conn, error) { return instance, nil }, // from http.DefaultTransport MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } client := http.Client{ Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } defer client.CloseIdleConnections() resp, err := client.Do(req) if err != nil { return } resp.Body.Close() t = uint16(time.Since(start) / time.Millisecond) return }