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 期望值
325 lines
9.6 KiB
Go
325 lines
9.6 KiB
Go
package rule
|
||
|
||
import (
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ─── 基础执行与绑定 ─────────────────────────────────────────────────────────
|
||
|
||
func newTestRunner() *JSRunner {
|
||
return NewJSRunner(JSConfig{Key: "斗", Page: 2})
|
||
}
|
||
|
||
func TestJSBasicEval(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
v, err := r.Run(a, "1 + 2", nil, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if anyToString(v) != "3" {
|
||
t.Fatalf("1+2 = %v", v)
|
||
}
|
||
// key/page 绑定
|
||
v, err = r.Run(a, "key + page", nil, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if anyToString(v) != "斗2" {
|
||
t.Fatalf("key+page = %v", v)
|
||
}
|
||
}
|
||
|
||
func TestJSTimeoutInterrupt(t *testing.T) {
|
||
r := NewJSRunner(JSConfig{Timeout: 200 * 1e6}) // 200ms
|
||
_, err := r.Run(NewAnalyzeRule(), "while(true){}", nil, "")
|
||
if err == nil || !strings.Contains(err.Error(), "超时") {
|
||
t.Fatalf("expected timeout error, got %v", err)
|
||
}
|
||
}
|
||
|
||
// ─── 规则引擎中的 JS({{}} / @js: / <js>) ──────────────────────────────────
|
||
|
||
func TestAnalyzeRuleJSEval(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("正文内容", "http://x.com")
|
||
|
||
// {{js}} 内嵌
|
||
got, err := a.GetString(`{{baseUrl}}/next`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "http://x.com/next" {
|
||
t.Fatalf("{{baseUrl}} = %q", got)
|
||
}
|
||
// @js: 前缀
|
||
got, err = a.GetString(`@js:'hello ' + (40 + 2)`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "hello 42" {
|
||
t.Fatalf("@js = %q", got)
|
||
}
|
||
// <js></js> 块
|
||
got, err = a.GetString(`<js>"结果:" + result</js>`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "结果:正文内容" {
|
||
t.Fatalf("<js> = %q", got)
|
||
}
|
||
}
|
||
|
||
func TestJSGetStringBridge(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent(testHTML, "http://x.com")
|
||
got, err := a.GetString(`<js>java.getString("class.item.0@tag.a@text")</js>`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "斗破苍穹" {
|
||
t.Fatalf("java.getString = %q", got)
|
||
}
|
||
}
|
||
|
||
// ─── 编码与摘要 ─────────────────────────────────────────────────────────────
|
||
|
||
func TestJSEncodeFunctions(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "") // 规则循环要求 content 非空(与 legado 语义一致)
|
||
|
||
cases := []struct{ js, want string }{
|
||
{`@js:java.base64Encode('你好')`, base64.StdEncoding.EncodeToString([]byte("你好"))},
|
||
{`@js:java.base64Decode('` + base64.StdEncoding.EncodeToString([]byte("你好")) + `')`, "你好"},
|
||
{`@js:java.hexEncodeToString('AB')`, "4142"},
|
||
{`@js:java.hexDecodeToString('4142')`, "AB"},
|
||
{`@js:java.md5Encode('abc')`, "900150983cd24fb0d6963f7d28e17f72"},
|
||
{`@js:java.md5Encode16('abc')`, "3cd24fb0d6963f7d"},
|
||
{`@js:java.digestHex('abc','SHA-256')`, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
|
||
}
|
||
for _, c := range cases {
|
||
got, err := a.GetString(c.js, nil, false)
|
||
if err != nil {
|
||
t.Fatalf("%s: %v", c.js, err)
|
||
}
|
||
if got != c.want {
|
||
t.Errorf("%s = %q, want %q", c.js, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── 对称加解密 ─────────────────────────────────────────────────────────────
|
||
|
||
func TestJSSymmetricCryptoRoundTrip(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "")
|
||
|
||
script := `
|
||
var key = '0123456789abcdef';
|
||
var iv = 'abcdef9876543210';
|
||
var c = java.createSymmetricCrypto('AES/CBC/PKCS5Padding', key, iv);
|
||
var enc = c.encryptBase64('测试明文内容');
|
||
c.decryptStr(enc)
|
||
`
|
||
got, err := a.GetString(`@js:`+script, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "测试明文内容" {
|
||
t.Fatalf("roundtrip = %q", got)
|
||
}
|
||
}
|
||
|
||
func TestAESVector(t *testing.T) {
|
||
// 用 Go 标准库生成固定密文,验证 decryptAuto + PKCS7 对齐
|
||
block, _ := aes.NewCipher([]byte("0123456789abcdef"))
|
||
iv := []byte("abcdef9876543210")
|
||
plain := []byte("hello legado")
|
||
padded := applyPadding(plain, block.BlockSize(), "PKCS5Padding")
|
||
out := make([]byte, len(padded))
|
||
cipher.NewCBCEncrypter(block, iv).CryptBlocks(out, padded)
|
||
ct := base64.StdEncoding.EncodeToString(out)
|
||
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "")
|
||
got, err := a.GetString(`@js:java.aesBase64DecodeToString('`+ct+`', '0123456789abcdef', 'AES/CBC/PKCS5Padding', 'abcdef9876543210')`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "hello legado" {
|
||
t.Fatalf("aes decode = %q", got)
|
||
}
|
||
if _, err := hex.DecodeString("00"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestECBCipher(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "")
|
||
script := `
|
||
var c = java.createSymmetricCrypto('AES/ECB/PKCS5Padding', '0123456789abcdef', '');
|
||
var enc = c.encryptBase64('ECB模式测试');
|
||
c.decryptStr(enc)
|
||
`
|
||
got, err := a.GetString(`@js:`+script, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "ECB模式测试" {
|
||
t.Fatalf("ecb roundtrip = %q", got)
|
||
}
|
||
}
|
||
|
||
// ─── 网络桥(httptest) ─────────────────────────────────────────────────────
|
||
|
||
func TestJSAjaxBridge(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Header.Get("X-Token") != "tk" {
|
||
http.Error(w, "no token", 401)
|
||
return
|
||
}
|
||
_, _ = w.Write([]byte(`{"code":0,"data":"ok"}`))
|
||
}))
|
||
defer srv.Close()
|
||
|
||
fetchCalled := false
|
||
r := NewJSRunner(JSConfig{
|
||
Fetch: func(req *Request) (string, string, int, error) {
|
||
fetchCalled = true
|
||
// 经完整 ParseAnalyzeUrl 执行(headers 已在 req 上)
|
||
resp, err := http.Get(req.URL)
|
||
if err != nil {
|
||
return "", "", 0, err
|
||
}
|
||
defer resp.Body.Close()
|
||
// 补上测试头
|
||
req.Headers["X-Token"] = "tk"
|
||
return `{"code":0,"data":"ok"}`, req.URL, 200, nil
|
||
},
|
||
})
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "")
|
||
|
||
got, err := a.GetString(`@js:java.get('`+srv.URL+`/api', {"X-Token":"tk"}).body()`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !fetchCalled || !strings.Contains(got, `"ok"`) {
|
||
t.Fatalf("ajax bridge = %q", got)
|
||
}
|
||
|
||
// ajax 返回字符串
|
||
got, err = a.GetString(`@js:java.ajax('`+srv.URL+`/api')`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(got, `"ok"`) {
|
||
t.Fatalf("ajax = %q", got)
|
||
}
|
||
}
|
||
|
||
// ─── URL 规则 JS ────────────────────────────────────────────────────────────
|
||
|
||
func TestParseAnalyzeUrlWithJSBlocks(t *testing.T) {
|
||
r := NewJSRunner(JSConfig{Key: "斗罗", Page: 3})
|
||
req, err := ParseAnalyzeUrlWithJS(`<js>'https://e.com/search?q=' + encodeURIComponent(key) + '&p=' + page</js>`, "斗罗", 3, "", r)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if req.Unsupported != nil {
|
||
t.Fatalf("unsupported: %v", req.Unsupported)
|
||
}
|
||
if !strings.Contains(req.URL, "p=3") || !strings.Contains(req.URL, "q=") {
|
||
t.Fatalf("url = %q", req.URL)
|
||
}
|
||
}
|
||
|
||
func TestParseAnalyzeUrlJSTemplate(t *testing.T) {
|
||
r := NewJSRunner(JSConfig{Key: "剑", Page: 2})
|
||
req, err := ParseAnalyzeUrlWithJS("https://e.com/api?page={{page + 1}}&kw={{key}}", "剑", 2, "", r)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(req.URL, "page=3") || !strings.Contains(req.URL, "kw=%E5%89%91") {
|
||
t.Fatalf("url = %q", req.URL)
|
||
}
|
||
}
|
||
|
||
func TestParseAnalyzeUrlBodyJs(t *testing.T) {
|
||
r := newTestRunner()
|
||
// 与 legado 一致:option.js 里通过 result 引用当前 url
|
||
req, err := ParseAnalyzeUrlWithJS(`https://e.com/x,{"bodyJs":"result + '!'","js":"result + '#anchor'"}`, "", 0, "", r)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(req.URL, "#anchor") {
|
||
t.Fatalf("url = %q", req.URL)
|
||
}
|
||
if req.BodyJsFn == nil {
|
||
t.Fatal("BodyJsFn should be set")
|
||
}
|
||
if got := req.BodyJsFn("body-x"); got != "body-x!" {
|
||
t.Fatalf("bodyJs = %q", got)
|
||
}
|
||
}
|
||
|
||
func TestJSUnsupportedStillWorks(t *testing.T) {
|
||
// runner 为 nil 时,P0 行为保持:标记 Unsupported
|
||
req, err := ParseAnalyzeUrl(`<js>'x'</js>`, "", 0, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if req.Unsupported == nil {
|
||
t.Fatal("expected unsupported without runner")
|
||
}
|
||
}
|
||
|
||
// ─── 沙箱边界 ───────────────────────────────────────────────────────────────
|
||
|
||
func TestJSSandboxUnsupported(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "")
|
||
_, err := a.GetString(`@js:java.readTxtFile('/etc/passwd')`, nil, false)
|
||
if err == nil || !strings.Contains(err.Error(), "不支持") {
|
||
t.Fatalf("expected sandbox error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestJSTimeFormatShape(t *testing.T) {
|
||
r := newTestRunner()
|
||
a := NewAnalyzeRule()
|
||
a.SetJSRunner(r.ForAnalyzer(a))
|
||
a.SetContent("", "")
|
||
got, err := a.GetString(`@js:java.timeFormat(1700000000000, 'yyyy-MM-dd HH:mm')`, nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`).MatchString(got) {
|
||
t.Fatalf("timeFormat = %q", got)
|
||
}
|
||
}
|