mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-30 16:04:23 +03:00
Add Surge MITM and scripts
This commit is contained in:
23
script/jsc/array.go
Normal file
23
script/jsc/array.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func NewUint8Array(runtime *goja.Runtime, data []byte) goja.Value {
|
||||
buffer := runtime.NewArrayBuffer(data)
|
||||
ctor, loaded := goja.AssertConstructor(runtimeGetUint8Array(runtime))
|
||||
if !loaded {
|
||||
panic(runtime.NewTypeError("missing UInt8Array constructor"))
|
||||
}
|
||||
array, err := ctor(nil, runtime.ToValue(buffer))
|
||||
if err != nil {
|
||||
panic(runtime.NewGoError(err))
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
//go:linkname runtimeGetUint8Array github.com/dop251/goja.(*Runtime).getUint8Array
|
||||
func runtimeGetUint8Array(r *goja.Runtime) *goja.Object
|
||||
18
script/jsc/array_test.go
Normal file
18
script/jsc/array_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package jsc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewUInt8Array(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
runtime.Set("hello", jsc.NewUint8Array(runtime, []byte("world")))
|
||||
result, err := runtime.RunString("hello instanceof Uint8Array")
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.ToBoolean())
|
||||
}
|
||||
124
script/jsc/assert.go
Normal file
124
script/jsc/assert.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func IsNil(value goja.Value) bool {
|
||||
return value == nil || goja.IsUndefined(value) || goja.IsNull(value)
|
||||
}
|
||||
|
||||
func AssertObject(vm *goja.Runtime, value goja.Value, name string, nilable bool) *goja.Object {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return nil
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
objectValue, isObject := value.(*goja.Object)
|
||||
if !isObject {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected object, but got ", value)))
|
||||
}
|
||||
return objectValue
|
||||
}
|
||||
|
||||
func AssertString(vm *goja.Runtime, value goja.Value, name string, nilable bool) string {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return ""
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
stringValue, isString := value.Export().(string)
|
||||
if !isString {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, but got ", value)))
|
||||
}
|
||||
return stringValue
|
||||
}
|
||||
|
||||
func AssertInt(vm *goja.Runtime, value goja.Value, name string, nilable bool) int64 {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return 0
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
integerValue, isNumber := value.Export().(int64)
|
||||
if !isNumber {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected integer, but got ", value)))
|
||||
}
|
||||
return integerValue
|
||||
}
|
||||
|
||||
func AssertBool(vm *goja.Runtime, value goja.Value, name string, nilable bool) bool {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return false
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
boolValue, isBool := value.Export().(bool)
|
||||
if !isBool {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected boolean, but got ", value)))
|
||||
}
|
||||
return boolValue
|
||||
}
|
||||
|
||||
func AssertBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return nil
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
switch exportedValue := value.Export().(type) {
|
||||
case []byte:
|
||||
return exportedValue
|
||||
case goja.ArrayBuffer:
|
||||
return exportedValue.Bytes()
|
||||
default:
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected Uint8Array or ArrayBuffer, but got ", value)))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertStringBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return nil
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
switch exportedValue := value.Export().(type) {
|
||||
case string:
|
||||
return []byte(exportedValue)
|
||||
case []byte:
|
||||
return exportedValue
|
||||
case goja.ArrayBuffer:
|
||||
return exportedValue.Bytes()
|
||||
default:
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, Uint8Array or ArrayBuffer, but got ", value)))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertFunction(vm *goja.Runtime, value goja.Value, name string) goja.Callable {
|
||||
if IsNil(value) {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
functionValue, isFunction := goja.AssertFunction(value)
|
||||
if !isFunction {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected function, but got ", value)))
|
||||
}
|
||||
return functionValue
|
||||
}
|
||||
|
||||
func AssertHTTPHeader(vm *goja.Runtime, value goja.Value, name string) http.Header {
|
||||
headersObject := AssertObject(vm, value, name, true)
|
||||
if headersObject == nil {
|
||||
return nil
|
||||
}
|
||||
return ObjectToHeaders(vm, headersObject, name)
|
||||
}
|
||||
192
script/jsc/class.go
Normal file
192
script/jsc/class.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
Runtime() *goja.Runtime
|
||||
}
|
||||
|
||||
type Class[M Module, C any] interface {
|
||||
Module() M
|
||||
Runtime() *goja.Runtime
|
||||
DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value))
|
||||
DefineMethod(name string, method func(this C, call goja.FunctionCall) any)
|
||||
DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any)
|
||||
DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C)
|
||||
ToValue() goja.Value
|
||||
New(instance C) *goja.Object
|
||||
Prototype() *goja.Object
|
||||
Is(value goja.Value) bool
|
||||
As(value goja.Value) C
|
||||
}
|
||||
|
||||
func GetClass[M Module, C any](runtime *goja.Runtime, exports *goja.Object, className string) Class[M, C] {
|
||||
objectValue := exports.Get(className)
|
||||
if objectValue == nil {
|
||||
panic(runtime.NewTypeError("Missing class: " + className))
|
||||
}
|
||||
object, isObject := objectValue.(*goja.Object)
|
||||
if !isObject {
|
||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
||||
}
|
||||
classObject, isClass := object.Get("_class").(*goja.Object)
|
||||
if !isClass {
|
||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
||||
}
|
||||
class, isClass := classObject.Export().(Class[M, C])
|
||||
if !isClass {
|
||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
||||
}
|
||||
return class
|
||||
}
|
||||
|
||||
type goClass[M Module, C any] struct {
|
||||
m M
|
||||
prototype *goja.Object
|
||||
constructor goja.Value
|
||||
}
|
||||
|
||||
func NewClass[M Module, C any](module M) Class[M, C] {
|
||||
class := &goClass[M, C]{
|
||||
m: module,
|
||||
prototype: module.Runtime().NewObject(),
|
||||
}
|
||||
clazz := module.Runtime().ToValue(class).(*goja.Object)
|
||||
clazz.Set("toString", module.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
return module.Runtime().ToValue("[sing-box Class]")
|
||||
}))
|
||||
class.prototype.DefineAccessorProperty("_class", class.Runtime().ToValue(func(call goja.FunctionCall) goja.Value { return clazz }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
return class
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Module() M {
|
||||
return c.m
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Runtime() *goja.Runtime {
|
||||
return c.m.Runtime()
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value)) {
|
||||
var (
|
||||
getterValue goja.Value
|
||||
setterValue goja.Value
|
||||
)
|
||||
if getter != nil {
|
||||
getterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
this, isThis := call.This.Export().(C)
|
||||
if !isThis {
|
||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.ExportType().String()))
|
||||
}
|
||||
return c.toValue(getter(this), goja.Null())
|
||||
})
|
||||
}
|
||||
if setter != nil {
|
||||
setterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
this, isThis := call.This.Export().(C)
|
||||
if !isThis {
|
||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
|
||||
}
|
||||
setter(this, call.Argument(0))
|
||||
return goja.Undefined()
|
||||
})
|
||||
}
|
||||
c.prototype.DefineAccessorProperty(name, getterValue, setterValue, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineMethod(name string, method func(this C, call goja.FunctionCall) any) {
|
||||
methodValue := c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
this, isThis := call.This.Export().(C)
|
||||
if !isThis {
|
||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
|
||||
}
|
||||
return c.toValue(method(this, call), goja.Undefined())
|
||||
})
|
||||
c.prototype.Set(name, methodValue)
|
||||
if name == "entries" {
|
||||
c.prototype.DefineDataPropertySymbol(goja.SymIterator, methodValue, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any) {
|
||||
c.prototype.Set(name, c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
return c.toValue(method(c, call), goja.Undefined())
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C) {
|
||||
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
value := constructor(c, call)
|
||||
object := c.toValue(value, goja.Undefined()).(*goja.Object)
|
||||
object.SetPrototype(call.This.Prototype())
|
||||
return object
|
||||
}).(*goja.Object)
|
||||
constructorObject.SetPrototype(c.prototype)
|
||||
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
|
||||
c.constructor = constructorObject
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) toValue(rawValue any, defaultValue goja.Value) goja.Value {
|
||||
switch value := rawValue.(type) {
|
||||
case nil:
|
||||
return defaultValue
|
||||
case time.Time:
|
||||
return TimeToValue(c.Runtime(), value)
|
||||
default:
|
||||
return c.Runtime().ToValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) ToValue() goja.Value {
|
||||
if c.constructor == nil {
|
||||
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
panic(c.Runtime().NewTypeError("Illegal constructor call"))
|
||||
}).(*goja.Object)
|
||||
constructorObject.SetPrototype(c.prototype)
|
||||
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
|
||||
c.constructor = constructorObject
|
||||
}
|
||||
return c.constructor
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) New(instance C) *goja.Object {
|
||||
object := c.Runtime().ToValue(instance).(*goja.Object)
|
||||
object.SetPrototype(c.prototype)
|
||||
return object
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Prototype() *goja.Object {
|
||||
return c.prototype
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Is(value goja.Value) bool {
|
||||
object, isObject := value.(*goja.Object)
|
||||
if !isObject {
|
||||
return false
|
||||
}
|
||||
prototype := object.Prototype()
|
||||
for prototype != nil {
|
||||
if prototype == c.prototype {
|
||||
return true
|
||||
}
|
||||
prototype = prototype.Prototype()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) As(value goja.Value) C {
|
||||
object, isObject := value.(*goja.Object)
|
||||
if !isObject {
|
||||
return common.DefaultValue[C]()
|
||||
}
|
||||
if !c.Is(object) {
|
||||
return common.DefaultValue[C]()
|
||||
}
|
||||
return object.Export().(C)
|
||||
}
|
||||
56
script/jsc/headers.go
Normal file
56
script/jsc/headers.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func HeadersToValue(runtime *goja.Runtime, headers http.Header) goja.Value {
|
||||
object := runtime.NewObject()
|
||||
for key, value := range headers {
|
||||
if len(value) == 1 {
|
||||
object.Set(key, value[0])
|
||||
} else {
|
||||
object.Set(key, ArrayToValue(runtime, value))
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
func ArrayToValue[T any](runtime *goja.Runtime, values []T) goja.Value {
|
||||
return runtime.NewArray(common.Map(values, func(it T) any { return it })...)
|
||||
}
|
||||
|
||||
func ObjectToHeaders(vm *goja.Runtime, object *goja.Object, name string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, key := range object.Keys() {
|
||||
valueObject := object.Get(key)
|
||||
switch headerValue := valueObject.(type) {
|
||||
case goja.String:
|
||||
headers.Set(key, headerValue.String())
|
||||
case *goja.Object:
|
||||
values := headerValue.Export()
|
||||
valueArray, isArray := values.([]any)
|
||||
if !isArray {
|
||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, "expected string or string array, got ", valueObject.String())))
|
||||
}
|
||||
newValues := make([]string, 0, len(valueArray))
|
||||
for _, value := range valueArray {
|
||||
stringValue, isString := value.(string)
|
||||
if !isString {
|
||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got array item type: ", reflect.TypeOf(value))))
|
||||
}
|
||||
newValues = append(newValues, stringValue)
|
||||
}
|
||||
headers[key] = newValues
|
||||
default:
|
||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got ", valueObject.String())))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
31
script/jsc/headers_test.go
Normal file
31
script/jsc/headers_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package jsc_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
runtime.Set("headers", jsc.HeadersToValue(runtime, http.Header{
|
||||
"My-Header": []string{"My-Value1", "My-Value2"},
|
||||
}))
|
||||
headers := runtime.Get("headers").(*goja.Object).Get("My-Header").(*goja.Object)
|
||||
fmt.Println(reflect.ValueOf(headers.Export()).Type().String())
|
||||
}
|
||||
|
||||
func TestBody(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
_, err := runtime.RunString(`
|
||||
var responseBody = new Uint8Array([1, 2, 3, 4, 5])
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(reflect.TypeOf(runtime.Get("responseBody").Export()))
|
||||
}
|
||||
36
script/jsc/iterator.go
Normal file
36
script/jsc/iterator.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package jsc
|
||||
|
||||
import "github.com/dop251/goja"
|
||||
|
||||
type Iterator[M Module, T any] struct {
|
||||
c Class[M, *Iterator[M, T]]
|
||||
values []T
|
||||
block func(this T) any
|
||||
}
|
||||
|
||||
func NewIterator[M Module, T any](class Class[M, *Iterator[M, T]], values []T, block func(this T) any) goja.Value {
|
||||
return class.New(&Iterator[M, T]{class, values, block})
|
||||
}
|
||||
|
||||
func CreateIterator[M Module, T any](module M) Class[M, *Iterator[M, T]] {
|
||||
class := NewClass[M, *Iterator[M, T]](module)
|
||||
class.DefineMethod("next", (*Iterator[M, T]).next)
|
||||
class.DefineMethod("toString", (*Iterator[M, T]).toString)
|
||||
return class
|
||||
}
|
||||
|
||||
func (i *Iterator[M, T]) next(call goja.FunctionCall) any {
|
||||
result := i.c.Runtime().NewObject()
|
||||
if len(i.values) == 0 {
|
||||
result.Set("done", true)
|
||||
} else {
|
||||
result.Set("done", false)
|
||||
result.Set("value", i.block(i.values[0]))
|
||||
i.values = i.values[1:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Iterator[M, T]) toString(call goja.FunctionCall) any {
|
||||
return "[sing-box Iterator]"
|
||||
}
|
||||
18
script/jsc/time.go
Normal file
18
script/jsc/time.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"time"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func TimeToValue(runtime *goja.Runtime, time time.Time) goja.Value {
|
||||
return runtimeNewDateObject(runtime, time, true, runtimeGetDatePrototype(runtime))
|
||||
}
|
||||
|
||||
//go:linkname runtimeNewDateObject github.com/dop251/goja.(*Runtime).newDateObject
|
||||
func runtimeNewDateObject(r *goja.Runtime, t time.Time, isSet bool, proto *goja.Object) *goja.Object
|
||||
|
||||
//go:linkname runtimeGetDatePrototype github.com/dop251/goja.(*Runtime).getDatePrototype
|
||||
func runtimeGetDatePrototype(r *goja.Runtime) *goja.Object
|
||||
20
script/jsc/time_test.go
Normal file
20
script/jsc/time_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package jsc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimeToValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
runtime := goja.New()
|
||||
now := time.Now()
|
||||
err := runtime.Set("now", jsc.TimeToValue(runtime, now))
|
||||
require.NoError(t, err)
|
||||
println(runtime.Get("now").String())
|
||||
}
|
||||
Reference in New Issue
Block a user