Files
truewhile c1ff55ffbf 开发
2026-09-30 19:22:24 +08:00

241 lines
6.1 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 rule
import (
"net"
"net/url"
"strings"
"sync"
"golang.org/x/net/publicsuffix"
)
// 本文件:书源会话状态(变量 / 登录信息 / 登录请求头 / Cookie)的抽象。
// 对应 legado BaseSource 的 getVariable/putVariable/getLoginInfo/putLoginInfo/
// getLoginHeader/putLoginHeader 与 CookieStore。实现由服务层落库提供;
// 未注入时退回进程内 MemoryState(单测、冒烟 CLI 用)。
// SourceState 书源会话状态读写接口。
type SourceState interface {
// GetVariable / SetVariable 对应 source.getVariable / source.setVariable。
GetVariable() string
SetVariable(v string)
// GetLoginInfo / SetLoginInfo 对应 source.getLoginInfo / source.putLoginInfo。
// 内容为登录表单的 JSON(键为字段名)。
GetLoginInfo() string
SetLoginInfo(v string)
// GetLoginHeader / SetLoginHeader 对应 source 的登录请求头(JSON),
// 其中 Cookie 键会在请求时合并进 Cookie 头。
GetLoginHeader() string
SetLoginHeader(v string)
// Cookie 读写:rawURL 可为完整 URL 或裸域名,按有效顶级域+1 归并。
GetCookie(rawURL string) string
GetCookieKey(rawURL, key string) string
SetCookie(rawURL, cookie string)
RemoveCookie(rawURL string)
// Toast 收集宿主提示(java.toast / java.longToast),登录反馈靠它回传前端。
Toast(msg string)
// OpenBrowser 记录需要浏览器完成的地址(java.startBrowser)——
// 服务端无法弹窗,前端据此提供「在新标签打开」。
OpenBrowser(url, title string)
}
// CookieDomain 取 URL 的有效顶级域 +1(对应 legado NetworkUtils.getSubDomain)。
// 裸域名(无 scheme)按 http 处理;IP 原样返回;解析失败回退 host 本身。
func CookieDomain(rawURL string) string {
s := strings.TrimSpace(rawURL)
if s == "" {
return ""
}
if !strings.Contains(s, "://") {
if strings.HasPrefix(s, "//") {
s = "http:" + s
} else {
s = "http://" + s
}
}
u, err := url.Parse(s)
if err != nil {
return ""
}
host := u.Hostname()
if host == "" {
return ""
}
if net.ParseIP(host) != nil {
return host
}
etld1, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
return host
}
return etld1
}
// ParseCookie 把 "a=1; b=2" 拆为键值对(值不 Trim 内部空白,仅去首尾)。
func ParseCookie(cookie string) map[string]string {
out := map[string]string{}
for _, part := range strings.Split(cookie, ";") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 || kv[0] == "" {
continue
}
out[kv[0]] = kv[1]
}
return out
}
// MergeCookie 将新 cookie 覆盖式合并进旧 cookie(对应 CookieStore.replaceCookie)。
func MergeCookie(old, newCookie string) string {
merged := ParseCookie(old)
for k, v := range ParseCookie(newCookie) {
merged[k] = v
}
return formatCookie(merged)
}
func formatCookie(kv map[string]string) string {
parts := make([]string, 0, len(kv))
for k, v := range kv {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, "; ")
}
// MemoryState 进程内会话状态(未注入持久化实现时的回退)。
type MemoryState struct {
mu sync.Mutex
variable string
loginInfo string
loginHeader string
cookies map[string]map[string]string // domain → name → value
toasts []string
browsers []BrowserRequest
}
// BrowserRequest 前端可代为打开的浏览器地址(java.startBrowser 收集)。
type BrowserRequest struct {
URL string `json:"url"`
Title string `json:"title"`
}
// NewMemoryState 创建进程内状态。
func NewMemoryState() *MemoryState {
return &MemoryState{cookies: map[string]map[string]string{}}
}
func (m *MemoryState) GetVariable() string { m.mu.Lock(); defer m.mu.Unlock(); return m.variable }
func (m *MemoryState) SetVariable(v string) {
m.mu.Lock()
defer m.mu.Unlock()
m.variable = v
}
func (m *MemoryState) GetLoginInfo() string { m.mu.Lock(); defer m.mu.Unlock(); return m.loginInfo }
func (m *MemoryState) SetLoginInfo(v string) {
m.mu.Lock()
defer m.mu.Unlock()
m.loginInfo = v
}
func (m *MemoryState) GetLoginHeader() string { m.mu.Lock(); defer m.mu.Unlock(); return m.loginHeader }
func (m *MemoryState) SetLoginHeader(v string) {
m.mu.Lock()
defer m.mu.Unlock()
m.loginHeader = v
}
func (m *MemoryState) GetCookie(rawURL string) string {
return m.GetCookieKey(rawURL, "")
}
func (m *MemoryState) GetCookieKey(rawURL, key string) string {
domain := CookieDomain(rawURL)
if domain == "" {
return ""
}
m.mu.Lock()
defer m.mu.Unlock()
jar, ok := m.cookies[domain]
if !ok {
return ""
}
if key != "" {
return jar[key]
}
return formatCookie(jar)
}
func (m *MemoryState) SetCookie(rawURL, cookie string) {
domain := CookieDomain(rawURL)
if domain == "" || strings.TrimSpace(cookie) == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
jar, ok := m.cookies[domain]
if !ok {
jar = map[string]string{}
m.cookies[domain] = jar
}
for k, v := range ParseCookie(cookie) {
jar[k] = v
}
}
func (m *MemoryState) RemoveCookie(rawURL string) {
domain := CookieDomain(rawURL)
if domain == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
delete(m.cookies, domain)
}
func (m *MemoryState) Toast(msg string) {
m.mu.Lock()
defer m.mu.Unlock()
m.toasts = append(m.toasts, msg)
}
func (m *MemoryState) OpenBrowser(url, title string) {
m.mu.Lock()
defer m.mu.Unlock()
m.browsers = append(m.browsers, BrowserRequest{URL: url, Title: title})
}
// Toasts 返回并清空已收集的宿主提示。
func (m *MemoryState) Toasts() []string {
m.mu.Lock()
defer m.mu.Unlock()
out := m.toasts
m.toasts = nil
return out
}
// Browsers 返回并清空已收集的浏览器地址。
func (m *MemoryState) Browsers() []BrowserRequest {
m.mu.Lock()
defer m.mu.Unlock()
out := m.browsers
m.browsers = nil
return out
}
// Snapshot 返回当前 Cookie(domain → cookie 串)副本。
func (m *MemoryState) Snapshot() map[string]string {
m.mu.Lock()
defer m.mu.Unlock()
out := make(map[string]string, len(m.cookies))
for d, jar := range m.cookies {
out[d] = formatCookie(jar)
}
return out
}
var _ SourceState = (*MemoryState)(nil)