urltest.go 2.5 KB

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