Files
truewhile c1ff55ffbf 开发
2026-09-30 19:22:24 +08:00

510 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rule
import (
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
"hash"
"strings"
"github.com/dop251/goja"
)
// 本文件对应 legado JsExtensions / JsEncodeUtils / RegexJsExtensions 中
// 注入为 `java` 对象的函数。函数名与 Kotlin 版一一对应(存量书源硬编码),
// 实现全部为 Go 原生;浏览器/文件/压缩包类能力明确抛出不支持错误。
func newJavaObject(vm *goja.Runtime, r *JSRunner, a *AnalyzeRule) *goja.Object {
o := vm.NewObject()
set := func(k string, v any) {
if err := o.Set(k, v); err != nil {
panic(vm.ToValue(err.Error()))
}
}
bridgeErr := func(name string, err error) goja.Value {
panic(vm.ToValue(fmt.Sprintf("java.%s: %v", name, err)))
}
// ── 网络(对应 JsExtensions.ajax/ajaxAll/connect/get/post/head) ──
fetchStr := func(name string, urlVal goja.Value, extraHeaders map[string]any) string {
if r.cfg.Fetch == nil {
bridgeErr(name, ErrJsUnsupported)
}
urlStr := urlVal.String()
if arr, ok := urlVal.Export().([]any); ok && len(arr) > 0 {
urlStr = fmt.Sprintf("%v", arr[0]) // 对应 legado:List 取第一个
}
req, err := ParseAnalyzeUrlWithJS(urlStr, "", 0, r.cfg.BaseURL, r)
if err != nil {
bridgeErr(name, err)
}
if req.Unsupported != nil {
bridgeErr(name, req.Unsupported)
}
for k, v := range extraHeaders {
if _, exists := req.Headers[k]; !exists {
req.Headers[k] = fmt.Sprintf("%v", v)
}
}
body, _, _, err := r.cfg.Fetch(req)
if err != nil {
bridgeErr(name, err)
}
return body
}
fetchResp := func(name string, args []goja.Value, method, body string) *goja.Object {
if r.cfg.Fetch == nil {
bridgeErr(name, ErrJsUnsupported)
}
urlStr := ""
if len(args) > 0 {
urlStr = args[0].String()
}
req, err := ParseAnalyzeUrlWithJS(urlStr, "", 0, r.cfg.BaseURL, r)
if err != nil {
bridgeErr(name, err)
}
if method == "POST" {
req.Method = "POST"
req.Body = body
trimmed := strings.TrimSpace(body)
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
req.IsJSON = true
} else if body != "" {
req.IsForm = true
req.Body = encodeParams(body, req.Charset, false)
}
}
if len(args) > 1 && !goja.IsUndefined(args[1]) && !goja.IsNull(args[1]) {
if extra, ok := args[1].Export().(map[string]any); ok {
for k, v := range extra {
req.Headers[k] = fmt.Sprintf("%v", v)
}
}
}
if req.Unsupported != nil {
bridgeErr(name, req.Unsupported)
}
respBody, finalURL, code, err := r.cfg.Fetch(req)
if err != nil {
bridgeErr(name, err)
}
return newResponseObject(vm, respBody, code, finalURL, nil)
}
set("ajax", func(call goja.FunctionCall) goja.Value {
extra := map[string]any(nil)
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
if m, ok := call.Arguments[1].Export().(map[string]any); ok {
extra = m
}
}
return vm.ToValue(fetchStr("ajax", call.Arguments[0], extra))
})
set("ajaxAll", func(call goja.FunctionCall) goja.Value {
var urls []any
if len(call.Arguments) > 0 {
if arr, ok := call.Arguments[0].Export().([]any); ok {
urls = arr
}
}
out := make([]any, 0, len(urls))
for _, u := range urls {
out = append(out, fetchStr("ajaxAll", vm.ToValue(u), nil))
}
return vm.ToValue(out)
})
set("connect", func(call goja.FunctionCall) goja.Value {
return fetchResp("connect", call.Arguments, "GET", "")
})
// get 双语义(对应 Kotlin 重载):get(url, headers) 走网络,get(key) 读变量
set("get", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) >= 2 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
return fetchResp("get", call.Arguments, "GET", "")
}
key := stringArg(call, 0)
if a != nil {
return vm.ToValue(a.Get(key))
}
return vm.ToValue(r.vars[key])
})
set("post", func(call goja.FunctionCall) goja.Value {
body := stringArgOr(call, 1, "")
return fetchResp("post", call.Arguments, "POST", body)
})
set("head", func(call goja.FunctionCall) goja.Value {
return fetchResp("head", call.Arguments, "HEAD", "")
})
// ── 规则回调(桥回当前 AnalyzeRule,对应 AnalyzeRule.getString 等公有方法) ──
ruleStr := func(name string, args []goja.Value) (string, error) {
if a == nil {
return "", fmt.Errorf("无规则上下文")
}
mContent := any(nil)
if len(args) > 1 && !goja.IsUndefined(args[1]) && !goja.IsNull(args[1]) {
mContent = args[1].Export()
}
isURL := len(args) > 2 && args[2].ToBoolean()
return a.GetString(args[0].String(), mContent, isURL)
}
set("getString", func(call goja.FunctionCall) goja.Value {
s, err := ruleStr("getString", call.Arguments)
if err != nil {
bridgeErr("getString", err)
}
return vm.ToValue(s)
})
set("getStringList", func(call goja.FunctionCall) goja.Value {
if a == nil {
return goja.Null()
}
list, err := a.GetStringList(stringArgOr(call, 0, ""), nil, false)
if err != nil {
bridgeErr("getStringList", err)
}
return vm.ToValue(list)
})
set("getElements", func(call goja.FunctionCall) goja.Value {
if a == nil {
return goja.Null()
}
els, err := a.GetElements(stringArgOr(call, 0, ""))
if err != nil {
bridgeErr("getElements", err)
}
out := make([]any, 0, len(els))
for _, el := range els {
out = append(out, resultString(el))
}
return vm.ToValue(out)
})
set("put", func(call goja.FunctionCall) goja.Value {
key := stringArg(call, 0)
val := stringArgOr(call, 1, "")
if a != nil {
return vm.ToValue(a.Put(key, val))
}
r.vars[key] = val
return vm.ToValue(val)
})
// ── 编码(对应 JsEncodeUtils / JsExtensions) ──
set("md5Encode", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(md5Hex(stringArg(call, 0), false))
})
set("md5Encode16", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(md5Hex(stringArg(call, 0), true))
})
set("base64Decode", func(call goja.FunctionCall) goja.Value {
s, err := base64DecodeString(stringArg(call, 0))
if err != nil {
bridgeErr("base64Decode", err)
}
return vm.ToValue(s)
})
set("base64DecodeToByteArray", func(call goja.FunctionCall) goja.Value {
b, err := base64DecodeBytes(stringArg(call, 0))
if err != nil {
bridgeErr("base64DecodeToByteArray", err)
}
return vm.ToValue(vm.NewArrayBuffer(b))
})
set("base64Encode", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(base64.StdEncoding.EncodeToString([]byte(stringArg(call, 0))))
})
set("hexDecodeToString", func(call goja.FunctionCall) goja.Value {
b, err := hex.DecodeString(strings.TrimSpace(stringArg(call, 0)))
if err != nil {
bridgeErr("hexDecodeToString", err)
}
return vm.ToValue(string(b))
})
set("hexDecodeToByteArray", func(call goja.FunctionCall) goja.Value {
b, err := hex.DecodeString(strings.TrimSpace(stringArg(call, 0)))
if err != nil {
bridgeErr("hexDecodeToByteArray", err)
}
return vm.ToValue(vm.NewArrayBuffer(b))
})
set("hexEncodeToString", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(hex.EncodeToString([]byte(stringArg(call, 0))))
})
set("strToBytes", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(vm.NewArrayBuffer([]byte(stringArg(call, 0))))
})
set("bytesToStr", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) == 0 || goja.IsUndefined(call.Arguments[0]) || goja.IsNull(call.Arguments[0]) {
return vm.ToValue("")
}
if buf, ok := call.Arguments[0].Export().([]byte); ok {
return vm.ToValue(string(buf))
}
return vm.ToValue(call.Arguments[0].String())
})
set("encodeURI", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(javaURLEncode(stringArg(call, 0), stringArgOr(call, 1, "utf-8")))
})
set("timeFormat", func(call goja.FunctionCall) goja.Value {
ts := int64(call.Arguments[0].ToFloat())
format := stringArgOr(call, 1, "yyyy-MM-dd HH:mm")
return vm.ToValue(javaTimeFormat(ts, format, 0))
})
set("timeFormatUTC", func(call goja.FunctionCall) goja.Value {
ts := int64(call.Arguments[0].ToFloat())
format := stringArgOr(call, 1, "yyyy-MM-dd HH:mm")
sh := int64(0)
if len(call.Arguments) > 2 {
sh = int64(call.Arguments[2].ToFloat())
}
return vm.ToValue(javaTimeFormat(ts, format, sh))
})
// ── 加解密(对应 JsEncodeUtils) ──
set("createSymmetricCrypto", func(call goja.FunctionCall) goja.Value {
transformation := stringArgOr(call, 0, "AES/CBC/PKCS5Padding")
key := stringArgOr(call, 1, "")
iv := stringArgOr(call, 2, "")
cipher, err := newSymmetricCipher(transformation, key, iv)
if err != nil {
bridgeErr("createSymmetricCrypto", err)
}
return newCipherObject(vm, cipher)
})
aesVariant := func(mode string) func(goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
data := stringArg(call, 0)
key := stringArgOr(call, 1, "")
defaultTrans := mode
trans := stringArgOr(call, 2, defaultTrans)
iv := stringArgOr(call, 3, "")
cipher, err := newSymmetricCipher(trans, key, iv)
if err != nil {
bridgeErr(mode, err)
}
out, err := cipher.decryptAuto(data)
if err != nil {
bridgeErr(mode, err)
}
return vm.ToValue(string(out))
}
}
set("aesDecodeToString", aesVariant("AES/CBC/PKCS5Padding"))
set("aesBase64DecodeToString", aesVariant("AES/CBC/PKCS5Padding"))
set("aesBase64DecodeToByteArray", aesVariant("AES/CBC/PKCS5Padding"))
set("desDecodeToString", aesVariant("DES/CBC/PKCS5Padding"))
set("desBase64DecodeToString", aesVariant("DES/CBC/PKCS5Padding"))
encryptStr := func(data, key, trans, iv string) string {
cipher, err := newSymmetricCipher(trans, key, iv)
if err != nil {
bridgeErr("encrypt", err)
}
out, err := cipher.encrypt([]byte(data))
if err != nil {
bridgeErr("encrypt", err)
}
return string(out)
}
set("aesEncodeToString", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(encryptStr(stringArg(call, 0), stringArgOr(call, 1, ""), stringArgOr(call, 2, "AES/CBC/PKCS5Padding"), stringArgOr(call, 3, "")))
})
set("desEncodeToString", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(encryptStr(stringArg(call, 0), stringArgOr(call, 1, ""), stringArgOr(call, 2, "DES/CBC/PKCS5Padding"), stringArgOr(call, 3, "")))
})
encryptB64 := func(data, key, trans, iv string) string {
cipher, err := newSymmetricCipher(trans, key, iv)
if err != nil {
bridgeErr("encrypt", err)
}
out, err := cipher.encrypt([]byte(data))
if err != nil {
bridgeErr("encrypt", err)
}
return base64.StdEncoding.EncodeToString(out)
}
set("aesEncodeToBase64String", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(encryptB64(stringArg(call, 0), stringArgOr(call, 1, ""), stringArgOr(call, 2, "AES/CBC/PKCS5Padding"), stringArgOr(call, 3, "")))
})
set("desEncodeToBase64String", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(encryptB64(stringArg(call, 0), stringArgOr(call, 1, ""), stringArgOr(call, 2, "DES/CBC/PKCS5Padding"), stringArgOr(call, 3, "")))
})
set("tripleDESDecodeStr", func(call goja.FunctionCall) goja.Value {
data := stringArg(call, 0)
key := stringArgOr(call, 1, "")
trans := "DESede/CBC/PKCS5Padding"
var ivv string
switch {
case len(call.Arguments) > 4: // (data, key, mode, padding, iv)
trans = "DESede/" + stringArgOr(call, 2, "CBC") + "/" + stringArgOr(call, 3, "PKCS5Padding")
ivv = stringArgOr(call, 4, "")
case len(call.Arguments) > 2: // (data, key, transformation, iv?)
trans = stringArgOr(call, 2, trans)
ivv = stringArgOr(call, 3, "")
}
cipher, err := newSymmetricCipher(trans, key, ivv)
if err != nil {
bridgeErr("tripleDESDecodeStr", err)
}
out, err := cipher.decryptAuto(data)
if err != nil {
bridgeErr("tripleDESDecodeStr", err)
}
return vm.ToValue(string(out))
})
set("digestHex", func(call goja.FunctionCall) goja.Value {
h, err := digestHash(stringArgOr(call, 1, "MD5"))
if err != nil {
bridgeErr("digestHex", err)
}
h.Write([]byte(stringArg(call, 0)))
return vm.ToValue(hex.EncodeToString(h.Sum(nil)))
})
set("digestBase64Str", func(call goja.FunctionCall) goja.Value {
h, err := digestHash(stringArgOr(call, 1, "MD5"))
if err != nil {
bridgeErr("digestBase64Str", err)
}
h.Write([]byte(stringArg(call, 0)))
return vm.ToValue(base64.StdEncoding.EncodeToString(h.Sum(nil)))
})
set("HMacHex", func(call goja.FunctionCall) goja.Value {
h, err := hmacHash(stringArgOr(call, 1, "HmacSHA256"), []byte(stringArgOr(call, 2, "")))
if err != nil {
bridgeErr("HMacHex", err)
}
h.Write([]byte(stringArg(call, 0)))
return vm.ToValue(hex.EncodeToString(h.Sum(nil)))
})
set("HMacBase64", func(call goja.FunctionCall) goja.Value {
h, err := hmacHash(stringArgOr(call, 1, "HmacSHA256"), []byte(stringArgOr(call, 2, "")))
if err != nil {
bridgeErr("HMacBase64", err)
}
h.Write([]byte(stringArg(call, 0)))
return vm.ToValue(base64.StdEncoding.EncodeToString(h.Sum(nil)))
})
// ── 其他 ──
set("log", func(call goja.FunctionCall) goja.Value {
if r.cfg.Log != nil {
r.cfg.Log(call.Arguments[0].String())
}
return goja.Null()
})
set("getWebViewUA", func(call goja.FunctionCall) goja.Value {
return vm.ToValue(defaultWebViewUA)
})
set("t2s", func(call goja.FunctionCall) goja.Value { return vm.ToValue(stringArg(call, 0)) })
set("s2t", func(call goja.FunctionCall) goja.Value { return vm.ToValue(stringArg(call, 0)) })
set("htmlFormat", func(call goja.FunctionCall) goja.Value { return vm.ToValue(stringArg(call, 0)) })
// ── 书源会话状态(legado 中 `java` 与 `source` 是同一对象) ──
if r.state != nil {
bindSourceState(vm, set, r.state, r.cfg.SourceProps)
}
// ── 宿主交互:服务端无 UI,转为可回传前端的提示 / 待打开链接 ──
if r.state != nil {
toast := func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) > 0 {
r.state.Toast(call.Arguments[0].String())
}
return goja.Null()
}
set("toast", toast)
set("longToast", toast)
// startBrowser(url, title):记录待打开地址,前端可代开新标签。
set("startBrowser", func(call goja.FunctionCall) goja.Value {
r.state.OpenBrowser(stringArg(call, 0), stringArgOr(call, 1, ""))
return goja.Null()
})
// startBrowserAwait:服务端无 WebView,无法等待人工校验。
// 记录地址后抛出明确错误,避免书源逻辑误把空 body 当成功。
set("startBrowserAwait", func(call goja.FunctionCall) goja.Value {
url := stringArg(call, 0)
r.state.OpenBrowser(url, stringArgOr(call, 1, ""))
panic(vm.ToValue("java.startBrowserAwait: 服务端无浏览器,需要人工操作的页面请手动打开:" + url))
})
}
// 设备标识:部分源用 deviceID/androidId 做"是否支持该环境"探测,
// 成功返回会让源走安卓分支,这里统一以异常告知不支持并回退到通用分支。
for _, name := range []string{"deviceID", "androidId"} {
set(name, func(call goja.FunctionCall) goja.Value {
panic(vm.ToValue("java." + name + ": 服务端无设备标识"))
})
}
// 刷新发现页 / 打开界面:纯 UI 动作,服务端空实现。
for _, name := range []string{"refreshExplore", "open", "showBrowser", "reLoginView", "qread"} {
set(name, func(call goja.FunctionCall) goja.Value { return goja.Null() })
}
// 需要真正无头浏览器/本地文件系统的能力:明确抛出不支持
unsupported := func(name string) func(goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
panic(vm.ToValue("java." + name + " 需要浏览器或本地文件能力,服务端不支持"))
}
}
for _, name := range []string{
"webView", "webViewGetSource", "webViewGetOverrideUrl",
"openVideoPlayer", "getVerificationCode",
"importScript", "cacheFile", "downloadFile", "readFile", "readTxtFile", "deleteFile",
"unzipFile", "un7zFile", "unrarFile", "unArchiveFile", "getTxtInFolder",
"getZipStringContent", "getZipByteArrayContent",
"getRarStringContent", "get7zStringContent",
} {
set(name, unsupported(name))
}
return o
}
const defaultWebViewUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
func md5Hex(s string, short bool) string {
sum := md5.Sum([]byte(s))
h := hex.EncodeToString(sum[:])
if short {
return h[8:24]
}
return h
}
func digestHash(algorithm string) (hash.Hash, error) {
switch strings.ToUpper(strings.NewReplacer("-", "", "_", "").Replace(algorithm)) {
case "MD5":
return md5.New(), nil
case "SHA1":
return sha1.New(), nil
case "SHA256":
return sha256.New(), nil
case "SHA384":
return sha512.New384(), nil
case "SHA512":
return sha512.New(), nil
default:
return nil, fmt.Errorf("不支持的摘要算法: %s", algorithm)
}
}
func hmacHash(algorithm string, key []byte) (hash.Hash, error) {
switch strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(algorithm)) {
case "hmacmd5":
return hmac.New(md5.New, key), nil
case "hmacsha1":
return hmac.New(sha1.New, key), nil
case "hmacsha256":
return hmac.New(sha256.New, key), nil
case "hmacsha512":
return hmac.New(sha512.New, key), nil
default:
return nil, fmt.Errorf("不支持的 HMAC 算法: %s", algorithm)
}
}