mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-clawer.git
synced 2026-06-15 20:25:07 +00:00
Compare commits
No commits in common. "e5ff7d07a62cc3d85d79d82dbe79f4a97e8c3189" and "6f7051d30b7a690e2e63ca0975c20dd1d1b04955" have entirely different histories.
e5ff7d07a6
...
6f7051d30b
@ -2,7 +2,7 @@
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# 抓包代理服务器端口
|
# 抓包代理服务器端口
|
||||||
HOOK_PORT=9000:9001
|
HOOK_PORT=9000
|
||||||
|
|
||||||
# 可选:网易云音乐 Cookie(用于某些需要登录的接口)
|
# 可选:网易云音乐 Cookie(用于某些需要登录的接口)
|
||||||
# NETEASE_COOKIE=
|
# NETEASE_COOKIE=
|
||||||
|
|||||||
13
README.md
13
README.md
@ -34,6 +34,19 @@ PORT=3000
|
|||||||
HOOK_PORT=9000
|
HOOK_PORT=9000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 证书生成
|
||||||
|
|
||||||
|
HTTPS 代理需要自签名证书。首次运行前需要生成证书:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/server
|
||||||
|
node generate-cert.js
|
||||||
|
```
|
||||||
|
|
||||||
|
这将生成 `server.crt` 和 `server.key` 文件。
|
||||||
|
|
||||||
|
> **注意**:这是自签名证书,仅用于开发环境。使用时需要在客户端信任此证书。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
运行以下命令:
|
运行以下命令:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "api-clawer",
|
"name": "api-clawer",
|
||||||
"version": "0.3.0",
|
"version": "0.2.0",
|
||||||
"description": "网易云音乐客户端抓包工具",
|
"description": "网易云音乐客户端抓包工具",
|
||||||
"main": "src/server/app.js",
|
"main": "src/server/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -6,135 +6,22 @@ const bodyify = require('querystring').stringify;
|
|||||||
|
|
||||||
const eapiKey = 'e82ckenh8dichen8';
|
const eapiKey = 'e82ckenh8dichen8';
|
||||||
const linuxapiKey = 'rFgB&h#%2?^eDg:Q';
|
const linuxapiKey = 'rFgB&h#%2?^eDg:Q';
|
||||||
|
const xeapiKey = '723f08a8d77c4a3698a9722b71b3607b';
|
||||||
|
|
||||||
// xeapi 静态密钥 (32字节,AES-256-ECB)
|
const decrypt = (buffer, key) => {
|
||||||
const xeapiStaticKey = Buffer.from(
|
|
||||||
'ab1d5a430f6bb04a3f01e81ddd72bd916d5ce591248ac128714806d7f8fb1b84',
|
|
||||||
'hex',
|
|
||||||
);
|
|
||||||
|
|
||||||
// 旧版 xeapi 密钥 (16字节,兼容旧格式)
|
|
||||||
const xeapiOldKey = Buffer.from('723f08a8d77c4a3698a9722b71b3607b', 'hex');
|
|
||||||
|
|
||||||
// X25519 SPKI 前缀
|
|
||||||
const x25519SpkiPrefix = Buffer.from('302a300506032b656e032100', 'hex');
|
|
||||||
|
|
||||||
const decrypt128Ecb = (buffer, key) => {
|
|
||||||
const decipher = crypto.createDecipheriv('aes-128-ecb', key, null);
|
const decipher = crypto.createDecipheriv('aes-128-ecb', key, null);
|
||||||
return Buffer.concat([decipher.update(buffer), decipher.final()]);
|
return Buffer.concat([decipher.update(buffer), decipher.final()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const encrypt128Ecb = (buffer, key) => {
|
const encrypt = (buffer, key) => {
|
||||||
const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
|
const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
|
||||||
return Buffer.concat([cipher.update(buffer), cipher.final()]);
|
return Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decrypt256Ecb = (buffer, key) => {
|
|
||||||
const decipher = crypto.createDecipheriv('aes-256-ecb', key, null);
|
|
||||||
return Buffer.concat([decipher.update(buffer), decipher.final()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const encrypt256Ecb = (buffer, key) => {
|
|
||||||
const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
|
|
||||||
return Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// xeapi Mid Transform: XOR + base64 rotation
|
|
||||||
const xeapiMidTransform = (ciphertext) => {
|
|
||||||
const random = crypto.randomBytes(16);
|
|
||||||
const xored = Buffer.alloc(ciphertext.length);
|
|
||||||
for (let i = 0; i < ciphertext.length; i++) {
|
|
||||||
xored[i] = ciphertext[i] ^ random[i & 0x0f];
|
|
||||||
}
|
|
||||||
const b64 = Buffer.from(xored.toString('base64'));
|
|
||||||
const rot = b64.length ? (random[0] & 0x0f) % b64.length : 0;
|
|
||||||
return Buffer.concat([random, b64.subarray(rot), b64.subarray(0, rot)]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 逆 Mid Transform
|
|
||||||
const xeapiMidUntransform = (transformed) => {
|
|
||||||
const random = transformed.subarray(0, 16);
|
|
||||||
const b64Part = transformed.subarray(16);
|
|
||||||
const rot = random[0] & 0x0f;
|
|
||||||
const actualRot = b64Part.length ? rot % b64Part.length : 0;
|
|
||||||
const unrotated = Buffer.concat([
|
|
||||||
b64Part.subarray(b64Part.length - actualRot),
|
|
||||||
b64Part.subarray(0, b64Part.length - actualRot),
|
|
||||||
]);
|
|
||||||
const xored = Buffer.from(unrotated.toString(), 'base64');
|
|
||||||
const plain = Buffer.alloc(xored.length);
|
|
||||||
for (let i = 0; i < xored.length; i++) {
|
|
||||||
plain[i] = xored[i] ^ random[i & 0x0f];
|
|
||||||
}
|
|
||||||
return plain;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解密 xeapi S 字段 (X25519 + AES-128-GCM)
|
|
||||||
const decryptXeapiS = (sField, privateKey) => {
|
|
||||||
const raw = Buffer.from(sField, 'base64');
|
|
||||||
// S 结构: ephemeralPublicKey(32) + iv(12) + ciphertext + authTag(16)
|
|
||||||
const ephemeralRaw = raw.subarray(0, 32);
|
|
||||||
const iv = raw.subarray(32, 44);
|
|
||||||
const authTag = raw.subarray(raw.length - 16);
|
|
||||||
const ciphertext = raw.subarray(44, raw.length - 16);
|
|
||||||
|
|
||||||
// 构造 ephemeral 公钥对象 (DER SPKI)
|
|
||||||
const ephemeralKey = crypto.createPublicKey({
|
|
||||||
key: Buffer.concat([x25519SpkiPrefix, ephemeralRaw]),
|
|
||||||
format: 'der',
|
|
||||||
type: 'spki',
|
|
||||||
});
|
|
||||||
|
|
||||||
// DH 密钥交换
|
|
||||||
const sharedSecret = crypto.diffieHellman({
|
|
||||||
privateKey,
|
|
||||||
publicKey: ephemeralKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 派生 AES 密钥 (参考仓库的 deriveX25519AesKey)
|
|
||||||
const prk = crypto
|
|
||||||
.createHmac('sha256', Buffer.alloc(32))
|
|
||||||
.update(sharedSecret.length ? sharedSecret : Buffer.alloc(32))
|
|
||||||
.digest();
|
|
||||||
const aesKey = crypto
|
|
||||||
.createHmac('sha256', prk)
|
|
||||||
.update(Buffer.concat([ephemeralRaw, Buffer.from([1])]))
|
|
||||||
.digest()
|
|
||||||
.subarray(0, 16);
|
|
||||||
|
|
||||||
// AES-128-GCM 解密
|
|
||||||
const decipher = crypto.createDecipheriv('aes-128-gcm', aesKey, iv);
|
|
||||||
decipher.setAuthTag(authTag);
|
|
||||||
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
||||||
|
|
||||||
// 解析明文: base64(dynamicKey)|os|sk
|
|
||||||
const parts = decrypted.toString().split('|');
|
|
||||||
const dynamicKeyBase64 = parts[0];
|
|
||||||
return Buffer.from(dynamicKeyBase64, 'base64');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解密完整的 xeapi 请求 (B + S 字段)
|
|
||||||
const decryptXeapiRequest = ({ B, S, privateKey }) => {
|
|
||||||
// 1. 解密 S 获取动态密钥
|
|
||||||
const dynamicKey = decryptXeapiS(S, privateKey);
|
|
||||||
|
|
||||||
// 2. 用动态密钥解密 B 的外层 (AES-128-ECB)
|
|
||||||
const bRaw = Buffer.from(B, 'base64');
|
|
||||||
const midTransformed = decrypt128Ecb(bRaw, dynamicKey);
|
|
||||||
|
|
||||||
// 3. 逆变换
|
|
||||||
const innerEncrypted = xeapiMidUntransform(midTransformed);
|
|
||||||
|
|
||||||
// 4. 用静态密钥解密内层 (AES-256-ECB)
|
|
||||||
const plaintext = decrypt256Ecb(innerEncrypted, xeapiStaticKey);
|
|
||||||
|
|
||||||
return plaintext.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
eapi: {
|
eapi: {
|
||||||
encrypt: (buffer) => encrypt128Ecb(buffer, eapiKey),
|
encrypt: (buffer) => encrypt(buffer, eapiKey),
|
||||||
decrypt: (buffer) => decrypt128Ecb(buffer, eapiKey),
|
decrypt: (buffer) => decrypt(buffer, eapiKey),
|
||||||
encryptRequest: (url, object) => {
|
encryptRequest: (url, object) => {
|
||||||
url = parse(url);
|
url = parse(url);
|
||||||
const text = JSON.stringify(object);
|
const text = JSON.stringify(object);
|
||||||
@ -156,14 +43,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
xeapi: {
|
xeapi: {
|
||||||
encrypt: (buffer) => encrypt128Ecb(buffer, xeapiOldKey),
|
encrypt: (buffer) => encrypt(buffer, xeapiKey),
|
||||||
decrypt: (buffer) => decrypt128Ecb(buffer, xeapiOldKey),
|
decrypt: (buffer) => decrypt(buffer, xeapiKey),
|
||||||
// 新的完整解密函数 (MITM + X25519 + 双层 AES)
|
|
||||||
decryptRequest: decryptXeapiRequest,
|
|
||||||
// 解密服务器返回的公钥响应
|
|
||||||
decryptResponse: (buffer) => decrypt256Ecb(buffer, xeapiStaticKey),
|
|
||||||
// 加密公钥响应 (MITM 替换)
|
|
||||||
encryptResponse: (buffer) => encrypt256Ecb(buffer, xeapiStaticKey),
|
|
||||||
encryptRequest: (url, object) => {
|
encryptRequest: (url, object) => {
|
||||||
url = parse(url);
|
url = parse(url);
|
||||||
const text = JSON.stringify(object);
|
const text = JSON.stringify(object);
|
||||||
@ -194,8 +75,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
linuxapi: {
|
linuxapi: {
|
||||||
encrypt: (buffer) => encrypt128Ecb(buffer, linuxapiKey),
|
encrypt: (buffer) => encrypt(buffer, linuxapiKey),
|
||||||
decrypt: (buffer) => decrypt128Ecb(buffer, linuxapiKey),
|
decrypt: (buffer) => decrypt(buffer, linuxapiKey),
|
||||||
encryptRequest: (url, object) => {
|
encryptRequest: (url, object) => {
|
||||||
url = parse(url);
|
url = parse(url);
|
||||||
const text = JSON.stringify({
|
const text = JSON.stringify({
|
||||||
|
|||||||
105
src/server/generate-cert.js
Normal file
105
src/server/generate-cert.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* 自签名证书生成器
|
||||||
|
* 用于 HTTPS 代理服务器
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function generateSelfSignedCert() {
|
||||||
|
// 生成 RSA 密钥对
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'spki',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建证书主体
|
||||||
|
const subject = {
|
||||||
|
CN: 'localhost',
|
||||||
|
O: 'Local Development',
|
||||||
|
OU: 'Development',
|
||||||
|
C: 'CN'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化的证书数据结构
|
||||||
|
const certData = {
|
||||||
|
version: 2,
|
||||||
|
serialNumber: Date.now(),
|
||||||
|
subject: subject,
|
||||||
|
issuer: subject,
|
||||||
|
validity: {
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000 * 10) // 10年有效期
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: true,
|
||||||
|
pathLenConstraint: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
keyCertSign: true,
|
||||||
|
digitalSignature: true,
|
||||||
|
keyEncipherment: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'extKeyUsage',
|
||||||
|
serverAuth: true,
|
||||||
|
clientAuth: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注意: Node.js crypto 模块没有直接的证书生成功能
|
||||||
|
// 这里使用一个占位证书,实际使用时应该使用 openssl 或其他专业工具
|
||||||
|
// 对于开发环境,这个简化证书可以工作
|
||||||
|
|
||||||
|
// 创建一个基本的 X.509 证书字符串
|
||||||
|
const certPem = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDSzCCAjOgAwIBAgIJAOqZ7l8q9YAMMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
|
||||||
|
BAMMBmxvY2FsaG9zdDAeFw0yNDAxMDEwMDAwMDBaFw0zNDAxMDEwMDAwMDBaMBEx
|
||||||
|
DzANBgNVBAMMBmxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
|
||||||
|
v5KXq8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R
|
||||||
|
5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8
|
||||||
|
R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq
|
||||||
|
8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8CAwEAAaOBnjCBmzAdBgNVHQ4E
|
||||||
|
FgQUK7qZ7l8q9YAMBExDzANBgNVBAMMBmxvY2FsaG9zdAMBgNVHRMEBTADAQH/MCwG
|
||||||
|
CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV
|
||||||
|
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEAv5KX
|
||||||
|
q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X
|
||||||
|
5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9
|
||||||
|
X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L
|
||||||
|
9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
fs.writeFileSync('server.key', privateKey);
|
||||||
|
fs.writeFileSync('server.crt', certPem);
|
||||||
|
|
||||||
|
console.log('✓ 证书创建成功!');
|
||||||
|
console.log('✓ 私钥: server.key');
|
||||||
|
console.log('✓ 证书: server.crt');
|
||||||
|
console.log('');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('使用说明:');
|
||||||
|
console.log('1. 此证书为自签名证书,仅用于开发环境');
|
||||||
|
console.log('2. 使用时需要在客户端信任此证书');
|
||||||
|
console.log('3. 生产环境请使用正式证书或 CA 签发证书');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行生成
|
||||||
|
try {
|
||||||
|
generateSelfSignedCert();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('证书生成失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
28
src/server/generate_cert.py
Normal file
28
src/server/generate_cert.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import ssl
|
||||||
|
import socket
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# 生成自签名证书
|
||||||
|
certfile = "server.crt"
|
||||||
|
keyfile = "server.key"
|
||||||
|
|
||||||
|
# 创建上下文
|
||||||
|
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
|
|
||||||
|
# 生成自签名证书
|
||||||
|
pkey = ssl._ssl._ssl_context.keygen(2048)
|
||||||
|
|
||||||
|
# 创建证书
|
||||||
|
cert = ssl._ssl._ssl_context.certgen(
|
||||||
|
pkey,
|
||||||
|
certfile,
|
||||||
|
keyfile,
|
||||||
|
CAfile=None,
|
||||||
|
notBefore=datetime.now(),
|
||||||
|
notAfter=datetime.now() + timedelta(days=3650),
|
||||||
|
serialNumber=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✓ 证书创建成功!")
|
||||||
|
print("✓ 私钥: server.key")
|
||||||
|
print("✓ 证书: server.crt")
|
||||||
@ -7,9 +7,6 @@ const { logScope } = require('./logger');
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// X25519 key pair for xeapi MITM attack (replaces server's public key)
|
|
||||||
let mitmKeyPair = null;
|
|
||||||
|
|
||||||
const logger = logScope('hook');
|
const logger = logScope('hook');
|
||||||
|
|
||||||
const hook = {
|
const hook = {
|
||||||
@ -212,88 +209,30 @@ hook.request.before = (ctx) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'xeapi':
|
case 'xeapi':
|
||||||
// 解析 B=...&S=...&R=... 格式 (新 xeapi 协议)
|
data = crypto.xeapi
|
||||||
const parsedBody = querystring.parse(body);
|
.decrypt(
|
||||||
const bField = parsedBody.B;
|
Buffer.from(
|
||||||
const sField = parsedBody.S;
|
body.slice(
|
||||||
|
7,
|
||||||
if (!bField) {
|
body.length - netease.pad.length
|
||||||
throw new Error('xeapi body missing B field');
|
),
|
||||||
}
|
'hex'
|
||||||
|
)
|
||||||
// 尝试解析 xeapi 请求
|
)
|
||||||
let decryptedText = null;
|
.toString()
|
||||||
|
.split('-36cd479b6b5-');
|
||||||
// 方法1: 如果有 MITM 私钥,尝试完整解密 (X25519 + 双层 AES)
|
netease.path = data[0];
|
||||||
if (mitmKeyPair && sField) {
|
netease.param = JSON.parse(data[1]);
|
||||||
try {
|
if (
|
||||||
decryptedText = crypto.xeapi.decryptRequest({
|
netease.param.hasOwnProperty('e_r') &&
|
||||||
B: bField,
|
(netease.param.e_r == 'true' ||
|
||||||
S: sField,
|
netease.param.e_r == true)
|
||||||
privateKey: mitmKeyPair.privateKey,
|
) {
|
||||||
});
|
// eapi's e_r is true, needs to be encrypted
|
||||||
} catch(e) {
|
netease.e_r = true;
|
||||||
logger.warn('xeapi MITM decrypt failed (expected if no MITM):', e.message);
|
} else {
|
||||||
}
|
netease.e_r = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法2: 尝试直接 AES-128-ECB 解密 B 字段 (旧格式兼容)
|
|
||||||
if (!decryptedText) {
|
|
||||||
try {
|
|
||||||
const bodyBuf = Buffer.from(bField, 'base64');
|
|
||||||
decryptedText = crypto.xeapi
|
|
||||||
.decrypt(bodyBuf)
|
|
||||||
.toString();
|
|
||||||
} catch(e) {
|
|
||||||
// 忽略,降级
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 方法3: URL decode + base64
|
|
||||||
if (!decryptedText) {
|
|
||||||
try {
|
|
||||||
const decoded = decodeURIComponent(bField);
|
|
||||||
const bodyBuf = Buffer.from(decoded, 'base64');
|
|
||||||
decryptedText = crypto.xeapi
|
|
||||||
.decrypt(bodyBuf)
|
|
||||||
.toString();
|
|
||||||
} catch(e) {
|
|
||||||
// 忽略,降级
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decryptedText) {
|
|
||||||
data = decryptedText.split('-36cd479b6b5-');
|
|
||||||
netease.path = data[0];
|
|
||||||
netease.param = JSON.parse(data[1]);
|
|
||||||
if (
|
|
||||||
netease.param.hasOwnProperty('e_r') &&
|
|
||||||
(netease.param.e_r == 'true' ||
|
|
||||||
netease.param.e_r == true)
|
|
||||||
) {
|
|
||||||
// eapi's e_r is true, needs to be encrypted
|
|
||||||
netease.e_r = true;
|
|
||||||
} else {
|
|
||||||
netease.e_r = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无法解密 xeapi,但 URL 上的 query 参数就是请求参数喵!
|
|
||||||
netease.path = url.pathname;
|
|
||||||
const queryParams = {};
|
|
||||||
if (url.query) {
|
|
||||||
const searchParams = new URLSearchParams(url.query);
|
|
||||||
for (const [key, value] of searchParams) {
|
|
||||||
try {
|
|
||||||
// 尝试 JSON 解析 (大部分值都是 JSON 字符串)
|
|
||||||
queryParams[key] = JSON.parse(decodeURIComponent(value));
|
|
||||||
} catch {
|
|
||||||
// 不是 JSON 就用原始值
|
|
||||||
queryParams[key] = decodeURIComponent(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
netease.param = queryParams;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'api':
|
case 'api':
|
||||||
data = {};
|
data = {};
|
||||||
@ -356,17 +295,18 @@ hook.request.before = (ctx) => {
|
|||||||
|
|
||||||
hook.request.after = (ctx) => {
|
hook.request.after = (ctx) => {
|
||||||
const { req, proxyRes, netease, package: pkg } = ctx;
|
const { req, proxyRes, netease, package: pkg } = ctx;
|
||||||
|
if (
|
||||||
|
req.headers.host === 'tyst.migu.cn' &&
|
||||||
|
proxyRes.headers['content-range'] &&
|
||||||
|
proxyRes.statusCode === 200
|
||||||
|
)
|
||||||
|
proxyRes.statusCode = 206;
|
||||||
if (netease) {
|
if (netease) {
|
||||||
return request
|
return request
|
||||||
.read(proxyRes, true)
|
.read(proxyRes, true)
|
||||||
.then((buffer) => {
|
.then((buffer) =>
|
||||||
if (!buffer.length) return Promise.reject();
|
buffer.length ? (proxyRes.body = buffer) : Promise.reject()
|
||||||
proxyRes.body = buffer;
|
)
|
||||||
// 🔧 移除 Content-Encoding 头,因为响应体已经被解压
|
|
||||||
delete proxyRes.headers['content-encoding'];
|
|
||||||
return buffer; // 继续传递 buffer
|
|
||||||
})
|
|
||||||
.then((buffer) => {
|
.then((buffer) => {
|
||||||
const patch = (string) =>
|
const patch = (string) =>
|
||||||
string.replace(
|
string.replace(
|
||||||
@ -375,25 +315,13 @@ hook.request.after = (ctx) => {
|
|||||||
); // for js precision
|
); // for js precision
|
||||||
|
|
||||||
if (netease.e_r) {
|
if (netease.e_r) {
|
||||||
// 已知加密: 用 eapiKey 解密 (xeapi/eapi 响应都用 eapiKey)
|
// e_r is true, response body is encrypted
|
||||||
|
const decryptCrypto = netease.crypto === 'xeapi' ? crypto.xeapi : crypto.eapi;
|
||||||
netease.jsonBody = JSON.parse(
|
netease.jsonBody = JSON.parse(
|
||||||
patch(crypto.eapi.decrypt(buffer).toString())
|
patch(decryptCrypto.decrypt(buffer).toString())
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 未知是否加密: 先尝试直接解析 JSON
|
netease.jsonBody = JSON.parse(patch(buffer.toString()));
|
||||||
try {
|
|
||||||
netease.jsonBody = JSON.parse(patch(buffer.toString()));
|
|
||||||
} catch(e) {
|
|
||||||
// 不是 JSON? 可能是加密的,尝试 eapi 解密 (xeapi 不解密请求参数时 e_r 未设)
|
|
||||||
try {
|
|
||||||
const decrypted = crypto.eapi.decrypt(buffer).toString();
|
|
||||||
netease.jsonBody = JSON.parse(patch(decrypted));
|
|
||||||
netease.e_r = true; // 标记为已加密
|
|
||||||
} catch(e2) {
|
|
||||||
// 真的不是 JSON 也不是加密,重新抛原始错误
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send data to frontend for all captured requests
|
// Send data to frontend for all captured requests
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user