- /manga 设备池签名路线全部失败时自动降级 wkbrowser 免签名路线(materials/skeleton 三段策略收集原图直链,无需设备/签名/解密),响应附 source: wkbrowser - 新增配置 fanqie.device_identity(默认 pad 保持现状;phone 对齐书源真机注册 payload:host_bit 64 + 补 hardware/device_device/fingerprint 字段) - README 与 openapi.yaml 同步文档,新增 manga_fallback 解析测试
411 lines
11 KiB
Go
411 lines
11 KiB
Go
package fqapi
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fanqie/internal/common"
|
||
"fanqie/internal/config"
|
||
"fmt"
|
||
"math/big"
|
||
"net"
|
||
"os"
|
||
"runtime"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// 使用共享的设备配置
|
||
var (
|
||
androidVersions = DeviceConfig.AndroidVersions
|
||
brandModels = DeviceConfig.BrandModels
|
||
deviceBrands = DeviceConfig.DeviceBrands
|
||
)
|
||
|
||
// generateUUID 生成UUID4字符串
|
||
func generateUUID() string {
|
||
return uuid.New().String()
|
||
}
|
||
|
||
// currentMillis 返回当前毫秒级时间戳
|
||
func currentMillis() int64 {
|
||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||
}
|
||
|
||
// getCustomBT 获取设备开机时间戳
|
||
// 模拟 Android 中的 m25453o() 方法,计算设备开机时间戳:
|
||
// custom_bt = current_millis - elapsed_ms_since_boot
|
||
func getCustomBT() string {
|
||
var uptimeSeconds float64
|
||
|
||
if runtime.GOOS == "linux" {
|
||
// 在Linux上读取/proc/uptime
|
||
data, err := os.ReadFile("/proc/uptime")
|
||
if err == nil {
|
||
// /proc/uptime格式: "系统运行时间(秒) 空闲时间(秒)"
|
||
parts := strings.Split(string(data), " ")
|
||
if len(parts) > 0 {
|
||
uptime, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
|
||
if err == nil {
|
||
uptimeSeconds = uptime
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果读取失败,使用time.monotonic作为备选
|
||
if uptimeSeconds == 0 {
|
||
// Go没有直接的monotonic API,使用启动时间估算
|
||
uptimeSeconds = float64(time.Now().Unix() % 86400)
|
||
}
|
||
} else {
|
||
// 非Linux系统,使用估算值
|
||
// 这里可以使用进程启动时间作为近似
|
||
uptimeSeconds = float64(time.Now().Unix() % 86400)
|
||
}
|
||
|
||
return fmt.Sprintf("%d", currentMillis()-int64(uptimeSeconds*1000))
|
||
}
|
||
|
||
// generateDeviceInfo 随机生成设备及操作系统信息
|
||
func generateDeviceInfo() map[string]interface{} {
|
||
osVersion := androidVersions[randInt(len(androidVersions))]
|
||
|
||
// 随机选择品牌
|
||
deviceBrand := deviceBrands[randInt(len(deviceBrands))]
|
||
|
||
// 根据品牌选择对应的型号
|
||
models := brandModels[deviceBrand]
|
||
deviceModel := models[randInt(len(models))]
|
||
|
||
releaseBytes := make([]byte, 7)
|
||
rand.Read(releaseBytes)
|
||
releaseBuild := hex.EncodeToString(releaseBytes)
|
||
|
||
openUDIDBytes := make([]byte, 10)
|
||
rand.Read(openUDIDBytes)
|
||
|
||
return map[string]interface{}{
|
||
"os": "Android",
|
||
"os_version": osVersion[0],
|
||
"os_api": osVersion[1],
|
||
"device_model": deviceModel,
|
||
"device_brand": deviceBrand,
|
||
"device_manufacturer": deviceBrand,
|
||
"cpu_abi": "arm64-v8a",
|
||
"release_build": releaseBuild,
|
||
"cdid": generateUUID(),
|
||
"sig_hash": "a4a27c2633195374c15651ffc3c4a497",
|
||
"openudid": hex.EncodeToString(openUDIDBytes),
|
||
"clientudid": generateUUID(),
|
||
"req_id": generateUUID(),
|
||
}
|
||
}
|
||
|
||
// randInt 生成随机整数
|
||
func randInt(max int) int {
|
||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||
return int(n.Int64())
|
||
}
|
||
|
||
// randomString 生成随机字符串
|
||
func randomString(length int) string {
|
||
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||
b := make([]byte, length)
|
||
for i := range b {
|
||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||
b[i] = charset[n.Int64()]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// generateOptionalFields 生成ROM信息等
|
||
func generateOptionalFields() (map[string]interface{}, map[string]int64) {
|
||
rom := randomString(14)
|
||
part1 := randomString(4)
|
||
part2 := randomString(6)
|
||
part3 := randomString(8)
|
||
|
||
genTime := currentMillis()
|
||
return map[string]interface{}{
|
||
"rom": rom,
|
||
"rom_version": fmt.Sprintf("coloros__%s.%s.%s release-keys", part1, part2, part3),
|
||
"apk_first_install_time": genTime - int64(randInt(365))*86400000,
|
||
}, map[string]int64{
|
||
"_gen_time": genTime,
|
||
}
|
||
}
|
||
|
||
// generateIPv6AddressLinkLocal 生成链路本地IPv6地址
|
||
func generateIPv6AddressLinkLocal() string {
|
||
ifaces, err := net.Interfaces()
|
||
if err != nil {
|
||
return "::1"
|
||
}
|
||
|
||
for _, iface := range ifaces {
|
||
addrs, err := iface.Addrs()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, addr := range addrs {
|
||
ipNet, ok := addr.(*net.IPNet)
|
||
if !ok {
|
||
continue
|
||
}
|
||
ip := ipNet.IP
|
||
if ip.To16() != nil && strings.HasPrefix(ip.String(), "fe80::") {
|
||
return strings.Split(ip.String(), "%")[0]
|
||
}
|
||
}
|
||
}
|
||
return "::1"
|
||
}
|
||
|
||
// generateIPv6AddressUniqueLocal 生成唯一本地IPv6地址
|
||
func generateIPv6AddressUniqueLocal() string {
|
||
b := make([]byte, 1)
|
||
rand.Read(b)
|
||
parts := []string{fmt.Sprintf("fd%02x", b[0])}
|
||
for i := 0; i < 7; i++ {
|
||
b := make([]byte, 2)
|
||
rand.Read(b)
|
||
parts = append(parts, fmt.Sprintf("%04x", uint16(b[0])<<8|uint16(b[1])))
|
||
}
|
||
return strings.Join(parts, ":")
|
||
}
|
||
|
||
// generateIPv6List 生成IPv6列表
|
||
func generateIPv6List() []map[string]string {
|
||
return []map[string]string{
|
||
{"type": "client_anpi", "value": generateIPv6AddressLinkLocal()},
|
||
{"type": "client_anpi", "value": generateIPv6AddressUniqueLocal()},
|
||
{"type": "client_anpi", "value": generateIPv6AddressUniqueLocal()},
|
||
}
|
||
}
|
||
|
||
// deviceIdentity 注册设备身份:pad(默认,历史行为)/ phone(对齐手机 App 书源注册 payload)
|
||
func deviceIdentity() string {
|
||
if cfg := config.GetConfig(); cfg != nil && cfg.Fanqie.DeviceIdentity == "phone" {
|
||
return "phone"
|
||
}
|
||
return "pad"
|
||
}
|
||
|
||
// deviceCustomFields 注册身份对应的 custom 字段
|
||
func deviceCustomFields() map[string]interface{} {
|
||
if deviceIdentity() == "phone" {
|
||
return map[string]interface{}{"host_bit": 64, "dragon_device_type": "phone"}
|
||
}
|
||
return map[string]interface{}{"host_bit": 32, "dragon_device_type": "pad"}
|
||
}
|
||
|
||
// GenerateFullRequestBody 生成完整的请求体数据
|
||
func GenerateFullRequestBody() map[string]interface{} {
|
||
rawData := map[string]interface{}{
|
||
"magic_tag": "ss_app_log",
|
||
"header": map[string]interface{}{
|
||
"display_name": "番茄免费小说",
|
||
"aid": 1967,
|
||
"channel": "43536163a",
|
||
"package": "com.dragon.read",
|
||
"sdk_version": "3.7.0-rc.25-fanqie-xiaoshuo",
|
||
"sdk_target_version": 29,
|
||
"git_hash": "711d1a7",
|
||
"density_dpi": 240,
|
||
"display_density": "hdpi",
|
||
"resolution": "720x1280",
|
||
"language": "zh",
|
||
"timezone": 8,
|
||
"access": "wifi",
|
||
"not_request_sender": 0,
|
||
"carrier": "CHINA MOBILE",
|
||
"mcc_mnc": "46000",
|
||
"region": "CN",
|
||
"tz_name": "Asia/Shanghai",
|
||
"tz_offset": 28800,
|
||
"sim_region": "cn",
|
||
"sim_serial_number": []interface{}{},
|
||
"oaid_may_support": false,
|
||
"device_platform": "android",
|
||
"custom": deviceCustomFields(),
|
||
"pre_installed_channel": "",
|
||
"is_system_app": 0,
|
||
"sdk_flavor": "china",
|
||
"guest_mode": 0,
|
||
},
|
||
}
|
||
|
||
deviceInfo := generateDeviceInfo()
|
||
optionalFields, genTime := generateOptionalFields()
|
||
|
||
header := rawData["header"].(map[string]interface{})
|
||
for k, v := range deviceInfo {
|
||
header[k] = v
|
||
}
|
||
for k, v := range optionalFields {
|
||
header[k] = v
|
||
}
|
||
header["ipv6_list"] = generateIPv6List()
|
||
|
||
// phone 身份补充真机指纹字段(对齐书源注册 payload,规避 pad 身份的差异化风控)
|
||
if deviceIdentity() == "phone" {
|
||
rom, _ := header["rom"].(string)
|
||
header["hardware"] = "qcom"
|
||
header["device_device"] = rom
|
||
header["fingerprint"] = fmt.Sprintf("%v/%v/%v:%v/%s/%s:user/release-keys",
|
||
header["device_brand"], header["device_model"], header["device_model"],
|
||
header["os_version"], rom, rom)
|
||
}
|
||
|
||
for k, v := range genTime {
|
||
rawData[k] = v
|
||
}
|
||
|
||
return rawData
|
||
}
|
||
|
||
// tryActivatePremium 尝试激活设备特权
|
||
func tryActivatePremium(header string) {
|
||
logger := common.NewLogger("device_register", "INFO", "")
|
||
|
||
// 构建查询参数
|
||
baseParams := []string{
|
||
"aid=1967",
|
||
"app_name=novelapp",
|
||
"channel=0",
|
||
"device_platform=android",
|
||
"os_version=0",
|
||
"version_code=" + ClientVersionCode,
|
||
"manifest_version_code=" + ClientVersionCode,
|
||
"update_version_code=" + ClientVersionCode,
|
||
header,
|
||
}
|
||
|
||
// 合并并排序参数
|
||
allParams := strings.Join(baseParams, "&")
|
||
paramList := strings.Split(allParams, "&")
|
||
sort.Strings(paramList)
|
||
sortedParams := strings.Join(paramList, "&")
|
||
|
||
requestBody := map[string]interface{}{
|
||
"add_count_daily": 0,
|
||
"amount": 2592000,
|
||
"privilege_id": 7210376203117531962,
|
||
"from": 8,
|
||
"unique_key": fmt.Sprintf("%d", time.Now().UnixNano()/1e6),
|
||
}
|
||
|
||
bodyJSON, _ := json.Marshal(requestBody)
|
||
|
||
url := "https://reading.snssdk.com/reading/user/privilege/add/v/?" + sortedParams
|
||
|
||
resp, err := GorgonRequestSync(url, map[string]interface{}{
|
||
"method": "post",
|
||
"headers": map[string]string{
|
||
"content-type": "application/json",
|
||
},
|
||
"body": bodyJSON,
|
||
})
|
||
|
||
if err != nil {
|
||
logger.Error("激活特权失败: " + err.Error())
|
||
return
|
||
}
|
||
|
||
var result map[string]interface{}
|
||
json.Unmarshal(resp.Content, &result)
|
||
|
||
if code, ok := result["code"].(float64); ok && code == 0 {
|
||
logger.Info("已成功激活设备特权")
|
||
} else {
|
||
logger.Warning("设备特权激活失败...")
|
||
}
|
||
}
|
||
|
||
// Register 设备注册
|
||
func Register() (string, error) {
|
||
logger := common.NewLogger("device_register", "INFO", "")
|
||
|
||
logger.Info("正在注册设备...")
|
||
|
||
// 生成完整请求体数据
|
||
data := GenerateFullRequestBody()
|
||
|
||
// 只记录关键设备信息
|
||
header := data["header"].(map[string]interface{})
|
||
logger.Info(fmt.Sprintf("生成设备信息 - 品牌: %s, 型号: %s, 系统: Android %v",
|
||
header["device_brand"],
|
||
header["device_model"],
|
||
header["os_version"]))
|
||
|
||
jsonData, err := json.Marshal(data)
|
||
if err != nil {
|
||
logger.Error("JSON序列化失败: " + err.Error())
|
||
return "", err
|
||
}
|
||
|
||
// 使用TTEncrypt加密
|
||
encrypted, err := TTEncrypt(jsonData)
|
||
if err != nil {
|
||
logger.Error("加密失败: " + err.Error())
|
||
return "", err
|
||
}
|
||
|
||
url := "https://i.snssdk.com/service/2/device_register/?tt_data=a"
|
||
headers := map[string]string{
|
||
"user-agent": "okhttp/4.10.0",
|
||
"content-type": "application/octet-stream; tt-data=a",
|
||
}
|
||
|
||
client := common.NewHTTPClient()
|
||
resp, err := client.Post(url, encrypted, headers)
|
||
if err != nil {
|
||
logger.Warning("设备注册失败!")
|
||
logger.Warning(err.Error())
|
||
return "", fmt.Errorf("设备注册错误")
|
||
}
|
||
|
||
var respData map[string]interface{}
|
||
err = json.Unmarshal(resp.Content, &respData)
|
||
if err != nil {
|
||
logger.Error("响应解析失败: " + err.Error())
|
||
return "", err
|
||
}
|
||
|
||
if resp.Status == 200 {
|
||
// 检查device_id
|
||
deviceID, ok := respData["device_id"]
|
||
if !ok {
|
||
logger.Warning("设备注册失败!响应中没有device_id")
|
||
return "", fmt.Errorf("设备注册失败")
|
||
}
|
||
|
||
// 转换device_id为float64并检查
|
||
deviceIDFloat, ok := deviceID.(float64)
|
||
if !ok || deviceIDFloat == 0 {
|
||
logger.Warning("设备注册失败!device_id无效")
|
||
return "", fmt.Errorf("设备注册失败")
|
||
}
|
||
|
||
deviceModel := data["header"].(map[string]interface{})["device_model"].(string)
|
||
header := fmt.Sprintf("device_id=%v&device_type=%s&iid=%v&version_name="+ClientVersionName,
|
||
respData["device_id_str"],
|
||
deviceModel,
|
||
respData["install_id_str"])
|
||
|
||
logger.Info("设备注册成功!")
|
||
|
||
// 尝试激活特权
|
||
tryActivatePremium(header)
|
||
|
||
return header, nil
|
||
}
|
||
|
||
logger.Warning("设备注册失败!")
|
||
return "", fmt.Errorf("设备注册失败")
|
||
}
|