Add Surge MITM and scripts

This commit is contained in:
世界
2025-02-02 17:03:27 +08:00
parent b55bfca7de
commit 5e28a80e63
53 changed files with 4437 additions and 15842 deletions

View File

@@ -0,0 +1,108 @@
package console
import (
"bytes"
"context"
"github.com/sagernet/sing-box/script/modules/require"
"github.com/sagernet/sing/common/logger"
"github.com/dop251/goja"
)
const ModuleName = "console"
type Console struct {
vm *goja.Runtime
}
func (c *Console) log(ctx context.Context, p func(ctx context.Context, values ...any)) func(goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
var buffer bytes.Buffer
var format string
if arg := call.Argument(0); !goja.IsUndefined(arg) {
format = arg.String()
}
var args []goja.Value
if len(call.Arguments) > 0 {
args = call.Arguments[1:]
}
c.Format(&buffer, format, args...)
p(ctx, buffer.String())
return nil
}
}
func (c *Console) Format(b *bytes.Buffer, f string, args ...goja.Value) {
pct := false
argNum := 0
for _, chr := range f {
if pct {
if argNum < len(args) {
if c.format(chr, args[argNum], b) {
argNum++
}
} else {
b.WriteByte('%')
b.WriteRune(chr)
}
pct = false
} else {
if chr == '%' {
pct = true
} else {
b.WriteRune(chr)
}
}
}
for _, arg := range args[argNum:] {
b.WriteByte(' ')
b.WriteString(arg.String())
}
}
func (c *Console) format(f rune, val goja.Value, w *bytes.Buffer) bool {
switch f {
case 's':
w.WriteString(val.String())
case 'd':
w.WriteString(val.ToNumber().String())
case 'j':
if json, ok := c.vm.Get("JSON").(*goja.Object); ok {
if stringify, ok := goja.AssertFunction(json.Get("stringify")); ok {
res, err := stringify(json, val)
if err != nil {
panic(err)
}
w.WriteString(res.String())
}
}
case '%':
w.WriteByte('%')
return false
default:
w.WriteByte('%')
w.WriteRune(f)
return false
}
return true
}
func Require(ctx context.Context, logger logger.ContextLogger) require.ModuleLoader {
return func(runtime *goja.Runtime, module *goja.Object) {
c := &Console{
vm: runtime,
}
o := module.Get("exports").(*goja.Object)
o.Set("log", c.log(ctx, logger.DebugContext))
o.Set("error", c.log(ctx, logger.ErrorContext))
o.Set("warn", c.log(ctx, logger.WarnContext))
o.Set("info", c.log(ctx, logger.InfoContext))
o.Set("debug", c.log(ctx, logger.DebugContext))
}
}
func Enable(runtime *goja.Runtime) {
runtime.Set("console", require.Require(runtime, ModuleName))
}

View File

@@ -0,0 +1,489 @@
package eventloop
import (
"sync"
"sync/atomic"
"time"
"github.com/dop251/goja"
)
type job struct {
cancel func() bool
fn func()
idx int
cancelled bool
}
type Timer struct {
job
timer *time.Timer
}
type Interval struct {
job
ticker *time.Ticker
stopChan chan struct{}
}
type Immediate struct {
job
}
type EventLoop struct {
vm *goja.Runtime
jobChan chan func()
jobs []*job
jobCount int32
canRun int32
auxJobsLock sync.Mutex
wakeupChan chan struct{}
auxJobsSpare, auxJobs []func()
stopLock sync.Mutex
stopCond *sync.Cond
running bool
terminated bool
errorHandler func(error)
}
func Enable(runtime *goja.Runtime, errorHandler func(error)) *EventLoop {
loop := &EventLoop{
vm: runtime,
jobChan: make(chan func()),
wakeupChan: make(chan struct{}, 1),
errorHandler: errorHandler,
}
loop.stopCond = sync.NewCond(&loop.stopLock)
runtime.Set("setTimeout", loop.setTimeout)
runtime.Set("setInterval", loop.setInterval)
runtime.Set("setImmediate", loop.setImmediate)
runtime.Set("clearTimeout", loop.clearTimeout)
runtime.Set("clearInterval", loop.clearInterval)
runtime.Set("clearImmediate", loop.clearImmediate)
return loop
}
func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value {
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
delay := call.Argument(1).ToInteger()
var args []goja.Value
if len(call.Arguments) > 2 {
args = append(args, call.Arguments[2:]...)
}
f := func() {
_, err := fn(nil, args...)
if err != nil {
loop.errorHandler(err)
}
}
loop.jobCount++
var job *job
var ret goja.Value
if repeating {
interval := loop.newInterval(f)
interval.start(loop, time.Duration(delay)*time.Millisecond)
job = &interval.job
ret = loop.vm.ToValue(interval)
} else {
timeout := loop.newTimeout(f)
timeout.start(loop, time.Duration(delay)*time.Millisecond)
job = &timeout.job
ret = loop.vm.ToValue(timeout)
}
job.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, job)
return ret
}
return nil
}
func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value {
return loop.schedule(call, false)
}
func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value {
return loop.schedule(call, true)
}
func (loop *EventLoop) setImmediate(call goja.FunctionCall) goja.Value {
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
var args []goja.Value
if len(call.Arguments) > 1 {
args = append(args, call.Arguments[1:]...)
}
f := func() {
_, err := fn(nil, args...)
if err != nil {
loop.errorHandler(err)
}
}
loop.jobCount++
return loop.vm.ToValue(loop.addImmediate(f))
}
return nil
}
// SetTimeout schedules to run the specified function in the context
// of the loop as soon as possible after the specified timeout period.
// SetTimeout returns a Timer which can be passed to ClearTimeout.
// The instance of goja.Runtime that is passed to the function and any Values derived
// from it must not be used outside the function. SetTimeout is
// safe to call inside or outside the loop.
// If the loop is terminated (see Terminate()) returns nil.
func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer {
t := loop.newTimeout(func() { fn(loop.vm) })
if loop.addAuxJob(func() {
t.start(loop, timeout)
loop.jobCount++
t.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, &t.job)
}) {
return t
}
return nil
}
// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet.
// ClearTimeout is safe to call inside or outside the loop.
func (loop *EventLoop) ClearTimeout(t *Timer) {
loop.addAuxJob(func() {
loop.clearTimeout(t)
})
}
// SetInterval schedules to repeatedly run the specified function in
// the context of the loop as soon as possible after every specified
// timeout period. SetInterval returns an Interval which can be
// passed to ClearInterval. The instance of goja.Runtime that is passed to the
// function and any Values derived from it must not be used outside
// the function. SetInterval is safe to call inside or outside the
// loop.
// If the loop is terminated (see Terminate()) returns nil.
func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval {
i := loop.newInterval(func() { fn(loop.vm) })
if loop.addAuxJob(func() {
i.start(loop, timeout)
loop.jobCount++
i.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, &i.job)
}) {
return i
}
return nil
}
// ClearInterval cancels an Interval returned by SetInterval.
// ClearInterval is safe to call inside or outside the loop.
func (loop *EventLoop) ClearInterval(i *Interval) {
loop.addAuxJob(func() {
loop.clearInterval(i)
})
}
func (loop *EventLoop) setRunning() {
loop.stopLock.Lock()
defer loop.stopLock.Unlock()
if loop.running {
panic("Loop is already started")
}
loop.running = true
atomic.StoreInt32(&loop.canRun, 1)
loop.auxJobsLock.Lock()
loop.terminated = false
loop.auxJobsLock.Unlock()
}
// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run
// after which it stops the loop and returns.
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
// outside the function.
// Do NOT use this function while the loop is already running. Use RunOnLoop() instead.
// If the loop is already started it will panic.
func (loop *EventLoop) Run(fn func(*goja.Runtime)) {
loop.setRunning()
fn(loop.vm)
loop.run(false)
}
// Start the event loop in the background. The loop continues to run until Stop() is called.
// If the loop is already started it will panic.
func (loop *EventLoop) Start() {
loop.setRunning()
go loop.run(true)
}
// StartInForeground starts the event loop in the current goroutine. The loop continues to run until Stop() is called.
// If the loop is already started it will panic.
// Use this instead of Start if you want to recover from panics that may occur while calling native Go functions from
// within setInterval and setTimeout callbacks.
func (loop *EventLoop) StartInForeground() {
loop.setRunning()
loop.run(true)
}
// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed
// by the loop. It is possible to call Start() or Run() again after this to resume the execution.
// Note, it does not cancel active timeouts (use Terminate() instead if you want this).
// It is not allowed to run Start() (or Run()) and Stop() or Terminate() concurrently.
// Calling Stop() on a non-running loop has no effect.
// It is not allowed to call Stop() from the loop, because it is synchronous and cannot complete until the loop
// is not running any jobs. Use StopNoWait() instead.
// return number of jobs remaining
func (loop *EventLoop) Stop() int {
loop.stopLock.Lock()
for loop.running {
atomic.StoreInt32(&loop.canRun, 0)
loop.wakeup()
loop.stopCond.Wait()
}
loop.stopLock.Unlock()
return int(loop.jobCount)
}
// StopNoWait tells the loop to stop and returns immediately. Can be used inside the loop. Calling it on a
// non-running loop has no effect.
func (loop *EventLoop) StopNoWait() {
loop.stopLock.Lock()
if loop.running {
atomic.StoreInt32(&loop.canRun, 0)
loop.wakeup()
}
loop.stopLock.Unlock()
}
// Terminate stops the loop and clears all active timeouts and intervals. After it returns there are no
// active timers or goroutines associated with the loop. Any attempt to submit a task (by using RunOnLoop(),
// SetTimeout() or SetInterval()) will not succeed.
// After being terminated the loop can be restarted again by using Start() or Run().
// This method must not be called concurrently with Stop*(), Start(), or Run().
func (loop *EventLoop) Terminate() {
loop.Stop()
loop.auxJobsLock.Lock()
loop.terminated = true
loop.auxJobsLock.Unlock()
loop.runAux()
for i := 0; i < len(loop.jobs); i++ {
job := loop.jobs[i]
if !job.cancelled {
job.cancelled = true
if job.cancel() {
loop.removeJob(job)
i--
}
}
}
for len(loop.jobs) > 0 {
(<-loop.jobChan)()
}
}
// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible.
// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop())
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
// outside the function. It is safe to call inside or outside the loop.
// Returns true on success or false if the loop is terminated (see Terminate()).
func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) bool {
return loop.addAuxJob(func() { fn(loop.vm) })
}
func (loop *EventLoop) runAux() {
loop.auxJobsLock.Lock()
jobs := loop.auxJobs
loop.auxJobs = loop.auxJobsSpare
loop.auxJobsLock.Unlock()
for i, job := range jobs {
job()
jobs[i] = nil
}
loop.auxJobsSpare = jobs[:0]
}
func (loop *EventLoop) run(inBackground bool) {
loop.runAux()
if inBackground {
loop.jobCount++
}
LOOP:
for loop.jobCount > 0 {
select {
case job := <-loop.jobChan:
job()
case <-loop.wakeupChan:
loop.runAux()
if atomic.LoadInt32(&loop.canRun) == 0 {
break LOOP
}
}
}
if inBackground {
loop.jobCount--
}
loop.stopLock.Lock()
loop.running = false
loop.stopLock.Unlock()
loop.stopCond.Broadcast()
}
func (loop *EventLoop) wakeup() {
select {
case loop.wakeupChan <- struct{}{}:
default:
}
}
func (loop *EventLoop) addAuxJob(fn func()) bool {
loop.auxJobsLock.Lock()
if loop.terminated {
loop.auxJobsLock.Unlock()
return false
}
loop.auxJobs = append(loop.auxJobs, fn)
loop.auxJobsLock.Unlock()
loop.wakeup()
return true
}
func (loop *EventLoop) newTimeout(f func()) *Timer {
t := &Timer{
job: job{fn: f},
}
t.cancel = t.doCancel
return t
}
func (t *Timer) start(loop *EventLoop, timeout time.Duration) {
t.timer = time.AfterFunc(timeout, func() {
loop.jobChan <- func() {
loop.doTimeout(t)
}
})
}
func (loop *EventLoop) newInterval(f func()) *Interval {
i := &Interval{
job: job{fn: f},
stopChan: make(chan struct{}),
}
i.cancel = i.doCancel
return i
}
func (i *Interval) start(loop *EventLoop, timeout time.Duration) {
// https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
if timeout <= 0 {
timeout = time.Millisecond
}
i.ticker = time.NewTicker(timeout)
go i.run(loop)
}
func (loop *EventLoop) addImmediate(f func()) *Immediate {
i := &Immediate{
job: job{fn: f},
}
loop.addAuxJob(func() {
loop.doImmediate(i)
})
return i
}
func (loop *EventLoop) doTimeout(t *Timer) {
loop.removeJob(&t.job)
if !t.cancelled {
t.cancelled = true
loop.jobCount--
t.fn()
}
}
func (loop *EventLoop) doInterval(i *Interval) {
if !i.cancelled {
i.fn()
}
}
func (loop *EventLoop) doImmediate(i *Immediate) {
if !i.cancelled {
i.cancelled = true
loop.jobCount--
i.fn()
}
}
func (loop *EventLoop) clearTimeout(t *Timer) {
if t != nil && !t.cancelled {
t.cancelled = true
loop.jobCount--
if t.doCancel() {
loop.removeJob(&t.job)
}
}
}
func (loop *EventLoop) clearInterval(i *Interval) {
if i != nil && !i.cancelled {
i.cancelled = true
loop.jobCount--
i.doCancel()
}
}
func (loop *EventLoop) removeJob(job *job) {
idx := job.idx
if idx < 0 {
return
}
if idx < len(loop.jobs)-1 {
loop.jobs[idx] = loop.jobs[len(loop.jobs)-1]
loop.jobs[idx].idx = idx
}
loop.jobs[len(loop.jobs)-1] = nil
loop.jobs = loop.jobs[:len(loop.jobs)-1]
job.idx = -1
}
func (loop *EventLoop) clearImmediate(i *Immediate) {
if i != nil && !i.cancelled {
i.cancelled = true
loop.jobCount--
}
}
func (i *Interval) doCancel() bool {
close(i.stopChan)
return false
}
func (t *Timer) doCancel() bool {
return t.timer.Stop()
}
func (i *Interval) run(loop *EventLoop) {
L:
for {
select {
case <-i.stopChan:
i.ticker.Stop()
break L
case <-i.ticker.C:
loop.jobChan <- func() {
loop.doInterval(i)
}
}
}
loop.jobChan <- func() {
loop.removeJob(&i.job)
}
}

View File

@@ -0,0 +1,231 @@
package require
import (
"errors"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"syscall"
"text/template"
js "github.com/dop251/goja"
"github.com/dop251/goja/parser"
)
type ModuleLoader func(*js.Runtime, *js.Object)
// SourceLoader represents a function that returns a file data at a given path.
// The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory.
// This error will be ignored by the resolver and the search will continue. Any other errors will be propagated.
type SourceLoader func(path string) ([]byte, error)
var (
InvalidModuleError = errors.New("Invalid module")
IllegalModuleNameError = errors.New("Illegal module name")
NoSuchBuiltInModuleError = errors.New("No such built-in module")
ModuleFileDoesNotExistError = errors.New("module file does not exist")
)
// Registry contains a cache of compiled modules which can be used by multiple Runtimes
type Registry struct {
sync.Mutex
native map[string]ModuleLoader
builtin map[string]ModuleLoader
compiled map[string]*js.Program
srcLoader SourceLoader
globalFolders []string
fsEnabled bool
}
type RequireModule struct {
r *Registry
runtime *js.Runtime
modules map[string]*js.Object
nodeModules map[string]*js.Object
}
func NewRegistry(opts ...Option) *Registry {
r := &Registry{}
for _, opt := range opts {
opt(r)
}
return r
}
type Option func(*Registry)
// WithLoader sets a function which will be called by the require() function in order to get a source code for a
// module at the given path. The same function will be used to get external source maps.
// Note, this only affects the modules loaded by the require() function. If you need to use it as a source map
// loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions()
func WithLoader(srcLoader SourceLoader) Option {
return func(r *Registry) {
r.srcLoader = srcLoader
}
}
// WithGlobalFolders appends the given paths to the registry's list of
// global folders to search if the requested module is not found
// elsewhere. By default, a registry's global folders list is empty.
// In the reference Node.js implementation, the default global folders
// list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and
// $PREFIX/lib/node, see
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders.
func WithGlobalFolders(globalFolders ...string) Option {
return func(r *Registry) {
r.globalFolders = globalFolders
}
}
func WithFsEnable(enabled bool) Option {
return func(r *Registry) {
r.fsEnabled = enabled
}
}
// Enable adds the require() function to the specified runtime.
func (r *Registry) Enable(runtime *js.Runtime) *RequireModule {
rrt := &RequireModule{
r: r,
runtime: runtime,
modules: make(map[string]*js.Object),
nodeModules: make(map[string]*js.Object),
}
runtime.Set("require", rrt.require)
return rrt
}
func (r *Registry) RegisterNodeModule(name string, loader ModuleLoader) {
r.Lock()
defer r.Unlock()
if r.builtin == nil {
r.builtin = make(map[string]ModuleLoader)
}
name = filepathClean(name)
r.builtin[name] = loader
}
func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) {
r.Lock()
defer r.Unlock()
if r.native == nil {
r.native = make(map[string]ModuleLoader)
}
name = filepathClean(name)
r.native[name] = loader
}
// DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem.
func DefaultSourceLoader(filename string) ([]byte, error) {
fp := filepath.FromSlash(filename)
f, err := os.Open(fp)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = ModuleFileDoesNotExistError
} else if runtime.GOOS == "windows" {
if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect.
err = ModuleFileDoesNotExistError
}
}
return nil, err
}
defer f.Close()
// On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories
// which means we cannot rely on read() returning an error, we have to do stat() instead.
if fi, err := f.Stat(); err == nil {
if fi.IsDir() {
return nil, ModuleFileDoesNotExistError
}
} else {
return nil, err
}
return io.ReadAll(f)
}
func (r *Registry) getSource(p string) ([]byte, error) {
srcLoader := r.srcLoader
if srcLoader == nil {
srcLoader = DefaultSourceLoader
}
return srcLoader(p)
}
func (r *Registry) getCompiledSource(p string) (*js.Program, error) {
r.Lock()
defer r.Unlock()
prg := r.compiled[p]
if prg == nil {
buf, err := r.getSource(p)
if err != nil {
return nil, err
}
s := string(buf)
if path.Ext(p) == ".json" {
s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')"
}
source := "(function(exports, require, module) {" + s + "\n})"
parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader))
if err != nil {
return nil, err
}
prg, err = js.CompileAST(parsed, false)
if err == nil {
if r.compiled == nil {
r.compiled = make(map[string]*js.Program)
}
r.compiled[p] = prg
}
return prg, err
}
return prg, nil
}
func (r *RequireModule) require(call js.FunctionCall) js.Value {
ret, err := r.Require(call.Argument(0).String())
if err != nil {
if _, ok := err.(*js.Exception); !ok {
panic(r.runtime.NewGoError(err))
}
panic(err)
}
return ret
}
func filepathClean(p string) string {
return path.Clean(p)
}
// Require can be used to import modules from Go source (similar to JS require() function).
func (r *RequireModule) Require(p string) (ret js.Value, err error) {
module, err := r.resolve(p)
if err != nil {
return
}
ret = module.Get("exports")
return
}
func Require(runtime *js.Runtime, name string) js.Value {
if r, ok := js.AssertFunction(runtime.Get("require")); ok {
mod, err := r(js.Undefined(), runtime.ToValue(name))
if err != nil {
panic(err)
}
return mod
}
panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)"))
}

View File

@@ -0,0 +1,277 @@
package require
import (
"encoding/json"
"errors"
"path"
"path/filepath"
"runtime"
"strings"
js "github.com/dop251/goja"
)
const NodePrefix = "node:"
// NodeJS module search algorithm described by
// https://nodejs.org/api/modules.html#modules_all_together
func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) {
origPath, modpath := modpath, filepathClean(modpath)
if modpath == "" {
return nil, IllegalModuleNameError
}
var start string
err = nil
if path.IsAbs(origPath) {
start = "/"
} else {
start = r.getCurrentModulePath()
}
p := path.Join(start, modpath)
if isFileOrDirectoryPath(origPath) && r.r.fsEnabled {
if module = r.modules[p]; module != nil {
return
}
module, err = r.loadAsFileOrDirectory(p)
if err == nil && module != nil {
r.modules[p] = module
}
} else {
module, err = r.loadNative(origPath)
if err == nil {
return
} else {
if err == InvalidModuleError {
err = nil
} else {
return
}
}
if module = r.nodeModules[p]; module != nil {
return
}
if r.r.fsEnabled {
module, err = r.loadNodeModules(modpath, start)
if err == nil && module != nil {
r.nodeModules[p] = module
}
}
}
if module == nil && err == nil {
err = InvalidModuleError
}
return
}
func (r *RequireModule) loadNative(path string) (*js.Object, error) {
module := r.modules[path]
if module != nil {
return module, nil
}
var ldr ModuleLoader
if r.r.native != nil {
ldr = r.r.native[path]
}
var isBuiltIn, withPrefix bool
if ldr == nil {
if r.r.builtin != nil {
ldr = r.r.builtin[path]
}
if ldr == nil && strings.HasPrefix(path, NodePrefix) {
ldr = r.r.builtin[path[len(NodePrefix):]]
if ldr == nil {
return nil, NoSuchBuiltInModuleError
}
withPrefix = true
}
isBuiltIn = true
}
if ldr != nil {
module = r.createModuleObject()
r.modules[path] = module
if isBuiltIn {
if withPrefix {
r.modules[path[len(NodePrefix):]] = module
} else {
if !strings.HasPrefix(path, NodePrefix) {
r.modules[NodePrefix+path] = module
}
}
}
ldr(r.runtime, module)
return module, nil
}
return nil, InvalidModuleError
}
func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) {
if module, err = r.loadAsFile(path); module != nil || err != nil {
return
}
return r.loadAsDirectory(path)
}
func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) {
if module, err = r.loadModule(path); module != nil || err != nil {
return
}
p := path + ".js"
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = path + ".json"
return r.loadModule(p)
}
func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) {
p := path.Join(modpath, "index.js")
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = path.Join(modpath, "index.json")
return r.loadModule(p)
}
func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) {
p := path.Join(modpath, "package.json")
buf, err := r.r.getSource(p)
if err != nil {
return r.loadIndex(modpath)
}
var pkg struct {
Main string
}
err = json.Unmarshal(buf, &pkg)
if err != nil || len(pkg.Main) == 0 {
return r.loadIndex(modpath)
}
m := path.Join(modpath, pkg.Main)
if module, err = r.loadAsFile(m); module != nil || err != nil {
return
}
return r.loadIndex(m)
}
func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) {
return r.loadAsFileOrDirectory(path.Join(start, modpath))
}
func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) {
for _, dir := range r.r.globalFolders {
if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil {
return
}
}
for {
var p string
if path.Base(start) != "node_modules" {
p = path.Join(start, "node_modules")
} else {
p = start
}
if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil {
return
}
if start == ".." { // Dir('..') is '.'
break
}
parent := path.Dir(start)
if parent == start {
break
}
start = parent
}
return
}
func (r *RequireModule) getCurrentModulePath() string {
var buf [2]js.StackFrame
frames := r.runtime.CaptureCallStack(2, buf[:0])
if len(frames) < 2 {
return "."
}
return path.Dir(frames[1].SrcName())
}
func (r *RequireModule) createModuleObject() *js.Object {
module := r.runtime.NewObject()
module.Set("exports", r.runtime.NewObject())
return module
}
func (r *RequireModule) loadModule(path string) (*js.Object, error) {
module := r.modules[path]
if module == nil {
module = r.createModuleObject()
r.modules[path] = module
err := r.loadModuleFile(path, module)
if err != nil {
module = nil
delete(r.modules, path)
if errors.Is(err, ModuleFileDoesNotExistError) {
err = nil
}
}
return module, err
}
return module, nil
}
func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error {
prg, err := r.r.getCompiledSource(path)
if err != nil {
return err
}
f, err := r.runtime.RunProgram(prg)
if err != nil {
return err
}
if call, ok := js.AssertFunction(f); ok {
jsExports := jsModule.Get("exports")
jsRequire := r.runtime.Get("require")
// Run the module source, with "jsExports" as "this",
// "jsExports" as the "exports" variable, "jsRequire"
// as the "require" variable and "jsModule" as the
// "module" variable (Nodejs capable).
_, err = call(jsExports, jsExports, jsRequire, jsModule)
if err != nil {
return err
}
} else {
return InvalidModuleError
}
return nil
}
func isFileOrDirectoryPath(path string) bool {
result := path == "." || path == ".." ||
strings.HasPrefix(path, "/") ||
strings.HasPrefix(path, "./") ||
strings.HasPrefix(path, "../")
if runtime.GOOS == "windows" {
result = result ||
strings.HasPrefix(path, `.\`) ||
strings.HasPrefix(path, `..\`) ||
filepath.IsAbs(path)
}
return result
}

View File

@@ -0,0 +1,147 @@
package sghttp
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"net/http/cookiejar"
"sync"
"time"
"github.com/sagernet/sing-box/script/jsc"
F "github.com/sagernet/sing/common/format"
"github.com/dop251/goja"
"golang.org/x/net/publicsuffix"
)
type SurgeHTTP struct {
vm *goja.Runtime
ctx context.Context
cookieAccess sync.RWMutex
cookieJar *cookiejar.Jar
errorHandler func(error)
}
func Enable(vm *goja.Runtime, ctx context.Context, errorHandler func(error)) {
sgHTTP := &SurgeHTTP{
vm: vm,
ctx: ctx,
errorHandler: errorHandler,
}
httpObject := vm.NewObject()
httpObject.Set("get", sgHTTP.request(http.MethodGet))
httpObject.Set("post", sgHTTP.request(http.MethodPost))
httpObject.Set("put", sgHTTP.request(http.MethodPut))
httpObject.Set("delete", sgHTTP.request(http.MethodDelete))
httpObject.Set("head", sgHTTP.request(http.MethodHead))
httpObject.Set("options", sgHTTP.request(http.MethodOptions))
httpObject.Set("patch", sgHTTP.request(http.MethodPatch))
httpObject.Set("trace", sgHTTP.request(http.MethodTrace))
vm.Set("$http", httpObject)
}
func (s *SurgeHTTP) request(method string) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 2 {
panic(s.vm.NewTypeError("invalid arguments"))
}
var (
url string
headers http.Header
body []byte
timeout = 5 * time.Second
insecure bool
autoCookie bool
autoRedirect bool
// policy string
binaryMode bool
)
switch optionsValue := call.Argument(0).(type) {
case goja.String:
url = optionsValue.String()
case *goja.Object:
url = jsc.AssertString(s.vm, optionsValue.Get("url"), "options.url", false)
headers = jsc.AssertHTTPHeader(s.vm, optionsValue.Get("headers"), "option.headers")
body = jsc.AssertStringBinary(s.vm, optionsValue.Get("body"), "options.body", true)
timeoutInt := jsc.AssertInt(s.vm, optionsValue.Get("timeout"), "options.timeout", true)
if timeoutInt > 0 {
timeout = time.Duration(timeoutInt) * time.Second
}
insecure = jsc.AssertBool(s.vm, optionsValue.Get("insecure"), "options.insecure", true)
autoCookie = jsc.AssertBool(s.vm, optionsValue.Get("auto-cookie"), "options.auto-cookie", true)
autoRedirect = jsc.AssertBool(s.vm, optionsValue.Get("auto-redirect"), "options.auto-redirect", true)
// policy = jsc.AssertString(s.vm, optionsValue.Get("policy"), "options.policy", true)
binaryMode = jsc.AssertBool(s.vm, optionsValue.Get("binary-mode"), "options.binary-mode", true)
default:
panic(s.vm.NewTypeError(F.ToString("invalid argument: options: expected string or object, but got ", optionsValue)))
}
callback := jsc.AssertFunction(s.vm, call.Argument(1), "callback")
httpClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
ForceAttemptHTTP2: true,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if autoRedirect {
return nil
}
return http.ErrUseLastResponse
},
}
if autoCookie {
s.cookieAccess.Lock()
if s.cookieJar == nil {
s.cookieJar, _ = cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
}
httpClient.Jar = s.cookieJar
s.cookieAccess.Lock()
}
request, err := http.NewRequestWithContext(s.ctx, method, url, bytes.NewReader(body))
if host := headers.Get("Host"); host != "" {
request.Host = host
headers.Del("Host")
}
request.Header = headers
if err != nil {
panic(s.vm.NewGoError(err))
}
go func() {
response, executeErr := httpClient.Do(request)
if err != nil {
_, err = callback(nil, s.vm.NewGoError(executeErr), nil, nil)
if err != nil {
s.errorHandler(err)
}
return
}
defer response.Body.Close()
var content []byte
content, err = io.ReadAll(response.Body)
if err != nil {
_, err = callback(nil, s.vm.NewGoError(err), nil, nil)
if err != nil {
s.errorHandler(err)
}
}
responseObject := s.vm.NewObject()
responseObject.Set("status", response.StatusCode)
responseObject.Set("headers", jsc.HeadersToValue(s.vm, response.Header))
var bodyValue goja.Value
if binaryMode {
bodyValue = jsc.NewUint8Array(s.vm, content)
} else {
bodyValue = s.vm.ToValue(string(content))
}
_, err = callback(nil, nil, responseObject, bodyValue)
}()
return goja.Undefined()
}
}

View File

@@ -0,0 +1,111 @@
package sgnotification
import (
"context"
"encoding/base64"
"strings"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/script/jsc"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/dop251/goja"
)
type SurgeNotification struct {
vm *goja.Runtime
logger logger.Logger
platformInterface platform.Interface
scriptTag string
}
func Enable(vm *goja.Runtime, ctx context.Context, logger logger.Logger) {
platformInterface := service.FromContext[platform.Interface](ctx)
notification := &SurgeNotification{
vm: vm,
logger: logger,
platformInterface: platformInterface,
}
notificationObject := vm.NewObject()
notificationObject.Set("post", notification.js_post)
vm.Set("$notification", notificationObject)
}
func (s *SurgeNotification) js_post(call goja.FunctionCall) goja.Value {
var (
title string
subtitle string
body string
openURL string
clipboard string
mediaURL string
mediaData []byte
mediaType string
autoDismiss int
)
title = jsc.AssertString(s.vm, call.Argument(0), "title", true)
subtitle = jsc.AssertString(s.vm, call.Argument(1), "subtitle", true)
body = jsc.AssertString(s.vm, call.Argument(2), "body", true)
options := jsc.AssertObject(s.vm, call.Argument(3), "options", true)
if options != nil {
action := jsc.AssertString(s.vm, options.Get("action"), "options.action", true)
switch action {
case "open-url":
openURL = jsc.AssertString(s.vm, options.Get("url"), "options.url", false)
case "clipboard":
clipboard = jsc.AssertString(s.vm, options.Get("clipboard"), "options.clipboard", false)
}
mediaURL = jsc.AssertString(s.vm, options.Get("media-url"), "options.media-url", true)
mediaBase64 := jsc.AssertString(s.vm, options.Get("media-base64"), "options.media-base64", true)
if mediaBase64 != "" {
mediaBinary, err := base64.StdEncoding.DecodeString(mediaBase64)
if err != nil {
panic(s.vm.NewGoError(E.Cause(err, "decode media-base64")))
}
mediaData = mediaBinary
mediaType = jsc.AssertString(s.vm, options.Get("media-base64-mime"), "options.media-base64-mime", false)
}
autoDismiss = int(jsc.AssertInt(s.vm, options.Get("auto-dismiss"), "options.auto-dismiss", true))
}
if title != "" && subtitle == "" && body == "" {
body = title
title = ""
} else if title != "" && subtitle != "" && body == "" {
body = subtitle
subtitle = ""
}
var builder strings.Builder
if title != "" {
builder.WriteString("[")
builder.WriteString(title)
if subtitle != "" {
builder.WriteString(" - ")
builder.WriteString(subtitle)
}
builder.WriteString("]: ")
}
builder.WriteString(body)
s.logger.Info("notification: " + builder.String())
if s.platformInterface != nil {
err := s.platformInterface.SendNotification(&platform.Notification{
Identifier: "surge-script-notification-" + s.scriptTag,
TypeName: "Surge Script Notification (" + s.scriptTag + ")",
TypeID: 11,
Title: title,
Subtitle: subtitle,
Body: body,
OpenURL: openURL,
Clipboard: clipboard,
MediaURL: mediaURL,
MediaData: mediaData,
MediaType: mediaType,
Timeout: autoDismiss,
})
if err != nil {
s.logger.Error(E.Cause(err, "send notification"))
}
}
return goja.Undefined()
}

View File

@@ -0,0 +1,76 @@
package sgstore
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing/service"
"github.com/dop251/goja"
)
type SurgePersistentStore struct {
vm *goja.Runtime
cacheFile adapter.CacheFile
data map[string]string
tag string
}
func Enable(vm *goja.Runtime, ctx context.Context) {
object := vm.NewObject()
cacheFile := service.FromContext[adapter.CacheFile](ctx)
tag := vm.Get("$script").(*goja.Object).Get("name").String()
store := &SurgePersistentStore{
vm: vm,
cacheFile: cacheFile,
tag: tag,
}
if cacheFile == nil {
store.data = make(map[string]string)
}
object.Set("read", store.js_read)
object.Set("write", store.js_write)
vm.Set("$persistentStore", object)
}
func (s *SurgePersistentStore) js_read(call goja.FunctionCall) goja.Value {
if len(call.Arguments) > 1 {
panic(s.vm.NewTypeError("invalid arguments"))
}
key := jsc.AssertString(s.vm, call.Argument(0), "key", true)
if key == "" {
key = s.tag
}
var value string
if s.cacheFile != nil {
value = s.cacheFile.SurgePersistentStoreRead(key)
} else {
value = s.data[key]
}
if value == "" {
return goja.Null()
} else {
return s.vm.ToValue(value)
}
}
func (s *SurgePersistentStore) js_write(call goja.FunctionCall) goja.Value {
if len(call.Arguments) == 0 || len(call.Arguments) > 2 {
panic(s.vm.NewTypeError("invalid arguments"))
}
data := jsc.AssertString(s.vm, call.Argument(0), "data", true)
key := jsc.AssertString(s.vm, call.Argument(1), "key", true)
if key == "" {
key = s.tag
}
if s.cacheFile != nil {
err := s.cacheFile.SurgePersistentStoreWrite(key, data)
if err != nil {
panic(s.vm.NewGoError(err))
}
} else {
s.data[key] = data
}
return goja.Undefined()
}

View File

@@ -0,0 +1,45 @@
package sgutils
import (
"bytes"
"compress/gzip"
"io"
"github.com/sagernet/sing-box/script/jsc"
E "github.com/sagernet/sing/common/exceptions"
"github.com/dop251/goja"
)
type SurgeUtils struct {
vm *goja.Runtime
}
func Enable(runtime *goja.Runtime) {
utils := &SurgeUtils{runtime}
object := runtime.NewObject()
object.Set("geoip", utils.js_stub)
object.Set("ipasn", utils.js_stub)
object.Set("ipaso", utils.js_stub)
object.Set("ungzip", utils.js_ungzip)
}
func (u *SurgeUtils) js_stub(call goja.FunctionCall) goja.Value {
panic(u.vm.NewGoError(E.New("not implemented")))
}
func (u *SurgeUtils) js_ungzip(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 1 {
panic(u.vm.NewGoError(E.New("invalid argument")))
}
binary := jsc.AssertBinary(u.vm, call.Argument(0), "binary", false)
reader, err := gzip.NewReader(bytes.NewReader(binary))
if err != nil {
panic(u.vm.NewGoError(err))
}
binary, err = io.ReadAll(reader)
if err != nil {
panic(u.vm.NewGoError(err))
}
return jsc.NewUint8Array(u.vm, binary)
}