- 正文参数支持语义值(plain/html/keep/remove)与别名(img_mode/review_mode),URL 参数优先于配置 - 修复纯文本被段评强制包回 HTML、HTML 模式段评注入失效的问题 - 修复 PKCS7 填充弱校验导致漫画/正文偶发解密乱码返回 200 的问题 - 搜索结果展平:去 search_tabs 层与 selected_tab_idx/server_ab_params,提升 has_more/next_offset - 全接口解包字符串形式的 JSON 字段(category_schema 等),缓存出口幂等处理 - 序列化关闭 HTML 转义,video/audio 播放链接可直接复制播放 - video 响应透传 CENC 加密状态(key_seed/kid/encryption_method) - 新增 gzip 中间件,JSON/文本响应压缩约 81%,二进制透传 - 漫画默认输出模式改为 proxy(url 模式链接 404 且内容加密不可用) - openapi.yaml:补全 23 个路径参数定义、tab_type 完整枚举、CDN 签名说明,示例更新
255 lines
5.7 KiB
Go
255 lines
5.7 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"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// KeyInfo 密钥信息
|
|
type KeyInfo struct {
|
|
Key string `json:"key"`
|
|
Ver int `json:"ver"`
|
|
}
|
|
|
|
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("正在获取密钥...")
|
|
|
|
// 构建参数
|
|
params := []string{
|
|
"aid=1967",
|
|
"app_name=novelapp",
|
|
"channel=0",
|
|
"device_platform=android",
|
|
"os_version=0",
|
|
"version_code=5.8.9.32",
|
|
header,
|
|
}
|
|
|
|
allParams := strings.Join(params, "&")
|
|
paramList := strings.Split(allParams, "&")
|
|
sort.Strings(paramList)
|
|
sortedParams := strings.Join(paramList, "&")
|
|
|
|
// 准备请求体
|
|
requestBody := map[string]string{
|
|
"content": content,
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
keyInfo = &KeyInfo{
|
|
Key: finalKey,
|
|
Ver: int(keyVer),
|
|
}
|
|
|
|
keyLogger.Info(fmt.Sprintf("密钥获取成功!"))
|
|
|
|
return keyInfo, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("获取密钥失败")
|
|
}
|
|
|
|
// GetKeyInfo 获取当前密钥信息
|
|
func GetKeyInfo() *KeyInfo {
|
|
return keyInfo
|
|
}
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|