| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- #!/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 个节点再测
- *
- * 环境变量(可选):
- * TEST_HTTPS_URL 默认 https://lepao.ctbu.edu.cn/(也可用具体 API,如 …/WpIndex/getOssSts)
- * TEST_POST 若为 1,则对 TEST_HTTPS_URL 发 POST(body 为空 JSON),更接近业务
- * 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 <host:port> | 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)
- })
|