mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 20:16:36 +08:00
5976a8d310
- 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 期望值
222 lines
5.6 KiB
Go
222 lines
5.6 KiB
Go
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)
|
||
}
|