lepao-proxy-tls-test.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/env node
  2. 'use strict'
  3. /**
  4. * 在学校服务器上测试「直连 vs HTTP 代理」访问 lepao HTTPS 时 TLS 是否正常。
  5. *
  6. * 用法(在项目根 ic-ctbu-backend 下执行):
  7. * node scripts/lepao-proxy-tls-test.js direct
  8. * node scripts/lepao-proxy-tls-test.js proxy 103.217.191.35:20028
  9. * node scripts/lepao-proxy-tls-test.js qg-fetch # 用 config.json 调青果 /get 取 1 个节点再测
  10. *
  11. * 环境与线上一致时务必对齐「真实 API + POST」(仅测主页 GET 通过不代表 beforeRun 等接口经代理可用):
  12. * TEST_HTTPS_URL=https://lepao.ctbu.edu.cn/v3/api.php/Run2/beforeRunV260 TEST_POST=1 \\
  13. * node scripts/lepao-proxy-tls-test.js qg-fetch
  14. *
  15. * 环境变量(可选):
  16. * TEST_HTTPS_URL 默认 https://lepao.ctbu.edu.cn/
  17. * TEST_POST 若为 1,则 POST,body 为 {}
  18. * TEST_TIMEOUT_MS 默认 20000
  19. * QG_PROXY_USER / QG_PROXY_PASSWORD 覆盖 config 里 authUser/authPassword
  20. * QG_AREA / QG_AREA_EX / QG_ISP / QG_DISTINCT qg-fetch 时传给青果(与线上一致可用来复现)
  21. *
  22. * NODE_TLS_REJECT_UNAUTHORIZED=0 不推荐生产使用;设为 0 时本脚本与其它行为一致会做不安全 TLS
  23. */
  24. const path = require('path')
  25. const fs = require('fs')
  26. const axios = require('axios')
  27. const HttpsProxyAgent = require('https-proxy-agent')
  28. const QG_GET_URL = 'https://share.proxy.qg.net/get'
  29. function loadConfig() {
  30. const p = path.join(__dirname, '..', 'config.json')
  31. const raw = fs.readFileSync(p, 'utf8')
  32. return JSON.parse(raw)
  33. }
  34. function buildProxyUrl(server, authUser, authPassword) {
  35. const parts = String(server || '').trim().split(':')
  36. const host = parts[0]
  37. const port = parts[1]
  38. if (!host || !port) throw new Error('代理 server 需为 host:port')
  39. let userPart = ''
  40. if (authUser) {
  41. const u = encodeURIComponent(authUser)
  42. const p = encodeURIComponent(authPassword != null ? String(authPassword) : '')
  43. userPart = `${u}:${p}@`
  44. }
  45. return `http://${userPart}${host}:${port}`
  46. }
  47. async function probe(label, outbound) {
  48. const url = process.env.TEST_HTTPS_URL || 'https://lepao.ctbu.edu.cn/'
  49. const timeout = Number(process.env.TEST_TIMEOUT_MS || 20000)
  50. const usePost = String(process.env.TEST_POST || '').trim() === '1'
  51. const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'
  52. const base = {
  53. timeout,
  54. validateStatus: () => true,
  55. ...outbound
  56. }
  57. console.log(`\n---------- ${label} ----------`)
  58. console.log('REQUEST:', usePost ? 'POST' : 'GET', url)
  59. console.log('timeout_ms:', timeout, 'rejectUnauthorized:', rejectUnauthorized)
  60. const t0 = Date.now()
  61. try {
  62. const res = usePost
  63. ? await axios.post(url, {}, { headers: { 'Content-Type': 'application/json' }, ...base })
  64. : await axios.get(url, base)
  65. const ms = Date.now() - t0
  66. console.log('RESULT: OK', ms + 'ms', 'HTTP', res.status)
  67. const bodyPreview = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
  68. console.log('body_preview:', bodyPreview.slice(0, 300).replace(/\s+/g, ' '))
  69. } catch (e) {
  70. const ms = Date.now() - t0
  71. console.log('RESULT: FAIL', ms + 'ms')
  72. console.log('message:', e.message)
  73. console.log('code:', e.code)
  74. if (e.response) console.log('http_status:', e.response.status)
  75. }
  76. }
  77. async function fetchOneProxyFromQg(config) {
  78. const q = config.qgChannelProxy || {}
  79. const key = String(q.extractKey || '').trim()
  80. if (!key) throw new Error('config.json 缺少 qgChannelProxy.extractKey')
  81. const params = { key, num: 1 }
  82. const area = String(process.env.QG_AREA || '').trim()
  83. const areaEx = String(process.env.QG_AREA_EX || '').trim()
  84. if (area) params.area = area
  85. if (areaEx) params.area_ex = areaEx
  86. const isp = process.env.QG_ISP
  87. if (isp === '1' || isp === '2' || isp === '3') params.isp = Number(isp)
  88. if (process.env.QG_DISTINCT === '0') params.distinct = false
  89. else params.distinct = true
  90. const res = await axios.get(QG_GET_URL, {
  91. params,
  92. timeout: 20000,
  93. proxy: false,
  94. validateStatus: () => true
  95. })
  96. const body = res.data
  97. const code = body && body.code
  98. if (code !== 'SUCCESS' || !Array.isArray(body.data) || !body.data[0]) {
  99. throw new Error(`青果 /get 失败: ${code || JSON.stringify(body).slice(0, 240)}`)
  100. }
  101. const item = body.data[0]
  102. return {
  103. server: item.server,
  104. proxy_ip: item.proxy_ip,
  105. deadline: item.deadline,
  106. request_id: body.request_id
  107. }
  108. }
  109. async function main() {
  110. const mode = (process.argv[2] || 'direct').toLowerCase()
  111. const cfg = loadConfig()
  112. const q = cfg.qgChannelProxy || {}
  113. const authUser = process.env.QG_PROXY_USER || q.authUser || ''
  114. const authPassword = process.env.QG_PROXY_PASSWORD || q.authPassword || ''
  115. if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
  116. console.warn('[WARN] NODE_TLS_REJECT_UNAUTHORIZED=0,TLS 未校验服务端证书。\n')
  117. }
  118. await probe('① 直连(无代理)', { proxy: false })
  119. if (mode === 'direct') return
  120. let serverArg
  121. if (mode === 'proxy') {
  122. serverArg = process.argv[3]
  123. if (!serverArg) {
  124. console.error('用法: node scripts/lepao-proxy-tls-test.js proxy host:port')
  125. process.exit(1)
  126. }
  127. } else if (mode === 'qg-fetch') {
  128. console.log('\n[qg-fetch] 正在请求青果 /get …')
  129. const got = await fetchOneProxyFromQg(cfg)
  130. serverArg = got.server
  131. console.log(
  132. '青果节点:',
  133. got.server,
  134. 'proxy_ip=',
  135. got.proxy_ip,
  136. 'deadline=',
  137. got.deadline,
  138. 'request_id=',
  139. got.request_id
  140. )
  141. if (!serverArg) throw new Error('青果返回无 server')
  142. } else {
  143. console.error('未知模式:', mode, '可用: direct | proxy <host:port> | qg-fetch')
  144. process.exit(1)
  145. }
  146. const proxyUrl = buildProxyUrl(serverArg, authUser, authPassword)
  147. const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'
  148. const agent = new HttpsProxyAgent(proxyUrl, { rejectUnauthorized })
  149. await probe(`② HTTP 代理 → 学校 HTTPS (${serverArg})`, { proxy: false, httpsAgent: agent })
  150. }
  151. main().catch((e) => {
  152. console.error(e.stack || e)
  153. process.exit(1)
  154. })