lepao-real-proxy-test.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. #!/usr/bin/env node
  2. 'use strict'
  3. /**
  4. * 「加密表单 + postLepaoSchool(青果代理/回退)」探测乐跑 HTTPS 链路;报文格式与线上相同。
  5. *
  6. * 默认不写库不传 token:内置伪造 uid/token/学号,仅用于测 CONNECT/TLS/代理;业务上会返回登录失效属正常。
  7. * 也可用环境变量覆盖伪造字段(见 loadFakeAccount)。
  8. *
  9. * 用法(项目根):
  10. *
  11. * node scripts/lepao-real-proxy-test.js
  12. * node scripts/lepao-real-proxy-test.js --api getOssSts
  13. *
  14. * 真实账号(可选):
  15. * node scripts/lepao-real-proxy-test.js --student-num 2025402496
  16. * node scripts/lepao-real-proxy-test.js --student-num 2025402496 --create-user xxx
  17. * LEPAO_TEST_UID=… LEPAO_TEST_TOKEN=… … node scripts/lepao-real-proxy-test.js --env
  18. *
  19. * --timeout 20000 Charles: LEPAO_DEBUG_PROXY=1
  20. */
  21. const { URLSearchParams } = require('url')
  22. const db = require('../plugin/DataBase/db')
  23. const Redis = require('../plugin/DataBase/Redis')
  24. const { dataEncrypt, dataDecrypt, dataSign } = require('../plugin/Lepao/Crypto')
  25. const { postLepaoSchool } = require('../lib/Lepao/lepaoSchoolHttp')
  26. const DEFAULT_UA =
  27. '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'
  28. const BASE = 'https://lepao.ctbu.edu.cn'
  29. const ENDPOINTS = {
  30. beforeRun: {
  31. path: '/v3/api.php/Run2/beforeRunV260',
  32. buildRaw: ({ uid, token, school_id, student_num }) => ({
  33. uid: Number(uid),
  34. token,
  35. school_id: Number(school_id),
  36. term_id: 0,
  37. course_id: 0,
  38. class_id: 0,
  39. student_num,
  40. card_id: student_num,
  41. timestamp: Number((Date.now() / 1000).toFixed(3)),
  42. version: 1,
  43. nonce: String(Math.floor(Math.random() * 900000 + 100000)),
  44. ostype: 5
  45. })
  46. },
  47. getOssSts: {
  48. path: '/v3/api.php/WpIndex/getOssSts',
  49. buildRaw: ({ uid, token, school_id, student_num }) => ({
  50. uid: Number(uid),
  51. token,
  52. school_id: Number(school_id),
  53. term_id: 0,
  54. course_id: 0,
  55. class_id: 0,
  56. student_num,
  57. card_id: student_num,
  58. timestamp: Number((Date.now() / 1000).toFixed(3)),
  59. version: 1,
  60. nonce: String(Math.floor(Math.random() * 900000 + 100000)),
  61. ostype: 5
  62. })
  63. }
  64. }
  65. const logger = {
  66. info(...a) {
  67. console.log('[INFO]', new Date().toISOString(), ...a)
  68. },
  69. warn(...a) {
  70. console.warn('[WARN]', new Date().toISOString(), ...a)
  71. },
  72. error(...a) {
  73. console.error('[ERROR]', new Date().toISOString(), ...a)
  74. }
  75. }
  76. function parseArgs(argv) {
  77. const out = { help: false, api: 'beforeRun', studentNum: null, createUser: null, useEnv: false, timeout: 20000 }
  78. for (let i = 2; i < argv.length; i++) {
  79. const a = argv[i]
  80. if (a === '--help' || a === '-h') out.help = true
  81. else if (a === '--env') out.useEnv = true
  82. else if (a === '--student-num' && argv[i + 1]) out.studentNum = argv[++i]
  83. else if (a === '--create-user' && argv[i + 1]) out.createUser = argv[++i]
  84. else if (a.startsWith('--api=')) out.api = a.slice('--api='.length)
  85. else if (a === '--api' && argv[i + 1]) out.api = argv[++i]
  86. else if (a === '--timeout' && argv[i + 1]) out.timeout = Number(argv[++i])
  87. else if (!a.startsWith('-') && !out.studentNum) out.studentNum = a
  88. }
  89. return out
  90. }
  91. async function loadAccountFromDb(studentNum, createUser) {
  92. const conditionSql = createUser ? 'student_num = ? AND create_user = ?' : 'student_num = ?'
  93. const queryParams = createUser ? [studentNum, createUser] : [studentNum]
  94. const rows = await db.query(
  95. `SELECT uid, token, school_id, userAgent FROM lepao_account WHERE ${conditionSql} LIMIT 5`,
  96. queryParams
  97. )
  98. if (!rows || rows.length === 0) {
  99. throw new Error('数据库中未找到该学号对应的 lepao_account')
  100. }
  101. if (rows.length > 1 && !createUser) {
  102. throw new Error(`学号 ${studentNum} 存在多条记录,请附加 --create-user 指定其一`)
  103. }
  104. return { ...rows[0], student_num: studentNum }
  105. }
  106. function loadAccountFromEnv() {
  107. const uid = Number(process.env.LEPAO_TEST_UID)
  108. const token = String(process.env.LEPAO_TEST_TOKEN || '').trim()
  109. const school_id = Number(process.env.LEPAO_TEST_SCHOOL_ID)
  110. const student_num = String(process.env.LEPAO_TEST_STUDENT_NUM || '').trim()
  111. const userAgent = process.env.LEPAO_TEST_USER_AGENT?.trim()
  112. if (!uid || !token || !school_id || !student_num) {
  113. throw new Error(
  114. '请设置 LEPAO_TEST_UID LEPAO_TEST_TOKEN LEPAO_TEST_SCHOOL_ID LEPAO_TEST_STUDENT_NUM(可选 LEPAO_TEST_USER_AGENT)'
  115. )
  116. }
  117. return {
  118. uid,
  119. token,
  120. school_id,
  121. student_num,
  122. userAgent: userAgent || DEFAULT_UA
  123. }
  124. }
  125. function numberFromEnv(key, fallback) {
  126. const v = process.env[key]
  127. if (v === undefined || v === '') return fallback
  128. const n = Number(v)
  129. return Number.isFinite(n) ? n : fallback
  130. }
  131. /** 伪造字段,仅校验加密/签名封装与出站链路;学校端通常返回登录失效属预期 */
  132. function loadFakeAccount() {
  133. const uid = numberFromEnv('LEPAO_FAKE_UID', 1009001)
  134. let token = String(process.env.LEPAO_FAKE_TOKEN || '').trim()
  135. if (!token) token = 'FACEFACEFACEFACEFACEFACEFACEFACE'
  136. const school_id = numberFromEnv('LEPAO_FAKE_SCHOOL_ID', 201)
  137. let student_num = String(process.env.LEPAO_FAKE_STUDENT_NUM || '').trim()
  138. if (!student_num) student_num = '2099123456'
  139. const userAgent = String(process.env.LEPAO_FAKE_UA || process.env.LEPAO_TEST_USER_AGENT || DEFAULT_UA).trim()
  140. const account = { uid, token, school_id, student_num, userAgent }
  141. logger.info('[fake payload]', { uid: account.uid, school_id: account.school_id, student_num: account.student_num })
  142. return account
  143. }
  144. function buildEncryptedForm(raw) {
  145. raw.sign = dataSign(raw)
  146. const form = new URLSearchParams()
  147. form.append('ostype', '5')
  148. const enc = dataEncrypt(JSON.stringify(raw))
  149. if (!enc) throw new Error('dataEncrypt 失败')
  150. form.append('data', enc)
  151. return form
  152. }
  153. function printSummary(body) {
  154. const result = typeof body === 'object' && body !== null ? body : null
  155. if (!result) {
  156. console.log('响应:', String(body).slice(0, 500))
  157. return
  158. }
  159. console.log('status 字段:', result.status, 'info/msg:', result.info || result.msg)
  160. let dataOut = result.data
  161. if (result.is_encrypt === 1 && typeof dataOut === 'string') {
  162. const dec = dataDecrypt(dataOut)
  163. try {
  164. dataOut = JSON.parse(dec)
  165. } catch {
  166. dataOut = dec
  167. }
  168. console.log('data(已解密):', JSON.stringify(dataOut, null, 2).slice(0, 4000))
  169. } else {
  170. console.log('data:', JSON.stringify(dataOut, null, 2).slice(0, 4000))
  171. }
  172. }
  173. async function shutdownConnections() {
  174. await db.close().catch(() => {})
  175. try {
  176. if (Redis.isOpen) {
  177. await Redis.quit()
  178. }
  179. } catch (_) {
  180. /* ignore */
  181. }
  182. }
  183. const HELP_TEXT = `lepao-real-proxy-test.js
  184. 默认使用伪造账号(不写库),用于测代理/TLS/加密报文。
  185. node scripts/lepao-real-proxy-test.js
  186. node scripts/lepao-real-proxy-test.js --api getOssSts
  187. 伪造字段可用环境变量覆盖: LEPAO_FAKE_UID LEPAO_FAKE_TOKEN LEPAO_FAKE_SCHOOL_ID LEPAO_FAKE_STUDENT_NUM LEPAO_FAKE_UA
  188. 真实账号: --student-num <学号> [--create-user …] 或 --env + LEPAO_TEST_*
  189. 其它: --timeout <ms> LEPAO_DEBUG_PROXY=1`
  190. async function main() {
  191. try {
  192. const args = parseArgs(process.argv)
  193. if (args.help) {
  194. console.log(HELP_TEXT)
  195. throw new Error('HELP')
  196. }
  197. const ep = ENDPOINTS[args.api]
  198. if (!ep) {
  199. console.error(`未知 --api=${args.api},可选 beforeRun | getOssSts`)
  200. throw new Error('BAD_API')
  201. }
  202. let account
  203. if (args.useEnv) {
  204. account = loadAccountFromEnv()
  205. } else if (args.studentNum) {
  206. account = await loadAccountFromDb(args.studentNum, args.createUser)
  207. } else {
  208. account = loadFakeAccount()
  209. }
  210. const raw = ep.buildRaw({
  211. uid: account.uid,
  212. token: account.token,
  213. school_id: account.school_id,
  214. student_num: String(account.student_num)
  215. })
  216. const form = buildEncryptedForm(raw)
  217. const headers = {
  218. 'Content-Type': 'application/x-www-form-urlencoded',
  219. Accept: '*/*',
  220. 'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
  221. Referer: 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html',
  222. 'User-Agent': account.userAgent || DEFAULT_UA
  223. }
  224. const fullUrl = BASE + ep.path
  225. const dataTag = args.useEnv ? '[env]' : args.studentNum ? '[db]' : '[fake]'
  226. logger.info('接口:', args.api, fullUrl, dataTag, '学号:', String(account.student_num))
  227. const apiRes = await postLepaoSchool(fullUrl, form, {
  228. headers,
  229. timeout: args.timeout,
  230. logger
  231. })
  232. logger.info('axios HTTP 状态:', apiRes.status)
  233. if (!args.studentNum && !args.useEnv) {
  234. logger.info('提示: 伪造 token 时业务 status 非 1、提示重新登录等属于预期,关注是否出现 ECONNRESET/TLS 类错误')
  235. }
  236. printSummary(apiRes.data)
  237. } finally {
  238. await shutdownConnections()
  239. }
  240. }
  241. main()
  242. .then(() => process.exit(0))
  243. .catch(err => {
  244. const m = String(err.message)
  245. if (m === 'HELP') {
  246. process.exit(0)
  247. }
  248. if (m !== 'BAD_API') {
  249. console.error(err.stack || err)
  250. }
  251. process.exit(1)
  252. })