module.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. package sghttp
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/tls"
  6. "io"
  7. "net/http"
  8. "net/http/cookiejar"
  9. "sync"
  10. "time"
  11. "github.com/sagernet/sing-box/script/jsc"
  12. F "github.com/sagernet/sing/common/format"
  13. "github.com/dop251/goja"
  14. "golang.org/x/net/publicsuffix"
  15. )
  16. type SurgeHTTP struct {
  17. vm *goja.Runtime
  18. ctx context.Context
  19. cookieAccess sync.RWMutex
  20. cookieJar *cookiejar.Jar
  21. errorHandler func(error)
  22. }
  23. func Enable(vm *goja.Runtime, ctx context.Context, errorHandler func(error)) {
  24. sgHTTP := &SurgeHTTP{
  25. vm: vm,
  26. ctx: ctx,
  27. errorHandler: errorHandler,
  28. }
  29. httpObject := vm.NewObject()
  30. httpObject.Set("get", sgHTTP.request(http.MethodGet))
  31. httpObject.Set("post", sgHTTP.request(http.MethodPost))
  32. httpObject.Set("put", sgHTTP.request(http.MethodPut))
  33. httpObject.Set("delete", sgHTTP.request(http.MethodDelete))
  34. httpObject.Set("head", sgHTTP.request(http.MethodHead))
  35. httpObject.Set("options", sgHTTP.request(http.MethodOptions))
  36. httpObject.Set("patch", sgHTTP.request(http.MethodPatch))
  37. httpObject.Set("trace", sgHTTP.request(http.MethodTrace))
  38. vm.Set("$http", httpObject)
  39. }
  40. func (s *SurgeHTTP) request(method string) func(call goja.FunctionCall) goja.Value {
  41. return func(call goja.FunctionCall) goja.Value {
  42. if len(call.Arguments) != 2 {
  43. panic(s.vm.NewTypeError("invalid arguments"))
  44. }
  45. var (
  46. url string
  47. headers http.Header
  48. body []byte
  49. timeout = 5 * time.Second
  50. insecure bool
  51. autoCookie bool
  52. autoRedirect bool
  53. // policy string
  54. binaryMode bool
  55. )
  56. switch optionsValue := call.Argument(0).(type) {
  57. case goja.String:
  58. url = optionsValue.String()
  59. case *goja.Object:
  60. url = jsc.AssertString(s.vm, optionsValue.Get("url"), "options.url", false)
  61. headers = jsc.AssertHTTPHeader(s.vm, optionsValue.Get("headers"), "option.headers")
  62. body = jsc.AssertStringBinary(s.vm, optionsValue.Get("body"), "options.body", true)
  63. timeoutInt := jsc.AssertInt(s.vm, optionsValue.Get("timeout"), "options.timeout", true)
  64. if timeoutInt > 0 {
  65. timeout = time.Duration(timeoutInt) * time.Second
  66. }
  67. insecure = jsc.AssertBool(s.vm, optionsValue.Get("insecure"), "options.insecure", true)
  68. autoCookie = jsc.AssertBool(s.vm, optionsValue.Get("auto-cookie"), "options.auto-cookie", true)
  69. autoRedirect = jsc.AssertBool(s.vm, optionsValue.Get("auto-redirect"), "options.auto-redirect", true)
  70. // policy = jsc.AssertString(s.vm, optionsValue.Get("policy"), "options.policy", true)
  71. binaryMode = jsc.AssertBool(s.vm, optionsValue.Get("binary-mode"), "options.binary-mode", true)
  72. default:
  73. panic(s.vm.NewTypeError(F.ToString("invalid argument: options: expected string or object, but got ", optionsValue)))
  74. }
  75. callback := jsc.AssertFunction(s.vm, call.Argument(1), "callback")
  76. httpClient := &http.Client{
  77. Timeout: timeout,
  78. Transport: &http.Transport{
  79. TLSClientConfig: &tls.Config{
  80. InsecureSkipVerify: insecure,
  81. },
  82. ForceAttemptHTTP2: true,
  83. },
  84. CheckRedirect: func(req *http.Request, via []*http.Request) error {
  85. if autoRedirect {
  86. return nil
  87. }
  88. return http.ErrUseLastResponse
  89. },
  90. }
  91. if autoCookie {
  92. s.cookieAccess.Lock()
  93. if s.cookieJar == nil {
  94. s.cookieJar, _ = cookiejar.New(&cookiejar.Options{
  95. PublicSuffixList: publicsuffix.List,
  96. })
  97. }
  98. httpClient.Jar = s.cookieJar
  99. s.cookieAccess.Lock()
  100. }
  101. request, err := http.NewRequestWithContext(s.ctx, method, url, bytes.NewReader(body))
  102. if host := headers.Get("Host"); host != "" {
  103. request.Host = host
  104. headers.Del("Host")
  105. }
  106. request.Header = headers
  107. if err != nil {
  108. panic(s.vm.NewGoError(err))
  109. }
  110. go func() {
  111. response, executeErr := httpClient.Do(request)
  112. if err != nil {
  113. _, err = callback(nil, s.vm.NewGoError(executeErr), nil, nil)
  114. if err != nil {
  115. s.errorHandler(err)
  116. }
  117. return
  118. }
  119. defer response.Body.Close()
  120. var content []byte
  121. content, err = io.ReadAll(response.Body)
  122. if err != nil {
  123. _, err = callback(nil, s.vm.NewGoError(err), nil, nil)
  124. if err != nil {
  125. s.errorHandler(err)
  126. }
  127. }
  128. responseObject := s.vm.NewObject()
  129. responseObject.Set("status", response.StatusCode)
  130. responseObject.Set("headers", jsc.HeadersToValue(s.vm, response.Header))
  131. var bodyValue goja.Value
  132. if binaryMode {
  133. bodyValue = jsc.NewUint8Array(s.vm, content)
  134. } else {
  135. bodyValue = s.vm.ToValue(string(content))
  136. }
  137. _, err = callback(nil, nil, responseObject, bodyValue)
  138. }()
  139. return goja.Undefined()
  140. }
  141. }