feat(reader): P2——goja JS 引擎接入 + Go 原生 java.* 桥接

- rule/goja.go:纯 Go JS 运行时(goja),脚本编译缓存 + 超时中断(默认10s),
  绑定集合对齐 legado evalJS(java/cookie/cache/source/book/result/baseUrl/
  chapter/title/src/page/key)
- rule/bridge.go:java 对象函数名与 legado JsExtensions 一一对应(存量书源
  硬编码这些名字),全部 Go 原生实现:
  网络 ajax/ajaxAll/connect/get/post/head(经 MeBox HTTP 客户端,响应对象
  带 body()/code()/url()/header());规则回调 getString/getStringList/
  getElements/put/get(桥回当前解析器);编解码 base64/hex/md5(16/32)/
  digest/HMac/encodeURI/timeFormat(SimpleDateFormat 转 Go 布局)
- 对称加密:createSymmetricCrypto + aes/des/3des 全家桶,transformation
  三段式解析(AES/DES/3DES × CBC/ECB/CTR/OFB/CFB × PKCS5/7/Zero/NoPadding,
  ECB 手工块循环),密钥长度自动补齐,decryptStr 自动识别 Base64/Hex
- get(url,headers) 与 get(key) 按 arity 分发(对齐 Kotlin 重载)
- 浏览器/文件/压缩包类函数明确抛不支持(服务端沙箱边界)
- URL 规则 JS:analyzeJs(@result 链式)、{{js}} 内嵌、option js/bodyJs
- execute 记录 Set-Cookie 进 cookie jar(java/cookie.getCookie 可读)
- 测试:编码/摘要/AES 向量/CBC+ECB 往返/ajax httptest/URL JS/沙箱边界,
  全部通过;修复 java.get 双注册、测试缺 SetContent 与 md5Encode16 期望值
This commit is contained in:
truewhile
2026-09-30 16:36:32 +08:00
parent d0b4255aa8
commit 5976a8d310
10 changed files with 1720 additions and 69 deletions
+467
View File
@@ -0,0 +1,467 @@
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)) })
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",
"startBrowser", "startBrowserAwait", "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)
}
}