mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36:37 +08:00
153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
package rule
|
||
|
||
import (
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"golang.org/x/text/encoding/simplifiedchinese"
|
||
)
|
||
|
||
// 本文件:java 文件接口(cacheFile / downloadFile / readTxtFile / deleteFile)
|
||
// 的语义回归。对应 legado JsExtensions:
|
||
// - cacheFile(url) → 返回文件**文本内容**(不是路径);
|
||
// - downloadFile(url) → 返回相对缓存根目录的路径;
|
||
// - readTxtFile(path[, charset]) → 返回文本;
|
||
// - 只允许访问缓存目录内的文件。
|
||
|
||
func newFileTestRunner(cacheDir, baseURL string, hits *int32) *JSRunner {
|
||
return NewJSRunner(JSConfig{
|
||
CacheDir: cacheDir,
|
||
BaseURL: baseURL,
|
||
Fetch: func(req *Request) (string, string, int, error) {
|
||
if hits != nil {
|
||
atomic.AddInt32(hits, 1)
|
||
}
|
||
resp, err := http.Get(req.URL) //nolint:gosec // 测试内固定 httptest 地址
|
||
if err != nil {
|
||
return "", "", 0, err
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
return string(b), req.URL, resp.StatusCode, nil
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestCacheFileReturnsTextContent(t *testing.T) {
|
||
var hits int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
_, _ = w.Write([]byte("第一章 开始\n第二章 继续"))
|
||
}))
|
||
defer srv.Close()
|
||
|
||
r := newFileTestRunner(t.TempDir(), srv.URL, &hits)
|
||
ar := NewAnalyzeRule()
|
||
v, err := r.Run(ar, "java.cacheFile('"+srv.URL+"/vol.txt')", nil, "")
|
||
if err != nil {
|
||
t.Fatalf("cacheFile 失败: %v", err)
|
||
}
|
||
if got := anyToString(v); got != "第一章 开始\n第二章 继续" {
|
||
t.Fatalf("cacheFile 应返回文本内容,实际 %q", got)
|
||
}
|
||
// 第二次应命中缓存,不再发起请求。
|
||
if _, err := r.Run(ar, "java.cacheFile('"+srv.URL+"/vol.txt')", nil, ""); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if n := atomic.LoadInt32(&hits); n != 1 {
|
||
t.Fatalf("重复调用应命中缓存(请求次数=%d)", n)
|
||
}
|
||
}
|
||
|
||
func TestCacheFileDecodesGBK(t *testing.T) {
|
||
gbk, err := simplifiedchinese.GBK.NewEncoder().Bytes([]byte("中文内容测试"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
_, _ = w.Write(gbk)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
r := newFileTestRunner(t.TempDir(), srv.URL, nil)
|
||
v, err := r.Run(NewAnalyzeRule(), "java.cacheFile('"+srv.URL+"/gbk.txt')", nil, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := anyToString(v); got != "中文内容测试" {
|
||
t.Fatalf("GBK 文本未正确解码: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestDownloadFileThenReadTxtFile(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
_, _ = w.Write([]byte("正文内容"))
|
||
}))
|
||
defer srv.Close()
|
||
|
||
cacheDir := t.TempDir()
|
||
r := newFileTestRunner(cacheDir, srv.URL, nil)
|
||
ar := NewAnalyzeRule()
|
||
|
||
rawPath, err := r.Run(ar, "java.downloadFile('"+srv.URL+"/a.txt')", nil, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rel := anyToString(rawPath)
|
||
if !strings.HasPrefix(rel, "/") || !strings.HasSuffix(rel, ".txt") {
|
||
t.Fatalf("downloadFile 应返回相对缓存路径,实际 %q", rel)
|
||
}
|
||
if _, err := os.Stat(filepath.Join(cacheDir, "reader", "files", strings.TrimPrefix(rel, "/"))); err != nil {
|
||
t.Fatalf("文件未落到缓存目录: %v", err)
|
||
}
|
||
|
||
v, err := r.Run(ar, "java.readTxtFile('"+rel+"')", nil, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := anyToString(v); got != "正文内容" {
|
||
t.Fatalf("readTxtFile = %q", got)
|
||
}
|
||
|
||
del, err := r.Run(ar, "String(java.deleteFile('"+rel+"'))", nil, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if anyToString(del) != "true" {
|
||
t.Fatalf("deleteFile 应返回 true,实际 %q", anyToString(del))
|
||
}
|
||
}
|
||
|
||
func TestFileOpsRejectPathTraversal(t *testing.T) {
|
||
cacheDir := t.TempDir()
|
||
secret := filepath.Join(cacheDir, "secret.txt")
|
||
if err := os.WriteFile(secret, []byte("不该被读到"), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r := newFileTestRunner(cacheDir, "", nil)
|
||
ar := NewAnalyzeRule()
|
||
|
||
for _, p := range []string{"/../../secret.txt", "../secret.txt", "/../reader/files/../../secret.txt"} {
|
||
v, err := r.Run(ar, "java.readTxtFile('"+p+"')", nil, "")
|
||
if err != nil {
|
||
t.Fatalf("%s 不应报错: %v", p, err)
|
||
}
|
||
if got := anyToString(v); strings.Contains(got, "不该被读到") {
|
||
t.Fatalf("越权读到了缓存目录外的文件(%s)", p)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFileOpsWithoutCacheDirFail(t *testing.T) {
|
||
r := NewJSRunner(JSConfig{})
|
||
_, err := r.Run(NewAnalyzeRule(), "java.cacheFile('https://example.com/a.txt')", nil, "")
|
||
if err == nil || !strings.Contains(err.Error(), "缓存目录") {
|
||
t.Fatalf("未配置缓存目录时应明确报错,实际 %v", err)
|
||
}
|
||
}
|