miguan v1

This commit is contained in:
tamakyi
2026-10-21 12:09:28 +08:00
parent 2d4609dc0e
commit 79051acae8
28 changed files with 2128 additions and 182 deletions
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package route
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/flamego/flamego"
"github.com/tamakyi/TamaBox/internal/security/autoban"
"github.com/tamakyi/TamaBox/internal/security/honeypot"
)
// newHoneypotTestServer 造一个只含蜜罐 NotFound 分流的最小 flamego 实例。
//
// 刻意不调用 route.New()(那会拉起 DB/Redis/session 依赖),而是复刻它的
// 关键结构:全局 Use 链 → 具体路由 → NotFound 蜜罐分流。
func newHoneypotTestServer(t *testing.T, enabled bool) *httptest.Server {
t.Helper()
f := flamego.New()
hp := honeypot.New(honeypot.Options{Enabled: enabled})
ab := autoban.New(autoban.Options{
Enabled: true,
ThresholdSensitive: 1, // 命中一次即封禁,便于断言
Window: 60,
Duration: 60,
})
hph := newHoneypotHandler(hp, ab, nil) // store=nil → 只走日志,不落库
f.Get("/login", func(ctx flamego.Context) {
ctx.ResponseWriter().WriteHeader(http.StatusOK)
_, _ = ctx.ResponseWriter().Write([]byte("REAL_LOGIN"))
})
f.Get("/api/v1/questions", func(ctx flamego.Context) {
ctx.ResponseWriter().WriteHeader(http.StatusOK)
_, _ = ctx.ResponseWriter().Write([]byte("REAL_API"))
})
f.NotFound(func(ctx flamego.Context) {
if hp.Match(ctx.Request().URL.Path) {
hph.Serve(ctx)
return
}
ctx.Redirect("/")
})
ts := httptest.NewServer(f)
t.Cleanup(ts.Close)
return ts
}
// TestHoneypotServesFakeLoginPage 验证:命中诱饵路径时返回假登录页(含 canary),
// 而不是 302 跳首页。
func TestHoneypotServesFakeLoginPage(t *testing.T) {
ts := newHoneypotTestServer(t, true)
resp, err := http.Get(ts.URL + "/wp-login.php")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want 200", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
t.Errorf("Content-Type = %q, want text/html", ct)
}
if !strings.Contains(string(body), honeypot.CanaryToken) {
t.Error("假登录页缺少 canary 标记,无法溯源")
}
if !strings.Contains(string(body), `name="log"`) || !strings.Contains(string(body), `name="pwd"`) {
t.Error("假登录页应使用 WordPress 经典字段名 log / pwd")
}
if strings.Contains(string(body), "REAL_LOGIN") {
t.Error("假登录页不应包含真实业务内容")
}
}
// TestHoneypotDoesNotHijackRealRoutes 是红线测试:真实业务路由绝不能被蜜罐接管。
func TestHoneypotDoesNotHijackRealRoutes(t *testing.T) {
ts := newHoneypotTestServer(t, true)
for path, want := range map[string]string{
"/login": "REAL_LOGIN",
"/api/v1/questions": "REAL_API",
} {
resp, err := http.Get(ts.URL + path)
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if !strings.Contains(string(body), want) {
t.Errorf("%s 返回了 %q,期望包含 %q(业务路由被劫持)", path, truncate(string(body), 80), want)
}
if strings.Contains(string(body), honeypot.CanaryToken) {
t.Errorf("%s 命中了蜜罐,业务路由被劫持", path)
}
}
}
// TestHoneypotDisabledFallsBackToRedirect 验证:蜜罐关闭时行为与原先一致(302 → /)。
func TestHoneypotDisabledFallsBackToRedirect(t *testing.T) {
ts := newHoneypotTestServer(t, false)
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(ts.URL + "/wp-login.php")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
t.Errorf("status = %d, want 302", resp.StatusCode)
}
if loc := resp.Header.Get("Location"); loc != "/" {
t.Errorf("Location = %q, want /", loc)
}
}
// TestHoneypotCapturesCredentials 验证:POST 提交的用户名/密码能被提取出来。
func TestHoneypotCapturesCredentials(t *testing.T) {
hp := honeypot.New(honeypot.Options{Enabled: true})
req := httptest.NewRequest(http.MethodPost, "/wp-login.php",
strings.NewReader("log=admin&pwd=hunter2&redirect_to=%2Fwp-admin%2F"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
creds := hp.ExtractCredentials(req)
if creds.Username != "admin" {
t.Errorf("Username = %q, want admin", creds.Username)
}
if creds.Password != "hunter2" {
t.Errorf("Password = %q, want hunter2", creds.Password)
}
}
// TestHoneypotPostReturnsFailurePage 验证:POST 后返回 200 的假登录页并带错误提示。
func TestHoneypotPostReturnsFailurePage(t *testing.T) {
ts := newHoneypotTestServer(t, true)
resp, err := http.Post(ts.URL+"/wp-login.php",
"application/x-www-form-urlencoded",
strings.NewReader("log=admin&pwd=admin"))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want 200", resp.StatusCode)
}
if !strings.Contains(string(body), honeypot.LoginFailureNotice) {
t.Error("提交凭据后应返回「用户名或密码无效」的假提示")
}
}