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
+221
View File
@@ -0,0 +1,221 @@
package rule
import (
"encoding/base64"
"encoding/hex"
"fmt"
"net/url"
"strings"
"sync"
"time"
"github.com/dop251/goja"
)
// 本文件:桥接层的小工具(编码字节转换、cookie/cache 对象、时间格式化)。
var b64EncodingVariants = []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
// base64DecodeBytes 宽松解码:自动补 padding、支持 URL-safe 变体
// (对应 legado Base64 解码的宽容行为)。
func base64DecodeBytes(s string) ([]byte, error) {
s = strings.Map(func(r rune) rune {
if r == '\n' || r == '\r' || r == ' ' || r == '\t' {
return -1
}
return r
}, s)
var lastErr error
for _, enc := range b64EncodingVariants {
b, err := enc.DecodeString(s)
if err == nil {
return b, nil
}
lastErr = err
}
// 缺 padding 的 std 变体
if padded := s + strings.Repeat("=", (4-len(s)%4)%4); padded != s {
if b, err := base64.StdEncoding.DecodeString(padded); err == nil {
return b, nil
}
}
if lastErr == nil {
lastErr = fmt.Errorf("无效 Base64")
}
return nil, lastErr
}
func base64DecodeString(s string) (string, error) {
b, err := base64DecodeBytes(s)
return string(b), err
}
func hexDecodeBytes(s string) ([]byte, error) { return hex.DecodeString(s) }
func base64StdEncodeBytes(b []byte) string { return base64.StdEncoding.EncodeToString(b) }
func hexEncodeBytes(b []byte) string { return hex.EncodeToString(b) }
// ─── cookie 对象(对应 legado CookieStore 注入的 `cookie`) ─────────────────
var cookieJar = struct {
mu sync.Mutex
m map[string]map[string]string // host → name → value
}{m: map[string]map[string]string{}}
// CookieJarRecord 供服务层在执行 HTTP 请求时记录 Set-Cookie。
func CookieJarRecord(rawURL string, cookies []string) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
host := u.Host
cookieJar.mu.Lock()
defer cookieJar.mu.Unlock()
jar, ok := cookieJar.m[host]
if !ok {
jar = map[string]string{}
cookieJar.m[host] = jar
}
for _, c := range cookies {
pair := strings.SplitN(c, ";", 2)[0]
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
continue
}
name := strings.TrimSpace(kv[0])
if name == "" {
continue
}
jar[name] = strings.TrimSpace(kv[1])
}
}
// CookieJarHeader 供书源 JS 查询(对应 CookieStore.getCookie(tag[, key]))。
func CookieJarHeader(rawURL, key string) string {
u, err := url.Parse(rawURL)
if err != nil {
return ""
}
cookieJar.mu.Lock()
defer cookieJar.mu.Unlock()
jar, ok := cookieJar.m[u.Host]
if !ok {
return ""
}
if key != "" {
return jar[key]
}
var parts []string
for k, v := range jar {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, "; ")
}
func newCookieObject(vm *goja.Runtime) *goja.Object {
o := vm.NewObject()
set := func(k string, v any) {
if err := o.Set(k, v); err != nil {
panic(vm.ToValue(err.Error()))
}
}
// getCookie(key) 或 getCookie(url, key)
set("getCookie", func(call goja.FunctionCall) goja.Value {
tag := stringArg(call, 0)
key := ""
if len(call.Arguments) > 1 {
key = stringArg(call, 1)
}
return vm.ToValue(CookieJarHeader(tag, key))
})
set("setCookie", func(call goja.FunctionCall) goja.Value {
rawURL := stringArg(call, 0)
cookie := stringArgOr(call, 1, "")
if cookie != "" {
CookieJarRecord(rawURL, []string{cookie})
}
return vm.ToValue(cookie)
})
return o
}
// ─── cache 对象(对应 legado CacheManager 注入的 `cache`) ──────────────────
var jsCache = struct {
mu sync.Mutex
m map[string]string
}{m: map[string]string{}}
func newCacheObject(vm *goja.Runtime) *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("put", func(call goja.FunctionCall) goja.Value {
key := stringArg(call, 0)
val := ""
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
val = call.Arguments[1].String()
}
jsCache.mu.Lock()
if len(jsCache.m) >= 4096 {
jsCache.m = map[string]string{}
}
jsCache.m[key] = val
jsCache.mu.Unlock()
return vm.ToValue(val)
})
set("get", func(call goja.FunctionCall) goja.Value {
key := stringArg(call, 0)
jsCache.mu.Lock()
v, ok := jsCache.m[key]
jsCache.mu.Unlock()
if !ok {
return goja.Null()
}
return vm.ToValue(v)
})
set("delete", func(call goja.FunctionCall) goja.Value {
key := stringArg(call, 0)
jsCache.mu.Lock()
delete(jsCache.m, key)
jsCache.mu.Unlock()
return goja.Null()
})
return o
}
// ─── 时间格式化(对应 JsExtensions.timeFormat/timeFormatUTC) ───────────────
// javaTimeFormat 将 Java SimpleDateFormat 常用 pattern 转 Go 布局。
// sh 为时区偏移小时数(timeFormatUTC 语义,0 表示本地时区)。
func javaTimeFormat(ts int64, format string, sh int64) string {
loc := time.Local
if sh != 0 {
loc = time.FixedZone(fmt.Sprintf("UTC%+d", sh), int(sh)*3600)
}
t := time.UnixMilli(ts).In(loc)
return t.Format(javaPatternToGo(format))
}
func javaPatternToGo(p string) string {
replacements := []struct{ java, goLayout string }{
{"yyyy", "2006"}, {"yy", "06"},
{"MM", "01"}, {"dd", "02"},
{"HH", "15"}, {"hh", "03"},
{"mm", "04"}, {"ss", "05"},
{"SSS", "000"},
{"a", "PM"},
}
// 优先替换长 token,避免 yyyy 被 yy+yy 拆坏
for _, r := range replacements {
p = strings.ReplaceAll(p, r.java, "\x00"+r.goLayout+"\x00")
}
return strings.NewReplacer("\x00", "").Replace(p)
}