mitm: Refactor & Add url

This commit is contained in:
世界
2025-02-04 15:00:59 +08:00
parent fb3007fa80
commit 003423745e
42 changed files with 3152 additions and 1164 deletions

View File

@@ -0,0 +1,65 @@
package surge
import (
"runtime"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/script/jsc"
"github.com/dop251/goja"
)
type Environment struct {
class jsc.Class[*Module, *Environment]
}
func createEnvironment(module *Module) jsc.Class[*Module, *Environment] {
class := jsc.NewClass[*Module, *Environment](module)
class.DefineField("system", (*Environment).getSystem, nil)
class.DefineField("surge-build", (*Environment).getSurgeBuild, nil)
class.DefineField("surge-version", (*Environment).getSurgeVersion, nil)
class.DefineField("language", (*Environment).getLanguage, nil)
class.DefineField("device-model", (*Environment).getDeviceModel, nil)
class.DefineMethod("toString", (*Environment).toString)
return class
}
func (e *Environment) getSystem() any {
switch runtime.GOOS {
case "ios":
return "iOS"
case "darwin":
return "macOS"
case "tvos":
return "tvOS"
case "linux":
return "Linux"
case "android":
return "Android"
case "windows":
return "Windows"
default:
return runtime.GOOS
}
}
func (e *Environment) getSurgeBuild() any {
return "N/A"
}
func (e *Environment) getSurgeVersion() any {
return "sing-box " + C.Version
}
func (e *Environment) getLanguage() any {
return locale.Current().Locale
}
func (e *Environment) getDeviceModel() any {
return "N/A"
}
func (e *Environment) toString(call goja.FunctionCall) any {
return "[sing-box Surge environment"
}

View File

@@ -0,0 +1,150 @@
package surge
import (
"bytes"
"crypto/tls"
"io"
"net/http"
"net/http/cookiejar"
"time"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/boxctx"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/dop251/goja"
"golang.org/x/net/publicsuffix"
)
type HTTP struct {
class jsc.Class[*Module, *HTTP]
cookieJar *cookiejar.Jar
httpTransport *http.Transport
}
func createHTTP(module *Module) jsc.Class[*Module, *HTTP] {
class := jsc.NewClass[*Module, *HTTP](module)
class.DefineConstructor(newHTTP)
class.DefineMethod("get", httpRequest(http.MethodGet))
class.DefineMethod("post", httpRequest(http.MethodPost))
class.DefineMethod("put", httpRequest(http.MethodPut))
class.DefineMethod("delete", httpRequest(http.MethodDelete))
class.DefineMethod("head", httpRequest(http.MethodHead))
class.DefineMethod("options", httpRequest(http.MethodOptions))
class.DefineMethod("patch", httpRequest(http.MethodPatch))
class.DefineMethod("trace", httpRequest(http.MethodTrace))
class.DefineMethod("toString", (*HTTP).toString)
return class
}
func newHTTP(class jsc.Class[*Module, *HTTP], call goja.ConstructorCall) *HTTP {
return &HTTP{
class: class,
cookieJar: common.Must1(cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})),
httpTransport: &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: &tls.Config{},
},
}
}
func httpRequest(method string) func(s *HTTP, call goja.FunctionCall) any {
return func(s *HTTP, call goja.FunctionCall) any {
if len(call.Arguments) != 2 {
panic(s.class.Runtime().NewTypeError("invalid arguments"))
}
context := boxctx.MustFromRuntime(s.class.Runtime())
var (
url string
headers http.Header
body []byte
timeout = 5 * time.Second
insecure bool
autoCookie bool = true
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.class.Runtime(), optionsValue.Get("url"), "options.url", false)
headers = jsc.AssertHTTPHeader(s.class.Runtime(), optionsValue.Get("headers"), "option.headers")
body = jsc.AssertStringBinary(s.class.Runtime(), optionsValue.Get("body"), "options.body", true)
timeoutInt := jsc.AssertInt(s.class.Runtime(), optionsValue.Get("timeout"), "options.timeout", true)
if timeoutInt > 0 {
timeout = time.Duration(timeoutInt) * time.Second
}
insecure = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("insecure"), "options.insecure", true)
autoCookie = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("auto-cookie"), "options.auto-cookie", true)
autoRedirect = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("auto-redirect"), "options.auto-redirect", true)
// policy = jsc.AssertString(s.class.Runtime(), optionsValue.Get("policy"), "options.policy", true)
binaryMode = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("binary-mode"), "options.binary-mode", true)
default:
panic(s.class.Runtime().NewTypeError(F.ToString("invalid argument: options: expected string or object, but got ", optionsValue)))
}
callback := jsc.AssertFunction(s.class.Runtime(), call.Argument(1), "callback")
s.httpTransport.TLSClientConfig.InsecureSkipVerify = insecure
httpClient := &http.Client{
Timeout: timeout,
Transport: s.httpTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if autoRedirect {
return nil
}
return http.ErrUseLastResponse
},
}
if autoCookie {
httpClient.Jar = s.cookieJar
}
request, err := http.NewRequestWithContext(context.Context, 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.class.Runtime().NewGoError(err))
}
go func() {
defer s.httpTransport.CloseIdleConnections()
response, executeErr := httpClient.Do(request)
if err != nil {
_, err = callback(nil, s.class.Runtime().NewGoError(executeErr), nil, nil)
if err != nil {
context.ErrorHandler(err)
}
return
}
defer response.Body.Close()
var content []byte
content, err = io.ReadAll(response.Body)
if err != nil {
_, err = callback(nil, s.class.Runtime().NewGoError(err), nil, nil)
if err != nil {
context.ErrorHandler(err)
}
}
responseObject := s.class.Runtime().NewObject()
responseObject.Set("status", response.StatusCode)
responseObject.Set("headers", jsc.HeadersToValue(s.class.Runtime(), response.Header))
var bodyValue goja.Value
if binaryMode {
bodyValue = jsc.NewUint8Array(s.class.Runtime(), content)
} else {
bodyValue = s.class.Runtime().ToValue(string(content))
}
_, err = callback(nil, nil, responseObject, bodyValue)
}()
return nil
}
}
func (h *HTTP) toString(call goja.FunctionCall) any {
return "[sing-box Surge HTTP]"
}

View File

@@ -0,0 +1,63 @@
package surge
import (
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/require"
"github.com/sagernet/sing/common"
"github.com/dop251/goja"
)
const ModuleName = "surge"
type Module struct {
runtime *goja.Runtime
classScript jsc.Class[*Module, *Script]
classEnvironment jsc.Class[*Module, *Environment]
classPersistentStore jsc.Class[*Module, *PersistentStore]
classHTTP jsc.Class[*Module, *HTTP]
classUtils jsc.Class[*Module, *Utils]
classNotification jsc.Class[*Module, *Notification]
}
func Require(runtime *goja.Runtime, module *goja.Object) {
m := &Module{
runtime: runtime,
}
m.classScript = createScript(m)
m.classEnvironment = createEnvironment(m)
m.classPersistentStore = createPersistentStore(m)
m.classHTTP = createHTTP(m)
m.classUtils = createUtils(m)
m.classNotification = createNotification(m)
exports := module.Get("exports").(*goja.Object)
exports.Set("Script", m.classScript.ToValue())
exports.Set("Environment", m.classEnvironment.ToValue())
exports.Set("PersistentStore", m.classPersistentStore.ToValue())
exports.Set("HTTP", m.classHTTP.ToValue())
exports.Set("Utils", m.classUtils.ToValue())
exports.Set("Notification", m.classNotification.ToValue())
}
func Enable(runtime *goja.Runtime, scriptType string, args []string) {
exports := require.Require(runtime, ModuleName).ToObject(runtime)
classScript := jsc.GetClass[*Module, *Script](runtime, exports, "Script")
classEnvironment := jsc.GetClass[*Module, *Environment](runtime, exports, "Environment")
classPersistentStore := jsc.GetClass[*Module, *PersistentStore](runtime, exports, "PersistentStore")
classHTTP := jsc.GetClass[*Module, *HTTP](runtime, exports, "HTTP")
classUtils := jsc.GetClass[*Module, *Utils](runtime, exports, "Utils")
classNotification := jsc.GetClass[*Module, *Notification](runtime, exports, "Notification")
runtime.Set("$script", classScript.New(&Script{class: classScript, ScriptType: scriptType}))
runtime.Set("$environment", classEnvironment.New(&Environment{class: classEnvironment}))
runtime.Set("$persistentStore", newPersistentStore(classPersistentStore))
runtime.Set("$http", classHTTP.New(newHTTP(classHTTP, goja.ConstructorCall{})))
runtime.Set("$utils", classUtils.New(&Utils{class: classUtils}))
runtime.Set("$notification", newNotification(classNotification))
runtime.Set("$argument", runtime.NewArray(common.Map(args, func(it string) any {
return it
})...))
}
func (m *Module) Runtime() *goja.Runtime {
return m.runtime
}

View File

@@ -0,0 +1,120 @@
package surge
import (
"encoding/base64"
"strings"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/boxctx"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/dop251/goja"
)
type Notification struct {
class jsc.Class[*Module, *Notification]
logger logger.ContextLogger
tag string
platformInterface platform.Interface
}
func createNotification(module *Module) jsc.Class[*Module, *Notification] {
class := jsc.NewClass[*Module, *Notification](module)
class.DefineMethod("post", (*Notification).post)
class.DefineMethod("toString", (*Notification).toString)
return class
}
func newNotification(class jsc.Class[*Module, *Notification]) goja.Value {
context := boxctx.MustFromRuntime(class.Runtime())
return class.New(&Notification{
class: class,
logger: context.Logger,
tag: context.Tag,
platformInterface: service.FromContext[platform.Interface](context.Context),
})
}
func (s *Notification) post(call goja.FunctionCall) any {
var (
title string
subtitle string
body string
openURL string
clipboard string
mediaURL string
mediaData []byte
mediaType string
autoDismiss int
)
title = jsc.AssertString(s.class.Runtime(), call.Argument(0), "title", true)
subtitle = jsc.AssertString(s.class.Runtime(), call.Argument(1), "subtitle", true)
body = jsc.AssertString(s.class.Runtime(), call.Argument(2), "body", true)
options := jsc.AssertObject(s.class.Runtime(), call.Argument(3), "options", true)
if options != nil {
action := jsc.AssertString(s.class.Runtime(), options.Get("action"), "options.action", true)
switch action {
case "open-url":
openURL = jsc.AssertString(s.class.Runtime(), options.Get("url"), "options.url", false)
case "clipboard":
clipboard = jsc.AssertString(s.class.Runtime(), options.Get("clipboard"), "options.clipboard", false)
}
mediaURL = jsc.AssertString(s.class.Runtime(), options.Get("media-url"), "options.media-url", true)
mediaBase64 := jsc.AssertString(s.class.Runtime(), options.Get("media-base64"), "options.media-base64", true)
if mediaBase64 != "" {
mediaBinary, err := base64.StdEncoding.DecodeString(mediaBase64)
if err != nil {
panic(s.class.Runtime().NewGoError(E.Cause(err, "decode media-base64")))
}
mediaData = mediaBinary
mediaType = jsc.AssertString(s.class.Runtime(), options.Get("media-base64-mime"), "options.media-base64-mime", false)
}
autoDismiss = int(jsc.AssertInt(s.class.Runtime(), 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.tag,
TypeName: "Surge Script Notification (" + s.tag + ")",
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 nil
}
func (s *Notification) toString(call goja.FunctionCall) any {
return "[sing-box Surge notification]"
}

View File

@@ -0,0 +1,78 @@
package surge
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/boxctx"
"github.com/sagernet/sing/service"
"github.com/dop251/goja"
)
type PersistentStore struct {
class jsc.Class[*Module, *PersistentStore]
cacheFile adapter.CacheFile
inMemoryCache *adapter.SurgeInMemoryCache
tag string
}
func createPersistentStore(module *Module) jsc.Class[*Module, *PersistentStore] {
class := jsc.NewClass[*Module, *PersistentStore](module)
class.DefineMethod("get", (*PersistentStore).get)
class.DefineMethod("set", (*PersistentStore).set)
class.DefineMethod("toString", (*PersistentStore).toString)
return class
}
func newPersistentStore(class jsc.Class[*Module, *PersistentStore]) goja.Value {
boxCtx := boxctx.MustFromRuntime(class.Runtime())
return class.New(&PersistentStore{
class: class,
cacheFile: service.FromContext[adapter.CacheFile](boxCtx.Context),
inMemoryCache: service.FromContext[adapter.ScriptManager](boxCtx.Context).SurgeCache(),
tag: boxCtx.Tag,
})
}
func (s *PersistentStore) get(call goja.FunctionCall) any {
key := jsc.AssertString(s.class.Runtime(), call.Argument(0), "key", true)
if key == "" {
key = s.tag
}
var value string
if s.cacheFile != nil {
value = s.cacheFile.SurgePersistentStoreRead(key)
} else {
s.inMemoryCache.RLock()
value = s.inMemoryCache.Data[key]
s.inMemoryCache.RUnlock()
}
if value == "" {
return goja.Null()
} else {
return value
}
}
func (s *PersistentStore) set(call goja.FunctionCall) any {
data := jsc.AssertString(s.class.Runtime(), call.Argument(0), "data", true)
key := jsc.AssertString(s.class.Runtime(), 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.class.Runtime().NewGoError(err))
}
} else {
s.inMemoryCache.Lock()
s.inMemoryCache.Data[key] = data
s.inMemoryCache.Unlock()
}
return goja.Undefined()
}
func (s *PersistentStore) toString(call goja.FunctionCall) any {
return "[sing-box Surge persistentStore]"
}

View File

@@ -0,0 +1,32 @@
package surge
import (
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/boxctx"
F "github.com/sagernet/sing/common/format"
)
type Script struct {
class jsc.Class[*Module, *Script]
ScriptType string
}
func createScript(module *Module) jsc.Class[*Module, *Script] {
class := jsc.NewClass[*Module, *Script](module)
class.DefineField("name", (*Script).getName, nil)
class.DefineField("type", (*Script).getType, nil)
class.DefineField("startTime", (*Script).getStartTime, nil)
return class
}
func (s *Script) getName() any {
return F.ToString("script:", boxctx.MustFromRuntime(s.class.Runtime()).Tag)
}
func (s *Script) getType() any {
return s.ScriptType
}
func (s *Script) getStartTime() any {
return boxctx.MustFromRuntime(s.class.Runtime()).StartedAt
}

View File

@@ -0,0 +1,50 @@
package surge
import (
"bytes"
"compress/gzip"
"io"
"github.com/sagernet/sing-box/script/jsc"
E "github.com/sagernet/sing/common/exceptions"
"github.com/dop251/goja"
)
type Utils struct {
class jsc.Class[*Module, *Utils]
}
func createUtils(module *Module) jsc.Class[*Module, *Utils] {
class := jsc.NewClass[*Module, *Utils](module)
class.DefineMethod("geoip", (*Utils).stub)
class.DefineMethod("ipasn", (*Utils).stub)
class.DefineMethod("ipaso", (*Utils).stub)
class.DefineMethod("ungzip", (*Utils).ungzip)
class.DefineMethod("toString", (*Utils).toString)
return class
}
func (u *Utils) stub(call goja.FunctionCall) any {
return nil
}
func (u *Utils) ungzip(call goja.FunctionCall) any {
if len(call.Arguments) != 1 {
panic(u.class.Runtime().NewGoError(E.New("invalid argument")))
}
binary := jsc.AssertBinary(u.class.Runtime(), call.Argument(0), "binary", false)
reader, err := gzip.NewReader(bytes.NewReader(binary))
if err != nil {
panic(u.class.Runtime().NewGoError(err))
}
binary, err = io.ReadAll(reader)
if err != nil {
panic(u.class.Runtime().NewGoError(err))
}
return jsc.NewUint8Array(u.class.Runtime(), binary)
}
func (u *Utils) toString(call goja.FunctionCall) any {
return "[sing-box Surge utils]"
}