#!/usr/bin/env node 'use strict' /** * 在学校服务器上测试「直连 vs HTTP 代理」访问 lepao HTTPS 时 TLS 是否正常。 * * 用法(在项目根 ic-ctbu-backend 下执行): * node scripts/lepao-proxy-tls-test.js direct * node scripts/lepao-proxy-tls-test.js proxy 103.217.191.35:20028 * node scripts/lepao-proxy-tls-test.js qg-fetch # 用 config.json 调青果 /get 取 1 个节点再测 * * 环境与线上一致时务必对齐「真实 API + POST」(仅测主页 GET 通过不代表 beforeRun 等接口经代理可用): * TEST_HTTPS_URL=https://lepao.ctbu.edu.cn/v3/api.php/Run2/beforeRunV260 TEST_POST=1 \\ * node scripts/lepao-proxy-tls-test.js qg-fetch * * 环境变量(可选): * TEST_HTTPS_URL 默认 https://lepao.ctbu.edu.cn/ * TEST_POST 若为 1,则 POST,body 为 {} * TEST_TIMEOUT_MS 默认 20000 * QG_PROXY_USER / QG_PROXY_PASSWORD 覆盖 config 里 authUser/authPassword * QG_AREA / QG_AREA_EX / QG_ISP / QG_DISTINCT qg-fetch 时传给青果(与线上一致可用来复现) * * NODE_TLS_REJECT_UNAUTHORIZED=0 不推荐生产使用;设为 0 时本脚本与其它行为一致会做不安全 TLS */ const path = require('path') const fs = require('fs') const axios = require('axios') const HttpsProxyAgent = require('https-proxy-agent') const QG_GET_URL = 'https://share.proxy.qg.net/get' function loadConfig() { const p = path.join(__dirname, '..', 'config.json') const raw = fs.readFileSync(p, 'utf8') return JSON.parse(raw) } function buildProxyUrl(server, authUser, authPassword) { const parts = String(server || '').trim().split(':') const host = parts[0] const port = parts[1] if (!host || !port) throw new Error('代理 server 需为 host:port') let userPart = '' if (authUser) { const u = encodeURIComponent(authUser) const p = encodeURIComponent(authPassword != null ? String(authPassword) : '') userPart = `${u}:${p}@` } return `http://${userPart}${host}:${port}` } async function probe(label, outbound) { const url = process.env.TEST_HTTPS_URL || 'https://lepao.ctbu.edu.cn/' const timeout = Number(process.env.TEST_TIMEOUT_MS || 20000) const usePost = String(process.env.TEST_POST || '').trim() === '1' const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0' const base = { timeout, validateStatus: () => true, ...outbound } console.log(`\n---------- ${label} ----------`) console.log('REQUEST:', usePost ? 'POST' : 'GET', url) console.log('timeout_ms:', timeout, 'rejectUnauthorized:', rejectUnauthorized) const t0 = Date.now() try { const res = usePost ? await axios.post(url, {}, { headers: { 'Content-Type': 'application/json' }, ...base }) : await axios.get(url, base) const ms = Date.now() - t0 console.log('RESULT: OK', ms + 'ms', 'HTTP', res.status) const bodyPreview = typeof res.data === 'string' ? res.data : JSON.stringify(res.data) console.log('body_preview:', bodyPreview.slice(0, 300).replace(/\s+/g, ' ')) } catch (e) { const ms = Date.now() - t0 console.log('RESULT: FAIL', ms + 'ms') console.log('message:', e.message) console.log('code:', e.code) if (e.response) console.log('http_status:', e.response.status) } } async function fetchOneProxyFromQg(config) { const q = config.qgChannelProxy || {} const key = String(q.extractKey || '').trim() if (!key) throw new Error('config.json 缺少 qgChannelProxy.extractKey') const params = { key, num: 1 } const area = String(process.env.QG_AREA || '').trim() const areaEx = String(process.env.QG_AREA_EX || '').trim() if (area) params.area = area if (areaEx) params.area_ex = areaEx const isp = process.env.QG_ISP if (isp === '1' || isp === '2' || isp === '3') params.isp = Number(isp) if (process.env.QG_DISTINCT === '0') params.distinct = false else params.distinct = true const res = await axios.get(QG_GET_URL, { params, timeout: 20000, proxy: false, validateStatus: () => true }) const body = res.data const code = body && body.code if (code !== 'SUCCESS' || !Array.isArray(body.data) || !body.data[0]) { throw new Error(`青果 /get 失败: ${code || JSON.stringify(body).slice(0, 240)}`) } const item = body.data[0] return { server: item.server, proxy_ip: item.proxy_ip, deadline: item.deadline, request_id: body.request_id } } async function main() { const mode = (process.argv[2] || 'direct').toLowerCase() const cfg = loadConfig() const q = cfg.qgChannelProxy || {} const authUser = process.env.QG_PROXY_USER || q.authUser || '' const authPassword = process.env.QG_PROXY_PASSWORD || q.authPassword || '' if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') { console.warn('[WARN] NODE_TLS_REJECT_UNAUTHORIZED=0,TLS 未校验服务端证书。\n') } await probe('① 直连(无代理)', { proxy: false }) if (mode === 'direct') return let serverArg if (mode === 'proxy') { serverArg = process.argv[3] if (!serverArg) { console.error('用法: node scripts/lepao-proxy-tls-test.js proxy host:port') process.exit(1) } } else if (mode === 'qg-fetch') { console.log('\n[qg-fetch] 正在请求青果 /get …') const got = await fetchOneProxyFromQg(cfg) serverArg = got.server console.log( '青果节点:', got.server, 'proxy_ip=', got.proxy_ip, 'deadline=', got.deadline, 'request_id=', got.request_id ) if (!serverArg) throw new Error('青果返回无 server') } else { console.error('未知模式:', mode, '可用: direct | proxy | qg-fetch') process.exit(1) } const proxyUrl = buildProxyUrl(serverArg, authUser, authPassword) const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0' const agent = new HttpsProxyAgent(proxyUrl, { rejectUnauthorized }) await probe(`② HTTP 代理 → 学校 HTTPS (${serverArg})`, { proxy: false, httpsAgent: agent }) } main().catch((e) => { console.error(e.stack || e) process.exit(1) })