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