|
|
@@ -0,0 +1,160 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+'use strict'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 测试通过 HTTP 代理访问 u.xxoo365.top(HTTPS)。
|
|
|
+ *
|
|
|
+ * 用法(项目根目录执行):
|
|
|
+ * node scripts/proxy-u-xxoo365-test.js direct
|
|
|
+ * node scripts/proxy-u-xxoo365-test.js proxy 103.217.191.15:30104
|
|
|
+ * node scripts/proxy-u-xxoo365-test.js qg-fetch
|
|
|
+ *
|
|
|
+ * 可选环境变量:
|
|
|
+ * TEST_URL=https://u.xxoo365.top/
|
|
|
+ * TEST_TIMEOUT_MS=20000
|
|
|
+ * TEST_METHOD=GET # GET / HEAD
|
|
|
+ * TEST_PATH=/health # 会拼到 TEST_URL 的 origin 后
|
|
|
+ * QG_AREA=... QG_AREA_EX=... QG_ISP=1|2|3 QG_DISTINCT=0|1
|
|
|
+ */
|
|
|
+
|
|
|
+const fs = require('fs')
|
|
|
+const path = require('path')
|
|
|
+const axios = require('axios')
|
|
|
+const HttpsProxyAgent = require('https-proxy-agent')
|
|
|
+
|
|
|
+const QG_GET_URL = 'https://share.proxy.qg.net/get'
|
|
|
+
|
|
|
+function readConfig() {
|
|
|
+ const cfgPath = path.join(__dirname, '..', 'config.json')
|
|
|
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
|
|
|
+}
|
|
|
+
|
|
|
+function buildTargetUrl() {
|
|
|
+ const base = (process.env.TEST_URL || 'https://u.xxoo365.top/').trim()
|
|
|
+ const pathOverride = (process.env.TEST_PATH || '').trim()
|
|
|
+ if (!pathOverride) return base
|
|
|
+ const u = new URL(base)
|
|
|
+ u.pathname = pathOverride.startsWith('/') ? pathOverride : `/${pathOverride}`
|
|
|
+ return u.toString()
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeMethod() {
|
|
|
+ const m = (process.env.TEST_METHOD || 'GET').toUpperCase()
|
|
|
+ return m === 'HEAD' ? 'HEAD' : 'GET'
|
|
|
+}
|
|
|
+
|
|
|
+function toProxyUrl(server, user, pass) {
|
|
|
+ const [host, portRaw] = String(server || '').trim().split(':')
|
|
|
+ const port = Number(portRaw)
|
|
|
+ if (!host || !Number.isFinite(port)) {
|
|
|
+ throw new Error(`代理地址非法: ${server}`)
|
|
|
+ }
|
|
|
+ let auth = ''
|
|
|
+ if (user) {
|
|
|
+ auth = `${encodeURIComponent(user)}:${encodeURIComponent(pass || '')}@`
|
|
|
+ }
|
|
|
+ return `http://${auth}${host}:${port}`
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchProxyFromQg(cfg) {
|
|
|
+ const q = cfg.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
|
|
|
+ if (['1', '2', '3'].includes(String(process.env.QG_ISP || ''))) {
|
|
|
+ params.isp = Number(process.env.QG_ISP)
|
|
|
+ }
|
|
|
+ params.distinct = String(process.env.QG_DISTINCT || '1') !== '0'
|
|
|
+
|
|
|
+ const res = await axios.get(QG_GET_URL, {
|
|
|
+ params,
|
|
|
+ timeout: 20000,
|
|
|
+ proxy: false,
|
|
|
+ validateStatus: () => true
|
|
|
+ })
|
|
|
+ const body = res.data || {}
|
|
|
+ if (body.code !== 'SUCCESS' || !Array.isArray(body.data) || !body.data[0]?.server) {
|
|
|
+ throw new Error(`青果 /get 失败: ${body.code || JSON.stringify(body).slice(0, 180)}`)
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ server: body.data[0].server,
|
|
|
+ proxy_ip: body.data[0].proxy_ip || null,
|
|
|
+ deadline: body.data[0].deadline || null,
|
|
|
+ request_id: body.request_id || null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function probe(label, requestConfig) {
|
|
|
+ const timeout = Number(process.env.TEST_TIMEOUT_MS || 20000)
|
|
|
+ const url = buildTargetUrl()
|
|
|
+ const method = normalizeMethod()
|
|
|
+ const t0 = Date.now()
|
|
|
+ console.log(`\n=== ${label} ===`)
|
|
|
+ console.log('REQUEST:', method, url)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await axios({
|
|
|
+ url,
|
|
|
+ method,
|
|
|
+ timeout,
|
|
|
+ validateStatus: () => true,
|
|
|
+ ...requestConfig
|
|
|
+ })
|
|
|
+ const ms = Date.now() - t0
|
|
|
+ console.log('RESULT: OK', `${ms}ms`, 'HTTP', res.status)
|
|
|
+ if (method !== 'HEAD') {
|
|
|
+ const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
|
|
|
+ console.log('BODY_PREVIEW:', body.slice(0, 200).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 main() {
|
|
|
+ const mode = (process.argv[2] || 'direct').toLowerCase()
|
|
|
+ const cfg = readConfig()
|
|
|
+ const q = cfg.qgChannelProxy || {}
|
|
|
+ const user = String(process.env.QG_PROXY_USER || q.authUser || '').trim()
|
|
|
+ const pass = String(process.env.QG_PROXY_PASSWORD || q.authPassword || '').trim()
|
|
|
+ const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'
|
|
|
+
|
|
|
+ if (mode === 'direct') {
|
|
|
+ await probe('直连', { proxy: false })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let server
|
|
|
+ if (mode === 'proxy') {
|
|
|
+ server = process.argv[3]
|
|
|
+ if (!server) throw new Error('proxy 模式需传 host:port')
|
|
|
+ } else if (mode === 'qg-fetch') {
|
|
|
+ const got = await fetchProxyFromQg(cfg)
|
|
|
+ server = got.server
|
|
|
+ console.log(
|
|
|
+ `[Qg] server=${got.server} proxy_ip=${got.proxy_ip || '—'} deadline=${got.deadline || '—'} request_id=${got.request_id || '—'}`
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ throw new Error('模式仅支持: direct | proxy <host:port> | qg-fetch')
|
|
|
+ }
|
|
|
+
|
|
|
+ const proxyUrl = toProxyUrl(server, user, pass)
|
|
|
+ const httpsAgent = new HttpsProxyAgent(proxyUrl, { rejectUnauthorized })
|
|
|
+ await probe(`代理 ${server}`, { proxy: false, httpsAgent })
|
|
|
+}
|
|
|
+
|
|
|
+main().catch(err => {
|
|
|
+ console.error(err.stack || err)
|
|
|
+ process.exit(1)
|
|
|
+})
|
|
|
+
|