| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- #!/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 <ms> 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)
- })
|