|
|
@@ -0,0 +1,279 @@
|
|
|
+#!/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)
|
|
|
+ })
|