script_surge.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. //go:build with_script
  2. package script
  3. import (
  4. "context"
  5. "net/http"
  6. "sync"
  7. "time"
  8. "unsafe"
  9. "github.com/sagernet/sing-box/adapter"
  10. C "github.com/sagernet/sing-box/constant"
  11. "github.com/sagernet/sing-box/log"
  12. "github.com/sagernet/sing-box/option"
  13. "github.com/sagernet/sing-box/script/jsc"
  14. "github.com/sagernet/sing-box/script/modules/surge"
  15. "github.com/sagernet/sing/common"
  16. E "github.com/sagernet/sing/common/exceptions"
  17. F "github.com/sagernet/sing/common/format"
  18. "github.com/sagernet/sing/common/logger"
  19. "github.com/adhocore/gronx"
  20. "github.com/dop251/goja"
  21. )
  22. const defaultSurgeScriptTimeout = 10 * time.Second
  23. var _ adapter.SurgeScript = (*SurgeScript)(nil)
  24. type SurgeScript struct {
  25. ctx context.Context
  26. logger logger.ContextLogger
  27. tag string
  28. source Source
  29. cronExpression string
  30. cronTimeout time.Duration
  31. cronArguments []string
  32. cronTimer *time.Timer
  33. cronDone chan struct{}
  34. }
  35. func NewSurgeScript(ctx context.Context, logger logger.ContextLogger, options option.Script) (adapter.Script, error) {
  36. source, err := NewSource(ctx, logger, options)
  37. if err != nil {
  38. return nil, err
  39. }
  40. cronOptions := common.PtrValueOrDefault(options.SurgeOptions.CronOptions)
  41. if cronOptions.Expression != "" {
  42. if !gronx.IsValid(cronOptions.Expression) {
  43. return nil, E.New("invalid cron expression: ", cronOptions.Expression)
  44. }
  45. }
  46. return &SurgeScript{
  47. ctx: ctx,
  48. logger: logger,
  49. tag: options.Tag,
  50. source: source,
  51. cronExpression: cronOptions.Expression,
  52. cronTimeout: time.Duration(cronOptions.Timeout),
  53. cronArguments: cronOptions.Arguments,
  54. cronDone: make(chan struct{}),
  55. }, nil
  56. }
  57. func (s *SurgeScript) Type() string {
  58. return C.ScriptTypeSurge
  59. }
  60. func (s *SurgeScript) Tag() string {
  61. return s.tag
  62. }
  63. func (s *SurgeScript) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error {
  64. return s.source.StartContext(ctx, startContext)
  65. }
  66. func (s *SurgeScript) PostStart() error {
  67. err := s.source.PostStart()
  68. if err != nil {
  69. return err
  70. }
  71. if s.cronExpression != "" {
  72. go s.loopCronEvents()
  73. }
  74. return nil
  75. }
  76. func (s *SurgeScript) loopCronEvents() {
  77. s.logger.Debug("starting event")
  78. err := s.ExecuteGeneric(s.ctx, "cron", s.cronTimeout, s.cronArguments)
  79. if err != nil {
  80. s.logger.Error(E.Cause(err, "running event"))
  81. }
  82. nextTick, err := gronx.NextTick(s.cronExpression, false)
  83. if err != nil {
  84. s.logger.Error(E.Cause(err, "determine next tick"))
  85. return
  86. }
  87. s.cronTimer = time.NewTimer(nextTick.Sub(time.Now()))
  88. s.logger.Debug("next event at: ", nextTick.Format(log.DefaultTimeFormat))
  89. for {
  90. select {
  91. case <-s.ctx.Done():
  92. return
  93. case <-s.cronDone:
  94. return
  95. case <-s.cronTimer.C:
  96. s.logger.Debug("starting event")
  97. err = s.ExecuteGeneric(s.ctx, "cron", s.cronTimeout, s.cronArguments)
  98. if err != nil {
  99. s.logger.Error(E.Cause(err, "running event"))
  100. }
  101. nextTick, err = gronx.NextTick(s.cronExpression, false)
  102. if err != nil {
  103. s.logger.Error(E.Cause(err, "determine next tick"))
  104. return
  105. }
  106. s.cronTimer.Reset(nextTick.Sub(time.Now()))
  107. s.logger.Debug("configured next event at: ", nextTick)
  108. }
  109. }
  110. }
  111. func (s *SurgeScript) Close() error {
  112. err := s.source.Close()
  113. if s.cronTimer != nil {
  114. s.cronTimer.Stop()
  115. close(s.cronDone)
  116. }
  117. return err
  118. }
  119. func (s *SurgeScript) ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error {
  120. program := s.source.Program()
  121. if program == nil {
  122. return E.New("invalid script")
  123. }
  124. ctx, cancel := context.WithCancelCause(ctx)
  125. defer cancel(nil)
  126. runtime := NewRuntime(ctx, cancel)
  127. SetModules(runtime, ctx, s.logger, cancel, s.tag)
  128. surge.Enable(runtime, scriptType, arguments)
  129. if timeout == 0 {
  130. timeout = defaultSurgeScriptTimeout
  131. }
  132. ctx, timeoutCancel := context.WithTimeout(ctx, timeout)
  133. defer timeoutCancel()
  134. done := make(chan struct{})
  135. doneFunc := common.OnceFunc(func() {
  136. close(done)
  137. })
  138. runtime.Set("done", func(call goja.FunctionCall) goja.Value {
  139. doneFunc()
  140. return goja.Undefined()
  141. })
  142. var (
  143. access sync.Mutex
  144. scriptErr error
  145. )
  146. go func() {
  147. _, err := runtime.RunProgram(program)
  148. if err != nil {
  149. access.Lock()
  150. scriptErr = err
  151. access.Unlock()
  152. doneFunc()
  153. }
  154. }()
  155. select {
  156. case <-ctx.Done():
  157. runtime.Interrupt(ctx.Err())
  158. return ctx.Err()
  159. case <-done:
  160. access.Lock()
  161. defer access.Unlock()
  162. if scriptErr != nil {
  163. runtime.Interrupt(scriptErr)
  164. } else {
  165. runtime.Interrupt("script done")
  166. }
  167. }
  168. return scriptErr
  169. }
  170. func (s *SurgeScript) ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*adapter.HTTPRequestScriptResult, error) {
  171. program := s.source.Program()
  172. if program == nil {
  173. return nil, E.New("invalid script")
  174. }
  175. ctx, cancel := context.WithCancelCause(ctx)
  176. defer cancel(nil)
  177. runtime := NewRuntime(ctx, cancel)
  178. SetModules(runtime, ctx, s.logger, cancel, s.tag)
  179. surge.Enable(runtime, "http-request", arguments)
  180. if timeout == 0 {
  181. timeout = defaultSurgeScriptTimeout
  182. }
  183. ctx, timeoutCancel := context.WithTimeout(ctx, timeout)
  184. defer timeoutCancel()
  185. runtime.ClearInterrupt()
  186. requestObject := runtime.NewObject()
  187. requestObject.Set("url", request.URL.String())
  188. requestObject.Set("method", request.Method)
  189. requestObject.Set("headers", jsc.HeadersToValue(runtime, request.Header))
  190. if !binaryBody {
  191. requestObject.Set("body", string(body))
  192. } else {
  193. requestObject.Set("body", jsc.NewUint8Array(runtime, body))
  194. }
  195. requestObject.Set("id", F.ToString(uintptr(unsafe.Pointer(request))))
  196. runtime.Set("request", requestObject)
  197. done := make(chan struct{})
  198. doneFunc := common.OnceFunc(func() {
  199. close(done)
  200. })
  201. var (
  202. access sync.Mutex
  203. result adapter.HTTPRequestScriptResult
  204. scriptErr error
  205. )
  206. runtime.Set("done", func(call goja.FunctionCall) goja.Value {
  207. defer doneFunc()
  208. resultObject := jsc.AssertObject(runtime, call.Argument(0), "done() argument", true)
  209. if resultObject == nil {
  210. panic(runtime.NewGoError(E.New("request rejected by script")))
  211. }
  212. access.Lock()
  213. defer access.Unlock()
  214. result.URL = jsc.AssertString(runtime, resultObject.Get("url"), "url", true)
  215. result.Headers = jsc.AssertHTTPHeader(runtime, resultObject.Get("headers"), "headers")
  216. result.Body = jsc.AssertStringBinary(runtime, resultObject.Get("body"), "body", true)
  217. responseObject := jsc.AssertObject(runtime, resultObject.Get("response"), "response", true)
  218. if responseObject != nil {
  219. result.Response = &adapter.HTTPRequestScriptResponse{
  220. Status: int(jsc.AssertInt(runtime, responseObject.Get("status"), "status", true)),
  221. Headers: jsc.AssertHTTPHeader(runtime, responseObject.Get("headers"), "headers"),
  222. Body: jsc.AssertStringBinary(runtime, responseObject.Get("body"), "body", true),
  223. }
  224. }
  225. return goja.Undefined()
  226. })
  227. go func() {
  228. _, err := runtime.RunProgram(program)
  229. if err != nil {
  230. access.Lock()
  231. scriptErr = err
  232. access.Unlock()
  233. doneFunc()
  234. }
  235. }()
  236. select {
  237. case <-ctx.Done():
  238. runtime.Interrupt(ctx.Err())
  239. return nil, ctx.Err()
  240. case <-done:
  241. access.Lock()
  242. defer access.Unlock()
  243. if scriptErr != nil {
  244. runtime.Interrupt(scriptErr)
  245. } else {
  246. runtime.Interrupt("script done")
  247. }
  248. }
  249. return &result, scriptErr
  250. }
  251. func (s *SurgeScript) ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*adapter.HTTPResponseScriptResult, error) {
  252. program := s.source.Program()
  253. if program == nil {
  254. return nil, E.New("invalid script")
  255. }
  256. ctx, cancel := context.WithCancelCause(ctx)
  257. defer cancel(nil)
  258. runtime := NewRuntime(ctx, cancel)
  259. SetModules(runtime, ctx, s.logger, cancel, s.tag)
  260. surge.Enable(runtime, "http-response", arguments)
  261. if timeout == 0 {
  262. timeout = defaultSurgeScriptTimeout
  263. }
  264. ctx, timeoutCancel := context.WithTimeout(ctx, timeout)
  265. defer timeoutCancel()
  266. runtime.ClearInterrupt()
  267. requestObject := runtime.NewObject()
  268. requestObject.Set("url", request.URL.String())
  269. requestObject.Set("method", request.Method)
  270. requestObject.Set("headers", jsc.HeadersToValue(runtime, request.Header))
  271. requestObject.Set("id", F.ToString(uintptr(unsafe.Pointer(request))))
  272. runtime.Set("request", requestObject)
  273. responseObject := runtime.NewObject()
  274. responseObject.Set("status", response.StatusCode)
  275. responseObject.Set("headers", jsc.HeadersToValue(runtime, response.Header))
  276. if !binaryBody {
  277. responseObject.Set("body", string(body))
  278. } else {
  279. responseObject.Set("body", jsc.NewUint8Array(runtime, body))
  280. }
  281. runtime.Set("response", responseObject)
  282. done := make(chan struct{})
  283. doneFunc := common.OnceFunc(func() {
  284. close(done)
  285. })
  286. var (
  287. access sync.Mutex
  288. result adapter.HTTPResponseScriptResult
  289. scriptErr error
  290. )
  291. runtime.Set("done", func(call goja.FunctionCall) goja.Value {
  292. resultObject := jsc.AssertObject(runtime, call.Argument(0), "done() argument", true)
  293. if resultObject == nil {
  294. panic(runtime.NewGoError(E.New("response rejected by script")))
  295. }
  296. access.Lock()
  297. defer access.Unlock()
  298. result.Status = int(jsc.AssertInt(runtime, resultObject.Get("status"), "status", true))
  299. result.Headers = jsc.AssertHTTPHeader(runtime, resultObject.Get("headers"), "headers")
  300. result.Body = jsc.AssertStringBinary(runtime, resultObject.Get("body"), "body", true)
  301. doneFunc()
  302. return goja.Undefined()
  303. })
  304. go func() {
  305. _, err := runtime.RunProgram(program)
  306. if err != nil {
  307. access.Lock()
  308. scriptErr = err
  309. access.Unlock()
  310. doneFunc()
  311. }
  312. }()
  313. select {
  314. case <-ctx.Done():
  315. runtime.Interrupt(ctx.Err())
  316. return nil, ctx.Err()
  317. case <-done:
  318. access.Lock()
  319. defer access.Unlock()
  320. if scriptErr != nil {
  321. runtime.Interrupt(scriptErr)
  322. } else {
  323. runtime.Interrupt("script done")
  324. }
  325. return &result, scriptErr
  326. }
  327. }