diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..afa4543 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Runtime data — generated by deploy.sh, consumed by app.py +# Copy this file to .env and adjust as needed, or export the vars before starting. + +# Flask session secret — MUST be changed in production! +# Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(48))" +SECRET_KEY=change-me-in-production + +# Bind address / port (gunicorn) +SQUIDMGR_HOST=0.0.0.0 +SQUIDMGR_PORT=5200 +SQUIDMGR_WORKERS=2 +SQUIDMGR_TIMEOUT=120 + +# Squid paths (override if squid is in a non-standard location) +# SQUID_BINARY=squid +# SQUID_CONF=/etc/squid/squid.conf +# SQUID_ACCESS_LOG=/var/log/squid/access.log +# SQUID_CACHE_LOG=/var/log/squid/cache.log + +# Session / security +SQUIDMGR_IDLE_TO=1800 +SQUIDMGR_ABS_TO=28800 +SQUIDMGR_THROTTLE_WIN=300 +SQUIDMGR_THROTTLE_MAX=5 +SQUIDMGR_THROTTLE_LOCK=900 +SQUIDMGR_CSRF=1 diff --git a/.gitignore b/.gitignore index 2ddf137..99b8cda 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,11 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -*.egg -*.egg-info/ -dist/ -build/ -.eggs/ -.installed.cfg -*.egg - -# Virtual envs -venv/ -env/ -.venv/ -ENV/ - -# pytest / coverage -.pytest_cache/ -.coverage -htmlcov/ -.tox/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo -.DS_Store - -# Logs -*.log - -# Local instance (sqlite db, uploaded files, generated certs) +# runtime data instance/ -uploads/ backups/ +uploads/ ssl_certs/ -alerts/ -instances/ -log_storage/ -security/ - -# Sample artifacts -*.pid -*.sock -*.pid.lock - -# Local config overrides +*.pyc +__pycache__/ +*.pyc +*.pyo .env -.env.local - -# OS -Thumbs.db +*.log diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..009fe38 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# ============================================================================= +# Squid Manager - 一键部署脚本 +# ============================================================================= +# 功能: +# 1. 安装系统依赖 (squid, python3, pip, gcc 等) +# 2. 创建 Python 虚拟环境并安装 pip 依赖 +# 3. 初始化数据库 & 默认管理员 +# 4. 生成 gunicorn + systemd 服务文件并启动 +# 5. 配置 squid (放行 localnet + 基本安全设置) +# +# 用法: +# bash deploy.sh # 全自动部署 (默认 /opt/squid-manager) +# INSTALL_DIR=/opt/sm bash deploy.sh # 自定义安装目录 +# bash deploy.sh --no-squid # 不安装/配置 squid +# bash deploy.sh --no-start # 不启动服务 (仅生成文件) +# ============================================================================= +set -euo pipefail + +# ---- 配置 ------------------------------------------------------------------- +INSTALL_DIR="${INSTALL_DIR:-/opt/squid-manager}" +SERVICE_NAME="${SERVICE_NAME:-squid-manager}" +GUNICORN_HOST="${SQUIDMGR_HOST:-0.0.0.0}" +GUNICORN_PORT="${SQUIDMGR_PORT:-5200}" +GUNICORN_WORKERS="${SQUIDMGR_WORKERS:-2}" +ADMIN_USER="${ADMIN_USER:-admin}" +ADMIN_PASS="${ADMIN_PASS:-admin123}" + +INSTALL_SQUID=true +START_SERVICE=true +SKIP_EXISTING=false # 若服务已存在是否跳过 + +for arg in "$@"; do + case "$arg" in + --no-squid) INSTALL_SQUID=false ;; + --no-start) START_SERVICE=false ;; + --skip-existing) SKIP_EXISTING=true ;; + -h|--help) + sed -n '2,20p' "$0" + exit 0 + ;; + *) + echo "未知参数: $arg" >&2; exit 1 ;; + esac +done + +# ---- 工具函数 --------------------------------------------------------------- +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +_generate_minimal_squid_conf() { + # 生成一份最小可用的 Squid 5.x 配置 + local out="$1" + cat > "$out" << 'SQUIDEOF' +# Minimal Squid 5.x configuration — generated by Squid Manager deploy.sh +# 可通过 Web UI (/config/raw) 继续编辑和扩展。 + +http_port 3128 +icp_port 3130 + +# Cache +cache_mem 256 MB +maximum_object_size 4096 KB +cache_dir ufs /var/spool/squid 100 16 256 + +# ACLs +acl localhost src 127.0.0.1/32 ::1 +acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1 +acl localnet src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 fc00::/7 fe80::/10 +acl Safe_ports port 80 443 21 70 210 1025-65535 +acl SSL_ports port 443 563 +acl CONNECT method CONNECT + +# Refresh patterns +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern ^gopher: 1440 0% 1440 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern . 0 20% 4320 + +# Access rules +http_access allow manager localhost +http_access deny manager +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost +http_access allow localnet +http_access deny all + +# Logging +access_log /var/log/squid/access.log squid +cache_log /var/log/squid/cache.log + +# Misc +coredump_dir /var/spool/squid +visible_hostname squid-manager +SQUIDEOF +} + +must_run_as_root() { + if [[ $EUID -ne 0 ]]; then + error "此脚本必须以 root 运行 (sudo $0)" + exit 1 + fi +} + +detect_os() { + if [[ -f /etc/os-release ]]; then + . /etc/os-release + OS_ID="$ID" + OS_VER="$VERSION_ID" + elif command -v apt-get &>/dev/null; then + OS_ID="debian" + elif command -v yum &>/dev/null; then + OS_ID="centos" + else + error "无法识别操作系统" + exit 1 + fi + info "操作系统: $OS_ID $OS_VER" +} + +pkg_install() { + info "安装系统包: $*" + case "$OS_ID" in + debian|ubuntu) + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq "$@" + ;; + centos|rhel|rocky|almalinux|ol) + yum install -y -q "$@" + ;; + *) + error "不支持的包管理器: $OS_ID" + exit 1 + ;; + esac +} + +# ---- 1. 前置检查 ------------------------------------------------------------ +must_run_as_root +detect_os + +info "安装目录: $INSTALL_DIR" +info "服务名: $SERVICE_NAME" +info "监听: $GUNICORN_HOST:$GUNICORN_PORT" + +# ---- 2. 安装系统依赖 -------------------------------------------------------- +SYS_PACKAGES=( + python3 python3-pip python3-venv python3-dev + gcc libffi-dev libssl-dev + curl +) + +if $INSTALL_SQUID; then + SYS_PACKAGES+=(squid squidclient) +fi + +pkg_install "${SYS_PACKAGES[@]}" + +# 确保 python3 / pip3 可用 +PYTHON_BIN="$(command -v python3)" +PIP_BIN="$(command -v pip3 || command -v pip)" +info "Python: $PYTHON_BIN" +info "Pip: $PIP_BIN" + +# ---- 3. 部署项目文件 -------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [[ "$SCRIPT_DIR" != "$INSTALL_DIR" ]]; then + if [[ -d "$INSTALL_DIR" ]] && $SKIP_EXISTING; then + warn "安装目录已存在且 --skip-existing,跳过文件复制" + else + info "复制项目文件到 $INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + # 复制源码(排除 .git / instance / backups 等运行时目录) + rsync -a --delete \ + --exclude='.git/' \ + --exclude='__pycache__/' \ + --exclude='*.pyc' \ + --exclude='instance/*.db' \ + --exclude='backups/' \ + --exclude='uploads/' \ + --exclude='ssl_certs/*.pem' \ + --exclude='.env' \ + "$SCRIPT_DIR/" "$INSTALL_DIR/" + fi +fi + +cd "$INSTALL_DIR" + +# 确保运行时目录存在 +mkdir -p instance backups uploads ssl_certs +chmod 700 instance ssl_certs + +# ---- 4. 创建虚拟环境 + 安装 pip 依赖 ---------------------------------------- +VENV_DIR="$INSTALL_DIR/.venv" + +if [[ -d "$VENV_DIR" ]] && $SKIP_EXISTING; then + warn "虚拟环境已存在且 --skip-existing,跳过依赖安装" +else + info "创建 Python 虚拟环境: $VENV_DIR" + $PYTHON_BIN -m venv "$VENV_DIR" + + info "升级 pip + 安装 Python 依赖" + "$VENV_DIR/bin/pip" install --upgrade pip setuptools wheel + "$VENV_DIR/bin/pip" install -r requirements.txt +fi + +PYTHON="$VENV_DIR/bin/python" +GUNICORN="$VENV_DIR/bin/gunicorn" + +# ---- 5. 生成 .env 配置 ------------------------------------------------------ +ENV_FILE="$INSTALL_DIR/.env" +if [[ ! -f "$ENV_FILE" ]] || ! $SKIP_EXISTING; then + info "生成 .env 配置文件" + SECRET_KEY="$($PYTHON -c "import secrets; print(secrets.token_urlsafe(48))")" + cat > "$ENV_FILE" </dev/null; then + warn "现有 squid.conf 语法校验失败,备份后用最小可用配置替换" + cp "$SQUID_CONF" "${SQUID_CONF}.broken.$(date +%s)" + _generate_minimal_squid_conf "$SQUID_CONF" + fi + else + info "squid.conf 不存在,生成最小可用配置" + mkdir -p "$(dirname "$SQUID_CONF")" + _generate_minimal_squid_conf "$SQUID_CONF" + fi + + if [[ -f "$SQUID_CONF" ]]; then + info "配置 Squid" + + # 备份原始配置 + if [[ ! -f "${SQUID_CONF}.orig" ]]; then + cp "$SQUID_CONF" "${SQUID_CONF}.orig" + info "已备份原始配置到 ${SQUID_CONF}.orig" + fi + + # 放行内网 (localnet) + # 1) 确保 localnet ACL 存在 + if grep -qE '^acl[[:space:]]+localnet' "$SQUID_CONF"; then + info "localnet ACL 已存在" + else + _localnet_def="acl localnet src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 fc00::/7 fe80::/10" + # 找到第一个 http_access 行之前插入 + if grep -n '^http_access' "$SQUID_CONF" | head -1 | cut -d: -f1 >/dev/null; then + _insert_line=$(grep -n '^http_access' "$SQUID_CONF" | head -1 | cut -d: -f1) + sed -i "${_insert_line}i${_localnet_def}" "$SQUID_CONF" + else + echo "$_localnet_def" >> "$SQUID_CONF" + fi + info "已添加 localnet ACL 定义" + fi + + # 2) 确保 http_access allow localnet 在 deny all 之前 + if grep -q '^http_access allow localnet' "$SQUID_CONF"; then + info "http_access allow localnet 已存在" + else + if grep -q '^http_access deny all' "$SQUID_CONF"; then + sed -i '/^http_access deny all/i http_access allow localnet' "$SQUID_CONF" + else + echo "http_access allow localnet" >> "$SQUID_CONF" + echo "http_access deny all" >> "$SQUID_CONF" + fi + info "已添加 http_access allow localnet" + fi + + # 3) 处理注释掉的老配置 + if grep -q '^# http_access allow localnet' "$SQUID_CONF"; then + sed -i 's/^# http_access allow localnet/http_access allow localnet/' "$SQUID_CONF" + fi + + # visible_hostname + if ! grep -q '^visible_hostname' "$SQUID_CONF"; then + echo "visible_hostname $(hostname -f 2>/dev/null || hostname)" >> "$SQUID_CONF" + fi + + # 语法校验 + if squid -k parse -f "$SQUID_CONF" &>/dev/null; then + info "Squid 配置语法校验通过" + else + warn "Squid 配置语法校验有警告,请检查 squid -k parse 输出" + squid -k parse -f "$SQUID_CONF" 2>&1 | tail -10 + fi + + # 初始化缓存目录 (首次启动需要 squid -z) + if [[ ! -d /var/spool/squid/00 ]]; then + info "初始化 Squid 缓存目录" + squid -z 2>/dev/null || true + chown -R proxy:proxy /var/spool/squid 2>/dev/null || true + fi + + # 启动 squid 服务 + if command -v systemctl &>/dev/null; then + systemctl enable --now squid 2>/dev/null || { + warn "squid 服务启动失败,请手动检查 (systemctl status squid)" + } + if systemctl is-active --quiet squid; then + info "Squid 服务运行正常 ✓" + fi + fi + else + warn "未生成 squid.conf,跳过 squid 配置" + fi +fi + +# ---- 8. 生成 systemd 服务文件 ---------------------------------------------- +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +info "生成 systemd 服务文件: $SERVICE_FILE" +cat > "$SERVICE_FILE" <:${GUNICORN_PORT}" +echo " 账号: ${ADMIN_USER} / ${ADMIN_PASS}" +echo " 安装目录: ${INSTALL_DIR}" +echo " 服务名: ${SERVICE_NAME}" +echo " 配置文件: ${INSTALL_DIR}/.env" +echo "" +echo " 常用命令:" +echo " systemctl status $SERVICE_NAME # 查看状态" +echo " systemctl restart $SERVICE_NAME # 重启服务" +echo " journalctl -u $SERVICE_NAME -f # 实时日志" +echo "" +echo " ⚠️ 请立即登录后修改默认密码!" +echo "================================================================" diff --git a/gunicorn_config.py b/gunicorn_config.py new file mode 100644 index 0000000..2915bc3 --- /dev/null +++ b/gunicorn_config.py @@ -0,0 +1,34 @@ +"""Gunicorn configuration for Squid Manager. + +Usage: + gunicorn -c gunicorn_config.py wsgi:app + +All values can be overridden via environment variables: + SQUIDMGR_HOST bind address (default: 0.0.0.0) + SQUIDMGR_PORT bind port (default: 5200) + SQUIDMGR_WORKERS worker count (default: 2) + SQUIDMGR_TIMEOUT worker timeout (default: 120) +""" +import multiprocessing +import os + +_host = os.environ.get("SQUIDMGR_HOST", "0.0.0.0") +_port = os.environ.get("SQUIDMGR_PORT", "5200") +_workers = int(os.environ.get("SQUIDMGR_WORKERS", 2)) +_timeout = int(os.environ.get("SQUIDMGR_TIMEOUT", 120)) + +bind = f"{_host}:{_port}" +workers = _workers +timeout = _timeout +worker_class = "sync" + +# Logging +accesslog = "-" # stdout → journald +errorlog = "-" # stderr → journald +loglevel = "info" + +# Preload app to share memory & fail fast on import errors +preload_app = True + +# PID file (for health-check scripts) +pidfile = os.path.join(os.path.dirname(os.path.abspath(__file__)), "instance", "gunicorn.pid") diff --git a/requirements.txt b/requirements.txt index b975f93..2958d9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,8 @@ Flask-SQLAlchemy==3.1.1 SQLAlchemy==2.0.30 Werkzeug==3.0.3 gunicorn==22.0.0 -# P3-3: WebSSH terminal +python-dotenv>=1.0.0 paramiko>=2.10.0 flask-sock>=0.7.0 +# TLS cert management / ssl_certs module +cryptography>=41.0.0 diff --git a/wsgi.py b/wsgi.py index 5aeef51..281a57a 100644 --- a/wsgi.py +++ b/wsgi.py @@ -7,10 +7,19 @@ Or via systemd unit. """ import os -# Ensure instance dir exists before app imports (it references the SQLite path) +# Ensure runtime dirs exist before app imports (it references SQLite path etc.) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -os.makedirs(os.path.join(BASE_DIR, "instance"), exist_ok=True) -os.makedirs(os.path.join(BASE_DIR, "backups"), exist_ok=True) +for _d in ("instance", "backups", "uploads", "ssl_certs"): + os.makedirs(os.path.join(BASE_DIR, _d), exist_ok=True) + +# Optional: load .env file if python-dotenv is available +try: + from dotenv import load_dotenv + _env_path = os.path.join(BASE_DIR, ".env") + if os.path.isfile(_env_path): + load_dotenv(_env_path) +except ImportError: + pass from app import app, db, seed_admin