| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- /**
- * 通过 RunForge-ProxyServer 的 /Proxy/Forward 发起出站 HTTP 请求。
- */
- const axios = require('axios')
- const { getRuntimeConfig } = require('./RuntimeConfig')
- async function getServerConfig() {
- let cfg = null
- try {
- cfg = await getRuntimeConfig('proxyForwardServer', { required: false, defaultValue: null })
- } catch (_) {
- return { url: '', enabled: false }
- }
- if (!cfg || typeof cfg !== 'object') return { url: '', enabled: false }
- return {
- url: String(cfg.url || '').trim().replace(/\/+$/, ''),
- enabled: cfg.enabled !== false,
- defaultTimeout: Number(cfg.timeout) || 120000
- }
- }
- async function isProxyForwardEnabled() {
- const { url, enabled } = await getServerConfig()
- return enabled && url.length > 0
- }
- function debugProxyEnabled() {
- return String(process.env.LEPAO_DEBUG_PROXY || '').trim() === '1'
- }
- function debugProxyAxiosFragment() {
- const host = process.env.LEPAO_DEBUG_PROXY_HOST || '127.0.0.1'
- const port = Number(process.env.LEPAO_DEBUG_PROXY_PORT || 9000)
- return {
- proxy: {
- host,
- port,
- protocol: 'http'
- }
- }
- }
- function briefUrlPath(fullUrl) {
- try {
- const u = new URL(fullUrl)
- return `${u.pathname}${u.search}`
- } catch {
- return fullUrl
- }
- }
- /** 转为可 JSON 序列化的纯对象,避免 AxiosHeaders 等类型丢失字段 */
- function normalizeForwardHeaders(headers) {
- if (headers == null) return {}
- let source = headers
- if (typeof headers === 'string') {
- try {
- source = JSON.parse(headers)
- } catch {
- return {}
- }
- }
- if (typeof source !== 'object' || Array.isArray(source)) return {}
- if (typeof source.toJSON === 'function') source = source.toJSON()
- const out = {}
- for (const [key, value] of Object.entries(source)) {
- if (value == null) continue
- out[String(key)] = Array.isArray(value) ? value.join(', ') : String(value)
- }
- return out
- }
- /**
- * URLSearchParams / Buffer 等无法被 JSON.stringify 正确序列化,需先转换再发给代理服务。
- */
- function serializeForwardBody(data) {
- if (data == null || data === '') return null
- if (typeof data === 'string') return data
- if (Buffer.isBuffer(data)) {
- return { __encoding: 'base64', data: data.toString('base64') }
- }
- if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) {
- return data.toString()
- }
- if (typeof data === 'object' && !Array.isArray(data)) return data
- return String(data)
- }
- function buildDirectAxiosConfig(extra = {}) {
- if (debugProxyEnabled()) {
- const { HttpsProxyAgent } = require('https-proxy-agent')
- const dbg = debugProxyAxiosFragment()
- const { host, port } = dbg.proxy
- const agent = new HttpsProxyAgent(`http://${host}:${port}`)
- return { proxy: false, httpAgent: agent, httpsAgent: agent, ...extra }
- }
- return { proxy: false, ...extra }
- }
- async function execDirect({ method, url, data, headers, timeout, validateStatus, responseType, transformResponse }) {
- const m = String(method || 'get').toLowerCase()
- const merged = {
- headers: headers || {},
- timeout,
- ...buildDirectAxiosConfig(),
- ...(validateStatus ? { validateStatus } : {}),
- ...(responseType ? { responseType } : {}),
- ...(transformResponse ? { transformResponse } : {})
- }
- return axios.request({ ...merged, method: m, url, data })
- }
- function assertProxyModeOk(outboundMode, meta) {
- if (outboundMode !== 'proxy') return
- if (meta?.fallback_direct) {
- const err = new Error('代理模式发生直连回退,任务中止')
- err.code = 'PROXY_REQUIRED_FALLBACK_DIRECT'
- err.retryable = true
- throw err
- }
- if (!meta?.used_proxy) {
- const err = new Error('代理模式未使用代理节点')
- err.code = 'PROXY_REQUIRED_NO_PROXY_USED'
- err.retryable = true
- throw err
- }
- }
- function toAxiosResponse(forwardData, responseType) {
- if (!forwardData || typeof forwardData !== 'object') return null
- const status = forwardData.status ?? forwardData.statusCode
- if (status == null) return null
- let data = forwardData.body
- if (data && typeof data === 'object' && data.__encoding === 'base64' && data.data) {
- data = Buffer.from(String(data.data), 'base64')
- }
- return {
- status,
- statusText: String(status),
- headers: forwardData.headers || {},
- data,
- config: {}
- }
- }
- function isValidForwardPayload(json) {
- return json && json.code === 0 && json.data != null && (json.data.status ?? json.data.statusCode) != null
- }
- /**
- * @param {{
- * method?: string
- * url: string
- * data?: any
- * headers?: object
- * timeout?: number
- * outboundMode?: 'auto'|'direct'|'proxy'
- * validateStatus?: Function
- * responseType?: string
- * transformResponse?: Function[]
- * logger?: object
- * logPrefix?: string
- * scene?: string
- * }} opts
- */
- async function forwardRequest(opts) {
- const {
- method = 'get',
- url,
- data,
- headers = {},
- timeout = 15000,
- outboundMode = 'auto',
- validateStatus,
- responseType,
- transformResponse,
- logger,
- logPrefix = '[ProxyForwardClient]',
- scene = 'outbound'
- } = opts
- const m = String(method).toLowerCase()
- const path = briefUrlPath(url)
- if (outboundMode === 'direct') {
- logger?.info?.(`${logPrefix} (${scene}) 直连 ${m.toUpperCase()} ${path}`)
- return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
- }
- if (!await isProxyForwardEnabled()) {
- logger?.info?.(`${logPrefix} (${scene}) 未配置代理服务,直连 ${path}`)
- if (outboundMode === 'proxy') {
- const err = new Error('未配置 proxyForwardServer.url')
- err.code = 'PROXY_REQUIRED_NOT_CONFIGURED'
- err.retryable = false
- throw err
- }
- return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
- }
- const { url: serverUrl, defaultTimeout } = await getServerConfig()
- logger?.info?.(`${logPrefix} (${scene}) 经代理服务 ${m.toUpperCase()} ${path}`)
- const reqHeaders = normalizeForwardHeaders(headers)
- const reqBody = serializeForwardBody(data)
- let forwardResp
- try {
- forwardResp = await axios.post(
- `${serverUrl}/Proxy/Forward`,
- {
- url,
- method: m.toUpperCase(),
- headers: reqHeaders,
- body: reqBody,
- timeout,
- ...(responseType ? { responseType } : {})
- },
- {
- timeout: Math.min(defaultTimeout, timeout + 10000),
- validateStatus: () => true,
- headers: { 'Content-Type': 'application/json' }
- }
- )
- } catch (e) {
- if (outboundMode === 'proxy') {
- const err = new Error(`代理服务不可达: ${e.message || e}`)
- err.code = 'PROXY_SERVER_UNREACHABLE'
- err.retryable = true
- throw err
- }
- logger?.warn?.(`${logPrefix} (${scene}) 代理服务请求失败,改直连: ${e.message || e}`)
- return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
- }
- const json = forwardResp.data || {}
- if (!isValidForwardPayload(json)) {
- const reason = json.code !== 0 ? (json.msg || 'unknown') : '代理响应缺少 status'
- if (outboundMode === 'proxy') {
- const err = new Error(json.msg || reason || '代理转发失败')
- err.code = 'PROXY_FORWARD_FAILED'
- err.retryable = true
- throw err
- }
- logger?.warn?.(`${logPrefix} (${scene}) 代理响应无效,改直连: ${reason}`)
- return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
- }
- const meta = json.data?.meta || {}
- assertProxyModeOk(outboundMode, meta)
- logger?.info?.(
- `${logPrefix} (${scene}) 完成 status=${json.data?.status ?? json.data?.statusCode} used_proxy=${meta.used_proxy} fallback=${meta.fallback_direct} ${meta.duration_ms}ms`
- )
- const axiosResp = toAxiosResponse(json.data, responseType)
- if (!axiosResp) {
- if (outboundMode === 'proxy') {
- const err = new Error('代理响应缺少 HTTP status')
- err.code = 'PROXY_FORWARD_BAD_RESPONSE'
- err.retryable = true
- throw err
- }
- logger?.warn?.(`${logPrefix} (${scene}) 代理响应缺少 status,改直连`)
- return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
- }
- if (validateStatus && !validateStatus(axiosResp.status)) {
- const err = new Error(`Request failed with status code ${axiosResp.status}`)
- err.response = axiosResp
- err.isAxiosError = true
- throw err
- }
- return axiosResp
- }
- module.exports = {
- forwardRequest,
- isProxyForwardEnabled,
- debugProxyEnabled,
- debugProxyAxiosFragment,
- briefUrlPath,
- buildDirectAxiosConfig,
- execDirect,
- getServerConfig,
- normalizeForwardHeaders,
- serializeForwardBody
- }
|