fix: 修复无法抓包

This commit is contained in:
ElyPrism 2026-03-13 21:54:38 +08:00
parent d745ec710f
commit 8c01d3fd5a
No known key found for this signature in database
15 changed files with 596 additions and 1296 deletions

14
ca.crt Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICGjCCAaCgAwIBAgIUXBIOR4IUZCnxKkT2yr8ZdS17y0wwCgYIKoZIzj0EAwMw
RDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v
dCBDQTEPMA0GA1UECgwGbm9ib2R5MB4XDTIyMDQwNDEyMDk1MloXDTI3MDQwMzEy
MDk1MlowRDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVz
aWMgUm9vdCBDQTEPMA0GA1UECgwGbm9ib2R5MHYwEAYHKoZIzj0CAQYFK4EEACID
YgAEZDE7xoQc0jylGpqesuWZKdSBAxD1kydeYRfJr296Pc2v32P1q2VQFBC8XY7d
nzfKN5zLEc9csWRsvmpwXVlXq2wJW/sJENjErQ8yeaw/4alxDUWHwwgiu/ienBl/
iIMWo1MwUTAdBgNVHQ4EFgQUZb6EKjjHbgAgwhhtSNHg1GWfPHUwHwYDVR0jBBgw
FoAUZb6EKjjHbgAgwhhtSNHg1GWfPHUwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjO
PQQDAwNoADBlAjEAuzn+fWQpEyFIUuQZC8TcfteCEvn2KettQT2ZOfQxHB3sIWhE
BC9cebRgdc74Lvv6AjBLQyq2SfDzrDE0PexyJxy8IQHAncP0+1QnAXVU5GO6YJYg
1gYZwdmbNOhh81S21l8=
-----END CERTIFICATE-----

16
server.crt Normal file
View File

@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICgjCCAgigAwIBAgIUafzDKFqhg8XS5YogLkI9oGsngkUwCgYIKoZIzj0EAwMw
RDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v
dCBDQTEPMA0GA1UECgwGbm9ib2R5MB4XDTI1MDcyNDA4NDY1NloXDTI2MDcyNDA4
NDY1NlowezELMAkGA1UEBhMCQ04xETAPBgNVBAcMCEhhbmd6aG91MSwwKgYDVQQK
DCNOZXRFYXNlIChIYW5nemhvdSkgTmV0d29yayBDby4sIEx0ZDERMA8GA1UECwwI
SVQgRGVwdC4xGDAWBgNVBAMMDyoubXVzaWMuMTYzLmNvbTB2MBAGByqGSM49AgEG
BSuBBAAiA2IABACkvqL3b68Pv11nQVwm/9BNWGuh1fBKlqYZqnyr9Gy1xEQtf3we
WlwgVmX9rqu9nNEiAbbxIt9aGliHurKkE22+FiOKC4vduYlyyIXKlCAFXigHKr/k
6uKgc2jPfeYoC6OBgzCBgDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAg
gg1tdXNpYy4xNjMuY29tgg8qLm11c2ljLjE2My5jb20wHQYDVR0OBBYEFEeHCj4z
PzDre8No6wsWbrpmQ8hPMB8GA1UdIwQYMBaAFGW+hCo4x24AIMIYbUjR4NRlnzx1
MAoGCCqGSM49BAMDA2gAMGUCMQDD0cOUV43GSLH8bbbkw4KD6kVoknm6z6sjE5vk
ZNmgjqduSn6XzCNiiwwhRrsmtVkCMCX8uRVIIG20mdcK2YSwlSgw5mHTJsNFRnfn
x8ft2l8Tk2yZcqFSsvuVKvBwZh7bWA==
-----END CERTIFICATE-----

9
server.key Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDDaWQTho9lbjl27ddVt9pC4req+U8iFhdMw0/RzWePrlF55fvmwRiff
GepKeBDdkx6gBwYFK4EEACKhZANiAAQApL6i92+vD79dZ0FcJv/QTVhrodXwSpam
Gap8q/RstcRELX98HlpcIFZl/a6rvZzRIgG28SLfWhpYh7qypBNtvhYjiguL3bmJ
csiFypQgBV4oByq/5OrioHNoz33mKAs=
-----END EC PRIVATE KEY-----

View File

@ -6,6 +6,7 @@ const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
let capturedData = []; let capturedData = [];
let clients = [];
app.use(express.json()); app.use(express.json());
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
@ -14,6 +15,10 @@ app.post('/api/capture', (req, res) => {
const data = req.body; const data = req.body;
capturedData.push(data); capturedData.push(data);
console.log('Captured data:', data.path); console.log('Captured data:', data.path);
// 通知所有 SSE 客户端有新数据
broadcastData();
res.status(200).send('OK'); res.status(200).send('OK');
}); });
@ -21,6 +26,34 @@ app.get('/api/data', (req, res) => {
res.json(capturedData); res.json(capturedData);
}); });
// SSE 端点
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 立即发送当前数据
res.write(`data: ${JSON.stringify(capturedData)}\n\n`);
// 添加到客户端列表
clients.push(res);
req.on('close', () => {
clients = clients.filter(client => client !== res);
});
});
function broadcastData() {
clients.forEach(client => {
try {
client.write(`data: ${JSON.stringify(capturedData)}\n\n`);
} catch (e) {
// 连接已断开,移除客户端
clients = clients.filter(c => c !== client);
}
});
}
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Frontend server running at http://localhost:${PORT}`); console.log(`Frontend server running at http://localhost:${PORT}`);
}); });

View File

@ -17,93 +17,340 @@
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { height: 100%; } html, body { height: 100%; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
.container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
@media (max-width: 480px) { .main-container {
.container { margin: 20px auto; padding: 0 16px; } display: flex;
header.site-header h1 { font-size: 22px; } height: 100vh;
.block { padding: 16px; } overflow: hidden;
} }
header.site-header { margin-bottom: 24px; }
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; color: var(--accent); } .left-panel {
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); } width: 33%;
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; } min-width: 300px;
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } max-width: 450px;
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; } border-right: 1px solid var(--border);
.kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; } background: var(--panel);
.kvs div:first-child { color: var(--muted); flex-shrink: 0; } display: flex;
.kvs div:last-child { word-break: break-all; overflow-wrap: anywhere; min-width: 0; overflow: hidden; } flex-direction: column;
@media (max-width: 480px) { overflow: hidden;
.kvs { grid-template-columns: 1fr; gap: 4px 12px; }
.kvs div:first-child { font-weight: 500; }
} }
.capture-item { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.capture-header { background: #fafafa; padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; } .right-panel {
.capture-path { font-weight: 600; color: var(--accent); } flex: 1;
.capture-time { font-size: 12px; color: var(--muted); } background: var(--bg);
.capture-body { padding: 16px; } overflow-y: auto;
.capture-section { margin-bottom: 12px; } padding: 20px;
.capture-section:last-child { margin-bottom: 0; } }
.capture-section strong { display: block; margin-bottom: 6px; font-size: 14px; color: var(--muted); }
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-size: 13px; } .left-content {
code { font-family: 'Courier New', monospace; font-size: 13px; } flex: 1;
@media (max-width: 480px) { overflow-y: auto;
code { font-size: 12px; } padding: 16px;
}
.header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
background: #fafafa;
flex-shrink: 0;
}
.header h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--accent);
}
.badge {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 10px;
font-size: 11px;
color: var(--muted);
}
.sub {
font-size: 13px;
color: var(--muted);
margin: 0;
}
.block {
background: #fafafa;
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.block h2 {
font-size: 14px;
font-weight: 600;
margin: 0 0 10px 0;
}
.kvs {
display: grid;
grid-template-columns: 80px 1fr;
gap: 6px 10px;
align-items: start;
font-size: 13px;
}
.kvs div:first-child {
color: var(--muted);
font-weight: 500;
}
.kvs div:last-child {
word-break: break-all;
overflow-wrap: anywhere;
}
.status {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-connected {
background: #e6f4ea;
color: #1e8e3e;
}
.status-waiting {
background: #fef7e0;
color: #f9ab00;
}
.capture-list {
margin-top: 12px;
}
.capture-list h2 {
font-size: 14px;
font-weight: 600;
margin: 0 0 10px 0;
}
.capture-list-item {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
background: var(--panel);
}
.capture-list-item:hover {
border-color: var(--accent);
background: #fff5f5;
}
.capture-list-item.active {
border-color: var(--accent);
background: #fff5f5;
box-shadow: 0 2px 4px rgba(194, 12, 12, 0.1);
}
.capture-list-path {
font-weight: 600;
color: var(--accent);
font-size: 13px;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.capture-list-time {
font-size: 12px;
color: var(--muted);
}
.empty-list {
text-align: center;
padding: 30px 20px;
color: var(--muted);
font-size: 13px;
}
.empty-list .icon {
font-size: 36px;
margin-bottom: 8px;
opacity: 0.5;
}
.detail-panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
}
.detail-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.detail-header h2 {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--accent);
}
.detail-time {
font-size: 13px;
color: var(--muted);
}
.detail-section {
margin-bottom: 20px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section strong {
display: block;
margin-bottom: 10px;
font-size: 14px;
color: var(--muted);
font-weight: 600;
}
.copy-btn {
display: inline-block;
padding: 6px 12px;
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
margin-left: 8px;
}
.copy-btn:hover {
background: #a00a0a;
}
.copy-btn:active {
transform: scale(0.98);
}
pre {
margin: 0;
background: #f9f9f9;
border: 1px solid var(--border);
border-radius: 6px;
padding: 14px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
font-size: 13px;
line-height: 1.5;
}
code {
font-family: 'Courier New', monospace;
font-size: 13px;
}
.no-selection {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--muted);
font-size: 14px;
}
.no-selection .icon {
font-size: 48px;
margin-right: 16px;
opacity: 0.5;
}
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
background: #333;
color: white;
border-radius: 6px;
font-size: 14px;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s;
z-index: 1000;
}
.toast.show {
opacity: 1;
transform: translateY(0);
} }
.status { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
.status-connected { background: #e6f4ea; color: #1e8e3e; }
.status-waiting { background: #fef7e0; color: #f9ab00; }
.no-data { text-align: center; padding: 40px 20px; color: var(--muted); }
.no-data .icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
footer.site-footer a:hover { color: var(--accent); }
</style> </style>
</head> </head>
<body> <body>
<main class="container"> <div class="main-container">
<header class="site-header"> <div class="left-panel">
<h1>网易云音乐抓包工具 <span class="badge">v0.0.1</span></h1> <div class="header">
<p class="sub">🔍 简易网易云音乐客户端抓包工具,适用于贡献 NeteaseCloudMusicApi 项目</p> <h1>网易云音乐抓包工具 <span class="badge">v0.0.1</span></h1>
</header> <p class="sub">🔍 简易网易云音乐客户端抓包工具</p>
<section class="block">
<h2>状态</h2>
<div class="kvs">
<div>连接状态</div><div><span id="status" class="status status-waiting">等待连接...</span></div>
<div>Base URL</div><div id="base-url"></div>
<div>当前页</div><div id="current-url"></div>
<div>抓包数量</div><div id="capture-count">0</div>
</div> </div>
</section> <div class="left-content">
<section class="block">
<h2>状态</h2>
<div class="kvs">
<div>连接状态</div><div><span id="status" class="status status-waiting">等待连接...</span></div>
<div>Base URL</div><div id="base-url"></div>
<div>抓包数量</div><div id="capture-count">0</div>
</div>
</section>
<section class="block"> <section class="block">
<h2>使用说明</h2> <h2>使用说明</h2>
<div class="kvs"> <div class="kvs">
<div>1. 启动服务</div><div>运行 <code>npm run dev</code> 启动抓包和前端服务</div> <div>1. 启动服务</div><div>运行 <code>npm run dev</code></div>
<div>2. 设置代理</div><div>将网易云音乐客户端代理设置为 <code>http://localhost:9000</code></div> <div>2. 设置代理</div><div>客户端代理: <code>localhost:9000</code></div>
<div>3. 开始抓包</div><div>在网易云音乐客户端中进行操作,抓包数据将实时显示</div> <div>3. 开始抓包</div><div>客户端中操作,数据实时显示</div>
</div> </div>
</section> </section>
<section class="block"> <div class="capture-list">
<h2>抓包数据</h2> <h2>抓包列表</h2>
<div id="data"> <div id="capture-list"></div>
<div class="no-data">
<div class="icon">📡</div>
<p>暂无抓包数据</p>
<p style="font-size: 12px; margin-top: 8px;">请设置网易云音乐客户端代理并开始使用</p>
</div> </div>
</div> </div>
</section> </div>
<footer class="site-footer"> <div class="right-panel">
<a href="https://github.com/NeteaseCloudMusicApiEnhanced/api-clawer" target="_blank">GitHub</a> <div id="detail-panel" class="detail-panel">
</footer> <div class="no-selection">
</main> <span class="icon">📡</span>
<span>请从左侧选择一个抓包记录查看详情</span>
</div>
</div>
</div>
</div>
<div id="toast" class="toast">已复制到剪贴板</div>
<script> <script>
const DATA_ENDPOINT = '/api/data'; let capturedData = [];
let lastDataCount = 0; let selectedIndex = -1;
function updateStatus() { function updateStatus() {
const statusEl = document.getElementById('status'); const statusEl = document.getElementById('status');
@ -111,59 +358,108 @@
statusEl.textContent = '已连接'; statusEl.textContent = '已连接';
} }
function displayData(data) { function renderCaptureList() {
const container = document.getElementById('data'); const container = document.getElementById('capture-list');
const countEl = document.getElementById('capture-count'); const countEl = document.getElementById('capture-count');
if (!data || data.length === 0) { if (!capturedData || capturedData.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="no-data"> <div class="empty-list">
<div class="icon">📡</div> <div class="icon">📡</div>
<p>暂无抓包数据</p> <p>暂无抓包数据</p>
<p style="font-size: 12px; margin-top: 8px;">请设置网易云音乐客户端代理并开始使用</p> <p style="font-size: 12px; margin-top: 6px; opacity: 0.7;">请设置网易云音乐客户端代理并开始使用</p>
</div> </div>
`; `;
countEl.textContent = '0'; countEl.textContent = '0';
return; return;
} }
// 如果是新数据,更新显示 countEl.textContent = capturedData.length;
if (data.length !== lastDataCount) {
lastDataCount = data.length;
countEl.textContent = data.length;
container.innerHTML = data.map(item => ` container.innerHTML = capturedData.map((item, index) => `
<div class="capture-item"> <div class="capture-list-item ${index === selectedIndex ? 'active' : ''}"
<div class="capture-header"> onclick="selectCapture(${index})">
<span class="capture-path">${escapeHtml(item.path)}</span> <div class="capture-list-path">${escapeHtml(item.path)}</div>
<span class="capture-time">${formatTime(item.timestamp)}</span> <div class="capture-list-time">${formatTime(item.timestamp)}</div>
</div> </div>
<div class="capture-body"> `).join('');
<div class="capture-section">
<strong>请求参数:</strong>
<pre><code>${formatJson(item.param)}</code></pre>
</div>
${item.response ? `
<div class="capture-section">
<strong>响应数据:</strong>
<pre><code>${formatJson(item.response)}</code></pre>
</div>
` : ''}
</div>
</div>
`).join('');
}
} }
async function fetchData() { function selectCapture(index) {
try { selectedIndex = index;
const response = await fetch(DATA_ENDPOINT); const item = capturedData[index];
const data = await response.json(); const detailPanel = document.getElementById('detail-panel');
displayData(data);
updateStatus(); if (!item) {
} catch (error) { detailPanel.innerHTML = `
console.error('Error fetching data:', error); <div class="no-selection">
<span class="icon">📡</span>
<span>请从左侧选择一个抓包记录查看详情</span>
</div>
`;
renderCaptureList();
return;
} }
const filteredParam = filterParam(item.param);
detailPanel.innerHTML = `
<div class="detail-header">
<h2>${escapeHtml(item.path)}</h2>
<div class="detail-time">${formatTime(item.timestamp)}</div>
</div>
<div class="detail-section">
<strong>
请求参数
<button class="copy-btn" onclick="copyParam(${index})">复制参数</button>
</strong>
<pre><code>${formatJson(filteredParam)}</code></pre>
</div>
${item.response ? `
<div class="detail-section">
<strong>响应数据</strong>
<pre><code>${formatJson(item.response)}</code></pre>
</div>
` : ''}
`;
renderCaptureList();
}
function filterParam(param) {
if (!param || typeof param !== 'object') return param;
const filtered = {};
for (const key in param) {
if (key !== 'e_r' && key !== 'header') {
filtered[key] = param[key];
}
}
return filtered;
}
function copyParam(index) {
const item = capturedData[index];
if (!item || !item.param) return;
const filteredParam = filterParam(item.param);
const jsonStr = formatJson(filteredParam);
navigator.clipboard.writeText(jsonStr).then(() => {
showToast('已复制到剪贴板');
}).catch(err => {
console.error('Copy failed:', err);
showToast('复制失败,请手动复制');
});
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
} }
function formatJson(obj) { function formatJson(obj) {
@ -189,16 +485,38 @@
return div.innerHTML; return div.innerHTML;
} }
function setupSSE() {
const eventSource = new EventSource('/api/events');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
// 更新状态为已连接
updateStatus();
// 检查是否有新数据
if (data.length !== capturedData.length) {
capturedData = data;
renderCaptureList();
// 如果是新数据且当前没有选中,自动选中第一个
if (selectedIndex === -1 && data.length > 0) {
selectCapture(0);
}
}
};
eventSource.onerror = function(error) {
console.error('SSE error:', error);
};
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var origin = window.location.origin; var origin = window.location.origin;
document.getElementById('base-url').textContent = origin; document.getElementById('base-url').textContent = origin;
document.getElementById('current-url').textContent = window.location.href;
// 初始加载 // 设置 SSE 连接
fetchData(); setupSSE();
// 每秒刷新数据
setInterval(fetchData, 1000);
}); });
</script> </script>
</body> </body>

View File

@ -13,35 +13,6 @@ const config = require('./cli.js')
metavar: 'address', metavar: 'address',
help: 'specify server host', help: 'specify server host',
}) })
.option(['-u', '--proxy-url'], {
metavar: 'url',
help: 'request through upstream proxy',
})
.option(['-f', '--force-host'], {
metavar: 'host',
help: 'force the netease server ip',
})
.option(['-o', '--match-order'], {
metavar: 'source',
nargs: '+',
help: 'set priority of sources',
})
.option(['-t', '--token'], {
metavar: 'token',
help: 'set up proxy authentication',
})
.option(['-e', '--endpoint'], {
metavar: 'url',
help: 'replace virtual endpoint with public host',
})
.option(['-s', '--strict'], {
action: 'store_true',
help: 'enable proxy limitation',
})
.option(['-c', '--cnrelay'], {
metavar: 'cnrelay',
help: 'Mainland China relay to get music url',
})
.option(['-h', '--help'], { action: 'help' }) .option(['-h', '--help'], { action: 'help' })
.parse(process.argv); .parse(process.argv);
@ -61,52 +32,19 @@ if (config.port.some(invalid)) {
if (!process.env.PORT) { if (!process.env.PORT) {
process.env.PORT = process.env.PORT || '3000'; process.env.PORT = process.env.PORT || '3000';
} }
if (config.proxyUrl && !/http(s?):\/\/.+:\d+/.test(config.proxyUrl)) {
console.log('Please check the proxy url.');
process.exit(1);
}
if (!config.endpoint) config.endpoint = 'https://music.163.com';
else if (config.endpoint === '-') config.endpoint = '';
else if (!/http(s?):\/\/.+/.test(config.endpoint)) {
console.log('Please check the endpoint host.');
process.exit(1);
}
if (config.forceHost && require('net').isIP(config.forceHost) === 0) {
console.log('Please check the server host.');
process.exit(1);
}
if (config.matchOrder) {
const provider = Object.keys(require('./consts').PROVIDERS);
const candidate = config.matchOrder;
if (candidate.some((key, index) => index != candidate.indexOf(key))) {
console.log('Please check the duplication in match order.');
process.exit(1);
} else if (candidate.some((key) => !provider.includes(key))) {
console.log('Please check the availability of match sources.');
process.exit(1);
}
global.source = candidate;
}
if (config.token && !/\S+:\S+/.test(config.token)) {
console.log('Please check the authentication token.');
process.exit(1);
}
const { logScope } = require('./logger'); const { logScope } = require('./logger');
const parse = require('url').parse; const parse = require('url').parse;
const hook = require('./hook'); const hook = require('./hook');
const server = require('./server'); const server = require('./server');
const { CacheStorageGroup } = require('./cache');
const logger = logScope('app'); const logger = logScope('app');
const random = (array) => array[Math.floor(Math.random() * array.length)];
const target = Array.from(hook.target.host); const target = Array.from(hook.target.host);
global.port = config.port; global.port = config.port;
global.proxy = config.proxyUrl ? parse(config.proxyUrl) : null; global.proxy = null;
global.hosts = target.reduce( global.hosts = {};
(result, host) => Object.assign(result, { [host]: config.forceHost }), global.endpoint = 'https://music.163.com';
{}
);
server.whitelist = [ server.whitelist = [
'://[\\w.]*music\\.126\\.net', '://[\\w.]*music\\.126\\.net',
'://[\\w.]*vod\\.126\\.net', '://[\\w.]*vod\\.126\\.net',
@ -114,14 +52,8 @@ server.whitelist = [
'://[\\w.]*\\.netease.com', '://[\\w.]*\\.netease.com',
'://[\\w.]*\\.163yun.com', '://[\\w.]*\\.163yun.com',
]; ];
global.cnrelay = config.cnrelay;
if (config.strict) server.blacklist.push('.*');
server.authentication = config.token || null;
global.endpoint = config.endpoint;
if (config.endpoint) server.whitelist.push(escape(config.endpoint));
// hosts['music.httpdns.c.163.com'] = random(['59.111.181.35', '59.111.181.38']) if (config.endpoint) server.whitelist.push(escape(config.endpoint));
// hosts['httpdns.n.netease.com'] = random(['59.111.179.213', '59.111.179.214'])
const dns = (host) => const dns = (host) =>
new Promise((resolve, reject) => new Promise((resolve, reject) =>
@ -131,44 +63,8 @@ const dns = (host) =>
: resolve(records.map((record) => record.address)) : resolve(records.map((record) => record.address))
) )
); );
const httpdns = (host) =>
require('./request')('POST', 'http://music.httpdns.c.163.com/d', {}, host)
.then((response) => response.json())
.then((jsonBody) =>
jsonBody.dns.reduce(
(result, domain) => result.concat(domain.ips),
[]
)
);
const httpdns2 = (host) =>
require('./request')(
'GET',
'http://httpdns.n.netease.com/httpdns/v2/d?domain=' + host
)
.then((response) => response.json())
.then((jsonBody) =>
Object.keys(jsonBody.data)
.map((key) => jsonBody.data[key])
.reduce((result, value) => result.concat(value.ip || []), [])
);
// Allow enabling HTTPDNS queries with `ENABLE_HTTPDNS=true` Promise.all(target.map(dns))
// It seems broken - BETTER TO NOT ENABLE IT!
const dnsSource =
process.env.ENABLE_HTTPDNS === 'true' ? [httpdns, httpdns2] : [];
// Start the "Clean Cache" background task.
const csgInstance = CacheStorageGroup.getInstance();
setInterval(
() => {
csgInstance.cleanup();
},
15 * 60 * 1000
);
Promise.all(
dnsSource.map((query) => query(target.join(','))).concat(target.map(dns))
)
.then((result) => { .then((result) => {
const { host } = hook.target; const { host } = hook.target;
result.forEach((array) => array.forEach(host.add, host)); result.forEach((array) => array.forEach(host.add, host));
@ -189,7 +85,6 @@ Promise.all(
server.https server.https
.listen(port[1], address) .listen(port[1], address)
.once('listening', () => log(1)); .once('listening', () => log(1));
if (cnrelay) logger.info(`CNRelay: ${cnrelay}`);
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);

0
src/server/certs.txt Normal file
View File

View File

@ -1,20 +0,0 @@
const DEFAULT_SOURCE = ['kugou', 'bodian', 'migu', 'ytdlp'];
const PROVIDERS = {
qq: require('./provider/qq'),
kugou: require('./provider/kugou'),
kuwo: require('./provider/kuwo'),
bodian: require('./provider/bodian'),
migu: require('./provider/migu'),
joox: require('./provider/joox'),
youtube: require('./provider/youtube'),
youtubedl: require('./provider/youtube-dl'),
ytdlp: require('./provider/yt-dlp'),
bilibili: require('./provider/bilibili'),
bilivideo: require('./provider/bilivideo'),
pyncmd: require('./provider/pyncmd'),
};
module.exports = {
DEFAULT_SOURCE,
PROVIDERS,
};

View File

@ -71,67 +71,6 @@ module.exports = {
}; };
}, },
}, },
miguapi: {
encryptBody: (object) => {
const text = JSON.stringify(object);
const derive = (password, salt, keyLength, ivSize) => {
// EVP_BytesToKey
salt = salt || Buffer.alloc(0);
const keySize = keyLength / 8;
const repeat = Math.ceil((keySize + ivSize * 8) / 32);
const buffer = Buffer.concat(
Array(repeat)
.fill(null)
.reduce(
(result) =>
result.concat(
crypto
.createHash('md5')
.update(
Buffer.concat([
result.slice(-1)[0],
password,
salt,
])
)
.digest()
),
[Buffer.alloc(0)]
)
);
return {
key: buffer.slice(0, keySize),
iv: buffer.slice(keySize, keySize + ivSize),
};
};
const password = Buffer.from(
crypto.randomBytes(32).toString('hex')
),
salt = crypto.randomBytes(8);
const key =
'-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----';
const secret = derive(password, salt, 256, 16);
const cipher = crypto.createCipheriv(
'aes-256-cbc',
secret.key,
secret.iv
);
return bodyify({
data: Buffer.concat([
Buffer.from('Salted__'),
salt,
cipher.update(Buffer.from(text)),
cipher.final(),
]).toString('base64'),
secKey: crypto
.publicEncrypt(
{ key, padding: crypto.constants.RSA_PKCS1_PADDING },
password
)
.toString('base64'),
});
},
},
base64: { base64: {
encode: (text, charset) => encode: (text, charset) =>
Buffer.from(text, charset) Buffer.from(text, charset)
@ -144,27 +83,6 @@ module.exports = {
'base64' 'base64'
).toString(charset), ).toString(charset),
}, },
uri: {
retrieve: (id) => {
id = id.toString().trim();
const key = '3go8&$8*3*3h0k(2)2';
const string = Array.from(Array(id.length).keys())
.map((index) =>
String.fromCharCode(
id.charCodeAt(index) ^
key.charCodeAt(index % key.length)
)
)
.join('');
const result = crypto
.createHash('md5')
.update(string)
.digest('base64')
.replace(/\//g, '_')
.replace(/\+/g, '-');
return `http://p1.music.126.net/${result}/${id}`;
},
},
md5: { md5: {
digest: (value) => crypto.createHash('md5').update(value).digest('hex'), digest: (value) => crypto.createHash('md5').update(value).digest('hex'),
pipe: (source) => pipe: (source) =>
@ -189,7 +107,3 @@ module.exports = {
uuid: () => crypto.randomUUID(), uuid: () => crypto.randomUUID(),
}, },
}; };
try {
module.exports.kuwoapi = require('./kwDES');
} catch (e) {}

View 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")

View File

@ -35,12 +35,6 @@ hook.target.host = new Set([
'apm3.music.163.com', 'apm3.music.163.com',
'interface.music.163.com.163jiasu.com', 'interface.music.163.com.163jiasu.com',
'interface3.music.163.com.163jiasu.com', 'interface3.music.163.com.163jiasu.com',
// 'mam.netease.com',
// 'api.iplay.163.com', // look living
// 'ac.dun.163yun.com',
// 'crash.163.com',
// 'clientlog.music.163.com',
// 'clientlog3.music.163.com'
]); ]);
hook.target.path = new Set([ hook.target.path = new Set([
@ -139,7 +133,6 @@ hook.request.before = (ctx) => {
) && ) &&
req.method === 'POST' && req.method === 'POST' &&
(url.path.startsWith('/eapi/') || // eapi (url.path.startsWith('/eapi/') || // eapi
// url.path.startsWith('/api/') || // api
url.path.startsWith('/api/linux/forward')) // linuxapi url.path.startsWith('/api/linux/forward')) // linuxapi
) { ) {
return request return request
@ -180,12 +173,12 @@ hook.request.before = (ctx) => {
), ),
'hex' 'hex'
) )
) )
.toString() .toString()
); );
netease.path = parse(data.url).path; netease.path = parse(data.url).path;
netease.param = data.params; netease.param = data.params;
break; break;
case 'eapi': case 'eapi':
data = crypto.eapi data = crypto.eapi
.decrypt( .decrypt(
@ -196,22 +189,22 @@ hook.request.before = (ctx) => {
), ),
'hex' 'hex'
) )
) )
.toString() .toString()
.split('-36cd479b6b5-'); .split('-36cd479b6b5-');
netease.path = data[0]; netease.path = data[0];
netease.param = JSON.parse(data[1]); netease.param = JSON.parse(data[1]);
if ( if (
netease.param.hasOwnProperty('e_r') && netease.param.hasOwnProperty('e_r') &&
(netease.param.e_r == 'true' || (netease.param.e_r == 'true' ||
netease.param.e_r == true) netease.param.e_r == true)
) { ) {
// eapi's e_r is true, needs to be encrypted // eapi's e_r is true, needs to be encrypted
netease.e_r = true; netease.e_r = true;
} else { } else {
netease.e_r = false; netease.e_r = false;
} }
break; break;
case 'api': case 'api':
data = {}; data = {};
decodeURIComponent(body) decodeURIComponent(body)
@ -220,9 +213,9 @@ hook.request.before = (ctx) => {
let [key, value] = pair.split('='); let [key, value] = pair.split('=');
data[key] = value; data[key] = value;
}); });
netease.path = url.path; netease.path = url.path;
netease.param = data; netease.param = data;
break; break;
default: default:
// unsupported crypto // unsupported crypto
break; break;
@ -230,30 +223,6 @@ hook.request.before = (ctx) => {
netease.path = netease.path.replace(/\/\d*$/, ''); netease.path = netease.path.replace(/\/\d*$/, '');
ctx.netease = netease; ctx.netease = netease;
console.log(netease.path, netease.param) // 这里输出了网易云音乐的抓包数据, 重点看这里 console.log(netease.path, netease.param) // 这里输出了网易云音乐的抓包数据, 重点看这里
if (netease.path === '/api/song/enhance/download/url')
return pretendPlay(ctx);
if (netease.path === '/api/song/enhance/download/url/v1')
return pretendPlayV1(ctx);
if (BLOCK_ADS) {
if (netease.path.startsWith('/api/ad')) {
ctx.error = new Error('ADs blocked.');
ctx.decision = 'close';
}
}
if (DISABLE_UPGRADE_CHECK) {
if (
netease.path.match(
/^\/api(\/v1)?\/(android|ios|osx|pc)\/(upgrade|version)/
)
) {
ctx.error = new Error('Upgrade check blocked.');
ctx.decision = 'close';
}
}
} }
}) })
.catch( .catch(
@ -287,9 +256,6 @@ hook.request.before = (ctx) => {
req.headers['cookie'] = null; req.headers['cookie'] = null;
ctx.package = { id }; ctx.package = { id };
ctx.decision = 'proxy'; ctx.decision = 'proxy';
// if (url.href.includes('google'))
// return request('GET', req.url, req.headers, null, parse('http://127.0.0.1:1080'))
// .then(response => (ctx.res.writeHead(response.statusCode, response.headers), response.pipe(ctx.res)))
} catch (error) { } catch (error) {
ctx.error = error; ctx.error = error;
ctx.decision = 'close'; ctx.decision = 'close';
@ -340,170 +306,6 @@ hook.request.after = (ctx) => {
}; };
axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend) axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend)
.catch(err => logger.error('Failed to send data to frontend:', err)); .catch(err => logger.error('Failed to send data to frontend:', err));
if (
netease.path === '/batch' ||
netease.path === '/api/batch' ||
netease.path === vipPath
) {
const info =
netease.path === vipPath
? netease.jsonBody
: netease.jsonBody[vipPath];
const defaultPackage = {
iconUrl: null,
dynamicIconUrl: null,
isSign: false,
isSignIap: false,
isSignDeduct: false,
isSignIapDeduct: false,
};
const vipLevel = 7; // ? months
if (
info &&
(LOCAL_VIP_UID.length === 0 ||
LOCAL_VIP_UID.includes(info.data.userId))
) {
try {
const nowTime =
info.data.now || new Date().getTime();
const expireTime = nowTime + 31622400000;
info.data.redVipLevel = vipLevel;
info.data.redVipAnnualCount = 1;
info.data.musicPackage = {
...defaultPackage,
...info.data.musicPackage,
vipCode: 230,
vipLevel,
expireTime,
};
info.data.associator = {
...defaultPackage,
...info.data.associator,
vipCode: 100,
vipLevel,
expireTime,
};
if (ENABLE_LOCAL_SVIP) {
info.data.redplus = {
...defaultPackage,
...info.data.redplus,
vipCode: 300,
vipLevel,
expireTime,
};
info.data.albumVip = {
...defaultPackage,
...info.data.albumVip,
vipCode: 400,
vipLevel: 0,
expireTime,
};
}
if (netease.path === vipPath)
netease.jsonBody = info;
else netease.jsonBody[vipPath] = info;
} catch (error) {
logger.debug(
{ err: error },
'Unable to apply the local VIP.'
);
}
}
}
}
}
if (
new Set([401, 512]).has(netease.jsonBody.code) &&
!netease.web
) {
if (netease.path.includes('/usertool/sound/'))
return unblockSoundEffects(netease.jsonBody);
else if (netease.path.includes('batch')) {
for (const key in netease.jsonBody) {
if (key.includes('/usertool/sound/'))
unblockSoundEffects(netease.jsonBody[key]);
}
} else if (netease.path.includes('/vipauth/app/auth/query'))
return unblockLyricsEffects(netease.jsonBody);
.then(() => {
['transfer-encoding', 'content-encoding', 'content-length']
.filter((key) => key in proxyRes.headers)
.forEach((key) => delete proxyRes.headers[key]);
const inject = (key, value) => {
if (typeof value === 'object' && value != null) {
if ('cp' in value) value['cp'] = 1;
if ('fee' in value) value['fee'] = 0;
if (
'downloadMaxbr' in value &&
value['downloadMaxbr'] === 0
)
value['downloadMaxbr'] = 320000;
if (
'dl' in value &&
'downloadMaxbr' in value &&
value['dl'] < value['downloadMaxbr']
)
value['dl'] = value['downloadMaxbr'];
if ('playMaxbr' in value && value['playMaxbr'] === 0)
value['playMaxbr'] = 320000;
if (
'pl' in value &&
'playMaxbr' in value &&
value['pl'] < value['playMaxbr']
)
value['pl'] = value['playMaxbr'];
if ('sp' in value && 'st' in value && 'subp' in value) {
// batch modify
value['sp'] = 7;
value['st'] = 0;
value['subp'] = 1;
}
if (
'start' in value &&
'end' in value &&
'playable' in value &&
'unplayableType' in value &&
'unplayableUserIds' in value
) {
value['start'] = 0;
value['end'] = 0;
value['playable'] = true;
value['unplayableType'] = 'unknown';
value['unplayableUserIds'] = [];
}
if ('noCopyrightRcmd' in value)
value['noCopyrightRcmd'] = null;
if ('payed' in value && value['payed'] == 0)
value['payed'] = 1;
if ('flLevel' in value && value['flLevel'] === 'none')
value['flLevel'] = 'exhigh';
if ('plLevel' in value && value['plLevel'] === 'none')
value['plLevel'] = 'exhigh';
if ('dlLevel' in value && value['dlLevel'] === 'none')
value['dlLevel'] = 'exhigh';
}
return value;
};
let body = JSON.stringify(netease.jsonBody, inject);
body = body.replace(
/([^\\]"\s*:\s*)"(\d{16,})L"(\s*[}|,])/g,
'$1$2$3'
); // for js precision
proxyRes.body = netease.e_r // eapi's e_r is true, needs to be encrypted
? crypto.eapi.encrypt(Buffer.from(body))
: body;
}) })
.catch( .catch(
(error) => (error) =>
@ -557,89 +359,4 @@ hook.negotiate.before = (ctx) => {
} }
}; };
const pretendPlay = (ctx) => {
const { req, netease } = ctx;
const turn = 'http://music.163.com/api/song/enhance/player/url';
let query;
const { id, br, e_r, header } = netease.param;
switch (netease.crypto) {
case 'linuxapi':
netease.param = { ids: `["${id}"]`, br };
query = crypto.linuxapi.encryptRequest(turn, netease.param);
break;
case 'eapi':
case 'api':
netease.param = { ids: `["${id}"]`, br, e_r, header };
if (netease.crypto == 'eapi')
query = crypto.eapi.encryptRequest(turn, netease.param);
else if (netease.crypto == 'api')
query = crypto.api.encryptRequest(turn, netease.param);
break;
default:
break;
}
req.url = query.url;
req.body = query.body + netease.pad;
};
const pretendPlayV1 = (ctx) => {
const { req, netease } = ctx;
const turn = 'http://music.163.com/api/song/enhance/player/url/v1';
let query;
const { id, level, immerseType, e_r, header } = netease.param;
switch (netease.crypto) {
case 'linuxapi':
netease.param = {
ids: `["${id}"]`,
level,
encodeType: 'flac',
immerseType,
};
query = crypto.linuxapi.encryptRequest(turn, netease.param);
break;
case 'eapi':
case 'api':
netease.param = {
ids: `["${id}"]`,
level,
encodeType: 'flac',
immerseType,
e_r,
header,
};
if (netease.crypto == 'eapi')
query = crypto.eapi.encryptRequest(turn, netease.param);
else if (netease.crypto == 'api')
query = crypto.api.encryptRequest(turn, netease.param);
break;
default:
break;
}
req.url = query.url;
req.body = query.body + netease.pad;
};
const unblockSoundEffects = (obj) => {
logger.debug('unblockSoundEffects() has been triggered.');
const { data, code } = obj;
if (code === 200) {
if (Array.isArray(data))
data.map((item) => {
if (item.type) item.type = 1;
});
else if (data.type) data.type = 1;
}
};
const unblockLyricsEffects = (obj) => {
logger.debug('unblockLyricsEffects() has been triggered.');
const { data, code } = obj;
if (code === 200 && Array.isArray(data)) {
data.forEach((item) => {
if ('canUse' in item) item.canUse = true;
if ('canNotUseReasonCode' in item) item.canNotUseReasonCode = 200;
});
}
};
module.exports = hook; module.exports = hook;

View File

@ -1,585 +0,0 @@
/*
Thanks to
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java
*/
const Long = (n) => {
const bN = BigInt(n);
return {
low: Number(bN),
valueOf: () => bN.valueOf(),
toString: () => bN.toString(),
not: () => Long(~bN),
isNegative: () => bN < 0,
or: (x) => Long(bN | BigInt(x)),
and: (x) => Long(bN & BigInt(x)),
xor: (x) => Long(bN ^ BigInt(x)),
equals: (x) => bN === BigInt(x),
multiply: (x) => Long(bN * BigInt(x)),
shiftLeft: (x) => Long(bN << BigInt(x)),
shiftRight: (x) => Long(bN >> BigInt(x)),
};
};
const range = (n) => Array.from(new Array(n).keys());
const power = (base, index) =>
Array(index)
.fill(null)
.reduce((result) => result.multiply(base), Long(1));
const LongArray = (...array) =>
array.map((n) => (n === -1 ? Long(-1, -1) : Long(n)));
// EXPANSION
const arrayE = LongArray(
31,
0,
1,
2,
3,
4,
-1,
-1,
3,
4,
5,
6,
7,
8,
-1,
-1,
7,
8,
9,
10,
11,
12,
-1,
-1,
11,
12,
13,
14,
15,
16,
-1,
-1,
15,
16,
17,
18,
19,
20,
-1,
-1,
19,
20,
21,
22,
23,
24,
-1,
-1,
23,
24,
25,
26,
27,
28,
-1,
-1,
27,
28,
29,
30,
31,
30,
-1,
-1
);
// INITIAL_PERMUTATION
const arrayIP = LongArray(
57,
49,
41,
33,
25,
17,
9,
1,
59,
51,
43,
35,
27,
19,
11,
3,
61,
53,
45,
37,
29,
21,
13,
5,
63,
55,
47,
39,
31,
23,
15,
7,
56,
48,
40,
32,
24,
16,
8,
0,
58,
50,
42,
34,
26,
18,
10,
2,
60,
52,
44,
36,
28,
20,
12,
4,
62,
54,
46,
38,
30,
22,
14,
6
);
// INVERSE_PERMUTATION
const arrayIP_1 = LongArray(
39,
7,
47,
15,
55,
23,
63,
31,
38,
6,
46,
14,
54,
22,
62,
30,
37,
5,
45,
13,
53,
21,
61,
29,
36,
4,
44,
12,
52,
20,
60,
28,
35,
3,
43,
11,
51,
19,
59,
27,
34,
2,
42,
10,
50,
18,
58,
26,
33,
1,
41,
9,
49,
17,
57,
25,
32,
0,
40,
8,
48,
16,
56,
24
);
// ROTATES
const arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
const arrayLsMask = LongArray(0, 0x100001, 0x300003);
const arrayMask = range(64).map((n) => power(2, n));
arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1);
// PERMUTATION
const arrayP = LongArray(
15,
6,
19,
20,
28,
11,
27,
16,
0,
14,
22,
25,
4,
17,
30,
9,
1,
7,
23,
13,
31,
26,
2,
8,
18,
12,
29,
5,
21,
10,
3,
24
);
// PERMUTED_CHOICE1
const arrayPC_1 = LongArray(
56,
48,
40,
32,
24,
16,
8,
0,
57,
49,
41,
33,
25,
17,
9,
1,
58,
50,
42,
34,
26,
18,
10,
2,
59,
51,
43,
35,
62,
54,
46,
38,
30,
22,
14,
6,
61,
53,
45,
37,
29,
21,
13,
5,
60,
52,
44,
36,
28,
20,
12,
4,
27,
19,
11,
3
);
// PERMUTED_CHOICE2
const arrayPC_2 = LongArray(
13,
16,
10,
23,
0,
4,
-1,
-1,
2,
27,
14,
5,
20,
9,
-1,
-1,
22,
18,
11,
3,
25,
7,
-1,
-1,
15,
6,
26,
19,
12,
1,
-1,
-1,
40,
51,
30,
36,
46,
54,
-1,
-1,
29,
39,
50,
44,
32,
47,
-1,
-1,
43,
48,
38,
55,
33,
52,
-1,
-1,
45,
41,
49,
35,
28,
31,
-1,
-1
);
const matrixNSBox = [
[
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15,
6, 9, 10, 1, 8, 12, 7, 8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8,
12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9, 5, 0, 4, 2, 11, 14, 1, 7, 8,
13,
],
[
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4,
0, 3, 14, 11, 13, 6, 4, 1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1,
7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15, 9, 5, 7, 1, 10, 12, 14, 2, 5, 9,
],
[
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3,
15, 4, 10, 14, 9, 7, 12, 5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13,
5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9, 11, 5, 9, 0, 14, 3, 10, 7, 1,
12,
],
[
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11,
12, 2, 3, 0, 5, 14, 10, 13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2,
5, 0, 13, 14, 2, 8, 15, 7, 4, 15, 1, 10, 7, 5, 6, 12, 11, 3, 8, 9, 14,
],
[
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10,
13, 0, 3, 1, 11, 15, 5, 6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12,
15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14, 9, 4, 12, 7, 10, 9, 1, 13, 6,
3,
],
[
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2,
8, 7, 13, 15, 5, 4, 10, 8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13,
1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5, 11, 0, 2, 12, 14, 7, 5, 10, 8, 13,
],
[
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3,
10, 5, 14, 13, 7, 8, 13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5,
0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4, 15, 2, 7, 8, 12, 15, 10, 7, 6, 12,
],
[
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15,
12, 0, 3, 4, 1, 14, 13, 1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14,
6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10, 14, 5, 8, 7, 11, 0, 4, 13, 2, 11,
],
];
const bitTransform = (arrInt, n, l) => {
// int[], int, long : long
let l2 = Long(0);
range(n).forEach((i) => {
if (arrInt[i].isNegative() || l.and(arrayMask[arrInt[i].low]).equals(0))
return;
l2 = l2.or(arrayMask[i]);
});
return l2;
};
const DES64 = (longs, l) => {
const pR = range(8).map(() => Long(0));
const pSource = [Long(0), Long(0)];
let L = Long(0);
let R = Long(0);
let out = bitTransform(arrayIP, 64, l);
pSource[0] = out.and(0xffffffff);
pSource[1] = out.and(-4294967296).shiftRight(32);
range(16).forEach((i) => {
let SOut = Long(0);
R = Long(pSource[1]);
R = bitTransform(arrayE, 64, R);
R = R.xor(longs[i]);
range(8).forEach((j) => {
pR[j] = R.shiftRight(j * 8).and(255);
});
range(8)
.reverse()
.forEach((sbi) => {
SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]);
});
R = bitTransform(arrayP, 32, SOut);
L = Long(pSource[0]);
pSource[0] = Long(pSource[1]);
pSource[1] = L.xor(R);
});
pSource.reverse();
out = pSource[1]
.shiftLeft(32)
.and(-4294967296)
.or(pSource[0].and(0xffffffff));
out = bitTransform(arrayIP_1, 64, out);
return out;
};
const subKeys = (l, longs, n) => {
// long, long[], int
let l2 = bitTransform(arrayPC_1, 56, l);
range(16).forEach((i) => {
l2 = l2
.and(arrayLsMask[arrayLs[i]])
.shiftLeft(28 - arrayLs[i])
.or(l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i]));
longs[i] = bitTransform(arrayPC_2, 64, l2);
});
if (n === 1) {
range(8).forEach((j) => {
[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]];
});
}
};
const crypt = (msg, key, mode) => {
// 处理密钥块
let l = Long(0);
range(8).forEach((i) => {
l = Long(key[i])
.shiftLeft(i * 8)
.or(l);
});
const j = Math.floor(msg.length / 8);
// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
const arrLong1 = range(16).map(() => Long(0));
subKeys(l, arrLong1, mode);
// arrLong2 存放的是前部分的明文
const arrLong2 = range(j).map(() => Long(0));
range(j).forEach((m) => {
range(8).forEach((n) => {
arrLong2[m] = Long(msg[n + m * 8])
.shiftLeft(n * 8)
.or(arrLong2[m]);
});
});
// 用于存放密文
const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() =>
Long(0)
);
// 计算前部的数据块(除了最后一部分)
range(j).forEach((i1) => {
arrLong3[i1] = DES64(arrLong1, arrLong2[i1]);
});
// 保存多出来的字节
const arrByte1 = msg.slice(j * 8);
let l2 = Long(0);
range(msg.length % 8).forEach((i1) => {
l2 = Long(arrByte1[i1])
.shiftLeft(i1 * 8)
.or(l2);
});
// 计算多出的那一位(最后一位)
if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2); // 解密不需要
// 将密文转为字节型
const arrByte2 = range(8 * arrLong3.length).map(() => 0);
let i4 = 0;
arrLong3.forEach((l3) => {
range(8).forEach((i6) => {
arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low;
i4 += 1;
});
});
return Buffer.from(arrByte2);
};
const SECRET_KEY = Buffer.from('ylzsxkwm');
const encrypt = (msg) => crypt(msg, SECRET_KEY, 0);
const decrypt = (msg) => crypt(msg, SECRET_KEY, 1);
const encryptQuery = (query) => encrypt(Buffer.from(query)).toString('base64');
module.exports = { encrypt, decrypt, encryptQuery };

View File

@ -1,8 +1,6 @@
const zlib = require('zlib'); const zlib = require('zlib');
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const ON_CANCEL = require('./cancel');
const RequestCancelled = require('./exceptions/RequestCancelled');
const { logScope } = require('./logger'); const { logScope } = require('./logger');
const parse = require('url').parse; const parse = require('url').parse;
const format = require('url').format; const format = require('url').format;
@ -62,7 +60,7 @@ const configure = (method, url, headers, proxy) => {
/** /**
* @template T * @template T
* @typedef {{url: string, body: RequestExtensionBody, json: () => Promise<T>, jsonp: () => Promise<T>}} RequestExtension * @typedef {{url: string, body: RequestExtensionBody, json: () => Promise<T>}} RequestExtension
*/ */
/** /**
@ -72,7 +70,6 @@ const configure = (method, url, headers, proxy) => {
* @param {Object?} receivedHeaders * @param {Object?} receivedHeaders
* @param {unknown?} body * @param {unknown?} body
* @param {unknown?} proxy * @param {unknown?} proxy
* @param {CancelRequest?} cancelRequest
* @return {Promise<http.IncomingMessage & RequestExtension<T>>} * @return {Promise<http.IncomingMessage & RequestExtension<T>>}
*/ */
const request = ( const request = (
@ -80,8 +77,7 @@ const request = (
receivedUrl, receivedUrl,
receivedHeaders, receivedHeaders,
body, body,
proxy, proxy
cancelRequest
) => { ) => {
const url = parse(receivedUrl); const url = parse(receivedUrl);
/* @type {Partial<Record<string,string>>} */ /* @type {Partial<Record<string,string>>} */
@ -105,14 +101,6 @@ const request = (
logger.debug(`Start requesting ${receivedUrl}`); logger.debug(`Start requesting ${receivedUrl}`);
const clientRequest = create(url, proxy)(options); const clientRequest = create(url, proxy)(options);
const destroyClientRequest = function () {
// We destroy the request and throw RequestCancelled
// when the request has been cancelled.
clientRequest.destroy(new RequestCancelled(format(url)));
};
cancelRequest?.on(ON_CANCEL, destroyClientRequest);
if (cancelRequest?.cancelled ?? false) destroyClientRequest();
clientRequest clientRequest
.setTimeout(timeoutThreshold, () => { .setTimeout(timeoutThreshold, () => {
@ -122,7 +110,7 @@ const request = (
}, },
`The request timed out, or the requester didn't handle the response.` `The request timed out, or the requester didn't handle the response.`
); );
destroyClientRequest(); clientRequest.destroy();
}) })
.on('response', (response) => resolve(response)) .on('response', (response) => resolve(response))
.on('connect', (_, socket) => { .on('connect', (_, socket) => {
@ -146,9 +134,6 @@ const request = (
}).then( }).then(
/** @param {http.IncomingMessage} response */ /** @param {http.IncomingMessage} response */
(response) => { (response) => {
if (cancelRequest?.cancelled ?? false)
return Promise.reject(new RequestCancelled(format(url)));
if ([201, 301, 302, 303, 307, 308].includes(response.statusCode)) { if ([201, 301, 302, 303, 307, 308].includes(response.statusCode)) {
const redirectTo = url.resolve( const redirectTo = url.resolve(
response.headers.location || url.href response.headers.location || url.href
@ -163,7 +148,6 @@ const request = (
url, url,
body: (raw) => read(response, raw), body: (raw) => read(response, raw),
json: () => json(response), json: () => json(response),
jsonp: () => jsonp(response),
}); });
} }
); );
@ -192,10 +176,6 @@ const read = (connect, raw) =>
}); });
const json = (connect) => read(connect, false).then((body) => JSON.parse(body)); const json = (connect) => read(connect, false).then((body) => JSON.parse(body));
const jsonp = (connect) =>
read(connect, false).then((body) =>
JSON.parse(body.slice(body.indexOf('(') + 1, -')'.length))
);
request.read = read; request.read = read;
request.create = create; request.create = create;

View File

@ -1,16 +1,16 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIJAOqZ7l8q9YAMMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV MIICgjCCAgigAwIBAgIUafzDKFqhg8XS5YogLkI9oGsngkUwCgYIKoZIzj0EAwMw
BAMMBmxvY2FsaG9zdDAeFw0yNDAxMDEwMDAwMDBaFw0zNDAxMDEwMDAwMDBaMBEx RDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v
DzANBgNVBAMMBmxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA dCBDQTEPMA0GA1UECgwGbm9ib2R5MB4XDTI1MDcyNDA4NDY1NloXDTI2MDcyNDA4
v5KXq8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R NDY1NlowezELMAkGA1UEBhMCQ04xETAPBgNVBAcMCEhhbmd6aG91MSwwKgYDVQQK
5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8 DCNOZXRFYXNlIChIYW5nemhvdSkgTmV0d29yayBDby4sIEx0ZDERMA8GA1UECwwI
R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq SVQgRGVwdC4xGDAWBgNVBAMMDyoubXVzaWMuMTYzLmNvbTB2MBAGByqGSM49AgEG
8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8CAwEAAaOBnjCBmzAdBgNVHQ4E BSuBBAAiA2IABACkvqL3b68Pv11nQVwm/9BNWGuh1fBKlqYZqnyr9Gy1xEQtf3we
FgQUK7qZ7l8q9YAMBExDzANBgNVBAMMBmxvY2FsaG9zdAMBgNVHRMEBTADAQH/MCwG WlwgVmX9rqu9nNEiAbbxIt9aGliHurKkE22+FiOKC4vduYlyyIXKlCAFXigHKr/k
CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV 6uKgc2jPfeYoC6OBgzCBgDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAg
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEAv5KX gg1tdXNpYy4xNjMuY29tgg8qLm11c2ljLjE2My5jb20wHQYDVR0OBBYEFEeHCj4z
q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X PzDre8No6wsWbrpmQ8hPMB8GA1UdIwQYMBaAFGW+hCo4x24AIMIYbUjR4NRlnzx1
5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9 MAoGCCqGSM49BAMDA2gAMGUCMQDD0cOUV43GSLH8bbbkw4KD6kVoknm6z6sjE5vk
X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L ZNmgjqduSn6XzCNiiwwhRrsmtVkCMCX8uRVIIG20mdcK2YSwlSgw5mHTJsNFRnfn
9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R x8ft2l8Tk2yZcqFSsvuVKvBwZh7bWA==
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,28 +1,9 @@
-----BEGIN PRIVATE KEY----- -----BEGIN EC PARAMETERS-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDK3UbsHMKuVo1c BgUrgQQAIg==
Ye/54XWH7SZM+2hDAKqrCcE3lo96CVazxtUHcd7hHFK/oVHC/9IgntOr0hunp0NH -----END EC PARAMETERS-----
jCUjor9ZAcDECQEmjU9CIO+TsuVKD8EaqJPArO3dKRvALGhE9Abs8MqphlRxgCxO -----BEGIN EC PRIVATE KEY-----
4BWaEffWSjl8+owYH/VZtTG+onQlY6uC2B50YnzJAtgv9nDZwfolxzYdK0xt/SaU MIGkAgEBBDDaWQTho9lbjl27ddVt9pC4req+U8iFhdMw0/RzWePrlF55fvmwRiff
JQbQxc4tQ5G/+roqpHSLTAqL7ts3V0GY8ygWzal9khbkNAR3E1zG+fESzysyOlDu GepKeBDdkx6gBwYFK4EEACKhZANiAAQApL6i92+vD79dZ0FcJv/QTVhrodXwSpam
oqd7FExnyAd6TSKDcmiQYgDKSEcZwunwNAvNi4UVG9XaPLzRq+KAe99R6KvjVf6u Gap8q/RstcRELX98HlpcIFZl/a6rvZzRIgG28SLfWhpYh7qypBNtvhYjiguL3bmJ
1IfTTWeVAgMBAAECggEABtqFYzXUM7hK2+AI82BYs5uvlFmP0S88P4SYEmez4lHt csiFypQgBV4oByq/5OrioHNoz33mKAs=
VoFOKyfjJBnP3PsTsbx/DBdOBr7OkCePeuL9w/+qXGMdVGzR5ejBXtnKOrmIMAGV -----END EC PRIVATE KEY-----
XKkXyT7ALLnWQoIoOzXqA8MIJcR+lvTXQ0NdQiJGYZHDRzkXSJDsuhRd0xdIZQbr
tI8H9WMH+2BZn4t+5PTlj6EbihBZiqCJ986Vu5z2enPS61PIhnDMF7yxG01G0gNk
QrZSFJWd0zPHdbhxrLXe6slFGwDBXE+ad9Shjjq/xaMrSzWaiEQImvTh8d3eLsJK
QjJL5z2bWm7EYb1+LQtiX4ioMjav22SavggVQKc11QKBgQDthprmGs+IVveZVLql
e3gESsS3+P72fLSF1F4kxMM6wWWypJo5/LYzveskDKK5sXTRR/lBGglpRVuZvnnm
UuOrixpfKM8W4nfnrZURbau/0Lfeull7LSJPuNkr4xEA5/zt/cSlg1L/IOPDgTtT
4XFd41kZYsAgK2jsXih7bpiZCwKBgQDapIZ9iJG0Yd6uAclD1Swo83UHaoxmJSZw
EiAyR07WWBycfb3P18FHW0wejIloyiq8LZR8upa5u8xFcyC3/Z8AlxpojdcHsDE9
uuT1Sibc483tto9I+p+YfWIBxJBWA3hMB5gS+t7ssueEdZdrfz1Y8aBYHwA5XFNk
7JbbUrSl3wKBgAgCfAK6cLkmRZ80Dj86VKfAZbXWfbKOLgA9UxdmUzcOAoHtrw25
ieNgyicjDfG5HDladftOB3c3UYlztOShcu/79t2yoJki9ewoHFjEHACR50Fpg072
DKwnjZs/QvmG2S6lWhZCwW+9CjEzkG6ZsZr66axDejsbe6RM4IyZBChVAoGAdId1
iphsF7iFxzX6f+WwqI7BE9fMxnAMYXS3pjRtJz5E2X8G2CyEvbRCCJIcdjYxuqOM
XUHRLWKTB3zJtmY9BUKDd7AJJ/bW97CRcM45kkbzrTs8eMfioZJJ1uldiApHZjYx
7gO5JmxfijBmKIvjNXFqZSz4oJm9dK/H41LcJv8CgYAPMpmgNuPSBq9+Ol/5Vf2v
8s4j87SJoOsPEepnlyobnxARnjTuS6BnEuaQHMyvXIMzXLzy0kA3jGFd9j2k31Fw
4JOTHVwHrgJboEp/F3MssHw+SYUKgHWqqVPlwME1Zgb5kgZpxeLIRkS69nPCEjD7
hmOSlIN+qgaqE5jIiK76RQ==
-----END PRIVATE KEY-----