refactor: 架构整固——分层单向化、全量 TypeScript、直接运行
- 移除 bd/mg 两平台及磁盘残留产物;清理 api-test 死脚本、musicSdk/message+options 死模块、infSign vendor - musicSdk/ 改名 sdk/;src/common、src/auth、src/apis 整体移入 sdk/,依赖方向定为 src → sdk 严格单向 - src/ 手写 JS 全量转 .ts,删除 JS 类型垫片 modules.d.ts;源码 import 统一 .ts 后缀 - Node 原生类型剥离直接运行 .ts(engines >=22.18),取消 tsc 发射产物,tsc 仅类型检查(pnpm typecheck) - 删除 kwdes 备用实现与 src/index.ts 转发垫片;测试移至 test/smoke-test.ts - common/request.ts 补显式导出类型;修复 set-cookie 单值形态、ApiError 类字段等隐患 验证:tsc 零错误,smoke-test 41/41
This commit is contained in:
1 parent
c8c939aa32
commit
354df2e739
126 files changed
+413
-2942
No files matched your search
+103
@@ -0,0 +1,103 @@
|
||||
// 酷狗登录 —— token 绑定与验证(网关 android 签名,与 KuGouMusicApi 对齐)
|
||||
// 完整的手机号/扫码登录需移植其 RSA/AES 全套协议,后续按需扩展;
|
||||
// token 可从酷狗 App 抓包获取(login.token)
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
import { httpFetch } from '../common/request.ts'
|
||||
import { getCredentials, setCredentials } from './store.ts'
|
||||
|
||||
const md5 = s => crypto.createHash('md5').update(s).digest('hex')
|
||||
const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d'
|
||||
const SALT_SIGN = 'OIlwieks28dk2k092lksi2UIkp'
|
||||
const APPID = 1005
|
||||
const CLIENTVER = 11430
|
||||
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIAG7QOELSYoIJvTFJhMpe1s/gbjDJX51HBNnEl5HXqTW6lQ7LC8jr9fWZTwusknp+sVGzwd40MwP6U5yDE27M/X1+UR4tvOGOqp94TJtQ1EPnWGWXngpeIW5GxoQGao1rmYWAu6oi1z9XkChrsUdC6DJE5E221wf/4WLFxwAtRQIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
const calculateMid = (guid) => BigInt(`0x${md5(guid)}`).toString()
|
||||
const randomString = (n) => {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
return Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
|
||||
}
|
||||
|
||||
export const androidSign = (params, data = '') => {
|
||||
const s = Object.keys(params).sort()
|
||||
.map(k => `${k}=${typeof params[k] === 'object' ? JSON.stringify(params[k]) : params[k]}`)
|
||||
.join('')
|
||||
return md5(SALT_SIGN + s + data + SALT_SIGN)
|
||||
}
|
||||
|
||||
/** 设备标识持久化(guid/mid/dfid 稳定复用,登录验证与取流共用同一指纹) */
|
||||
export const getDevice = () => {
|
||||
const dev = getCredentials('_kg_device') ?? { guid: crypto.randomUUID(), mid: '', dfid: randomString(24) }
|
||||
if (!dev.mid) {
|
||||
dev.mid = calculateMid(dev.guid)
|
||||
setCredentials('_kg_device', dev)
|
||||
}
|
||||
return dev
|
||||
}
|
||||
|
||||
/** 网关移动端签名请求(gateway.kugou.com + x-router) */
|
||||
const gatewayRequest = async(url, { params: extraParams, data, router }) => {
|
||||
const dev = getDevice()
|
||||
const params = {
|
||||
dfid: dev.dfid, mid: dev.mid, uuid: '-', appid: APPID, clientver: CLIENTVER,
|
||||
clienttime: Math.floor(Date.now() / 1000),
|
||||
...extraParams,
|
||||
}
|
||||
const signature = androidSign(params, data ? JSON.stringify(data) : '')
|
||||
const qs = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')
|
||||
const r = await httpFetch(`https://gateway.kugou.com${url}?${qs}&signature=${signature}`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
dfid: dev.dfid, mid: dev.mid, clienttime: String(params.clienttime),
|
||||
'kg-rc': '1', 'kg-thash': '5d816a0', 'kg-rec': '1', 'kg-rf': 'B9EDA08A64250DEFFBCADDEE00F8F25F',
|
||||
'x-router': router,
|
||||
'User-Agent': 'Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi',
|
||||
},
|
||||
body: data ?? undefined,
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
return r.body ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 token 并返回账号资料(usercenter /v3/get_my_info)
|
||||
* p = RSA 原始加密(零填充 128 字节){ token, clienttime }
|
||||
*/
|
||||
export const verifyToken = async({ token, userid }) => {
|
||||
const unixSec = Math.floor(Date.now() / 1000)
|
||||
const payload = Buffer.from(JSON.stringify({ token, clienttime: unixSec }))
|
||||
const padded = Buffer.concat([Buffer.alloc(128 - payload.length), payload])
|
||||
const p = crypto.publicEncrypt({ key: PUBLIC_KEY, padding: crypto.constants.RSA_NO_PADDING }, padded).toString('hex').toUpperCase()
|
||||
|
||||
const body = await gatewayRequest('/v3/get_my_info', {
|
||||
router: 'usercenter.kugou.com',
|
||||
params: { plat: 1 },
|
||||
data: { visit_time: unixSec, usertype: 1, p, userid: Number(userid) || 0 },
|
||||
})
|
||||
if (body.status !== 1 || !body.data) {
|
||||
const err = new Error(body.error ?? 'token 无效或已过期')
|
||||
;(err as any).status = 401
|
||||
throw err
|
||||
}
|
||||
return {
|
||||
userid: body.data.userid ?? Number(userid),
|
||||
nickname: body.data.nickname ?? body.data.username ?? null,
|
||||
vip: body.data.vip ? Boolean(body.data.vip) : null,
|
||||
}
|
||||
}
|
||||
|
||||
/** 绑定并验证 kg token */
|
||||
export const loginByToken = async({ token, userid }) => {
|
||||
const profile = await verifyToken({ token, userid })
|
||||
setCredentials('kg', { token, userid: profile.userid, nickname: profile.nickname })
|
||||
return profile
|
||||
}
|
||||
|
||||
/** 当前 kg 登录资料(未登录返回 null) */
|
||||
export const status = () => {
|
||||
const cred = getCredentials('kg')
|
||||
return cred?.token ? { userid: cred.userid, nickname: cred.nickname ?? null } : null
|
||||
}
|
||||
Reference in new issue
Block a user