优化华为云网关脚本:支持容器环境、生产级WSGI、智能内存扫描
- 新增 systemd 检测,容器环境自动使用 nohup 模式 - 优先使用 waitress/gunicorn 生产服务器 - 智能进程优先级扫描 + 分段内存读取 - 多镜像源下载回退机制 - 增加健康检查端点 /health - 配置自动备份 + 端口有效性验证 - 虚拟环境支持避免 root pip 警告
This commit is contained in:
+524
-120
@@ -7,250 +7,631 @@ YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;36m'
|
||||
PLAIN='\033[0m'
|
||||
|
||||
# ================= 工具函数 =================
|
||||
log_info() { echo -e "${GREEN}[INFO]${PLAIN} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${PLAIN} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${PLAIN} $1"; }
|
||||
log_step() { echo -e "\n${BLUE}[${1}]${PLAIN} ${2}"; }
|
||||
|
||||
# 检查是否有 systemd
|
||||
has_systemd() {
|
||||
[ -d /run/systemd/system ] || [ -d /var/run/systemd/system ] || systemctl --version &>/dev/null
|
||||
}
|
||||
|
||||
# 安全的进程启动(支持 systemd 和 nohup 两种模式)
|
||||
safe_start_service() {
|
||||
local name=$1
|
||||
local cmd=$2
|
||||
local service_file=$3
|
||||
|
||||
if has_systemd; then
|
||||
systemctl daemon-reload 2>/dev/null
|
||||
systemctl enable "$name" --now 2>/dev/null
|
||||
log_info "$name 已通过 systemd 启动"
|
||||
else
|
||||
# 容器/无 systemd 环境:使用 nohup + 简单的 PID 文件管理
|
||||
local pid_file="/var/run/${name}.pid"
|
||||
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
||||
log_warn "$name 已在运行 (PID: $(cat "$pid_file"))"
|
||||
return 0
|
||||
fi
|
||||
nohup $cmd > "/var/log/${name}.log" 2>&1 &
|
||||
echo $! > "$pid_file"
|
||||
log_info "$name 已通过 nohup 启动 (PID: $!)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查服务状态
|
||||
get_service_status() {
|
||||
local name=$1
|
||||
if has_systemd; then
|
||||
systemctl is-active "$name" 2>/dev/null || echo "unknown"
|
||||
else
|
||||
local pid_file="/var/run/${name}.pid"
|
||||
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
||||
echo "active (nohup, PID: $(cat "$pid_file"))"
|
||||
else
|
||||
echo "inactive"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart_service() {
|
||||
local name=$1
|
||||
if has_systemd; then
|
||||
systemctl restart "$name" 2>/dev/null
|
||||
else
|
||||
local pid_file="/var/run/${name}.pid"
|
||||
[ -f "$pid_file" ] && kill "$(cat "$pid_file")" 2>/dev/null
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo -e "${BLUE}==================================================${PLAIN}"
|
||||
echo -e "${GREEN} 华为云 Token 动态网关 (6小时高能效缓存版) 部署 ${PLAIN}"
|
||||
echo -e "${BLUE}==================================================${PLAIN}"
|
||||
|
||||
# 1. 检查权限
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}错误:请使用 root 用户运行此脚本!${PLAIN}"
|
||||
log_error "请使用 root 用户运行此脚本!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检测环境类型
|
||||
IS_CONTAINER=false
|
||||
if [ -f /.dockerenv ] || grep -qE 'docker|containerd|kubepods' /proc/1/cgroup 2>/dev/null; then
|
||||
IS_CONTAINER=true
|
||||
log_warn "检测到容器环境,将使用 nohup 模式替代 systemd"
|
||||
fi
|
||||
|
||||
# ================= 检测并安装 frpc =================
|
||||
echo -e "\n${YELLOW}[1/5] 正在检测 frpc 环境...${PLAIN}"
|
||||
log_step "1/5" "正在检测 frpc 环境..."
|
||||
if ! command -v frpc &>/dev/null && [ ! -f "/usr/local/bin/frpc" ] && [ ! -f "/usr/bin/frpc" ]; then
|
||||
echo -e "${RED}未检测到 frpc!${PLAIN}"
|
||||
log_warn "未检测到 frpc!"
|
||||
read -p "是否需要为您自动下载并安装最新版 frpc (使用国内加速镜像)? (y/n, 默认 y): " INSTALL_FRP
|
||||
INSTALL_FRP=${INSTALL_FRP:-y}
|
||||
|
||||
if [[ "$INSTALL_FRP" == "y" || "$INSTALL_FRP" == "Y" ]]; then
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
FRP_ARCH="amd64"
|
||||
elif [ "$ARCH" = "aarch64" ]; then
|
||||
FRP_ARCH="arm64"
|
||||
else
|
||||
echo -e "${RED}错误:不支持的系统架构 $ARCH,请手动安装 frpc。${PLAIN}"
|
||||
case "$ARCH" in
|
||||
x86_64) FRP_ARCH="amd64" ;;
|
||||
aarch64) FRP_ARCH="arm64" ;;
|
||||
armv7l) FRP_ARCH="arm" ;;
|
||||
i386|i686) FRP_ARCH="386" ;;
|
||||
*)
|
||||
log_error "不支持的系统架构 $ARCH,请手动安装 frpc。"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
FRP_VERSION="0.61.0"
|
||||
log_info "开始通过国内节点下载 frpc v${FRP_VERSION} (${FRP_ARCH})..."
|
||||
|
||||
# 使用多个镜像源,自动回退
|
||||
MIRRORS=(
|
||||
"https://ghfast.top/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
|
||||
"https://mirror.ghproxy.com/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
|
||||
"https://github.moeyy.xyz/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
|
||||
)
|
||||
|
||||
DOWNLOADED=false
|
||||
for url in "${MIRRORS[@]}"; do
|
||||
log_info "尝试下载: $url"
|
||||
if wget --timeout=30 --tries=2 --no-check-certificate -O frp.tar.gz "$url" 2>/dev/null; then
|
||||
DOWNLOADED=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$DOWNLOADED" != "true" ]; then
|
||||
log_error "所有镜像源下载失败!请检查网络或手动安装 frpc。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FRP_VERSION="0.58.1"
|
||||
echo -e "${GREEN}开始通过国内节点下载 frpc v${FRP_VERSION} ($FRP_ARCH)...${PLAIN}"
|
||||
FRP_URL="https://mirror.ghproxy.com/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
|
||||
|
||||
wget --no-check-certificate -O frp.tar.gz "$FRP_URL"
|
||||
if [ $? -ne 0 ]; then echo -e "${RED}下载失败!${PLAIN}"; exit 1; fi
|
||||
|
||||
tar -zxvf frp.tar.gz > /dev/null
|
||||
cd frp_${FRP_VERSION}_linux_${FRP_ARCH} || { echo -e "${RED}错误:无法进入解压目录!${PLAIN}"; exit 1; }
|
||||
tar -zxf frp.tar.gz > /dev/null 2>&1 || { log_error "解压失败"; exit 1; }
|
||||
cd frp_${FRP_VERSION}_linux_${FRP_ARCH} || { log_error "无法进入解压目录"; exit 1; }
|
||||
cp frpc /usr/local/bin/ && chmod +x /usr/local/bin/frpc
|
||||
mkdir -p /etc/frp
|
||||
|
||||
if [ ! -f "/etc/frp/frpc.toml" ]; then
|
||||
cat << EOF > /etc/frp/frpc.toml
|
||||
cat << 'FRPCFG' > /etc/frp/frpc.toml
|
||||
serverAddr = "x.x.x.x"
|
||||
serverPort = 7000
|
||||
EOF
|
||||
auth.method = "token"
|
||||
auth.token = "your_token_here"
|
||||
FRPCFG
|
||||
log_warn "已生成 frpc 配置模板,请编辑 /etc/frp/frpc.toml 填入正确的服务器信息"
|
||||
fi
|
||||
|
||||
cat << EOF > /etc/systemd/system/frpc.service
|
||||
# 生成 systemd 服务文件(即使当前无 systemd,也保留以便后续使用)
|
||||
cat << 'FRPSVC' > /etc/systemd/system/frpc.service
|
||||
[Unit]
|
||||
Description=Frp Client Service
|
||||
After=network.target
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
StartLimitInterval=60s
|
||||
StartLimitBurst=3
|
||||
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
|
||||
ExecReload=/usr/local/bin/frpc reload -c /etc/frp/frpc.toml
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
FRPSVC
|
||||
|
||||
if has_systemd; then
|
||||
systemctl daemon-reload && systemctl enable frpc --now
|
||||
fi
|
||||
cd .. && rm -rf frp_${FRP_VERSION}_linux_${FRP_ARCH} frp.tar.gz
|
||||
echo -e "${GREEN}frpc 安装并注册成功!配置文件位于 /etc/frp/frpc.toml${PLAIN}"
|
||||
log_info "frpc 安装完成!配置文件位于 /etc/frp/frpc.toml"
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}系统已安装 frpc。${PLAIN}"
|
||||
log_info "系统已安装 frpc。"
|
||||
fi
|
||||
|
||||
# ================= 配置网关与映射端口 =================
|
||||
echo -e "\n${YELLOW}[2/5] 配置动态网关参数:${PLAIN}"
|
||||
log_step "2/5" "配置动态网关参数:"
|
||||
read -p "请输入 Python 动态网关的本地监听端口 (默认 8080): " GATEWAY_PORT
|
||||
GATEWAY_PORT=${GATEWAY_PORT:-8080}
|
||||
|
||||
read -p "请输入你想在 frps 公网端暴露的端口 (默认 18000): " REMOTE_PORT
|
||||
REMOTE_PORT=${REMOTE_PORT:-18000}
|
||||
|
||||
# 端口有效性检查
|
||||
if ! [[ "$GATEWAY_PORT" =~ ^[0-9]+$ ]] || [ "$GATEWAY_PORT" -lt 1 ] || [ "$GATEWAY_PORT" -gt 65535 ]; then
|
||||
log_error "无效的本地端口: $GATEWAY_PORT"
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ "$REMOTE_PORT" =~ ^[0-9]+$ ]] || [ "$REMOTE_PORT" -lt 1 ] || [ "$REMOTE_PORT" -gt 65535 ]; then
|
||||
log_error "无效的远程端口: $REMOTE_PORT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ================= 查找 frpc 配置文件 =================
|
||||
echo -e "\n${YELLOW}[3/5] 检测 frpc 配置文件位置以追加代理规则:${PLAIN}"
|
||||
log_step "3/5" "检测 frpc 配置文件位置以追加代理规则:"
|
||||
FRPC_CONFIG=""
|
||||
if [ -f "/etc/frp/frpc.toml" ]; then FRPC_CONFIG="/etc/frp/frpc.toml"; FRPC_TYPE="toml";
|
||||
elif [ -f "/etc/frp/frpc.ini" ]; then FRPC_CONFIG="/etc/frp/frpc.ini"; FRPC_TYPE="ini";
|
||||
else
|
||||
FRPC_PATH_SEARCH=$(find /etc /usr/local/etc /opt /home -maxdepth 4 -name "frpc.toml" -o -name "frpc.ini" 2>/dev/null | head -n 1)
|
||||
FRPC_TYPE=""
|
||||
|
||||
# 按优先级查找配置文件
|
||||
for path in "/etc/frp/frpc.toml" "/etc/frp/frpc.ini" "/usr/local/etc/frp/frpc.toml" "/usr/local/etc/frp/frpc.ini"; do
|
||||
if [ -f "$path" ]; then
|
||||
FRPC_CONFIG="$path"
|
||||
[[ "$path" == *.toml ]] && FRPC_TYPE="toml" || FRPC_TYPE="ini"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 如果没找到,尝试搜索(限制深度避免长时间等待)
|
||||
if [ -z "$FRPC_CONFIG" ]; then
|
||||
FRPC_PATH_SEARCH=$(find /etc /usr/local/etc /opt /home -maxdepth 3 \( -name "frpc.toml" -o -name "frpc.ini" \) 2>/dev/null | head -n 1)
|
||||
if [ -n "$FRPC_PATH_SEARCH" ]; then
|
||||
FRPC_CONFIG="$FRPC_PATH_SEARCH"
|
||||
if [[ "$FRPC_PATH_SEARCH" == *.toml ]]; then FRPC_TYPE="toml"; else FRPC_TYPE="ini"; fi
|
||||
[[ "$FRPC_PATH_SEARCH" == *.toml ]] && FRPC_TYPE="toml" || FRPC_TYPE="ini"
|
||||
fi
|
||||
fi
|
||||
|
||||
APPEND_FRP="n"
|
||||
if [ -n "$FRPC_CONFIG" ]; then
|
||||
echo -e "${GREEN}识别到 frpc 配置: $FRPC_CONFIG (${FRPC_TYPE})${PLAIN}"
|
||||
log_info "识别到 frpc 配置: $FRPC_CONFIG (${FRPC_TYPE})"
|
||||
read -p "是否自动将华为云转发规则追加到该文件? (y/n, 默认 y): " APPEND_FRP
|
||||
APPEND_FRP=${APPEND_FRP:-y}
|
||||
else
|
||||
APPEND_FRP="n"
|
||||
log_warn "未找到 frpc 配置文件,跳过自动追加"
|
||||
log_warn "请手动在 frpc 配置中添加以下规则:"
|
||||
echo -e "${YELLOW}--- TOML 格式 ---${PLAIN}"
|
||||
echo -e "[[proxies]]"
|
||||
echo -e "name = \"huawei-dynamic-gateway\""
|
||||
echo -e "type = \"tcp\""
|
||||
echo -e "local_ip = \"127.0.0.1\""
|
||||
echo -e "local_port = ${GATEWAY_PORT}"
|
||||
echo -e "remote_port = ${REMOTE_PORT}"
|
||||
fi
|
||||
|
||||
# ================= 安装 Python 依赖 =================
|
||||
echo -e "\n${YELLOW}[4/5] 部署 Python 运行环境...${PLAIN}"
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
if command -v apt &> /dev/null; then apt update -y && apt install -y python3 python3-pip;
|
||||
elif command -v yum &> /dev/null; then yum install -y python3 python3-pip; fi
|
||||
fi
|
||||
python3 -m pip install flask requests --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
log_step "4/5" "部署 Python 运行环境..."
|
||||
|
||||
# ================= 创建网关核心代码 (核心优化点) =================
|
||||
echo -e "\n${YELLOW}[5/5] 生成网关代码并启动服务...${PLAIN}"
|
||||
cat << 'EOF' > /usr/local/bin/huawei_gateway.py
|
||||
# 检查 python3
|
||||
PYTHON_CMD=""
|
||||
for cmd in python3 python3.11 python3.10 python3.9 python3.8; do
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
PYTHON_CMD="$cmd"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
log_warn "未找到 Python3,尝试自动安装..."
|
||||
if command -v apt &>/dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq python3 python3-pip
|
||||
elif command -v yum &>/dev/null; then
|
||||
yum install -y -q python3 python3-pip
|
||||
elif command -v apk &>/dev/null; then
|
||||
apk add --no-cache python3 py3-pip
|
||||
else
|
||||
log_error "无法自动安装 Python,请手动安装 python3 和 pip"
|
||||
exit 1
|
||||
fi
|
||||
PYTHON_CMD="python3"
|
||||
fi
|
||||
|
||||
# 检查 pip 并安装依赖
|
||||
if ! $PYTHON_CMD -m pip --version &>/dev/null 2>&1; then
|
||||
log_warn "pip 未安装,尝试安装..."
|
||||
$PYTHON_CMD -m ensurepip --upgrade 2>/dev/null || {
|
||||
if command -v apt &>/dev/null; then apt-get install -y -qq python3-pip; fi
|
||||
}
|
||||
fi
|
||||
|
||||
# 使用虚拟环境避免权限问题(可选)
|
||||
USE_VENV=false
|
||||
VENV_PATH="/opt/huawei-gateway/venv"
|
||||
if [ "$IS_CONTAINER" = true ] || [ "$EUID" -eq 0 ]; then
|
||||
read -p "是否使用虚拟环境安装 Python 依赖? (推荐, y/n, 默认 y): " USE_VENV_INPUT
|
||||
USE_VENV_INPUT=${USE_VENV_INPUT:-y}
|
||||
[[ "$USE_VENV_INPUT" == "y" || "$USE_VENV_INPUT" == "Y" ]] && USE_VENV=true
|
||||
fi
|
||||
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
mkdir -p /opt/huawei-gateway
|
||||
$PYTHON_CMD -m venv "$VENV_PATH" 2>/dev/null || {
|
||||
log_warn "无法创建虚拟环境,将使用系统 Python"
|
||||
USE_VENV=false
|
||||
}
|
||||
fi
|
||||
|
||||
PIP_CMD="$PYTHON_CMD -m pip"
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
PIP_CMD="$VENV_PATH/bin/pip"
|
||||
PYTHON_CMD="$VENV_PATH/bin/python"
|
||||
fi
|
||||
|
||||
# 安装依赖(使用国内镜像)
|
||||
log_info "正在安装 Flask 和 requests..."
|
||||
$PIP_CMD install --upgrade pip --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple 2>/dev/null
|
||||
$PIP_CMD install flask requests --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# ================= 创建网关核心代码 =================
|
||||
log_step "5/5" "生成网关代码并启动服务..."
|
||||
mkdir -p /usr/local/bin /var/log /var/run
|
||||
|
||||
cat << 'PYEOF' > /usr/local/bin/huawei_gateway.py
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
华为云 Token 动态网关
|
||||
- 6小时缓存机制
|
||||
- 支持内存扫描自动刷新
|
||||
- 401 自动重试
|
||||
- 兼容生产环境 (Waitress/Gunicorn)
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
import requests
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from flask import Flask, request, Response
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
app = Flask(__name__)
|
||||
logging.basicConfig(level=logging.WARNING,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
|
||||
# 内存缓存字典:6小时有效期,这里设置为安全线 5.5 小时 (19800秒)
|
||||
cache = {"token": None, "expires_at": 0}
|
||||
cache_lock = threading.Lock()
|
||||
|
||||
def scan_pid_mem(pid):
|
||||
# 尝试导入 requests,失败则给出明确提示
|
||||
try:
|
||||
with open(f'/proc/{pid}/maps', 'r') as f:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("错误:缺少 requests 模块。请运行: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
# ================= 配置 =================
|
||||
CACHE_TTL = 19800 # 5.5 小时(安全线)
|
||||
MAX_WORKERS = 8 # 内存扫描线程数
|
||||
MAX_MEM_SEGMENT = 200 * 1024 * 1024 # 单段最大扫描 200MB
|
||||
TOKEN_PATTERN = re.compile(b'Bearer ([A-Za-z0-9+/=_-]{100,})')
|
||||
TARGET_HOST = 'tokenhub.developer.huaweicloud.com'
|
||||
|
||||
# ================= 日志 =================
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('huawei-gateway')
|
||||
|
||||
# ================= 缓存 =================
|
||||
class TokenCache:
|
||||
def __init__(self):
|
||||
self._token = None
|
||||
self._expires_at = 0
|
||||
self._lock = threading.RLock()
|
||||
self._last_scan = 0
|
||||
self._scan_interval = 60 # 扫描间隔最小 60 秒
|
||||
|
||||
def get(self):
|
||||
with self._lock:
|
||||
now = time.time()
|
||||
if self._token and now < self._expires_at:
|
||||
return self._token
|
||||
return None
|
||||
|
||||
def set(self, token, ttl=CACHE_TTL):
|
||||
with self._lock:
|
||||
self._token = token
|
||||
self._expires_at = time.time() + ttl
|
||||
self._last_scan = time.time()
|
||||
|
||||
def is_scan_cooldown(self):
|
||||
with self._lock:
|
||||
return (time.time() - self._last_scan) < self._scan_interval
|
||||
|
||||
def clear(self):
|
||||
with self._lock:
|
||||
self._token = None
|
||||
self._expires_at = 0
|
||||
|
||||
|
||||
cache = TokenCache()
|
||||
|
||||
# ================= 内存扫描 =================
|
||||
def scan_pid_mem(pid):
|
||||
"""扫描单个进程的内存寻找 Token"""
|
||||
maps_path = f'/proc/{pid}/maps'
|
||||
mem_path = f'/proc/{pid}/mem'
|
||||
|
||||
if not os.path.exists(maps_path) or not os.path.exists(mem_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(maps_path, 'r') as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) < 2 or 'r' not in parts[1]: continue
|
||||
addrs = parts[0].split('-')
|
||||
if len(addrs) != 2: continue
|
||||
start, end = int(addrs[0], 16), int(addrs[1], 16)
|
||||
if (end - start) > 200 * 1024 * 1024: continue
|
||||
try:
|
||||
with open(f'/proc/{pid}/mem', 'rb') as mem:
|
||||
mem.seek(start)
|
||||
data = mem.read(end - start)
|
||||
for m in re.finditer(b'Bearer ([A-Za-z0-9+/=_-]+)', data):
|
||||
val = m.group(1).decode(errors='replace')
|
||||
if len(val) > 200:
|
||||
return val
|
||||
except Exception:
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
except Exception:
|
||||
perms = parts[1]
|
||||
if 'r' not in perms or 'w' not in perms: # 只扫描可读可写段(更精确)
|
||||
continue
|
||||
|
||||
addrs = parts[0].split('-')
|
||||
if len(addrs) != 2:
|
||||
continue
|
||||
|
||||
start = int(addrs[0], 16)
|
||||
end = int(addrs[1], 16)
|
||||
size = end - start
|
||||
|
||||
# 跳过过大或过小的段
|
||||
if size > MAX_MEM_SEGMENT or size < 1024:
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(mem_path, 'rb') as mem:
|
||||
mem.seek(start)
|
||||
# 分段读取避免大内存占用
|
||||
chunk_size = 64 * 1024
|
||||
remaining = size
|
||||
while remaining > 0:
|
||||
to_read = min(chunk_size, remaining)
|
||||
data = mem.read(to_read)
|
||||
if not data:
|
||||
break
|
||||
|
||||
for match in TOKEN_PATTERN.finditer(data):
|
||||
token = match.group(1).decode('ascii', errors='replace')
|
||||
# 验证 token 格式(华为云 token 通常是 JWT 格式)
|
||||
if len(token) > 200 and token.count('.') >= 2:
|
||||
return token
|
||||
|
||||
remaining -= len(data)
|
||||
except (PermissionError, OSError, ValueError):
|
||||
continue
|
||||
|
||||
except (PermissionError, OSError, ProcessLookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def force_refresh_token():
|
||||
"""强制重新从内存扫描 Token 并更新缓存时间"""
|
||||
|
||||
def find_token_in_memory():
|
||||
"""在所有进程中扫描 Token"""
|
||||
if cache.is_scan_cooldown() and cache.get():
|
||||
return cache.get()
|
||||
|
||||
try:
|
||||
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
results = executor.map(scan_pid_mem, pids)
|
||||
for token in results:
|
||||
except OSError:
|
||||
logger.error("无法访问 /proc 目录")
|
||||
return None
|
||||
|
||||
# 优先扫描常见进程(python, node, java 等可能持有 token 的进程)
|
||||
priority_pids = []
|
||||
other_pids = []
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
exe_path = os.readlink(f'/proc/{pid}/exe')
|
||||
if any(x in exe_path for x in ['python', 'node', 'java', 'chrome', 'electron']):
|
||||
priority_pids.append(pid)
|
||||
else:
|
||||
other_pids.append(pid)
|
||||
except (OSError, PermissionError):
|
||||
other_pids.append(pid)
|
||||
|
||||
# 先扫描优先级进程
|
||||
all_pids = priority_pids + other_pids
|
||||
|
||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||
futures = {executor.submit(scan_pid_mem, pid): pid for pid in all_pids}
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
token = future.result(timeout=5)
|
||||
if token:
|
||||
with cache_lock:
|
||||
cache["token"] = token
|
||||
cache["expires_at"] = time.time() + 19800 # 缓存 5.5 小时
|
||||
cache.set(token)
|
||||
logger.info(f"Token 已刷新 (来源 PID: {futures[future]})")
|
||||
return token
|
||||
return cache["token"]
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def get_latest_token():
|
||||
now = time.time()
|
||||
with cache_lock:
|
||||
# 如果缓存存在且没过期,直接返回缓存(消耗几乎为 0)
|
||||
if cache["token"] and now < cache["expires_at"]:
|
||||
return cache["token"]
|
||||
# 否则,或者过期了,才去扫描内存
|
||||
return force_refresh_token()
|
||||
return cache.get() # 返回可能过期的缓存作为兜底
|
||||
|
||||
@app.route('/v2/<path:subpath>', methods=['POST', 'GET', 'OPTIONS'])
|
||||
|
||||
# ================= Flask 应用 =================
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
"""健康检查端点"""
|
||||
token = cache.get()
|
||||
return {
|
||||
"status": "healthy",
|
||||
"token_cached": token is not None,
|
||||
"token_expires_in": max(0, cache._expires_at - time.time()) if hasattr(cache, '_expires_at') else 0
|
||||
}, 200
|
||||
|
||||
|
||||
@app.route('/v2/<path:subpath>', methods=['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE'])
|
||||
def proxy(subpath):
|
||||
if request.method == 'OPTIONS':
|
||||
return Response(), 200
|
||||
return Response(status=200, headers={
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
})
|
||||
|
||||
real_token = get_latest_token()
|
||||
real_token = find_token_in_memory()
|
||||
if not real_token:
|
||||
logging.error("未在内存中找到华为云Token")
|
||||
return {"error": "未在内存中找到华为云Token"}, 500
|
||||
logger.error("未在内存中找到华为云 Token")
|
||||
return {"error": "未在内存中找到华为云Token,请确保华为云相关应用正在运行"}, 500
|
||||
|
||||
# 构建请求头
|
||||
headers = {}
|
||||
for k, v in request.headers:
|
||||
kl = k.lower()
|
||||
if kl not in ('host', 'content-length', 'connection', 'accept-encoding'):
|
||||
headers[k] = v
|
||||
|
||||
headers = {k: v for k, v in request.headers if k.lower() != 'host'}
|
||||
headers['Authorization'] = f'Bearer {real_token}'
|
||||
headers['Host'] = 'tokenhub.developer.huaweicloud.com'
|
||||
headers['Host'] = TARGET_HOST
|
||||
|
||||
target_url = f'https://{TARGET_HOST}/v2/{subpath}'
|
||||
|
||||
target_url = f'https://tokenhub.developer.huaweicloud.com/v2/{subpath}'
|
||||
try:
|
||||
resp = requests.request(
|
||||
method=request.method, url=target_url, headers=headers,
|
||||
data=request.get_data(), cookies=request.cookies,
|
||||
allow_redirects=False, timeout=60
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
headers=headers,
|
||||
data=request.get_data(),
|
||||
cookies=request.cookies,
|
||||
allow_redirects=False,
|
||||
timeout=60,
|
||||
stream=False
|
||||
)
|
||||
|
||||
# 兜底保障:如果华为主端提早失效返回了 401,立即强制重新扫描内存并重试一次请求
|
||||
# 401 兜底:Token 可能提前过期,强制刷新重试一次
|
||||
if resp.status_code == 401:
|
||||
new_token = force_refresh_token()
|
||||
if new_token:
|
||||
logger.warning("收到 401,尝试强制刷新 Token 并重试...")
|
||||
cache.clear()
|
||||
new_token = find_token_in_memory()
|
||||
if new_token and new_token != real_token:
|
||||
headers['Authorization'] = f'Bearer {new_token}'
|
||||
resp = requests.request(
|
||||
method=request.method, url=target_url, headers=headers,
|
||||
data=request.get_data(), cookies=request.cookies,
|
||||
allow_redirects=False, timeout=60
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
headers=headers,
|
||||
data=request.get_data(),
|
||||
cookies=request.cookies,
|
||||
allow_redirects=False,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
return Response(resp.content, resp.status_code, resp.headers.items())
|
||||
# 构建响应
|
||||
response_headers = []
|
||||
for k, v in resp.headers.items():
|
||||
if k.lower() not in ('transfer-encoding', 'content-encoding', 'content-length'):
|
||||
response_headers.append((k, v))
|
||||
|
||||
return Response(
|
||||
resp.content,
|
||||
status=resp.status_code,
|
||||
headers=response_headers
|
||||
)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("请求华为云 API 超时")
|
||||
return {"error": "网关超时,请稍后重试"}, 504
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("无法连接到华为云 API")
|
||||
return {"error": "无法连接到华为云服务"}, 502
|
||||
except Exception as e:
|
||||
logging.error(f"网关转发失败: {traceback.format_exc()}")
|
||||
logger.error(f"网关转发失败: {traceback.format_exc()}")
|
||||
return {"error": f"网关转发失败: {str(e)}"}, 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
def main():
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
|
||||
app.run(host='127.0.0.1', port=port, debug=False)
|
||||
host = sys.argv[2] if len(sys.argv) > 2 else '127.0.0.1'
|
||||
|
||||
EOF
|
||||
# 尝试使用生产级 WSGI 服务器
|
||||
try:
|
||||
import waitress
|
||||
logger.info(f"使用 Waitress 启动网关 ({host}:{port})")
|
||||
waitress.serve(app, host=host, port=port, threads=16)
|
||||
except ImportError:
|
||||
try:
|
||||
import gunicorn.app.wsgiapp
|
||||
logger.info(f"使用 Gunicorn 启动网关 ({host}:{port})")
|
||||
os.execlp('gunicorn', 'gunicorn', '-w', '4', '-b', f'{host}:{port}', '--access-logfile', '-', 'huawei_gateway:app')
|
||||
except (ImportError, OSError):
|
||||
logger.warning("未安装 Waitress/Gunicorn,使用 Flask 开发服务器(建议生产环境安装 waitress)")
|
||||
logger.info(f"启动网关 ({host}:{port})")
|
||||
app.run(host=host, port=port, debug=False, threaded=True)
|
||||
|
||||
# 注册网关服务
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
PYEOF
|
||||
|
||||
chmod +x /usr/local/bin/huawei_gateway.py
|
||||
|
||||
# ================= 注册网关服务 =================
|
||||
if has_systemd; then
|
||||
cat << EOF > /etc/systemd/system/huawei-gateway.service
|
||||
[Unit]
|
||||
Description=Huawei Dynamic Gateway (6H Cache)
|
||||
After=network.target
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/huawei_gateway.py $GATEWAY_PORT
|
||||
ExecStart=$PYTHON_CMD /usr/local/bin/huawei_gateway.py $GATEWAY_PORT
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StartLimitInterval=60s
|
||||
StartLimitBurst=3
|
||||
StandardOutput=append:/var/log/huawei-gateway.log
|
||||
StandardError=append:/var/log/huawei-gateway.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
fi
|
||||
|
||||
systemctl daemon-reload && systemctl enable huawei-gateway --now &>/dev/null
|
||||
# 启动服务
|
||||
safe_start_service "huawei-gateway" "$PYTHON_CMD /usr/local/bin/huawei_gateway.py $GATEWAY_PORT" "/etc/systemd/system/huawei-gateway.service"
|
||||
|
||||
# ================= 追加 frpc 规则 =================
|
||||
if [[ "$APPEND_FRP" == "y" || "$APPEND_FRP" == "Y" ]] && [ -f "$FRPC_CONFIG" ]; then
|
||||
if [[ "$APPEND_FRP" == "y" || "$APPEND_FRP" == "Y" ]] && [ -n "$FRPC_CONFIG" ] && [ -f "$FRPC_CONFIG" ]; then
|
||||
if grep -q "huawei-dynamic-gateway" "$FRPC_CONFIG" 2>/dev/null; then
|
||||
echo -e "${YELLOW}检测到 frpc 配置中已存在 huawei-dynamic-gateway 规则,跳过追加。${PLAIN}"
|
||||
log_warn "frpc 配置中已存在 huawei-dynamic-gateway 规则,跳过追加"
|
||||
else
|
||||
# 备份原配置
|
||||
cp "$FRPC_CONFIG" "${FRPC_CONFIG}.backup.$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
if [ "$FRPC_TYPE" = "toml" ]; then
|
||||
cat << EOF >> "$FRPC_CONFIG"
|
||||
|
||||
@@ -271,18 +652,41 @@ local_port = $GATEWAY_PORT
|
||||
remote_port = $REMOTE_PORT
|
||||
EOF
|
||||
fi
|
||||
echo -e "${GREEN}frpc 代理规则已追加到 $FRPC_CONFIG${PLAIN}"
|
||||
log_info "frpc 代理规则已追加到 $FRPC_CONFIG"
|
||||
log_info "原配置已备份"
|
||||
fi
|
||||
|
||||
# 重启 frpc
|
||||
if has_systemd; then
|
||||
if systemctl list-units --type=service 2>/dev/null | grep -q "frpc"; then
|
||||
systemctl restart frpc 2>/dev/null && log_info "frpc 服务已重启"
|
||||
fi
|
||||
else
|
||||
restart_service "frpc"
|
||||
# 尝试直接启动 frpc
|
||||
if command -v frpc &>/dev/null; then
|
||||
nohup frpc -c "$FRPC_CONFIG" > /var/log/frpc.log 2>&1 &
|
||||
log_info "frpc 已通过 nohup 启动"
|
||||
fi
|
||||
fi
|
||||
if systemctl list-units --type=service | grep -q "frpc"; then systemctl restart frpc; fi
|
||||
fi
|
||||
|
||||
# ================= 部署总结 =================
|
||||
echo -e "\n${BLUE}==================================================${PLAIN}"
|
||||
echo -e "${GREEN} 🎉 6小时长效缓存网关部署完成 🎉 ${PLAIN}"
|
||||
echo -e "${BLUE}==================================================${PLAIN}"
|
||||
echo -e "1. 动态网关状态:$(systemctl is-active huawei-gateway)"
|
||||
echo -e "2. 您的本地一劳永逸调用地址:"
|
||||
echo -e "1. 动态网关状态:$(get_service_status huawei-gateway)"
|
||||
echo -e "2. 健康检查: http://127.0.0.1:${GATEWAY_PORT}/health"
|
||||
echo -e "3. 您的本地一劳永逸调用地址:"
|
||||
echo -e " - ${GREEN}API Base URL:${PLAIN} http://【你的公网frps_IP】:$REMOTE_PORT/v2"
|
||||
echo -e " - ${GREEN}API Key:${PLAIN} sk-anything"
|
||||
echo -e " - ${GREEN}Model:${PLAIN} glm-5.1"
|
||||
|
||||
if [ "$IS_CONTAINER" = true ]; then
|
||||
echo -e "\n${YELLOW}[容器环境提示]${PLAIN}"
|
||||
echo -e " - 服务通过 nohup 运行,日志位于 /var/log/huawei-gateway.log"
|
||||
echo -e " - 如需停止服务: kill \$(cat /var/run/huawei-gateway.pid)"
|
||||
echo -e " - 建议安装 waitress 提升性能: pip install waitress"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}==================================================${PLAIN}\n"
|
||||
Reference in New Issue
Block a user