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
+285
View File
@@ -0,0 +1,285 @@
package rule
import (
"crypto/aes"
"crypto/cipher"
"crypto/des"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/dop251/goja"
)
// 对称加密:对应 legado JsEncodeUtils.createSymmetricCrypto(hutool SymmetricCrypto 语义)。
// transformation 形如 "AES/CBC/PKCS5Padding" / "DES/ECB/NoPadding" / "DESede/CBC/PKCS5Padding"。
type symmetricCipher struct {
block cipher.Block
mode string
padding string
iv []byte
}
func newSymmetricCipher(transformation, key, iv string) (*symmetricCipher, error) {
parts := strings.Split(strings.TrimSpace(transformation), "/")
algo := strings.ToUpper(strings.TrimSpace(parts[0]))
mode := "CBC"
padding := "PKCS5Padding"
if len(parts) > 1 && strings.TrimSpace(parts[1]) != "" {
mode = strings.ToUpper(strings.TrimSpace(parts[1]))
}
if len(parts) > 2 && strings.TrimSpace(parts[2]) != "" {
padding = strings.TrimSpace(parts[2])
}
keyBytes := normalizeKey([]byte(key), algo)
if len(keyBytes) == 0 {
return nil, errors.New("加密密钥为空")
}
var block cipher.Block
var err error
switch algo {
case "AES":
block, err = aes.NewCipher(keyBytes)
case "DES":
block, err = des.NewCipher(keyBytes)
case "DESEDE", "3DES", "TRIPLEDES":
block, err = des.NewTripleDESCipher(keyBytes)
default:
return nil, fmt.Errorf("不支持的加密算法: %s", algo)
}
if err != nil {
return nil, err
}
ivBytes := []byte(iv)
if mode == "ECB" {
ivBytes = nil
}
return &symmetricCipher{block: block, mode: mode, padding: padding, iv: ivBytes}, nil
}
// normalizeKey 对齐 hutool 的密钥处理:按算法要求补齐/截断密钥长度。
func normalizeKey(key []byte, algo string) []byte {
var sizes []int
switch algo {
case "AES":
sizes = []int{16, 24, 32}
case "DES":
sizes = []int{8}
case "DESEDE", "3DES", "TRIPLEDES":
sizes = []int{24}
default:
return key
}
for _, n := range sizes {
if len(key) == n {
return key
}
}
for _, n := range sizes {
if len(key) < n {
out := make([]byte, n)
copy(out, key)
return out
}
}
return key[:sizes[len(sizes)-1]]
}
func (c *symmetricCipher) blockSize() int { return c.block.BlockSize() }
func (c *symmetricCipher) crypt(dst, src []byte, decrypt bool) error {
bs := c.blockSize()
if c.mode == "ECB" {
for i := 0; i < len(src); i += bs {
if decrypt {
c.block.Decrypt(dst[i:i+bs], src[i:i+bs])
} else {
c.block.Encrypt(dst[i:i+bs], src[i:i+bs])
}
}
return nil
}
iv := make([]byte, bs)
if len(c.iv) > 0 {
copy(iv, c.iv[:min(len(c.iv), bs)])
}
switch c.mode {
case "CBC":
if decrypt {
cipher.NewCBCDecrypter(c.block, iv).CryptBlocks(dst, src)
} else {
cipher.NewCBCEncrypter(c.block, iv).CryptBlocks(dst, src)
}
case "CTR":
cipher.NewCTR(c.block, iv).XORKeyStream(dst, src)
case "OFB":
cipher.NewOFB(c.block, iv).XORKeyStream(dst, src)
case "CFB", "CFB8":
stream := cipher.NewCFBEncrypter(c.block, iv)
if decrypt {
stream = cipher.NewCFBDecrypter(c.block, iv)
}
stream.XORKeyStream(dst, src)
default:
return fmt.Errorf("不支持的加密模式: %s", c.mode)
}
return nil
}
func (c *symmetricCipher) encrypt(data []byte) ([]byte, error) {
bs := c.blockSize()
data = applyPadding(data, bs, c.padding)
if len(data)%bs != 0 {
return nil, errors.New("加密数据长度未按块对齐")
}
out := make([]byte, len(data))
if err := c.crypt(out, data, false); err != nil {
return nil, err
}
return out, nil
}
func (c *symmetricCipher) decrypt(data []byte) ([]byte, error) {
bs := c.blockSize()
if len(data) == 0 || len(data)%bs != 0 {
return nil, errors.New("密文长度不是块大小的整数倍")
}
out := make([]byte, len(data))
if err := c.crypt(out, data, true); err != nil {
return nil, err
}
return stripPadding(out, c.padding), nil
}
// decryptAuto 对应 hutool decryptStr(String):输入自动识别 Base64 / Hex / 原文。
func (c *symmetricCipher) decryptAuto(s string) ([]byte, error) {
if b, err := base64DecodeBytes(s); err == nil && len(b) > 0 && len(b)%c.blockSize() == 0 {
return c.decrypt(b)
}
if b, err := hex.DecodeString(strings.TrimSpace(s)); err == nil && len(b) > 0 && len(b)%c.blockSize() == 0 {
return c.decrypt(b)
}
return c.decrypt([]byte(s))
}
func applyPadding(data []byte, blockSize int, padding string) []byte {
switch padding {
case "NoPadding":
return data
case "ZeroPadding":
n := (len(data)+blockSize-1)/blockSize*blockSize
if n == 0 {
n = blockSize
}
out := make([]byte, n)
copy(out, data)
return out
default: // PKCS5Padding / PKCS7Padding
pad := blockSize - len(data)%blockSize
out := make([]byte, len(data)+pad)
copy(out, data)
for i := len(data); i < len(out); i++ {
out[i] = byte(pad)
}
return out
}
}
func stripPadding(data []byte, padding string) []byte {
switch padding {
case "NoPadding":
return data
case "ZeroPadding":
for len(data) > 0 && data[len(data)-1] == 0 {
data = data[:len(data)-1]
}
return data
default:
if len(data) == 0 {
return data
}
pad := int(data[len(data)-1])
if pad <= 0 || pad > len(data) {
return data
}
for _, b := range data[len(data)-pad:] {
if int(b) != pad {
return data
}
}
return data[:len(data)-pad]
}
}
// newCipherObject 对应 hutool SymmetricCrypto 的 JS 方法面:
// encrypt(data) / encryptBase64(data) / encryptHex(data) / decrypt(data) /
// decryptStr(data) / decryptBase64(data) / decryptHex(data)。
func newCipherObject(vm *goja.Runtime, c *symmetricCipher) *goja.Object {
o := vm.NewObject()
set := func(k string, v any) {
if err := o.Set(k, v); err != nil {
panic(vm.ToValue(err.Error()))
}
}
set("encrypt", func(call goja.FunctionCall) goja.Value {
out, err := c.encrypt([]byte(stringArg(call, 0)))
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(vm.NewArrayBuffer(out))
})
set("encryptBase64", func(call goja.FunctionCall) goja.Value {
out, err := c.encrypt([]byte(stringArg(call, 0)))
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(base64StdEncodeBytes(out))
})
set("encryptHex", func(call goja.FunctionCall) goja.Value {
out, err := c.encrypt([]byte(stringArg(call, 0)))
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(hexEncodeBytes(out))
})
set("decrypt", func(call goja.FunctionCall) goja.Value {
out, err := c.decryptAuto(stringArg(call, 0))
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(vm.NewArrayBuffer(out))
})
set("decryptStr", func(call goja.FunctionCall) goja.Value {
out, err := c.decryptAuto(stringArg(call, 0))
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(string(out))
})
set("decryptBase64", func(call goja.FunctionCall) goja.Value {
b, err := base64DecodeBytes(stringArg(call, 0))
if err != nil {
panic(vm.ToValue(err.Error()))
}
out, err := c.decrypt(b)
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(string(out))
})
set("decryptHex", func(call goja.FunctionCall) goja.Value {
b, err := hexDecodeBytes(strings.TrimSpace(stringArg(call, 0)))
if err != nil {
panic(vm.ToValue(err.Error()))
}
out, err := c.decrypt(b)
if err != nil {
panic(vm.ToValue(err.Error()))
}
return vm.ToValue(string(out))
})
return o
}