Files
eh-downloader/internal/eh/contentwarning_test.go
toor a078a00f45 fix(eh): 自动放行 EH 的 Content Warning 拦截页
现象:部分画廊(多被标成 Offensive For Everyone)解析必然失败,报
「页面结构无法解析(EH 可能又改版)」——这条报错是误导的,跟改版无关。

根因:EH 对这类画廊第一次访问只返回一个 3~4KB 的黄框拦截页,
<title> 和 meta 都在,但一个 /s/ 链接都没有,正则自然一条都匹配不上。
放行需要请求一次 ?nw=always,而 EH 靠下发的 nw=1 Cookie 记住
「别再警告」——可 Client 根本没设 CookieJar,Cookie 下发即丢,
于是永远过不去。

实测:4166903 / 4021921 / 3799635 / 3627469 四个画廊修复后
分别解析出 45/95/36/73 页,与站点声明完全一致。

改动:
- Client 持有 cookiejar 并挂到 http.Client.Jar
  (Go 1.26 起 http.Transport 已经没有 Jar 字段了,只能挂 Client)
- 新增 getTextAutoWarn:撞上拦截页自动 ?nw=always 放行一次再重试,
  nw=1 进 jar 后全站不再警告
- 放行失败时的报错明说是内容警告,并提示去浏览器点一次,
  不再甩锅给「EH 可能又改版」
2026-10-11 11:41:00 +08:00

137 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package eh
// 回归测试:EH「Content Warning」拦截页(2026-10 线上问题)
//
// 现象:部分画廊(多被标成 Offensive For Everyone)第一次访问只返回
// 一个 3~4KB 的黄框页 —— <title> 和 meta 都在,但一个 /s/ 链接都没有,
// 旧代码因此报「页面结构无法解析(EH 可能又改版)」,纯属误导。
//
// 放行方式:请求一次 ?nw=always,EH 会种下 nw=1 Cookie。
// 关键是这个 Cookie 必须被记住,所以 Client 必须带 cookie jar。
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// warnPage 复刻 EH 的内容警告页(关键特征:没有 /s/ 链接、没有 taglist)
func warnPage(gid, token string) string {
return fmt.Sprintf(`<!DOCTYPE html><html><head>
<title>[Some Circle] Flagged Gallery - E-Hentai Galleries</title>
<meta name="description" content="Free Hentai Doujinshi Gallery: [Some Circle] Flagged Gallery" />
</head><body>
<div style="background:#FF6; width:800px; margin:100px auto">
<h1>Content Warning</h1>
<p>This gallery has been flagged as <strong>Offensive For Everyone</strong>.</p>
<p>[<a href="%s/g/%s/%s/?nw=session">View Gallery</a>]</p>
<p>[<a href="%s/g/%s/%s/?nw=always">Never Warn Me Again</a>]</p>
</div></body></html>`, EHBase, gid, token, EHBase, gid, token)
}
// 拦截页 + nw=always 放行(真实行为:带 nw=always 的请求会被种下 nw=1)
func TestFetchGalleryPageContentWarningAutoBypass(t *testing.T) {
const (
gid = "4166903"
tok = "97d35eebb2"
)
real := mockGalleryHTML(t, gid, tok, 25)[0]
mux := http.NewServeMux()
mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) {
// 真实 EH:看到 nw=always 就种下 nw=1 Cookie,然后进画廊
if r.URL.Query().Get("nw") == "always" {
http.SetCookie(w, &http.Cookie{Name: "nw", Value: "1", Path: "/"})
w.Write([]byte(real))
return
}
c, err := r.Cookie("nw")
if err != nil || c.Value != "1" {
w.Write([]byte(warnPage(gid, tok))) // 还没放行 -> 拦截页
return
}
w.Write([]byte(real)) // 已放行 -> 真正的画廊页
})
srv := httptest.NewServer(mux)
defer srv.Close()
oldBase := EHBase
EHBase = srv.URL
defer func() { EHBase = oldBase }()
c := NewClient(5*time.Second, "", "off", "auto")
defer c.Close()
g, err := c.FetchGalleryPage(context.Background(), gid, tok, nil)
if err != nil {
t.Fatalf("撞到内容警告页应该自动放行,不该失败: %v", err)
}
if len(g.Pages) == 0 {
t.Fatal("自动放行后应至少解析出 1 页")
}
if len(g.Pages) != 20 {
t.Errorf("第 1 屏应解析出 20 页,实际 %d", len(g.Pages))
}
t.Logf("自动放行成功,解析出 %d 页(总 %d)", len(g.Pages), g.TotalPages)
}
// 只在 jar 缺失时才会失败——所以单独锁住「Client 必须带 jar」
func TestClientHasCookieJar(t *testing.T) {
c := NewClient(5*time.Second, "", "off", "auto")
defer c.Close()
if c.httpClient(0).Jar == nil {
t.Fatal("http.Client 没有 cookie jar —— EH 的 nw=1 记不住,内容警告页永远过不去")
}
}
// 拦截页里识别不出来时的报错文案,不能再说「EH 可能又改版」了
func TestContentWarningDetection(t *testing.T) {
if !isContentWarning(warnPage("1", "aa")) {
t.Error("应识别出内容警告页")
}
if !isContentWarning(`<h1>Content Warning</h1>`) {
t.Error("大小写/空白差异也应识别")
}
if isContentWarning(`<html><body><a href="/s/k/1-1">thumb</a></body></html>`) {
t.Error("正常画廊页不该被误判成警告页")
}
}
func TestContentWarningErrorMessage(t *testing.T) {
const (
gid = "4999999"
tok = "deadbeef01"
)
// 永远只返回拦截页:即使放行也拿不到内容
mux := http.NewServeMux()
mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(warnPage(gid, tok)))
})
srv := httptest.NewServer(mux)
defer srv.Close()
oldBase := EHBase
EHBase = srv.URL
defer func() { EHBase = oldBase }()
c := NewClient(5*time.Second, "", "off", "auto")
defer c.Close()
_, err := c.FetchGalleryPage(context.Background(), gid, tok, nil)
if err == nil {
t.Fatal("应当失败")
}
if !strings.Contains(err.Error(), "Offensive For Everyone") {
t.Errorf("报错应说明是内容警告,实际: %v", err)
}
if strings.Contains(err.Error(), "改版") {
t.Errorf("报错不该再提「改版」,那会误导排查: %v", err)
}
}