123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- package script
- import (
- "context"
- "net/http"
- "sync"
- "time"
- "unsafe"
- "github.com/sagernet/sing-box/adapter"
- C "github.com/sagernet/sing-box/constant"
- "github.com/sagernet/sing-box/log"
- "github.com/sagernet/sing-box/option"
- "github.com/sagernet/sing-box/script/jsc"
- "github.com/sagernet/sing-box/script/modules/surge"
- "github.com/sagernet/sing/common"
- E "github.com/sagernet/sing/common/exceptions"
- F "github.com/sagernet/sing/common/format"
- "github.com/sagernet/sing/common/logger"
- "github.com/adhocore/gronx"
- "github.com/dop251/goja"
- )
- const defaultSurgeScriptTimeout = 10 * time.Second
- var _ adapter.SurgeScript = (*SurgeScript)(nil)
- type SurgeScript struct {
- ctx context.Context
- logger logger.ContextLogger
- tag string
- source Source
- cronExpression string
- cronTimeout time.Duration
- cronArguments []string
- cronTimer *time.Timer
- cronDone chan struct{}
- }
- func NewSurgeScript(ctx context.Context, logger logger.ContextLogger, options option.Script) (adapter.Script, error) {
- source, err := NewSource(ctx, logger, options)
- if err != nil {
- return nil, err
- }
- cronOptions := common.PtrValueOrDefault(options.SurgeOptions.CronOptions)
- if cronOptions.Expression != "" {
- if !gronx.IsValid(cronOptions.Expression) {
- return nil, E.New("invalid cron expression: ", cronOptions.Expression)
- }
- }
- return &SurgeScript{
- ctx: ctx,
- logger: logger,
- tag: options.Tag,
- source: source,
- cronExpression: cronOptions.Expression,
- cronTimeout: time.Duration(cronOptions.Timeout),
- cronArguments: cronOptions.Arguments,
- cronDone: make(chan struct{}),
- }, nil
- }
- func (s *SurgeScript) Type() string {
- return C.ScriptTypeSurge
- }
- func (s *SurgeScript) Tag() string {
- return s.tag
- }
- func (s *SurgeScript) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error {
- return s.source.StartContext(ctx, startContext)
- }
- func (s *SurgeScript) PostStart() error {
- err := s.source.PostStart()
- if err != nil {
- return err
- }
- if s.cronExpression != "" {
- go s.loopCronEvents()
- }
- return nil
- }
- func (s *SurgeScript) loopCronEvents() {
- s.logger.Debug("starting event")
- err := s.ExecuteGeneric(s.ctx, "cron", s.cronTimeout, s.cronArguments)
- if err != nil {
- s.logger.Error(E.Cause(err, "running event"))
- }
- nextTick, err := gronx.NextTick(s.cronExpression, false)
- if err != nil {
- s.logger.Error(E.Cause(err, "determine next tick"))
- return
- }
- s.cronTimer = time.NewTimer(nextTick.Sub(time.Now()))
- s.logger.Debug("next event at: ", nextTick.Format(log.DefaultTimeFormat))
- for {
- select {
- case <-s.ctx.Done():
- return
- case <-s.cronDone:
- return
- case <-s.cronTimer.C:
- s.logger.Debug("starting event")
- err = s.ExecuteGeneric(s.ctx, "cron", s.cronTimeout, s.cronArguments)
- if err != nil {
- s.logger.Error(E.Cause(err, "running event"))
- }
- nextTick, err = gronx.NextTick(s.cronExpression, false)
- if err != nil {
- s.logger.Error(E.Cause(err, "determine next tick"))
- return
- }
- s.cronTimer.Reset(nextTick.Sub(time.Now()))
- s.logger.Debug("configured next event at: ", nextTick)
- }
- }
- }
- func (s *SurgeScript) Close() error {
- err := s.source.Close()
- if s.cronTimer != nil {
- s.cronTimer.Stop()
- close(s.cronDone)
- }
- return err
- }
- func (s *SurgeScript) ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error {
- program := s.source.Program()
- if program == nil {
- return E.New("invalid script")
- }
- ctx, cancel := context.WithCancelCause(ctx)
- defer cancel(nil)
- runtime := NewRuntime(ctx, cancel)
- SetModules(runtime, ctx, s.logger, cancel, s.tag)
- surge.Enable(runtime, scriptType, arguments)
- if timeout == 0 {
- timeout = defaultSurgeScriptTimeout
- }
- ctx, timeoutCancel := context.WithTimeout(ctx, timeout)
- defer timeoutCancel()
- done := make(chan struct{})
- doneFunc := common.OnceFunc(func() {
- close(done)
- })
- runtime.Set("done", func(call goja.FunctionCall) goja.Value {
- doneFunc()
- return goja.Undefined()
- })
- var (
- access sync.Mutex
- scriptErr error
- )
- go func() {
- _, err := runtime.RunProgram(program)
- if err != nil {
- access.Lock()
- scriptErr = err
- access.Unlock()
- doneFunc()
- }
- }()
- select {
- case <-ctx.Done():
- runtime.Interrupt(ctx.Err())
- return ctx.Err()
- case <-done:
- access.Lock()
- defer access.Unlock()
- if scriptErr != nil {
- runtime.Interrupt(scriptErr)
- } else {
- runtime.Interrupt("script done")
- }
- }
- return scriptErr
- }
- func (s *SurgeScript) ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*adapter.HTTPRequestScriptResult, error) {
- program := s.source.Program()
- if program == nil {
- return nil, E.New("invalid script")
- }
- ctx, cancel := context.WithCancelCause(ctx)
- defer cancel(nil)
- runtime := NewRuntime(ctx, cancel)
- SetModules(runtime, ctx, s.logger, cancel, s.tag)
- surge.Enable(runtime, "http-request", arguments)
- if timeout == 0 {
- timeout = defaultSurgeScriptTimeout
- }
- ctx, timeoutCancel := context.WithTimeout(ctx, timeout)
- defer timeoutCancel()
- runtime.ClearInterrupt()
- requestObject := runtime.NewObject()
- requestObject.Set("url", request.URL.String())
- requestObject.Set("method", request.Method)
- requestObject.Set("headers", jsc.HeadersToValue(runtime, request.Header))
- if !binaryBody {
- requestObject.Set("body", string(body))
- } else {
- requestObject.Set("body", jsc.NewUint8Array(runtime, body))
- }
- requestObject.Set("id", F.ToString(uintptr(unsafe.Pointer(request))))
- runtime.Set("request", requestObject)
- done := make(chan struct{})
- doneFunc := common.OnceFunc(func() {
- close(done)
- })
- var (
- access sync.Mutex
- result adapter.HTTPRequestScriptResult
- scriptErr error
- )
- runtime.Set("done", func(call goja.FunctionCall) goja.Value {
- defer doneFunc()
- resultObject := jsc.AssertObject(runtime, call.Argument(0), "done() argument", true)
- if resultObject == nil {
- panic(runtime.NewGoError(E.New("request rejected by script")))
- }
- access.Lock()
- defer access.Unlock()
- result.URL = jsc.AssertString(runtime, resultObject.Get("url"), "url", true)
- result.Headers = jsc.AssertHTTPHeader(runtime, resultObject.Get("headers"), "headers")
- result.Body = jsc.AssertStringBinary(runtime, resultObject.Get("body"), "body", true)
- responseObject := jsc.AssertObject(runtime, resultObject.Get("response"), "response", true)
- if responseObject != nil {
- result.Response = &adapter.HTTPRequestScriptResponse{
- Status: int(jsc.AssertInt(runtime, responseObject.Get("status"), "status", true)),
- Headers: jsc.AssertHTTPHeader(runtime, responseObject.Get("headers"), "headers"),
- Body: jsc.AssertStringBinary(runtime, responseObject.Get("body"), "body", true),
- }
- }
- return goja.Undefined()
- })
- go func() {
- _, err := runtime.RunProgram(program)
- if err != nil {
- access.Lock()
- scriptErr = err
- access.Unlock()
- doneFunc()
- }
- }()
- select {
- case <-ctx.Done():
- runtime.Interrupt(ctx.Err())
- return nil, ctx.Err()
- case <-done:
- access.Lock()
- defer access.Unlock()
- if scriptErr != nil {
- runtime.Interrupt(scriptErr)
- } else {
- runtime.Interrupt("script done")
- }
- }
- return &result, scriptErr
- }
- 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) {
- program := s.source.Program()
- if program == nil {
- return nil, E.New("invalid script")
- }
- ctx, cancel := context.WithCancelCause(ctx)
- defer cancel(nil)
- runtime := NewRuntime(ctx, cancel)
- SetModules(runtime, ctx, s.logger, cancel, s.tag)
- surge.Enable(runtime, "http-response", arguments)
- if timeout == 0 {
- timeout = defaultSurgeScriptTimeout
- }
- ctx, timeoutCancel := context.WithTimeout(ctx, timeout)
- defer timeoutCancel()
- runtime.ClearInterrupt()
- requestObject := runtime.NewObject()
- requestObject.Set("url", request.URL.String())
- requestObject.Set("method", request.Method)
- requestObject.Set("headers", jsc.HeadersToValue(runtime, request.Header))
- requestObject.Set("id", F.ToString(uintptr(unsafe.Pointer(request))))
- runtime.Set("request", requestObject)
- responseObject := runtime.NewObject()
- responseObject.Set("status", response.StatusCode)
- responseObject.Set("headers", jsc.HeadersToValue(runtime, response.Header))
- if !binaryBody {
- responseObject.Set("body", string(body))
- } else {
- responseObject.Set("body", jsc.NewUint8Array(runtime, body))
- }
- runtime.Set("response", responseObject)
- done := make(chan struct{})
- doneFunc := common.OnceFunc(func() {
- close(done)
- })
- var (
- access sync.Mutex
- result adapter.HTTPResponseScriptResult
- scriptErr error
- )
- runtime.Set("done", func(call goja.FunctionCall) goja.Value {
- resultObject := jsc.AssertObject(runtime, call.Argument(0), "done() argument", true)
- if resultObject == nil {
- panic(runtime.NewGoError(E.New("response rejected by script")))
- }
- access.Lock()
- defer access.Unlock()
- result.Status = int(jsc.AssertInt(runtime, resultObject.Get("status"), "status", true))
- result.Headers = jsc.AssertHTTPHeader(runtime, resultObject.Get("headers"), "headers")
- result.Body = jsc.AssertStringBinary(runtime, resultObject.Get("body"), "body", true)
- doneFunc()
- return goja.Undefined()
- })
- go func() {
- _, err := runtime.RunProgram(program)
- if err != nil {
- access.Lock()
- scriptErr = err
- access.Unlock()
- doneFunc()
- }
- }()
- select {
- case <-ctx.Done():
- runtime.Interrupt(ctx.Err())
- return nil, ctx.Err()
- case <-done:
- access.Lock()
- defer access.Unlock()
- if scriptErr != nil {
- runtime.Interrupt(scriptErr)
- } else {
- runtime.Interrupt("script done")
- }
- return &result, scriptErr
- }
- }
|