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