#!/usr/bin/env node 'use strict' /** * 「加密表单 + postLepaoSchool(青果代理/回退)」探测乐跑 HTTPS 链路;报文格式与线上相同。 * * 默认不写库不传 token:内置伪造 uid/token/学号,仅用于测 CONNECT/TLS/代理;业务上会返回登录失效属正常。 * 也可用环境变量覆盖伪造字段(见 loadFakeAccount)。 * * 用法(项目根): * * node scripts/lepao-real-proxy-test.js * node scripts/lepao-real-proxy-test.js --api getOssSts * * 真实账号(可选): * node scripts/lepao-real-proxy-test.js --student-num 2025402496 * node scripts/lepao-real-proxy-test.js --student-num 2025402496 --create-user xxx * LEPAO_TEST_UID=… LEPAO_TEST_TOKEN=… … node scripts/lepao-real-proxy-test.js --env * * --timeout 20000 Charles: LEPAO_DEBUG_PROXY=1 */ const { URLSearchParams } = require('url') const db = require('../plugin/DataBase/db') const Redis = require('../plugin/DataBase/Redis') const { dataEncrypt, dataDecrypt, dataSign } = require('../plugin/Lepao/Crypto') const { postLepaoSchool } = require('../lib/Lepao/lepaoSchoolHttp') const DEFAULT_UA = 'Mozilla/5.0 (Linux; Android 16; 2211133C Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36' const BASE = 'https://lepao.ctbu.edu.cn' const ENDPOINTS = { beforeRun: { path: '/v3/api.php/Run2/beforeRunV260', buildRaw: ({ uid, token, school_id, student_num }) => ({ uid: Number(uid), token, school_id: Number(school_id), term_id: 0, course_id: 0, class_id: 0, student_num, card_id: student_num, timestamp: Number((Date.now() / 1000).toFixed(3)), version: 1, nonce: String(Math.floor(Math.random() * 900000 + 100000)), ostype: 5 }) }, getOssSts: { path: '/v3/api.php/WpIndex/getOssSts', buildRaw: ({ uid, token, school_id, student_num }) => ({ uid: Number(uid), token, school_id: Number(school_id), term_id: 0, course_id: 0, class_id: 0, student_num, card_id: student_num, timestamp: Number((Date.now() / 1000).toFixed(3)), version: 1, nonce: String(Math.floor(Math.random() * 900000 + 100000)), ostype: 5 }) } } const logger = { info(...a) { console.log('[INFO]', new Date().toISOString(), ...a) }, warn(...a) { console.warn('[WARN]', new Date().toISOString(), ...a) }, error(...a) { console.error('[ERROR]', new Date().toISOString(), ...a) } } function parseArgs(argv) { const out = { help: false, api: 'beforeRun', studentNum: null, createUser: null, useEnv: false, timeout: 20000 } for (let i = 2; i < argv.length; i++) { const a = argv[i] if (a === '--help' || a === '-h') out.help = true else if (a === '--env') out.useEnv = true else if (a === '--student-num' && argv[i + 1]) out.studentNum = argv[++i] else if (a === '--create-user' && argv[i + 1]) out.createUser = argv[++i] else if (a.startsWith('--api=')) out.api = a.slice('--api='.length) else if (a === '--api' && argv[i + 1]) out.api = argv[++i] else if (a === '--timeout' && argv[i + 1]) out.timeout = Number(argv[++i]) else if (!a.startsWith('-') && !out.studentNum) out.studentNum = a } return out } async function loadAccountFromDb(studentNum, createUser) { const conditionSql = createUser ? 'student_num = ? AND create_user = ?' : 'student_num = ?' const queryParams = createUser ? [studentNum, createUser] : [studentNum] const rows = await db.query( `SELECT uid, token, school_id, userAgent FROM lepao_account WHERE ${conditionSql} LIMIT 5`, queryParams ) if (!rows || rows.length === 0) { throw new Error('数据库中未找到该学号对应的 lepao_account') } if (rows.length > 1 && !createUser) { throw new Error(`学号 ${studentNum} 存在多条记录,请附加 --create-user 指定其一`) } return { ...rows[0], student_num: studentNum } } function loadAccountFromEnv() { const uid = Number(process.env.LEPAO_TEST_UID) const token = String(process.env.LEPAO_TEST_TOKEN || '').trim() const school_id = Number(process.env.LEPAO_TEST_SCHOOL_ID) const student_num = String(process.env.LEPAO_TEST_STUDENT_NUM || '').trim() const userAgent = process.env.LEPAO_TEST_USER_AGENT?.trim() if (!uid || !token || !school_id || !student_num) { throw new Error( '请设置 LEPAO_TEST_UID LEPAO_TEST_TOKEN LEPAO_TEST_SCHOOL_ID LEPAO_TEST_STUDENT_NUM(可选 LEPAO_TEST_USER_AGENT)' ) } return { uid, token, school_id, student_num, userAgent: userAgent || DEFAULT_UA } } function numberFromEnv(key, fallback) { const v = process.env[key] if (v === undefined || v === '') return fallback const n = Number(v) return Number.isFinite(n) ? n : fallback } /** 伪造字段,仅校验加密/签名封装与出站链路;学校端通常返回登录失效属预期 */ function loadFakeAccount() { const uid = numberFromEnv('LEPAO_FAKE_UID', 1009001) let token = String(process.env.LEPAO_FAKE_TOKEN || '').trim() if (!token) token = 'FACEFACEFACEFACEFACEFACEFACEFACE' const school_id = numberFromEnv('LEPAO_FAKE_SCHOOL_ID', 201) let student_num = String(process.env.LEPAO_FAKE_STUDENT_NUM || '').trim() if (!student_num) student_num = '2099123456' const userAgent = String(process.env.LEPAO_FAKE_UA || process.env.LEPAO_TEST_USER_AGENT || DEFAULT_UA).trim() const account = { uid, token, school_id, student_num, userAgent } logger.info('[fake payload]', { uid: account.uid, school_id: account.school_id, student_num: account.student_num }) return account } function buildEncryptedForm(raw) { raw.sign = dataSign(raw) const form = new URLSearchParams() form.append('ostype', '5') const enc = dataEncrypt(JSON.stringify(raw)) if (!enc) throw new Error('dataEncrypt 失败') form.append('data', enc) return form } function printSummary(body) { const result = typeof body === 'object' && body !== null ? body : null if (!result) { console.log('响应:', String(body).slice(0, 500)) return } console.log('status 字段:', result.status, 'info/msg:', result.info || result.msg) let dataOut = result.data if (result.is_encrypt === 1 && typeof dataOut === 'string') { const dec = dataDecrypt(dataOut) try { dataOut = JSON.parse(dec) } catch { dataOut = dec } console.log('data(已解密):', JSON.stringify(dataOut, null, 2).slice(0, 4000)) } else { console.log('data:', JSON.stringify(dataOut, null, 2).slice(0, 4000)) } } async function shutdownConnections() { await db.close().catch(() => {}) try { if (Redis.isOpen) { await Redis.quit() } } catch (_) { /* ignore */ } } const HELP_TEXT = `lepao-real-proxy-test.js 默认使用伪造账号(不写库),用于测代理/TLS/加密报文。 node scripts/lepao-real-proxy-test.js node scripts/lepao-real-proxy-test.js --api getOssSts 伪造字段可用环境变量覆盖: LEPAO_FAKE_UID LEPAO_FAKE_TOKEN LEPAO_FAKE_SCHOOL_ID LEPAO_FAKE_STUDENT_NUM LEPAO_FAKE_UA 真实账号: --student-num <学号> [--create-user …] 或 --env + LEPAO_TEST_* 其它: --timeout LEPAO_DEBUG_PROXY=1` async function main() { try { const args = parseArgs(process.argv) if (args.help) { console.log(HELP_TEXT) throw new Error('HELP') } const ep = ENDPOINTS[args.api] if (!ep) { console.error(`未知 --api=${args.api},可选 beforeRun | getOssSts`) throw new Error('BAD_API') } let account if (args.useEnv) { account = loadAccountFromEnv() } else if (args.studentNum) { account = await loadAccountFromDb(args.studentNum, args.createUser) } else { account = loadFakeAccount() } const raw = ep.buildRaw({ uid: account.uid, token: account.token, school_id: account.school_id, student_num: String(account.student_num) }) const form = buildEncryptedForm(raw) const headers = { 'Content-Type': 'application/x-www-form-urlencoded', Accept: '*/*', 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', Referer: 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html', 'User-Agent': account.userAgent || DEFAULT_UA } const fullUrl = BASE + ep.path const dataTag = args.useEnv ? '[env]' : args.studentNum ? '[db]' : '[fake]' logger.info('接口:', args.api, fullUrl, dataTag, '学号:', String(account.student_num)) const apiRes = await postLepaoSchool(fullUrl, form, { headers, timeout: args.timeout, logger }) logger.info('axios HTTP 状态:', apiRes.status) if (!args.studentNum && !args.useEnv) { logger.info('提示: 伪造 token 时业务 status 非 1、提示重新登录等属于预期,关注是否出现 ECONNRESET/TLS 类错误') } printSummary(apiRes.data) } finally { await shutdownConnections() } } main() .then(() => process.exit(0)) .catch(err => { const m = String(err.message) if (m === 'HELP') { process.exit(0) } if (m !== 'BAD_API') { console.error(err.stack || err) } process.exit(1) })