- 新增 /rank 免签名榜单接口(多域名容错)、/bookshelf 与 /read-history(sessionid 登录态,书架支持分组过滤) - 搜索重写:翻页透传上游 passback/search_id/next_offset/corrected_query 并按 book_id 去重,15~20 位纯数字关键词直查详情(ID 直搜) - 删除 /login/check 登录校验端点(login.go、路由与文档同步移除,sessionid 说明改为直接供书架/历史使用) - 正文链路重构:singleflight 合并同章节并发回源防击穿,缓存与解析解耦统一在出口处理,decodeContentBody 按 crypt_status 分支并兜底 500003 明文 - 缓存层:SQL 持久层 TTL 与业务 TTL 解耦(不可变内容过期后由 SQL 回填),动态短缓存只走 Redis,withCache 合并并发 miss - 密钥池:KeyTs 随正文请求上传降低密钥不匹配重试,密钥超 24 小时异步刷新并防刷新风暴,last_used_at 批量落库 - 签名补 X-Helios(与 X-Ladon 同值,对齐书源实现);设备注册版本号统一走 ClientVersionCode/Name - 分类 landing 透传新版标签 selected_items;multi-detail 批量详情供书架/历史复用 - README 与 openapi.yaml 补充新接口文档
257 lines
6.1 KiB
Go
257 lines
6.1 KiB
Go
package fqapi
|
||
|
||
import (
|
||
"bytes"
|
||
"compress/gzip"
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fanqie/internal/common"
|
||
"fmt"
|
||
"math/rand"
|
||
"regexp"
|
||
)
|
||
|
||
// KeyInfo 密钥信息
|
||
type KeyInfo struct {
|
||
Key string `json:"key"`
|
||
Ver int `json:"ver"`
|
||
Ts int64 `json:"ts"` // 密钥注册时间戳(key_register_ts),随正文请求上传可降低密钥不匹配重试
|
||
}
|
||
|
||
var (
|
||
keyInfo *KeyInfo
|
||
keyLogger = common.NewLogger("register_key", "INFO", "")
|
||
)
|
||
|
||
// rHex 将数字转换为反转的16进制字符串
|
||
func rHex(num int64) string {
|
||
hexStr := fmt.Sprintf("%032x", num)
|
||
result := ""
|
||
for i := len(hexStr); i > 0; i -= 2 {
|
||
result += hexStr[i-2 : i]
|
||
}
|
||
return result
|
||
}
|
||
|
||
// createSymmetricCrypto 创建对称加密器
|
||
func createSymmetricCrypto(algorithm string, key, iv []byte) (func([]byte) ([]byte, error), func([]byte) ([]byte, error), error) {
|
||
if algorithm != "AES/CBC/PKCS5Padding" {
|
||
return nil, nil, fmt.Errorf("不支持的加密算法")
|
||
}
|
||
|
||
block, err := aes.NewCipher(key)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
|
||
encrypt := func(data []byte) ([]byte, error) {
|
||
// PKCS7填充
|
||
padding := aes.BlockSize - len(data)%aes.BlockSize
|
||
padText := make([]byte, padding)
|
||
for i := range padText {
|
||
padText[i] = byte(padding)
|
||
}
|
||
paddedData := append(data, padText...)
|
||
|
||
ciphertext := make([]byte, len(paddedData))
|
||
mode := cipher.NewCBCEncrypter(block, iv)
|
||
mode.CryptBlocks(ciphertext, paddedData)
|
||
|
||
return ciphertext, nil
|
||
}
|
||
|
||
decrypt := func(data []byte) ([]byte, error) {
|
||
mode := cipher.NewCBCDecrypter(block, iv)
|
||
decrypted := make([]byte, len(data))
|
||
mode.CryptBlocks(decrypted, data)
|
||
|
||
// 移除PKCS7填充(完整校验:密钥错误时尾部为随机字节,
|
||
// 弱校验会让乱码静默通过并返回 200)
|
||
padding := int(decrypted[len(decrypted)-1])
|
||
if padding < 1 || padding > len(decrypted) || padding > aes.BlockSize {
|
||
return nil, fmt.Errorf("无效的填充")
|
||
}
|
||
for _, b := range decrypted[len(decrypted)-padding:] {
|
||
if int(b) != padding {
|
||
return nil, fmt.Errorf("无效的填充")
|
||
}
|
||
}
|
||
|
||
return decrypted[:len(decrypted)-padding], nil
|
||
}
|
||
|
||
return encrypt, decrypt, nil
|
||
}
|
||
|
||
// En AES加密
|
||
func En(data, key, iv []byte) ([]byte, error) {
|
||
encrypt, _, err := createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return encrypt(data)
|
||
}
|
||
|
||
// De AES解密
|
||
func De(data, key, iv []byte) ([]byte, error) {
|
||
_, decrypt, err := createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return decrypt(data)
|
||
}
|
||
|
||
// randomStringKey 生成随机字符串(密钥用)
|
||
func randomStringKey(length int) string {
|
||
const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
b := make([]byte, length)
|
||
for i := range b {
|
||
b[i] = charset[rand.Intn(len(charset))]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// GetKey 获取密钥
|
||
func GetKey(header string) (*KeyInfo, error) {
|
||
if header == "" {
|
||
keyLogger.Error("请先注册设备")
|
||
return nil, fmt.Errorf("请先注册设备")
|
||
}
|
||
|
||
key, _ := base64.StdEncoding.DecodeString("rCXGfd2POMGzeiNIgo4iLg==")
|
||
iv := []byte(randomStringKey(16))
|
||
|
||
// 提取device_id
|
||
re := regexp.MustCompile(`device_id=(\d+)`)
|
||
matches := re.FindStringSubmatch(header)
|
||
if len(matches) < 2 {
|
||
return nil, fmt.Errorf("无法从header中提取device_id")
|
||
}
|
||
|
||
var deviceIDInt int64
|
||
fmt.Sscanf(matches[1], "%d", &deviceIDInt)
|
||
|
||
// 转换device_id
|
||
hexStr := rHex(deviceIDInt)
|
||
data, _ := hex.DecodeString(hexStr)
|
||
|
||
keyLogger.Debug(fmt.Sprintf("rKey: %v", iv))
|
||
|
||
// 加密
|
||
encrypted, err := En(data, key, iv)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 组合iv和加密数据
|
||
newArr := append(iv, encrypted...)
|
||
content := base64.StdEncoding.EncodeToString(newArr)
|
||
|
||
keyLogger.Info("正在获取密钥...")
|
||
|
||
// 构建参数(form 编码:机型含空格等字符时 URL 与签名一致才不会注册失败)
|
||
params := []string{
|
||
"aid=1967",
|
||
"app_name=novelapp",
|
||
"channel=0",
|
||
"device_platform=android",
|
||
"os_version=0",
|
||
"version_code=" + ClientVersionCode,
|
||
header,
|
||
}
|
||
sortedParams := buildSortedQuery(params...)
|
||
|
||
// 准备请求体
|
||
requestBody := map[string]string{
|
||
"content": content,
|
||
"keyver": "1",
|
||
}
|
||
|
||
bodyJSON, _ := json.Marshal(requestBody)
|
||
|
||
// gzip压缩
|
||
var gzipBuf bytes.Buffer
|
||
gzipWriter := gzip.NewWriter(&gzipBuf)
|
||
gzipWriter.Write(bodyJSON)
|
||
gzipWriter.Close()
|
||
|
||
reqURL := "https://reading.snssdk.com/reading/crypt/registerkey?" + sortedParams
|
||
|
||
resp, err := GorgonRequest(reqURL, map[string]interface{}{
|
||
"method": "POST",
|
||
"body": gzipBuf.Bytes(),
|
||
"headers": map[string]string{
|
||
"User-Agent": "com.dragon.read",
|
||
"Content-Encoding": "gzip",
|
||
},
|
||
})
|
||
|
||
if err != nil {
|
||
keyLogger.Error("获取密钥失败: " + err.Error())
|
||
return nil, err
|
||
}
|
||
|
||
if resp.Status == 200 {
|
||
var respData map[string]interface{}
|
||
json.Unmarshal(resp.Content, &respData)
|
||
|
||
// 上游判定该设备无需解密(正文明文下发),返回空密钥信息由调用方分支处理
|
||
if code, ok := respData["code"].(float64); ok && int(code) == 500003 {
|
||
keyLogger.Info("上游标记无需解密(code=500003)")
|
||
return &KeyInfo{}, nil
|
||
}
|
||
|
||
dataObj, ok := respData["data"].(map[string]interface{})
|
||
if !ok {
|
||
return nil, fmt.Errorf("响应格式错误")
|
||
}
|
||
|
||
keyStr, ok := dataObj["key"].(string)
|
||
if !ok {
|
||
return nil, fmt.Errorf("密钥字段不存在")
|
||
}
|
||
|
||
// 解码返回的密钥
|
||
res, _ := base64.StdEncoding.DecodeString(keyStr)
|
||
if len(res) < 32 {
|
||
return nil, fmt.Errorf("密钥长度不足")
|
||
}
|
||
|
||
data := res[16:]
|
||
ivResp := res[0:16]
|
||
|
||
// 解密密钥
|
||
decryptedKey, err := De(data, key, ivResp)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
finalKey := base64.StdEncoding.EncodeToString(decryptedKey)
|
||
|
||
keyVer, _ := dataObj["keyver"].(float64)
|
||
keyTs, _ := dataObj["key_register_ts"].(float64)
|
||
if keyTs == 0 {
|
||
keyTs, _ = dataObj["register_time"].(float64)
|
||
}
|
||
keyInfo = &KeyInfo{
|
||
Key: finalKey,
|
||
Ver: int(keyVer),
|
||
Ts: int64(keyTs),
|
||
}
|
||
|
||
keyLogger.Info("密钥获取成功!")
|
||
|
||
return keyInfo, nil
|
||
}
|
||
|
||
return nil, fmt.Errorf("获取密钥失败")
|
||
}
|
||
|
||
// GetKeyInfo 获取当前密钥信息
|
||
func GetKeyInfo() *KeyInfo {
|
||
return keyInfo
|
||
}
|