Browse Source

✨ feat: 增加ip自显功能

Pchen0 1 hour ago
parent
commit
17bdb1e710
3 changed files with 174 additions and 1 deletions
  1. 121 0
      apis/Corn/ProxySelfCheck.js
  2. 39 0
      apis/Corn/ProxySelfIpEcho.js
  3. 14 1
      lib/Lepao/lepaoProxyLogDisplay.js

+ 121 - 0
apis/Corn/ProxySelfCheck.js

@@ -0,0 +1,121 @@
+const axios = require('axios')
+const HttpsProxyAgent = require('https-proxy-agent')
+const config = require('../../config.json')
+const API = require('../../lib/API')
+const { BaseStdResponse } = require('../../BaseStdResponse')
+const QgProxyManager = require('../../lib/Lepao/QgProxyManager')
+
+function buildAxiosOutboundConfig(fragment) {
+    if (!fragment || fragment.proxy === false || !fragment.proxy) {
+        return { proxy: false }
+    }
+    const { host, port, auth } = fragment.proxy
+    let userPart = ''
+    if (auth && String(auth.username || '').length > 0) {
+        const u = encodeURIComponent(auth.username)
+        const p = encodeURIComponent(auth.password != null ? String(auth.password) : '')
+        userPart = `${u}:${p}@`
+    }
+    const proxyUrl = `http://${userPart}${host}:${port}`
+    const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'
+    return {
+        proxy: false,
+        httpsAgent: new HttpsProxyAgent(proxyUrl, { rejectUnauthorized })
+    }
+}
+
+function resolveSelfEchoUrl() {
+    const base = String(config.url || '').trim()
+    if (!base) return `http://127.0.0.1:${config.port}/Corn/ProxySelfIpEcho`
+    return `${base.replace(/\/+$/, '')}/Corn/ProxySelfIpEcho`
+}
+
+class ProxySelfCheck extends API {
+    constructor() {
+        super()
+        this.noEncrypt()
+        this.setPath('/Corn/ProxySelfCheck')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        try {
+            const qgOn = await QgProxyManager.isOutboundProxyEnabled()
+            if (!qgOn) {
+                await QgProxyManager.recordLog({
+                    event: 'proxy_self_check_skip',
+                    detail: { reason: 'proxy_disabled' }
+                })
+                return res.json({
+                    ...BaseStdResponse.OK,
+                    data: { skipped: true, reason: 'proxy_disabled' }
+                })
+            }
+
+            const frag = await QgProxyManager.getOutboundAxiosFragment({ forceRefresh: false })
+            if (frag.proxy === false) {
+                await QgProxyManager.recordLog({
+                    event: 'proxy_self_check_skip',
+                    detail: { reason: 'no_proxy_available' }
+                })
+                return res.json({
+                    ...BaseStdResponse.OK,
+                    data: { skipped: true, reason: 'no_proxy_available' }
+                })
+            }
+
+            const outbound = buildAxiosOutboundConfig(frag)
+            const echoUrl = resolveSelfEchoUrl()
+            const rsp = await axios.get(echoUrl, {
+                timeout: 15000,
+                validateStatus: () => true,
+                ...outbound
+            })
+
+            const body = rsp.data || {}
+            const now = Date.now()
+            const cached = await QgProxyManager.getCachedParsed()
+            const proxyIp = body?.data?.ip || null
+
+            await QgProxyManager.recordLog({
+                event: 'proxy_self_check',
+                server: cached?.server || `${frag.proxy.host}:${frag.proxy.port}`,
+                deadline: cached?.deadline || null,
+                detail: {
+                    code: body?.code,
+                    http_status: rsp.status,
+                    target: echoUrl,
+                    proxy_ip: proxyIp,
+                    x_forwarded_for: body?.data?.x_forwarded_for || '',
+                    checked_at: now
+                }
+            })
+
+            return res.json({
+                ...BaseStdResponse.OK,
+                data: {
+                    proxy_ip: proxyIp,
+                    http_status: rsp.status,
+                    target: echoUrl
+                }
+            })
+        } catch (e) {
+            const msg = e?.message || String(e)
+            await QgProxyManager.recordLog({
+                event: 'proxy_self_check_fail',
+                detail: {
+                    message: msg,
+                    code: e?.code,
+                    status: e?.response?.status
+                }
+            })
+            this.logger?.error?.(`[ProxySelfCheck] ${e?.stack || e}`)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: `代理自检失败: ${msg}`
+            })
+        }
+    }
+}
+
+module.exports.ProxySelfCheck = ProxySelfCheck

+ 39 - 0
apis/Corn/ProxySelfIpEcho.js

@@ -0,0 +1,39 @@
+const API = require('../../lib/API')
+const { BaseStdResponse } = require('../../BaseStdResponse')
+
+function getClientIp(req) {
+    let ip = null
+    if (req.headers['x-forwarded-for']) {
+        ip = String(req.headers['x-forwarded-for']).split(',')[0].trim()
+    } else if (req.headers['x-real-ip']) {
+        ip = String(req.headers['x-real-ip']).trim()
+    } else {
+        ip = req.connection?.remoteAddress || req.socket?.remoteAddress || ''
+    }
+
+    if (String(ip).startsWith('::ffff:')) ip = String(ip).replace('::ffff:', '')
+    return ip || '0.0.0.0'
+}
+
+class ProxySelfIpEcho extends API {
+    constructor() {
+        super()
+        this.noEncrypt()
+        this.setPath('/Corn/ProxySelfIpEcho')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        return res.json({
+            ...BaseStdResponse.OK,
+            data: {
+                ip: getClientIp(req),
+                x_forwarded_for: req.headers['x-forwarded-for'] || '',
+                x_real_ip: req.headers['x-real-ip'] || '',
+                remote_address: req.connection?.remoteAddress || req.socket?.remoteAddress || ''
+            }
+        })
+    }
+}
+
+module.exports.ProxySelfIpEcho = ProxySelfIpEcho

+ 14 - 1
lib/Lepao/lepaoProxyLogDisplay.js

@@ -15,7 +15,10 @@ const EVENT_META = {
     fetch: { label: '提取 IP', color: 'green' },
     invalidate: { label: '作废缓存', color: 'orangered' },
     fallback_direct: { label: '回退直连', color: 'red' },
-    config_change: { label: '配置变更', color: 'arcoblue' }
+    config_change: { label: '配置变更', color: 'arcoblue' },
+    proxy_self_check: { label: '代理自检', color: 'purple' },
+    proxy_self_check_skip: { label: '自检跳过', color: 'gray' },
+    proxy_self_check_fail: { label: '自检失败', color: 'red' }
 }
 
 function summarizeLogRow(record) {
@@ -48,6 +51,16 @@ function summarizeLogRow(record) {
         if (d.distinct_extract !== undefined) lines.push(`去重提取:${d.distinct_extract ? '是' : '否'}`)
         if (d.invalidate_cache) lines.push('已勾选清空服务端 IP 缓存')
         if (d.operator) lines.push(`操作者 UUID:${d.operator}`)
+    } else if (event === 'proxy_self_check') {
+        if (d.proxy_ip) lines.push(`代理出口IP:${d.proxy_ip}`)
+        if (d.http_status !== undefined) lines.push(`HTTP:${d.http_status}`)
+        if (d.target) lines.push(`目标:${d.target}`)
+    } else if (event === 'proxy_self_check_skip') {
+        if (d.reason) lines.push(`原因:${d.reason}`)
+    } else if (event === 'proxy_self_check_fail') {
+        if (d.message) lines.push(`说明:${d.message}`)
+        if (d.code) lines.push(`错误码:${d.code}`)
+        if (d.status !== undefined) lines.push(`HTTP:${d.status}`)
     } else if (Object.keys(d).length) {
         if (d._text) lines.push(String(d._text))
         else {