311 lines
9.9 KiB
JavaScript
311 lines
9.9 KiB
JavaScript
/**
|
||
* @fileoverview 通用工具函数库
|
||
*
|
||
* 提供酷狗音乐 API 项目中常用的工具函数,包括:
|
||
* - 随机字符串/数字生成
|
||
* - Cookie 解析与格式化
|
||
* - KRC 歌词解码(XOR 解密 + zlib 解压)
|
||
* - 设备 MID 计算(基于 MD5 的大整数转换)
|
||
* - GUID 生成(UUID v4 格式)
|
||
* - WebGL 指纹哈希生成(浏览器/Node 双环境支持)
|
||
*
|
||
* @module util
|
||
* @requires pako - zlib 解压库(用于 KRC 歌词解码)
|
||
* @requires crypto-js - 加密库(用于 MID 计算中的 MD5 哈希)
|
||
* @requires big-integer - 大整数库(用于 MID 的进制转换)
|
||
*/
|
||
|
||
const pako = require('pako');
|
||
const CryptoJS = require('crypto-js');
|
||
const bigInt = require('big-integer');
|
||
|
||
/**
|
||
* 生成随机字符串(大写字母 + 数字)
|
||
*
|
||
* 字符池: 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ(36个字符)
|
||
* 每次从字符池中随机选取一个字符,拼接为指定长度的字符串。
|
||
*
|
||
* @param {number} [len=16] - 字符串长度,默认 16
|
||
* @returns {string} 随机字符串
|
||
*
|
||
* @example
|
||
* randomString(8) // => "A3B7K9X2"
|
||
*/
|
||
const randomString = (len = 16) => {
|
||
const keyString = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||
const _key = [];
|
||
const keyStringArr = keyString.split('');
|
||
for (let i = 0; i < len; i += 1) {
|
||
const ceil = Math.ceil((keyStringArr.length - 1) * Math.random());
|
||
const _tmp = keyStringArr[ceil];
|
||
_key.push(_tmp);
|
||
}
|
||
|
||
return _key.join('');
|
||
};
|
||
|
||
/**
|
||
* 生成随机数字字符串
|
||
*
|
||
* 字符池: 1234567890(10个数字字符)
|
||
* 每次从字符池中随机选取一个数字字符,拼接为指定长度的字符串。
|
||
*
|
||
* @param {number} [len=16] - 字符串长度,默认 16
|
||
* @returns {string} 随机数字字符串
|
||
*
|
||
* @example
|
||
* randomNumber(6) // => "384729"
|
||
*/
|
||
const randomNumber = (len = 16) => {
|
||
const keyString = '1234567890';
|
||
const _key = [];
|
||
const keyStringArr = keyString.split('');
|
||
for (let i = 0; i < len; i += 1) {
|
||
const ceil = Math.ceil((keyStringArr.length - 1) * Math.random());
|
||
const _tmp = keyStringArr[ceil];
|
||
_key.push(_tmp);
|
||
}
|
||
|
||
return _key.join('');
|
||
};
|
||
|
||
/**
|
||
* 格式化 Cookie 字符串
|
||
*
|
||
* 移除 Cookie 中的非数据字段(Domain、path、expires、HttpOnly),
|
||
* 仅保留有效的键值对数据。
|
||
*
|
||
* @param {string} cookie - 原始 Cookie 字符串
|
||
* @returns {string} 格式化后的 Cookie 字符串
|
||
*
|
||
* @example
|
||
* parseCookieString('token=abc; Domain=.kugou.com; path=/; HttpOnly')
|
||
* // => 'token=abc'
|
||
*/
|
||
const parseCookieString = (cookie) => {
|
||
const t = cookie.replace(/\s*(Domain|domain|path|expires)=[^(;|$)]+;*/g, '');
|
||
return t.replace(/;HttpOnly/g, '');
|
||
};
|
||
|
||
/**
|
||
* Cookie 字符串转 JSON 对象
|
||
*
|
||
* 将 Cookie 字符串按 `;` 分割,每个键值对按 `=` 分割为 key 和 value。
|
||
*
|
||
* @param {string} cookie - Cookie 字符串
|
||
* @returns {Object} Cookie 键值对对象
|
||
*
|
||
* @example
|
||
* cookieToJson('token=abc; userid=123')
|
||
* // => { token: 'abc', userid: '123' }
|
||
*/
|
||
const cookieToJson = (cookie) => {
|
||
if (!cookie) return {};
|
||
let cookieArr = cookie.split(';');
|
||
let obj = {};
|
||
cookieArr.forEach((i) => {
|
||
let arr = i.split('=');
|
||
obj[arr[0]] = arr[1];
|
||
});
|
||
return obj;
|
||
};
|
||
|
||
/**
|
||
* KRC 歌词解码
|
||
*
|
||
* 酷狗 KRC 歌词文件的解码流程:
|
||
* 1. 跳过前 4 字节(文件头标识)
|
||
* 2. 剩余字节与固定密钥进行 XOR 异或解密
|
||
* 3. 使用 pako(zlib)解压得到明文歌词
|
||
*
|
||
* XOR 密钥(16字节循环使用):
|
||
* [64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105]
|
||
*
|
||
* @param {string | Uint8Array | Buffer} val - 加密的歌词数据
|
||
* - string: Base64 编码的歌词数据
|
||
* - Uint8Array: 原始字节数组
|
||
* - Buffer: Node.js Buffer
|
||
* @returns {string} 解码后的明文歌词,解码失败返回空字符串
|
||
*/
|
||
const decodeLyrics = (val) => {
|
||
let bytes = null;
|
||
if (val instanceof Uint8Array) bytes = val;
|
||
if (Buffer.isBuffer(val)) bytes = new Uint8Array(val);
|
||
if (typeof val === 'string') bytes = new Uint8Array(Buffer.from(val, 'base64'));
|
||
if (bytes === null) return '';
|
||
|
||
// XOR 解密密钥(16字节,循环使用)
|
||
const enKey = [64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105];
|
||
const krcBytes = bytes.slice(4); // 跳过前 4 字节文件头
|
||
const len = krcBytes.byteLength;
|
||
|
||
// XOR 异或解密
|
||
for (let index = 0; index < len; index += 1) {
|
||
krcBytes[index] = krcBytes[index] ^ enKey[index % enKey.length];
|
||
}
|
||
|
||
// zlib 解压
|
||
try {
|
||
const inflate = pako.inflate(krcBytes);
|
||
return Buffer.from(inflate).toString('utf8');
|
||
} catch {
|
||
return '';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 计算设备 MID
|
||
*
|
||
* 将输入字符串(通常是 GUID)进行 MD5 哈希,然后将哈希值作为 16 进制大整数
|
||
* 转换为 10 进制字符串表示。
|
||
*
|
||
* 算法:
|
||
* 1. 对输入字符串取 MD5 哈希(32位hex)
|
||
* 2. 将 hex 字符串视为 16 进制数
|
||
* 3. 逐位累加: sum += digit * 16^(position)
|
||
* 4. 返回十进制字符串
|
||
*
|
||
* @param {string} str - 输入字符串(通常为设备 GUID)
|
||
* @returns {string} MID 的十进制字符串表示
|
||
*
|
||
* @example
|
||
* calculateMid('550e8400-e29b-41d4-a716-446655440000')
|
||
* // => "123456789012345678901234567890"
|
||
*/
|
||
const calculateMid = (str) => {
|
||
let bigInteger = bigInt(0);
|
||
const bigInteger2 = bigInt(16); // 进制基数
|
||
const digest = CryptoJS.MD5(str).toString(CryptoJS.enc.Hex); // MD5 哈希
|
||
const length = digest.length;
|
||
for (let i = 0; i < length; i += 1) {
|
||
const charValue = bigInt(parseInt(digest.charAt(i), 16)); // 当前位的值
|
||
const powerValue = bigInteger2.pow(length - 1 - i); // 16 的幂次
|
||
bigInteger = bigInteger.add(charValue.multiply(powerValue)); // 累加
|
||
}
|
||
return bigInteger.toString();
|
||
};
|
||
|
||
/**
|
||
* 生成随机 GUID(UUID v4 格式)
|
||
*
|
||
* 格式: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||
* - 第三段以 4 开头(UUID v4 标识)
|
||
* - 第四段以 8/9/a/b 开头(UUID v4 变体标识)
|
||
*
|
||
* @returns {string} UUID v4 格式的 GUID 字符串
|
||
*
|
||
* @example
|
||
* getGuid() // => "550e8400-e29b-41d4-a716-446655440000"
|
||
*/
|
||
const getGuid = () => {
|
||
const e = () => {
|
||
return ((65536 * (1 + Math.random())) | 0).toString(16).substring(1);
|
||
};
|
||
|
||
return `${e()}${e()}-${e()}-${e()}-${e()}-${e()}${e()}${e()}`;
|
||
};
|
||
|
||
/**
|
||
* 生成 WebGL 指纹哈希值
|
||
*
|
||
* WebGL 指纹是浏览器指纹的重要组成部分,通过以下信息生成唯一哈希:
|
||
*
|
||
* 浏览器环境:
|
||
* 1. 编译顶点/片段着色器,创建 WebGL 程序
|
||
* 2. 绘制一个三角形并读取像素数据
|
||
* 3. 获取显卡厂商、渲染器名称、WebGL 版本等元数据
|
||
* 4. 使用 FNV-1a 64-bit 哈希算法对像素数据 + 元数据进行哈希
|
||
*
|
||
* Node 环境或 WebGL 不可用时:
|
||
* 生成随机 uint64 作为模拟指纹
|
||
*
|
||
* @returns {string} WebGL 指纹的十进制字符串表示
|
||
*/
|
||
const generateWebGLHash = () => {
|
||
// 浏览器环境:通过 canvas 获取真实的 WebGL 渲染器信息
|
||
if (typeof document !== 'undefined') {
|
||
try {
|
||
const c = document.createElement('canvas');
|
||
c.width = 200;
|
||
c.height = 50;
|
||
const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
|
||
if (gl) {
|
||
// --- 编译着色器(和 WASM 中的逻辑一致)---
|
||
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||
gl.shaderSource(vs, 'attribute vec4 position;void main(){gl_Position=position;}');
|
||
gl.compileShader(vs);
|
||
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||
gl.shaderSource(fs, 'void main(){gl_FragColor=vec4(1.0,1.0,1.0,1.0);}');
|
||
gl.compileShader(fs);
|
||
const prog = gl.createProgram();
|
||
gl.attachShader(prog, vs);
|
||
gl.attachShader(prog, fs);
|
||
gl.linkProgram(prog);
|
||
gl.useProgram(prog);
|
||
|
||
// --- 绘制一个三角形 ---
|
||
const buf = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1]), gl.STATIC_DRAW);
|
||
const pos = gl.getAttribLocation(prog, 'position');
|
||
gl.enableVertexAttribArray(pos);
|
||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||
gl.viewport(0, 0, 200, 50);
|
||
gl.clearColor(0, 0, 0, 1);
|
||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||
|
||
// --- 读取渲染结果并哈希 ---
|
||
const pixels = new Uint8Array(200 * 50 * 4);
|
||
gl.readPixels(0, 0, 200, 50, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
|
||
// 同时加入元数据(显卡厂商、渲染器名称、WebGL 版本)
|
||
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
||
const vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : '';
|
||
const renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
|
||
const version = gl.getParameter(gl.VERSION);
|
||
|
||
// FNV-1a 64-bit 哈希算法
|
||
let h = BigInt('14695981039346656037'); // FNV offset basis
|
||
const prime = BigInt('1099511628211'); // FNV prime
|
||
|
||
// 哈希像素数据
|
||
for (let i = 0; i < pixels.length; i++) {
|
||
h = ((h ^ BigInt(pixels[i])) * prime) & BigInt('0xFFFFFFFFFFFFFFFF');
|
||
}
|
||
// 哈希元数据
|
||
const meta = vendor + '|' + renderer + '|' + version;
|
||
for (let i = 0; i < meta.length; i++) {
|
||
h = ((h ^ BigInt(meta.charCodeAt(i))) * prime) & BigInt('0xFFFFFFFFFFFFFFFF');
|
||
}
|
||
return h.toString();
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
// Node 环境或 WebGL 不可用:生成随机 uint64 作为模拟指纹
|
||
const hi = Math.floor(Math.random() * 0xffffffff);
|
||
const lo = Math.floor(Math.random() * 0xffffffff);
|
||
return (BigInt(hi) * BigInt(0x100000000) + BigInt(lo)).toString();
|
||
};
|
||
|
||
/**
|
||
* 判断是否为UUID v4
|
||
* @param {string} str
|
||
* @returns { Boolean }
|
||
*/
|
||
const isUUIDv4 = (str) => {
|
||
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str);
|
||
}
|
||
|
||
module.exports = {
|
||
decodeLyrics,
|
||
cookieToJson,
|
||
parseCookieString,
|
||
randomString,
|
||
randomNumber,
|
||
calculateMid,
|
||
getGuid,
|
||
generateWebGLHash,
|
||
isUUIDv4
|
||
};
|