refactor: 将文件从子目录提升到仓库根目录
所有文件从 hello_js_reverse_skill/ 子目录移至仓库根目录, 使项目结构更清晰,便于作为 Skill 直接引用。 Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 主脚本模板 - 纯 Node.js 算法还原
|
||||
*
|
||||
* 适用场景:加密逻辑可完整提取,无浏览器环境依赖
|
||||
*
|
||||
* TODO: 根据逆向分析结果修改以下配置和逻辑
|
||||
*/
|
||||
|
||||
const { generateM, generateSign } = require('./utils/encrypt');
|
||||
const { RequestClient } = require('./utils/request');
|
||||
|
||||
// ============ 配置区域 ============
|
||||
|
||||
const CONFIG = {
|
||||
// 目标信息
|
||||
name: '示例项目',
|
||||
description: '采集全部5页数据,计算数值总和',
|
||||
|
||||
// 接口配置
|
||||
baseURL: 'https://target.com',
|
||||
apiPath: '/api/data',
|
||||
referer: 'https://target.com/page',
|
||||
|
||||
// 采集配置
|
||||
totalPages: 5,
|
||||
delay: 1500,
|
||||
|
||||
// 认证信息
|
||||
cookies: {
|
||||
// sessionid: 'your_session_id',
|
||||
},
|
||||
|
||||
// 自定义请求头
|
||||
headers: {
|
||||
// 'X-Custom-Header': 'value',
|
||||
},
|
||||
};
|
||||
|
||||
// ============ 主逻辑 ============
|
||||
|
||||
async function fetchPage(client, page) {
|
||||
// TODO: 根据实际接口修改参数构造逻辑
|
||||
const m = generateM(page);
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const params = {
|
||||
page,
|
||||
m,
|
||||
t: timestamp,
|
||||
};
|
||||
|
||||
const response = await client.requestWithDelay({
|
||||
method: 'GET',
|
||||
url: CONFIG.apiPath,
|
||||
params,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function extractData(responseData) {
|
||||
// TODO: 根据实际响应格式修改数据提取逻辑
|
||||
if (responseData && responseData.data) {
|
||||
return responseData.data;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function calculateResult(allData) {
|
||||
// TODO: 根据题目要求修改计算逻辑
|
||||
// 示例:求所有数值的总和
|
||||
const sum = allData.reduce((acc, item) => {
|
||||
const value = typeof item === 'number' ? item : (item.value || 0);
|
||||
return acc + value;
|
||||
}, 0);
|
||||
return sum;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`[*] 项目:${CONFIG.name}`);
|
||||
console.log(`[*] 目标:${CONFIG.description}`);
|
||||
console.log('');
|
||||
|
||||
const client = new RequestClient({
|
||||
baseURL: CONFIG.baseURL,
|
||||
cookies: CONFIG.cookies,
|
||||
headers: CONFIG.headers,
|
||||
referer: CONFIG.referer,
|
||||
delay: CONFIG.delay,
|
||||
});
|
||||
|
||||
const allData = [];
|
||||
|
||||
for (let page = 1; page <= CONFIG.totalPages; page++) {
|
||||
try {
|
||||
const responseData = await fetchPage(client, page);
|
||||
const pageData = extractData(responseData);
|
||||
allData.push(...pageData);
|
||||
console.log(`[+] 正在采集第 ${page}/${CONFIG.totalPages} 页... ✓ 获取 ${pageData.length} 条数据`);
|
||||
} catch (error) {
|
||||
console.error(`[-] 第 ${page} 页采集失败: ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(` 状态码: ${error.response.status}`);
|
||||
console.error(` 响应: ${JSON.stringify(error.response.data).substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n[+] 采集完成,共 ${allData.length} 条数据`);
|
||||
|
||||
const result = calculateResult(allData);
|
||||
|
||||
console.log('\n' + '='.repeat(30) + ' 计算结果 ' + '='.repeat(30));
|
||||
console.log(`答案:${result}`);
|
||||
console.log('='.repeat(70));
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('\n[!] 运行错误:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "js-reverse-project",
|
||||
"version": "1.0.0",
|
||||
"description": "JS逆向 - 纯Node.js算法还原模板",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "node main.js",
|
||||
"test": "node main.js --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.0",
|
||||
"crypto-js": "^4.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 加密参数生成模块
|
||||
* 根据逆向分析还原的加密逻辑,生成请求所需的加密参数
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
function md5(str) {
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
|
||||
function sha256(str) {
|
||||
return crypto.createHash('sha256').update(str).digest('hex');
|
||||
}
|
||||
|
||||
function hmacSha256(message, secret) {
|
||||
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
||||
}
|
||||
|
||||
function aesEncrypt(plaintext, key, iv, mode = 'cbc') {
|
||||
const algorithm = `aes-${key.length * 8}-${mode}`;
|
||||
const cipher = crypto.createCipheriv(algorithm, key, mode === 'ecb' ? null : iv);
|
||||
cipher.setAutoPadding(true);
|
||||
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
function aesDecrypt(ciphertext, key, iv, mode = 'cbc') {
|
||||
const algorithm = `aes-${key.length * 8}-${mode}`;
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, mode === 'ecb' ? null : iv);
|
||||
decipher.setAutoPadding(true);
|
||||
let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求签名
|
||||
* TODO: 根据实际逆向分析修改此函数
|
||||
* @param {Object} params - 请求参数
|
||||
* @returns {string} 签名值
|
||||
*/
|
||||
function generateSign(params) {
|
||||
const { page, timestamp } = params;
|
||||
// 示例:sign = md5(page + "|" + timestamp + "|" + SECRET)
|
||||
const SECRET = 'your_secret_here';
|
||||
return md5(`${page}|${timestamp}|${SECRET}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成加密参数 m
|
||||
* TODO: 根据实际逆向分析修改此函数
|
||||
* @param {number} page - 页码
|
||||
* @returns {string} 加密后的参数
|
||||
*/
|
||||
function generateM(page) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const sign = generateSign({ page, timestamp });
|
||||
return `${sign}|${timestamp}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
md5,
|
||||
sha256,
|
||||
hmacSha256,
|
||||
aesEncrypt,
|
||||
aesDecrypt,
|
||||
generateSign,
|
||||
generateM,
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 请求封装模块
|
||||
* 封装 HTTP 请求,处理 Headers、Cookie、重试、频率控制
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const DEFAULT_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
class RequestClient {
|
||||
constructor(options = {}) {
|
||||
this.baseURL = options.baseURL || '';
|
||||
this.cookies = options.cookies || {};
|
||||
this.headers = { ...DEFAULT_HEADERS, ...options.headers };
|
||||
this.delay = options.delay || 1000;
|
||||
this.maxRetries = options.maxRetries || 3;
|
||||
this.referer = options.referer || '';
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
timeout: options.timeout || 30000,
|
||||
headers: this.headers,
|
||||
maxRedirects: 5,
|
||||
});
|
||||
}
|
||||
|
||||
getCookieString() {
|
||||
return Object.entries(this.cookies)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
setCookie(name, value) {
|
||||
this.cookies[name] = value;
|
||||
}
|
||||
|
||||
parseCookies(setCookieHeader) {
|
||||
if (!setCookieHeader) return;
|
||||
const headers = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||||
headers.forEach(h => {
|
||||
const mainPart = h.split(';')[0];
|
||||
const eqIdx = mainPart.indexOf('=');
|
||||
if (eqIdx > 0) {
|
||||
this.cookies[mainPart.substring(0, eqIdx).trim()] = mainPart.substring(eqIdx + 1).trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async request(config) {
|
||||
const finalConfig = {
|
||||
...config,
|
||||
headers: {
|
||||
...this.headers,
|
||||
...config.headers,
|
||||
'Cookie': this.getCookieString(),
|
||||
},
|
||||
};
|
||||
|
||||
if (this.referer) {
|
||||
finalConfig.headers['Referer'] = this.referer;
|
||||
}
|
||||
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await this.client.request(finalConfig);
|
||||
this.parseCookies(response.headers['set-cookie']);
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const status = error.response?.status;
|
||||
console.error(`[Request] 第 ${attempt}/${this.maxRetries} 次请求失败: ${status || error.message}`);
|
||||
|
||||
if (status === 429) {
|
||||
const waitTime = this.delay * attempt * 2;
|
||||
console.log(`[Request] 频率限制,等待 ${waitTime}ms...`);
|
||||
await this.sleep(waitTime);
|
||||
} else if (status >= 500) {
|
||||
await this.sleep(this.delay * attempt);
|
||||
} else if (status === 403 || status === 412) {
|
||||
console.error('[Request] 访问被拒绝,可能需要更新 Cookie 或检查加密参数');
|
||||
throw error;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async get(url, params = {}, extraHeaders = {}) {
|
||||
return this.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
params,
|
||||
headers: extraHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
async post(url, data = {}, extraHeaders = {}) {
|
||||
return this.request({
|
||||
method: 'POST',
|
||||
url,
|
||||
data,
|
||||
headers: extraHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async requestWithDelay(config) {
|
||||
const response = await this.request(config);
|
||||
const jitter = Math.random() * this.delay * 0.5;
|
||||
await this.sleep(this.delay + jitter);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RequestClient, DEFAULT_HEADERS };
|
||||
Reference in New Issue
Block a user