diff --git a/backend/server.js b/backend/server.js index befc27a..0b76136 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2329,6 +2329,115 @@ app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res) } }); +// ============================================ +// 管理员:上传工具管理 +// ============================================ + +// 检查上传工具是否存在 +app.get('/api/admin/check-upload-tool', authMiddleware, adminMiddleware, (req, res) => { + try { + const toolPath = path.join(__dirname, '..', 'upload-tool', 'dist', '玩玩云上传工具.exe'); + + if (fs.existsSync(toolPath)) { + const stats = fs.statSync(toolPath); + const sizeMB = (stats.size / (1024 * 1024)).toFixed(2); + + res.json({ + success: true, + exists: true, + fileInfo: { + path: toolPath, + size: stats.size, + sizeMB: sizeMB, + modifiedAt: stats.mtime + } + }); + } else { + res.json({ + success: true, + exists: false, + message: '上传工具不存在' + }); + } + } catch (error) { + console.error('检查上传工具失败:', error); + res.status(500).json({ + success: false, + message: '检查失败: ' + error.message + }); + } +}); + +// 上传工具文件 +const uploadToolStorage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadDir = path.join(__dirname, '..', 'upload-tool', 'dist'); + // 确保目录存在 + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + // 固定文件名 + cb(null, '玩玩云上传工具.exe'); + } +}); + +const uploadTool = multer({ + storage: uploadToolStorage, + limits: { + fileSize: 100 * 1024 * 1024 // 限制100MB + }, + fileFilter: (req, file, cb) => { + // 只允许.exe文件 + if (!file.originalname.toLowerCase().endsWith('.exe')) { + return cb(new Error('只允许上传.exe文件')); + } + cb(null, true); + } +}); + +app.post('/api/admin/upload-tool', authMiddleware, adminMiddleware, uploadTool.single('file'), (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: '请选择要上传的文件' + }); + } + + const fileSizeMB = (req.file.size / (1024 * 1024)).toFixed(2); + + // 验证文件大小(至少20MB,上传工具通常很大) + if (req.file.size < 20 * 1024 * 1024) { + // 删除上传的文件 + fs.unlinkSync(req.file.path); + return res.status(400).json({ + success: false, + message: '文件大小异常,上传工具通常大于20MB' + }); + } + + console.log(`[上传工具] 管理员上传成功: ${fileSizeMB}MB`); + + res.json({ + success: true, + message: '上传工具已上传', + fileInfo: { + size: req.file.size, + sizeMB: fileSizeMB + } + }); + } catch (error) { + console.error('上传工具失败:', error); + res.status(500).json({ + success: false, + message: '上传失败: ' + error.message + }); + } +}); + // 分享页面访问路由 app.get("/s/:code", (req, res) => { const shareCode = req.params.code; diff --git a/frontend/app.html b/frontend/app.html index 3d163c7..b110986 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -1005,7 +1005,7 @@ -
+

SFTP配置

请配置SFTP服务器 @@ -1381,6 +1381,73 @@
+ + +
+

+ 上传工具管理 +

+ + +
+
+
+
+
+ 上传工具已存在 +
+
+ 文件大小: {{ uploadToolStatus.fileInfo.sizeMB }} MB +
+
+ 最后修改: {{ formatDate(uploadToolStatus.fileInfo.modifiedAt) }} +
+
+ +
+
+ +
+
+ 上传工具不存在 +
+
+ 普通用户将无法下载上传工具,请上传工具文件 +
+
+
+ + +
+ + + + + +
+ + +
+
+ 说明: +
    +
  • 上传工具文件应为 .exe 格式,大小通常在 20-50 MB
  • +
  • 上传后,普通用户可以在设置页面下载该工具
  • +
  • 如果安装脚本下载失败,可以在这里手动上传
  • +
+
+
+
diff --git a/frontend/app.js b/frontend/app.js index 6a46f76..a41a548 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -171,7 +171,15 @@ createApp({ }, // 定期检查用户配置更新的定时器 - profileCheckInterval: null + profileCheckInterval: null, + + // 上传工具管理 + uploadToolStatus: null, // 上传工具状态 { exists, fileInfo: { size, sizeMB, modifiedAt } } + checkingUploadTool: false, // 是否正在检测上传工具 + uploadingTool: false, // 是否正在上传工具 + + // 强制显示SFTP配置(用于本地存储模式下临时显示SFTP配置) + forceSftpConfigVisible: false }; }, @@ -293,7 +301,21 @@ handleDragLeave(e) { this.localQuota = this.user.local_storage_quota || 0; this.localUsed = this.user.local_storage_used || 0; - console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType); + console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'SFTP配置:', this.user.has_ftp_config); + + // 智能存储类型修正:如果当前是SFTP但未配置,且用户有本地存储权限,自动切换到本地 + if (this.storageType === 'sftp' && !this.user.has_ftp_config) { + if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { + console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储'); + this.storageType = 'local'; + // 异步更新到后端(不等待,避免阻塞登录流程) + axios.post( + `${this.apiBase}/api/user/switch-storage`, + { storage_type: 'local' }, + { headers: { Authorization: `Bearer ${this.token}` } } + ).catch(err => console.error('[登录] 自动切换存储类型失败:', err)); + } + } // 启动定期检查用户配置 this.startProfileSync(); @@ -369,6 +391,28 @@ handleDragLeave(e) { alert('SFTP配置已保存!'); // 更新用户信息 this.user.has_ftp_config = 1; + + // 如果用户有 user_choice 权限,自动切换到 SFTP 存储 + if (this.storagePermission === 'user_choice' || this.storagePermission === 'sftp_only') { + try { + const switchResponse = await axios.post( + `${this.apiBase}/api/user/switch-storage`, + { storage_type: 'sftp' }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (switchResponse.data.success) { + this.storageType = 'sftp'; + console.log('[SFTP配置] 已自动切换到SFTP存储模式'); + } + } catch (err) { + console.error('[SFTP配置] 自动切换存储模式失败:', err); + } + } + + // 重置强制显示标志 + this.forceSftpConfigVisible = false; + // 刷新到文件页面 this.currentView = 'files'; this.loadFiles('/'); @@ -1511,7 +1555,6 @@ handleDragLeave(e) { console.error('加载用户资料失败:', error); } }, - // 启动定期检查用户配置 startProfileSync() { // 清除已有的定时器 @@ -1538,12 +1581,28 @@ handleDragLeave(e) { } }, - // 用户切换存储方式 + // 用户切换存储方式 async switchStorage(type) { // 检查是否尝试切换到SFTP但未配置 if (type === 'sftp' && !this.user.has_ftp_config) { - this.showToast('warning', '提示', '请先在设置页面配置SFTP信息'); - this.currentView = 'settings'; + const goToSettings = confirm('您还未配置SFTP服务器。\n\n是否现在前往设置页面进行配置?配置完成后将自动切换到SFTP存储。'); + if (goToSettings) { + // 直接设置视图并加载配置,避免switchView的重复检查 + this.currentView = 'settings'; + // 强制显示SFTP配置区域 + this.forceSftpConfigVisible = true; + // 如果是普通用户,手动加载SFTP配置 + if (this.user && !this.user.is_admin) { + this.loadFtpConfig(); + } + // 等待DOM更新后滚动到SFTP配置区域 + this.$nextTick(() => { + const sftpSection = document.getElementById('sftp-config-section'); + if (sftpSection) { + sftpSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + } return; } @@ -1582,6 +1641,11 @@ handleDragLeave(e) { this.currentView = view; + // 离开settings页面时,重置强制显示SFTP配置的标志 + if (this.currentView !== 'settings') { + this.forceSftpConfigVisible = false; + } + // 根据视图类型自动加载对应数据 switch (view) { case 'files': @@ -1762,7 +1826,92 @@ handleDragLeave(e) { this.showToast('error', '错误', '更新系统设置失败'); } } - }, +, + + // ===== 上传工具管理 ===== + + // 检测上传工具是否存在 + async checkUploadTool() { + this.checkingUploadTool = true; + try { + const response = await axios.get( + `${this.apiBase}/api/admin/check-upload-tool`, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.uploadToolStatus = response.data; + if (response.data.exists) { + this.showToast('success', '检测完成', '上传工具文件存在'); + } else { + this.showToast('warning', '提示', '上传工具文件不存在,请上传'); + } + } + } catch (error) { + console.error('检测上传工具失败:', error); + this.showToast('error', '错误', '检测失败: ' + (error.response?.data?.message || error.message)); + } finally { + this.checkingUploadTool = false; + } + }, + + // 处理上传工具文件 + async handleUploadToolFile(event) { + const file = event.target.files[0]; + if (!file) return; + + // 验证文件类型 + if (!file.name.toLowerCase().endsWith('.exe')) { + this.showToast('error', '错误', '只能上传 .exe 文件'); + event.target.value = ''; + return; + } + + // 验证文件大小(至少20MB) + const minSizeMB = 20; + const fileSizeMB = file.size / (1024 * 1024); + if (fileSizeMB < minSizeMB) { + this.showToast('error', '错误', `文件大小过小(${fileSizeMB.toFixed(2)}MB),上传工具通常大于${minSizeMB}MB`); + event.target.value = ''; + return; + } + + // 确认上传 + if (!confirm(`确定要上传 ${file.name} (${fileSizeMB.toFixed(2)}MB) 吗?`)) { + event.target.value = ''; + return; + } + + this.uploadingTool = true; + + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await axios.post( + `${this.apiBase}/api/admin/upload-tool`, + formData, + { + headers: { + Authorization: `Bearer ${this.token}`, + 'Content-Type': 'multipart/form-data' + } + } + ); + + if (response.data.success) { + this.showToast('success', '成功', '上传工具已上传'); + // 重新检测 + await this.checkUploadTool(); + } + } catch (error) { + console.error('上传工具失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '上传失败'); + } finally { + this.uploadingTool = false; + event.target.value = ''; // 清空input,允许重复上传 + } + } }, mounted() { // 阻止全局拖拽默认行为(防止拖到区域外打开新页面) diff --git a/install.sh b/install.sh index 5a9a0ee..867e06b 100644 --- a/install.sh +++ b/install.sh @@ -16,6 +16,8 @@ elif [[ "$1" == "--update" ]] || [[ "$1" == "--upgrade" ]] || [[ "$1" == "update MODE="update" elif [[ "$1" == "--repair" ]] || [[ "$1" == "--fix" ]] || [[ "$1" == "repair" ]]; then MODE="repair" +elif [[ "$1" == "--ssl" ]] || [[ "$1" == "--cert" ]] || [[ "$1" == "ssl" ]]; then + MODE="ssl" fi # 颜色定义 @@ -648,9 +650,10 @@ install_pm2() { } ################################################################################ -# 端口检测和配置 +# 智能端口检测和配置 ################################################################################ +# 检查端口是否可用(保留用于兼容性) check_port_available() { local port=$1 if command -v netstat &> /dev/null; then @@ -665,22 +668,188 @@ check_port_available() { return 0 # 端口可用 } +# 智能检测端口状态和占用进程 +check_port_status() { + local port=$1 + + # 1. 检查端口是否被监听 + if command -v netstat &> /dev/null; then + if ! netstat -tuln 2>/dev/null | grep -q ":${port} "; then + echo "available" + return 0 + fi + elif command -v ss &> /dev/null; then + if ! ss -tuln 2>/dev/null | grep -q ":${port} "; then + echo "available" + return 0 + fi + else + echo "available" + return 0 + fi + + # 2. 端口被占用,检查是什么进程 + local process="" + + if command -v netstat &> /dev/null; then + process=$(netstat -tulnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f2 | head -1) + fi + + if [[ -z "$process" ]] && command -v ss &> /dev/null; then + # 使用sed替代grep -oP以提高兼容性 + process=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*users:(("\([^"]*\)".*/\1/p' | head -1) + fi + + # 3. 根据进程返回状态(始终返回0以避免set -e导致脚本退出) + if [[ -z "$process" ]]; then + # 无法获取进程名(可能权限不足) + echo "occupied" + elif [[ "$process" == "nginx" ]] || [[ "$process" =~ ^nginx: ]]; then + # Nginx占用 + echo "nginx" + elif [[ "$process" == "apache2" ]] || [[ "$process" == "httpd" ]] || [[ "$process" =~ apache ]]; then + # Apache占用 + echo "apache" + else + # 其他进程 + echo "other:$process" + fi + + # 始终返回0,避免set -e导致脚本退出 + return 0 +} + +# 改进的端口配置函数 configure_ports() { - print_step "端口配置" + print_step "智能端口配置" echo "" - # 检测80端口 - if ! check_port_available 80; then - print_warning "检测到 80 端口已被占用" - echo "" - echo "80端口被其他服务占用,您可以:" - echo -e "${GREEN}[1]${NC} 使用其他HTTP端口 (例如: 8080, 8888)" - echo -e "${GREEN}[2]${NC} 停止占用80端口的服务" - echo "" + # 全局标志:是否共用Nginx端口 + SHARE_NGINX=false - read -p "请选择 [1-2]: " port_choice < /dev/tty + # ========== 检测80端口 ========== + port_80_status=$(check_port_status 80) + + case $port_80_status in + "available") + print_success "80 端口可用" + HTTP_PORT=80 + ;; + + "nginx") + print_info "检测到 Nginx 已占用 80 端口" + echo "" + echo "🎯 好消息:可以通过虚拟主机配置与现有Nginx共用此端口!" + echo "" + echo "请选择部署方式:" + echo "" + echo -e "${GREEN}[1]${NC} 共用80端口(推荐)" + echo " ✅ 需要配置不同的域名" + echo " ✅ 访问: http://your-domain.com" + echo " ✅ 不需要端口号" + echo "" + echo -e "${GREEN}[2]${NC} 使用其他HTTP端口" + echo " ℹ️ 独立端口" + echo " ℹ️ 访问: http://your-domain.com:8080" + echo "" + + while true; do + read -p "请选择 [1-2]: " choice < /dev/tty + + if [[ "$choice" == "1" ]]; then + HTTP_PORT=80 + SHARE_NGINX=true + print_success "将与现有Nginx共用80端口(虚拟主机模式)" + print_info "提示: 请确保使用不同的域名区分站点" + break + elif [[ "$choice" == "2" ]]; then + # 选择其他端口的逻辑 + while true; do + read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty + custom_port=${custom_port:-8080} + + if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then + print_error "端口范围: 1024-65535" + continue + fi + + if ! check_port_available $custom_port; then + print_error "端口 $custom_port 已被占用,请选择其他端口" + continue + fi + + HTTP_PORT=$custom_port + print_success "将使用 HTTP 端口: $HTTP_PORT" + break + done + break + else + print_error "无效选项,请重新选择" + fi + done + ;; + + "apache") + print_warning "检测到 Apache 已占用 80 端口" + echo "" + echo "⚠️ Apache和Nginx不能同时监听同一端口" + echo "" + echo "请选择解决方案:" + echo "" + echo -e "${GREEN}[1]${NC} 停止Apache,改用Nginx" + echo " ⚠️ 需要迁移Apache配置" + echo "" + echo -e "${GREEN}[2]${NC} 使用其他HTTP端口(推荐)" + echo " ✅ 不影响现有Apache服务" + echo "" + + while true; do + read -p "请选择 [1-2]: " choice < /dev/tty + + if [[ "$choice" == "1" ]]; then + print_info "正在停止Apache..." + systemctl stop apache2 2>/dev/null || systemctl stop httpd 2>/dev/null || true + systemctl disable apache2 2>/dev/null || systemctl disable httpd 2>/dev/null || true + HTTP_PORT=80 + print_success "Apache已停止,将使用80端口" + break + elif [[ "$choice" == "2" ]]; then + # 选择其他端口 + while true; do + read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty + custom_port=${custom_port:-8080} + + if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then + print_error "端口范围: 1024-65535" + continue + fi + + if ! check_port_available $custom_port; then + print_error "端口 $custom_port 已被占用,请选择其他端口" + continue + fi + + HTTP_PORT=$custom_port + print_success "将使用 HTTP 端口: $HTTP_PORT" + break + done + break + else + print_error "无效选项,请重新选择" + fi + done + ;; + + "occupied"|other:*) + process=${port_80_status#other:} + if [[ "$port_80_status" == "occupied" ]]; then + print_warning "80 端口已被占用(无法识别进程)" + else + print_warning "80 端口被进程 ${process} 占用" + fi + echo "" + echo "请选择其他HTTP端口" - if [[ "$port_choice" == "1" ]]; then while true; do read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty custom_port=${custom_port:-8080} @@ -699,47 +868,113 @@ configure_ports() { print_success "将使用 HTTP 端口: $HTTP_PORT" break done - else - print_info "请手动停止占用80端口的服务后重新运行此脚本" - echo "" - print_info "查看端口占用: netstat -tunlp | grep :80" - print_info "或者: ss -tunlp | grep :80" - exit 1 - fi - else - print_success "80 端口可用" - fi + ;; + esac - # 检测443端口(仅在使用HTTPS时需要) + echo "" + + # ========== 检测443端口(仅在使用HTTPS时需要)========== if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then - if ! check_port_available 443; then - print_warning "检测到 443 端口已被占用" - echo "" + port_443_status=$(check_port_status 443) - while true; do - read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty - custom_https_port=${custom_https_port:-8443} + case $port_443_status in + "available") + print_success "443 端口可用" + HTTPS_PORT=443 + ;; - if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then - print_error "端口范围: 1024-65535" - continue + "nginx") + print_info "检测到 Nginx 已占用 443 端口" + echo "" + + if [[ "$SHARE_NGINX" == "true" ]]; then + # 如果HTTP端口也是共用的,默认继续共用 + echo "🎯 将继续与现有Nginx共用443端口(虚拟主机模式)" + HTTPS_PORT=443 + print_success "将与现有Nginx共用443端口" + else + echo "请选择部署方式:" + echo "" + echo -e "${GREEN}[1]${NC} 共用443端口" + echo " ✅ 需要配置不同的域名" + echo "" + echo -e "${GREEN}[2]${NC} 使用其他HTTPS端口" + echo " ℹ️ 独立端口(如8443)" + echo "" + + while true; do + read -p "请选择 [1-2]: " choice < /dev/tty + + if [[ "$choice" == "1" ]]; then + HTTPS_PORT=443 + SHARE_NGINX=true + print_success "将与现有Nginx共用443端口" + break + elif [[ "$choice" == "2" ]]; then + # 选择其他端口 + while true; do + read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty + custom_https_port=${custom_https_port:-8443} + + if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then + print_error "端口范围: 1024-65535" + continue + fi + + if ! check_port_available $custom_https_port; then + print_error "端口 $custom_https_port 已被占用,请选择其他端口" + continue + fi + + HTTPS_PORT=$custom_https_port + print_success "将使用 HTTPS 端口: $HTTPS_PORT" + break + done + break + else + print_error "无效选项,请重新选择" + fi + done fi + ;; - if ! check_port_available $custom_https_port; then - print_error "端口 $custom_https_port 已被占用,请选择其他端口" - continue + "apache"|"occupied"|other:*) + # Apache或其他进程占用443,需要换端口 + if [[ "$port_443_status" == "apache" ]]; then + print_warning "检测到 Apache 已占用 443 端口" + elif [[ "$port_443_status" == "occupied" ]]; then + print_warning "443 端口已被占用" + else + process=${port_443_status#other:} + print_warning "443 端口被进程 ${process} 占用" fi + echo "" - HTTPS_PORT=$custom_https_port - print_success "将使用 HTTPS 端口: $HTTPS_PORT" - break - done - else - print_success "443 端口可用" - fi + while true; do + read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty + custom_https_port=${custom_https_port:-8443} + + if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then + print_error "端口范围: 1024-65535" + continue + fi + + if ! check_port_available $custom_https_port; then + print_error "端口 $custom_https_port 已被占用,请选择其他端口" + continue + fi + + HTTPS_PORT=$custom_https_port + print_success "将使用 HTTPS 端口: $HTTPS_PORT" + break + done + ;; + esac + + echo "" fi - # 检测后端端口 + # ========== 检测后端端口 ========== if ! check_port_available 40001; then print_warning "检测到 40001 端口已被占用" echo "" @@ -773,6 +1008,9 @@ configure_ports() { echo " - HTTPS端口: $HTTPS_PORT" fi echo " - 后端端口: $BACKEND_PORT" + if [[ "$SHARE_NGINX" == "true" ]]; then + echo " - 模式: 虚拟主机共用端口 ✅" + fi echo "" } @@ -851,36 +1089,112 @@ configure_domain() { # SSL证书配置 ################################################################################ +# 配置acme.sh自动续期 +setup_acme_auto_renew() { + echo "" + print_step "配置SSL证书自动续期..." + + # acme.sh安装时会自动创建cron任务,这里验证并确保其正常工作 + + # 1. 检查cron服务是否运行 + if systemctl is-active --quiet cron 2>/dev/null || systemctl is-active --quiet crond 2>/dev/null; then + print_success "Cron服务运行正常" + else + print_warning "Cron服务未运行,正在启动..." + systemctl start cron 2>/dev/null || systemctl start crond 2>/dev/null || true + systemctl enable cron 2>/dev/null || systemctl enable crond 2>/dev/null || true + fi + + # 2. 检查acme.sh cron任务 + if crontab -l 2>/dev/null | grep -q "acme.sh.*--cron"; then + print_success "acme.sh自动续期任务已配置" + else + print_warning "未检测到acme.sh cron任务,正在添加..." + # acme.sh会自动安装cron,这里手动触发一次 + ~/.acme.sh/acme.sh --install-cronjob 2>/dev/null || true + fi + + # 3. 显示续期信息 + echo "" + print_info "SSL证书自动续期已配置:" + echo " - 检查频率: 每天自动检查" + echo " - 续期时机: 证书到期前30天自动续期" + echo " - 续期后操作: 自动重载Nginx" + echo "" + + # 显示下次续期时间 + if [[ -f ~/.acme.sh/${DOMAIN}/${DOMAIN}.conf ]]; then + NEXT_RENEW=$(grep "Le_NextRenewTime=" ~/.acme.sh/${DOMAIN}/${DOMAIN}.conf 2>/dev/null | cut -d'=' -f2) + if [[ -n "$NEXT_RENEW" ]]; then + RENEW_DATE=$(date -d "@${NEXT_RENEW}" "+%Y年%m月%d日 %H:%M:%S" 2>/dev/null || date -r ${NEXT_RENEW} "+%Y年%m月%d日 %H:%M:%S" 2>/dev/null || echo "未知") + print_info "预计续期时间: ${RENEW_DATE}" + fi + fi + + # 4. 测试续期命令(不实际续期,只检查) + print_info "验证续期配置..." + if ~/.acme.sh/acme.sh --list 2>/dev/null | grep -q "$DOMAIN"; then + print_success "证书续期配置验证通过" + else + print_warning "证书列表中未找到域名,续期可能需要手动配置" + fi + + echo "" +} + choose_ssl_method() { echo "" print_step "选择SSL证书部署方式" echo "" echo -e "${YELLOW}【推荐方案】${NC}" - echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)" - echo " - 最稳定可靠,支持自动续期" + echo -e "${GREEN}[1]${NC} acme.sh + Let's Encrypt" + echo " - 纯Shell脚本,轻量级稳定" + echo " - 自动续期,无需手动操作" echo "" echo -e "${YELLOW}【备选方案】${NC}" - echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt" - echo " - 纯Shell脚本,更轻量级" - echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL" + echo -e "${GREEN}[2]${NC} acme.sh + ZeroSSL" echo " - Let's Encrypt的免费替代品" - echo -e "${GREEN}[4]${NC} acme.sh + Buypass" + echo -e "${GREEN}[3]${NC} acme.sh + Buypass" echo " - 挪威免费CA,有效期180天" echo "" echo -e "${YELLOW}【云服务商证书】${NC}" - echo -e "${GREEN}[5]${NC} 阿里云免费证书 (需提供AccessKey)" - echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)" + echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)" + echo -e "${GREEN}[5]${NC} 腾讯云免费证书 (需提供SecretKey)" echo "" echo -e "${YELLOW}【其他选项】${NC}" - echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)" - echo -e "${GREEN}[8]${NC} 暂不配置HTTPS (可后续配置)" + echo -e "${GREEN}[6]${NC} 使用已有证书 (手动上传)" + echo -e "${GREEN}[7]${NC} 暂不配置HTTPS (可后续配置)" echo "" while true; do - read -p "请输入选项 [1-8]: " ssl_choice < /dev/tty + read -p "请输入选项 [1-7]: " ssl_choice < /dev/tty case $ssl_choice in - 1|2|3|4|5|6|7|8) - SSL_METHOD=$ssl_choice + 1) + SSL_METHOD="2" # acme.sh + Let's Encrypt + break + ;; + 2) + SSL_METHOD="3" # acme.sh + ZeroSSL + break + ;; + 3) + SSL_METHOD="5" # acme.sh + Buypass + break + ;; + 4) + SSL_METHOD="4" # 阿里云 + break + ;; + 5) + SSL_METHOD="6" # 腾讯云 + break + ;; + 6) + SSL_METHOD="7" # 手动上传 + break + ;; + 7) + SSL_METHOD="8" # 不配置HTTPS break ;; *) @@ -897,23 +1211,20 @@ deploy_ssl() { fi case $SSL_METHOD in - 1) - deploy_certbot || ssl_fallback - ;; 2) - deploy_acme_letsencrypt || ssl_fallback + deploy_acme_letsencrypt || ssl_fallback "2" ;; 3) - deploy_acme_zerossl || ssl_fallback + deploy_acme_zerossl || ssl_fallback "3" ;; 4) - deploy_acme_buypass || ssl_fallback + deploy_aliyun_ssl || ssl_fallback "4" ;; 5) - deploy_aliyun_ssl || ssl_fallback + deploy_acme_buypass || ssl_fallback "5" ;; 6) - deploy_tencent_ssl || ssl_fallback + deploy_tencent_ssl || ssl_fallback "6" ;; 7) deploy_manual_ssl @@ -926,35 +1237,73 @@ deploy_ssl() { } ssl_fallback() { + local failed_method=$1 # 接收失败的方案编号 + print_error "SSL证书部署失败" echo "" print_warning "建议尝试备选方案:" - echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt (推荐)" - echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL" - echo -e "${GREEN}[4]${NC} acme.sh + Buypass" + echo "" + + # 动态显示可用选项(排除已失败的) + local available_options=() + + # 方案2: acme.sh + Let's Encrypt + if [[ "$failed_method" != "2" ]]; then + echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt" + available_options+=("2") + fi + + # 方案3: acme.sh + ZeroSSL + if [[ "$failed_method" != "3" ]]; then + echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL" + available_options+=("3") + fi + + # 方案5: acme.sh + Buypass + if [[ "$failed_method" != "5" ]]; then + echo -e "${GREEN}[5]${NC} acme.sh + Buypass" + available_options+=("5") + fi + + # 方案8: 不配置HTTPS echo -e "${GREEN}[8]${NC} 暂不配置HTTPS" + available_options+=("8") + + echo "" + echo -e "${YELLOW}提示: 方案 $failed_method 已失败,已从列表中移除${NC}" echo "" while true; do - read -p "请选择备选方案 [2-4/8]: " retry_choice < /dev/tty + read -p "请选择备选方案: " retry_choice < /dev/tty + + # 检查输入是否在可用选项中 + if [[ ! " ${available_options[@]} " =~ " ${retry_choice} " ]]; then + print_error "无效选项或该方案已失败" + continue + fi + case $retry_choice in 2) deploy_acme_letsencrypt && return 0 + # 如果再次失败,继续调用fallback但排除方案2 + ssl_fallback "2" + return $? ;; 3) deploy_acme_zerossl && return 0 + ssl_fallback "3" + return $? ;; - 4) + 5) deploy_acme_buypass && return 0 + ssl_fallback "5" + return $? ;; 8) print_info "跳过HTTPS配置" SSL_METHOD=8 return 0 ;; - *) - print_error "无效选项" - ;; esac done } @@ -962,96 +1311,616 @@ ssl_fallback() { deploy_certbot() { print_step "使用 Certbot 部署SSL证书..." - # 安装certbot - case $PKG_MANAGER in - apt) - apt-get install -y certbot python3-certbot-nginx - ;; - yum) - yum install -y certbot python3-certbot-nginx - ;; - dnf) - dnf install -y certbot python3-certbot-nginx - ;; - zypper) - zypper install -y certbot python3-certbot-nginx - ;; - esac + # 检查certbot是否已安装 + if ! command -v certbot &> /dev/null; then + print_info "正在安装 Certbot..." - # 申请证书 - certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}" --redirect + # 安装certbot + case $PKG_MANAGER in + apt) + # Ubuntu/Debian: 优先使用snap(官方推荐,避免Python依赖冲突) + if command -v snap &> /dev/null; then + print_info "使用snap安装Certbot(官方推荐方式)..." + snap install --classic certbot 2>/dev/null || true + ln -sf /snap/bin/certbot /usr/bin/certbot 2>/dev/null || true - # 配置自动续期 - systemctl enable certbot.timer + # 验证snap安装是否成功 + if /snap/bin/certbot --version &> /dev/null; then + print_success "Certbot (snap版) 安装成功" + else + print_warning "snap安装失败,尝试apt安装..." + # 修复urllib3依赖问题 + apt-get remove -y python3-urllib3 2>/dev/null || true + apt-get install -y certbot python3-certbot-nginx + fi + else + print_info "snap不可用,使用apt安装..." + # 修复urllib3依赖问题 + apt-get remove -y python3-urllib3 2>/dev/null || true + apt-get install -y certbot python3-certbot-nginx + fi + ;; + yum) + # 修复urllib3依赖问题 + yum remove -y python3-urllib3 2>/dev/null || true + yum install -y certbot python3-certbot-nginx + ;; + dnf) + # 修复urllib3依赖问题 + dnf remove -y python3-urllib3 2>/dev/null || true + dnf install -y certbot python3-certbot-nginx + ;; + zypper) + zypper install -y certbot python3-certbot-nginx + ;; + esac - print_success "Certbot SSL证书部署成功" - return 0 + # 最终验证certbot是否可用 + if ! command -v certbot &> /dev/null; then + print_error "Certbot安装失败" + return 1 + fi + else + print_success "Certbot 已安装: $(certbot --version 2>&1 | head -1)" + fi + + # 修复已安装certbot的urllib3依赖冲突 + if ! certbot --version &> /dev/null; then + print_warning "检测到Certbot依赖问题,正在修复..." + case $PKG_MANAGER in + apt) + apt-get remove -y python3-urllib3 2>/dev/null || true + apt-get install --reinstall -y certbot python3-certbot-nginx + ;; + yum|dnf) + $PKG_MANAGER remove -y python3-urllib3 2>/dev/null || true + $PKG_MANAGER reinstall -y certbot python3-certbot-nginx + ;; + esac + + # 再次验证 + if ! certbot --version &> /dev/null; then + print_error "Certbot依赖修复失败,建议尝试其他SSL方案" + return 1 + fi + print_success "Certbot依赖已修复" + fi + + # 申请证书(使用webroot模式,不自动修改Nginx配置) + echo "" + print_info "正在申请 Let's Encrypt 证书..." + + if certbot certonly --webroot -w "${PROJECT_DIR}/frontend" -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}"; then + # 将证书复制到Nginx SSL目录 + mkdir -p /etc/nginx/ssl + ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt" + ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key" + + # 配置自动续期 + systemctl enable certbot.timer 2>/dev/null || true + + print_success "Certbot SSL证书申请成功" + return 0 + else + # 检查证书是否已存在 + if [[ -d "/etc/letsencrypt/live/${DOMAIN}" ]]; then + print_warning "检测到证书已存在,使用已有证书" + mkdir -p /etc/nginx/ssl + ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt" + ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key" + print_success "已有证书已链接到Nginx目录" + return 0 + else + print_error "Certbot SSL证书申请失败" + echo "" + print_warning "常见失败原因:" + echo " 1. 域名未正确解析到此服务器" + echo " 2. 防火墙阻止了80端口" + echo " 3. Nginx未正确配置或未启动" + echo " 4. Let's Encrypt速率限制" + echo "" + return 1 + fi + fi } deploy_acme_letsencrypt() { print_step "使用 acme.sh + Let's Encrypt 部署SSL证书..." # 安装acme.sh - if [[ ! -d ~/.acme.sh ]]; then - curl https://get.acme.sh | sh + if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then + echo "" + print_info "正在安装 acme.sh..." + + # 如果目录存在但文件不存在,先清理 + if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_warning "检测到不完整的安装,正在清理..." + rm -rf ~/.acme.sh + fi + + print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)" + + # 使用官方安装方法:直接通过curl管道执行 + print_info "正在下载并安装..." + + if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then + install_result=$? + print_info "安装脚本执行完成,退出码: $install_result" + else + install_result=$? + print_error "安装脚本执行失败,退出码: $install_result" + fi + + # 重新加载环境变量 + source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true + + # 等待文件系统同步 + print_info "等待安装完成..." + sleep 3 + + # 验证安装是否真正成功 + if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then + print_success "acme.sh 安装成功" + else + print_error "acme.sh 安装失败" + echo "" + print_warning "诊断信息:" + echo " - 安装命令退出码: $install_result" + echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')" + echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')" + echo " - HOME变量: $HOME" + echo " - 当前用户: $(whoami)" + echo "" + + if [[ -d ~/.acme.sh ]]; then + print_info "~/.acme.sh 目录内容:" + ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录" + echo "" + fi + + print_info "尝试查找acme.sh安装位置..." + find /root -name "acme.sh" -type f 2>/dev/null | head -5 || echo " 未找到" + echo "" + print_warning "可能的原因:" + echo " 1. 网络连接问题或下载超时" + echo " 2. GitHub访问受限(国内网络)" + echo " 3. curl版本过低或不支持某些功能" + echo "" + print_warning "建议尝试其他SSL方案:" + echo " 1. 返回选择 Certbot (推荐)" + echo " 2. 或选择 [8] 暂不配置HTTPS" + echo "" + return 1 + fi fi + # 确认acme.sh可用 + echo "" + print_info "验证 acme.sh 安装..." + + # 等待文件系统同步 + sleep 2 + + # 检查安装目录 + if [[ ! -d ~/.acme.sh ]]; then + print_error "安装目录不存在: ~/.acme.sh" + return 1 + fi + + # 检查主脚本文件 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "主脚本文件不存在: ~/.acme.sh/acme.sh" + print_info "目录内容:" + ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录" + return 1 + fi + + # 检查脚本是否可执行 + if [[ ! -x ~/.acme.sh/acme.sh ]]; then + print_warning "脚本不可执行,正在添加执行权限..." + chmod +x ~/.acme.sh/acme.sh + fi + + # 测试脚本是否能运行 + if ! ~/.acme.sh/acme.sh --version &> /dev/null; then + print_error "acme.sh 无法运行" + return 1 + fi + + print_success "acme.sh 验证通过" + # 申请证书 - ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --nginx + echo "" + print_info "正在申请 Let's Encrypt 证书..." + + # 再次确认acme.sh存在 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" + return 1 + fi + + # 使用webroot模式申请证书(更可靠) + # 先尝试正常申请,如果证书已存在则使用--force强制更新 + if ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then + print_success "证书申请成功" + else + # 检查是否是因为证书已存在 + if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then + print_warning "检测到证书已存在,使用已有证书" + print_success "将直接安装现有证书" + else + print_error "证书申请失败" + echo "" + print_warning "常见失败原因:" + echo " 1. 域名未正确解析到此服务器" + echo " 2. Nginx未正确配置或未启动" + echo " 3. 80端口被占用或防火墙阻止" + echo " 4. 前端目录权限不足" + echo "" + return 1 + fi + fi # 安装证书 - mkdir -p /etc/nginx/ssl - ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ - --key-file /etc/nginx/ssl/${DOMAIN}.key \ - --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \ - --reloadcmd "systemctl reload nginx" + echo "" + print_info "正在安装证书到Nginx..." - print_success "acme.sh SSL证书部署成功" - return 0 + # 再次确认acme.sh存在 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" + return 1 + fi + + mkdir -p /etc/nginx/ssl + + # 确保nginx服务已启动(证书安装时需要reload) + if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then + print_warning "Nginx未运行,正在启动..." + systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true + sleep 2 + fi + + # 先不带reload命令安装证书(避免nginx未启动导致失败) + if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ + --key-file /etc/nginx/ssl/${DOMAIN}.key \ + --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then + print_success "证书文件已安装到: /etc/nginx/ssl/" + + # 手动reload nginx + if systemctl is-active --quiet nginx 2>/dev/null; then + systemctl reload nginx && print_success "Nginx配置已重载" + elif pgrep -x nginx > /dev/null; then + nginx -s reload && print_success "Nginx配置已重载" + else + print_warning "Nginx未运行,将在后续步骤启动" + fi + + # 配置自动续期 + setup_acme_auto_renew + + return 0 + else + print_error "证书安装失败" + return 1 + fi } deploy_acme_zerossl() { print_step "使用 acme.sh + ZeroSSL 部署SSL证书..." - # 安装acme.sh - if [[ ! -d ~/.acme.sh ]]; then - curl https://get.acme.sh | sh + # 安装acme.sh(使用改进的安装逻辑) + if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then + echo "" + print_info "正在安装 acme.sh..." + + # 如果目录存在但文件不存在,先清理 + if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_warning "检测到不完整的安装,正在清理..." + rm -rf ~/.acme.sh + fi + + print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)" + + # 使用官方安装方法:直接通过curl管道执行 + print_info "正在下载并安装..." + + if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then + install_result=$? + print_info "安装脚本执行完成,退出码: $install_result" + else + install_result=$? + print_error "安装脚本执行失败,退出码: $install_result" + fi + + # 重新加载环境变量 + source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true + + # 等待文件系统同步 + print_info "等待安装完成..." + sleep 3 + + # 验证安装 + if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then + print_success "acme.sh 安装成功" + else + print_error "acme.sh 安装失败" + echo "" + print_warning "诊断信息:" + echo " - 安装命令退出码: $install_result" + echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')" + echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')" + echo "" + + if [[ -d ~/.acme.sh ]]; then + print_info "~/.acme.sh 目录内容:" + ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录" + echo "" + fi + + return 1 + fi fi + # 确认acme.sh可用 + echo "" + print_info "验证 acme.sh 安装..." + + # 等待文件系统同步 + sleep 2 + + # 检查安装目录 + if [[ ! -d ~/.acme.sh ]]; then + print_error "安装目录不存在: ~/.acme.sh" + return 1 + fi + + # 检查主脚本文件 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "主脚本文件不存在: ~/.acme.sh/acme.sh" + print_info "目录内容:" + ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录" + return 1 + fi + + # 检查脚本是否可执行 + if [[ ! -x ~/.acme.sh/acme.sh ]]; then + print_warning "脚本不可执行,正在添加执行权限..." + chmod +x ~/.acme.sh/acme.sh + fi + + # 测试脚本是否能运行 + if ! ~/.acme.sh/acme.sh --version &> /dev/null; then + print_error "acme.sh 无法运行" + return 1 + fi + + print_success "acme.sh 验证通过" + # 申请证书 - ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --nginx + echo "" + print_info "正在申请 ZeroSSL 证书..." + + # 再次确认acme.sh存在 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" + return 1 + fi + + # 使用webroot模式申请证书(更可靠) + if ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then + print_success "证书申请成功" + else + # 检查是否是因为证书已存在 + if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then + print_warning "检测到证书已存在,使用已有证书" + print_success "将直接安装现有证书" + else + print_error "证书申请失败" + return 1 + fi + fi # 安装证书 - mkdir -p /etc/nginx/ssl - ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ - --key-file /etc/nginx/ssl/${DOMAIN}.key \ - --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \ - --reloadcmd "systemctl reload nginx" + echo "" + print_info "正在安装证书到Nginx..." - print_success "ZeroSSL证书部署成功" - return 0 + # 再次确认acme.sh存在 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" + return 1 + fi + + mkdir -p /etc/nginx/ssl + + # 确保nginx服务已启动(证书安装时需要reload) + if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then + print_warning "Nginx未运行,正在启动..." + systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true + sleep 2 + fi + + # 先不带reload命令安装证书(避免nginx未启动导致失败) + if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ + --key-file /etc/nginx/ssl/${DOMAIN}.key \ + --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then + print_success "证书文件已安装到: /etc/nginx/ssl/" + + # 手动reload nginx + if systemctl is-active --quiet nginx 2>/dev/null; then + systemctl reload nginx && print_success "Nginx配置已重载" + elif pgrep -x nginx > /dev/null; then + nginx -s reload && print_success "Nginx配置已重载" + else + print_warning "Nginx未运行,将在后续步骤启动" + fi + + # 配置自动续期 + setup_acme_auto_renew + + return 0 + else + print_error "证书安装失败" + return 1 + fi } deploy_acme_buypass() { print_step "使用 acme.sh + Buypass 部署SSL证书..." - # 安装acme.sh - if [[ ! -d ~/.acme.sh ]]; then - curl https://get.acme.sh | sh + # 安装acme.sh(使用改进的安装逻辑) + if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then + echo "" + print_info "正在安装 acme.sh..." + + # 如果目录存在但文件不存在,先清理 + if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_warning "检测到不完整的安装,正在清理..." + rm -rf ~/.acme.sh + fi + + print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)" + + # 使用官方安装方法:直接通过curl管道执行 + print_info "正在下载并安装..." + + if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then + install_result=$? + print_info "安装脚本执行完成,退出码: $install_result" + else + install_result=$? + print_error "安装脚本执行失败,退出码: $install_result" + fi + + # 重新加载环境变量 + source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true + + # 等待文件系统同步 + print_info "等待安装完成..." + sleep 3 + + # 验证安装 + if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then + print_success "acme.sh 安装成功" + else + print_error "acme.sh 安装失败" + echo "" + print_warning "诊断信息:" + echo " - 安装命令退出码: $install_result" + echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')" + echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')" + echo "" + + if [[ -d ~/.acme.sh ]]; then + print_info "~/.acme.sh 目录内容:" + ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录" + echo "" + fi + + return 1 + fi fi + # 确认acme.sh可用 + echo "" + print_info "验证 acme.sh 安装..." + + # 等待文件系统同步 + sleep 2 + + # 检查安装目录 + if [[ ! -d ~/.acme.sh ]]; then + print_error "安装目录不存在: ~/.acme.sh" + return 1 + fi + + # 检查主脚本文件 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "主脚本文件不存在: ~/.acme.sh/acme.sh" + print_info "目录内容:" + ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录" + return 1 + fi + + # 检查脚本是否可执行 + if [[ ! -x ~/.acme.sh/acme.sh ]]; then + print_warning "脚本不可执行,正在添加执行权限..." + chmod +x ~/.acme.sh/acme.sh + fi + + # 测试脚本是否能运行 + if ! ~/.acme.sh/acme.sh --version &> /dev/null; then + print_error "acme.sh 无法运行" + return 1 + fi + + print_success "acme.sh 验证通过" + # 申请证书 - ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --nginx + echo "" + print_info "正在申请 Buypass 证书..." + + # 再次确认acme.sh存在 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" + return 1 + fi + + # 使用webroot模式申请证书(更可靠) + if ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then + print_success "证书申请成功" + else + # 检查是否是因为证书已存在 + if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then + print_warning "检测到证书已存在,使用已有证书" + print_success "将直接安装现有证书" + else + print_error "证书申请失败" + return 1 + fi + fi # 安装证书 - mkdir -p /etc/nginx/ssl - ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ - --key-file /etc/nginx/ssl/${DOMAIN}.key \ - --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \ - --reloadcmd "systemctl reload nginx" + echo "" + print_info "正在安装证书到Nginx..." - print_success "Buypass SSL证书部署成功" - return 0 + # 再次确认acme.sh存在 + if [[ ! -f ~/.acme.sh/acme.sh ]]; then + print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" + return 1 + fi + + mkdir -p /etc/nginx/ssl + + # 确保nginx服务已启动(证书安装时需要reload) + if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then + print_warning "Nginx未运行,正在启动..." + systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true + sleep 2 + fi + + # 先不带reload命令安装证书(避免nginx未启动导致失败) + if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ + --key-file /etc/nginx/ssl/${DOMAIN}.key \ + --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then + print_success "证书文件已安装到: /etc/nginx/ssl/" + + # 手动reload nginx + if systemctl is-active --quiet nginx 2>/dev/null; then + systemctl reload nginx && print_success "Nginx配置已重载" + elif pgrep -x nginx > /dev/null; then + nginx -s reload && print_success "Nginx配置已重载" + else + print_warning "Nginx未运行,将在后续步骤启动" + fi + + # 配置自动续期 + setup_acme_auto_renew + + return 0 + else + print_error "证书安装失败" + return 1 + fi } deploy_aliyun_ssl() { @@ -1367,6 +2236,255 @@ build_upload_tool() { fi } +################################################################################ +# Nginx配置 - 分步骤执行 +################################################################################ + +# 步骤1: 先配置HTTP Nginx(为SSL证书验证做准备) +configure_nginx_http_first() { + print_step "配置基础HTTP Nginx(用于SSL证书验证)..." + + # 总是先配置HTTP模式 + local server_name="${DOMAIN:-_}" + + # 检测Nginx配置目录结构并创建必要的目录 + if [[ -d /www/server/nginx ]]; then + # 宝塔面板 (BT Panel) + NGINX_CONF_DIR="/www/server/panel/vhost/nginx" + NGINX_ENABLED_DIR="" + USE_SYMLINK=false + IS_BT_PANEL=true + + # 确保目录存在 + mkdir -p ${NGINX_CONF_DIR} + print_info "检测到宝塔面板,使用宝塔Nginx配置目录" + elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then + # Debian/Ubuntu: 使用sites-available + NGINX_CONF_DIR="/etc/nginx/sites-available" + NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" + USE_SYMLINK=true + IS_BT_PANEL=false + + # 确保目录存在 + mkdir -p ${NGINX_CONF_DIR} + mkdir -p ${NGINX_ENABLED_DIR} + else + # CentOS/RHEL: 使用conf.d + NGINX_CONF_DIR="/etc/nginx/conf.d" + NGINX_ENABLED_DIR="" + USE_SYMLINK=false + IS_BT_PANEL=false + + # 确保目录存在 + mkdir -p ${NGINX_CONF_DIR} + fi + + cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF +server { + listen ${HTTP_PORT}; + server_name ${server_name}; + + # 文件上传大小限制(10GB) + client_max_body_size 10G; + + # 前端静态文件 + location / { + root ${PROJECT_DIR}/frontend; + index index.html; + try_files \$uri \$uri/ /index.html; + } + + # 后端API + location /api { + proxy_pass http://localhost:${BACKEND_PORT}; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + + # 上传超时设置 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 300s; + } + + # 分享页面 + location /s/ { + proxy_pass http://localhost:${BACKEND_PORT}; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + # 静态资源 + location /libs { + alias ${PROJECT_DIR}/frontend/libs; + expires 30d; + } + + # 上传工具下载 + location /download-tool { + alias ${PROJECT_DIR}/upload-tool/dist; + } +} +EOF + + # 根据系统类型处理配置文件 + if [[ "$USE_SYMLINK" == "true" ]]; then + # Debian/Ubuntu: 创建软链接 + ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf + # 删除默认站点 + rm -f ${NGINX_ENABLED_DIR}/default + elif [[ "$IS_BT_PANEL" != "true" ]]; then + # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载 + rm -f /etc/nginx/conf.d/default.conf + fi + # 宝塔面板:配置文件已自动包含,无需额外操作 + + # 测试nginx配置 + if ! nginx -t; then + print_error "Nginx配置测试失败" + return 1 + fi + + # 启动或重载Nginx + if [[ "$IS_BT_PANEL" == "true" ]]; then + # 宝塔面板:尝试多种方式 + print_info "宝塔环境,尝试重载Nginx..." + + # 方式1: 使用宝塔命令行工具(如果存在) + if [[ -f /etc/init.d/bt ]]; then + /etc/init.d/bt restart 2>/dev/null + fi + + # 方式2: 直接使用nginx命令reload + if [[ -f /www/server/nginx/sbin/nginx ]]; then + /www/server/nginx/sbin/nginx -s reload 2>/dev/null + if [[ $? -eq 0 ]]; then + print_success "已使用nginx -s reload重载配置" + else + # 如果reload失败,尝试启动 + /www/server/nginx/sbin/nginx 2>/dev/null + if [[ $? -eq 0 ]]; then + print_success "已启动Nginx" + else + print_warning "Nginx reload失败,尝试systemctl..." + fi + fi + fi + + # 方式3: 尝试systemctl(备用) + if systemctl is-active --quiet nginx 2>/dev/null; then + systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置" + else + systemctl start nginx 2>/dev/null && print_info "已使用systemctl启动Nginx" + fi + else + # 标准Nginx:重启 + systemctl restart nginx + fi + + # 验证Nginx是否运行 + sleep 2 + if [[ "$IS_BT_PANEL" == "true" ]]; then + # 宝塔:检查进程 + if pgrep -x nginx > /dev/null; then + print_success "Nginx运行正常" + else + print_error "Nginx未运行" + print_warning "请在宝塔面板中手动启动Nginx,或运行:" + print_warning "/www/server/nginx/sbin/nginx" + return 1 + fi + else + # 标准Nginx:使用systemctl检查 + if ! systemctl is-active --quiet nginx; then + print_error "Nginx启动失败" + return 1 + fi + fi + + print_success "基础HTTP Nginx配置完成" + echo "" +} + +# 步骤2: 根据SSL结果配置最终Nginx +configure_nginx_final() { + print_step "配置最终Nginx..." + + # 检查SSL是否成功部署 + local ssl_deployed=false + if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then + # 检查SSL证书文件是否存在 + if [[ -f /etc/nginx/ssl/${DOMAIN}.crt ]] && [[ -f /etc/nginx/ssl/${DOMAIN}.key ]]; then + ssl_deployed=true + print_info "检测到SSL证书,配置HTTPS..." + else + print_warning "SSL证书不存在,保持HTTP配置" + fi + fi + + # 根据SSL状态配置 + if [[ "$ssl_deployed" == "true" ]]; then + # 配置HTTPS + configure_nginx_https + else + # 保持HTTP(已在第一步配置,这里只需确认) + print_info "使用HTTP配置" + fi + + # 测试nginx配置 + if ! nginx -t; then + print_error "Nginx配置测试失败" + return 1 + fi + + # 重载nginx - 兼容宝塔面板 + if [[ "$IS_BT_PANEL" == "true" ]]; then + # 宝塔面板:尝试多种方式 + print_info "宝塔环境,重载Nginx配置..." + + # 方式1: 使用宝塔命令行工具(如果存在) + if [[ -f /etc/init.d/bt ]]; then + /etc/init.d/bt restart 2>/dev/null + fi + + # 方式2: 直接使用nginx命令reload(最可靠) + if [[ -f /www/server/nginx/sbin/nginx ]]; then + /www/server/nginx/sbin/nginx -s reload 2>/dev/null + if [[ $? -eq 0 ]]; then + print_success "已使用nginx -s reload重载配置" + else + # 如果reload失败,尝试启动 + /www/server/nginx/sbin/nginx 2>/dev/null + if [[ $? -eq 0 ]]; then + print_success "已启动Nginx" + else + print_warning "Nginx reload失败,尝试systemctl..." + fi + fi + fi + + # 方式3: 尝试systemctl(备用) + if systemctl is-active --quiet nginx 2>/dev/null; then + systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置" + else + systemctl start nginx 2>/dev/null && print_info "已使用systemctl启动Nginx" + fi + else + # 标准Nginx:重载 + systemctl reload nginx + fi + + print_success "Nginx最终配置完成" + echo "" +} + configure_nginx() { print_step "配置Nginx..." @@ -1396,7 +2514,30 @@ configure_nginx() { configure_nginx_http() { local server_name="${DOMAIN:-_}" - cat > /etc/nginx/sites-available/${PROJECT_NAME}.conf << EOF + # 检测Nginx配置目录结构并创建必要的目录 + if [[ -d /www/server/nginx ]]; then + # 宝塔面板 + NGINX_CONF_DIR="/www/server/panel/vhost/nginx" + USE_SYMLINK=false + IS_BT_PANEL=true + mkdir -p ${NGINX_CONF_DIR} + elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then + # Debian/Ubuntu + NGINX_CONF_DIR="/etc/nginx/sites-available" + NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" + USE_SYMLINK=true + IS_BT_PANEL=false + mkdir -p ${NGINX_CONF_DIR} + mkdir -p ${NGINX_ENABLED_DIR} + else + # CentOS/RHEL + NGINX_CONF_DIR="/etc/nginx/conf.d" + USE_SYMLINK=false + IS_BT_PANEL=false + mkdir -p ${NGINX_CONF_DIR} + fi + + cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF server { listen ${HTTP_PORT}; server_name ${server_name}; @@ -1407,8 +2548,8 @@ server { # 前端静态文件 location / { root ${PROJECT_DIR}/frontend; - index app.html; - try_files \$uri \$uri/ /app.html; + index index.html; + try_files \$uri \$uri/ /index.html; } # 后端API @@ -1451,19 +2592,54 @@ server { } EOF - # 创建软链接 - ln -sf /etc/nginx/sites-available/${PROJECT_NAME}.conf /etc/nginx/sites-enabled/${PROJECT_NAME}.conf - - # 删除默认站点 - rm -f /etc/nginx/sites-enabled/default + # 根据系统类型处理配置文件 + if [[ "$USE_SYMLINK" == "true" ]]; then + # Debian/Ubuntu: 创建软链接 + ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf + # 删除默认站点 + rm -f ${NGINX_ENABLED_DIR}/default + elif [[ "$IS_BT_PANEL" != "true" ]]; then + # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载 + rm -f /etc/nginx/conf.d/default.conf + fi } configure_nginx_https() { - cat > /etc/nginx/sites-available/${PROJECT_NAME}.conf << EOF + # 检测Nginx配置目录结构并创建必要的目录 + if [[ -d /www/server/nginx ]]; then + # 宝塔面板 + NGINX_CONF_DIR="/www/server/panel/vhost/nginx" + USE_SYMLINK=false + IS_BT_PANEL=true + mkdir -p ${NGINX_CONF_DIR} + elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then + # Debian/Ubuntu + NGINX_CONF_DIR="/etc/nginx/sites-available" + NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" + USE_SYMLINK=true + IS_BT_PANEL=false + mkdir -p ${NGINX_CONF_DIR} + mkdir -p ${NGINX_ENABLED_DIR} + else + # CentOS/RHEL + NGINX_CONF_DIR="/etc/nginx/conf.d" + USE_SYMLINK=false + IS_BT_PANEL=false + mkdir -p ${NGINX_CONF_DIR} + fi + + # 根据HTTPS端口生成正确的重定向URL + if [[ "$HTTPS_PORT" == "443" ]]; then + REDIRECT_URL="https://\$server_name\$request_uri" + else + REDIRECT_URL="https://\$server_name:${HTTPS_PORT}\$request_uri" + fi + + cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF server { listen ${HTTP_PORT}; server_name ${DOMAIN}; - return 301 https://\$server_name:\${HTTPS_PORT}\$request_uri; + return 301 ${REDIRECT_URL}; } server { @@ -1483,8 +2659,8 @@ server { # 前端静态文件 location / { root ${PROJECT_DIR}/frontend; - index app.html; - try_files \$uri \$uri/ /app.html; + index index.html; + try_files \$uri \$uri/ /index.html; } # 后端API @@ -1527,11 +2703,16 @@ server { } EOF - # 创建软链接 - ln -sf /etc/nginx/sites-available/${PROJECT_NAME}.conf /etc/nginx/sites-enabled/${PROJECT_NAME}.conf - - # 删除默认站点 - rm -f /etc/nginx/sites-enabled/default + # 根据系统类型处理配置文件 + if [[ "$USE_SYMLINK" == "true" ]]; then + # Debian/Ubuntu: 创建软链接 + ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf + # 删除默认站点 + rm -f ${NGINX_ENABLED_DIR}/default + elif [[ "$IS_BT_PANEL" != "true" ]]; then + # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载 + rm -f /etc/nginx/conf.d/default.conf + fi } start_backend_service() { @@ -1566,7 +2747,7 @@ health_check() { fi # 检查端口 - if netstat -tunlp | grep -q ":${BACKEND_PORT}"; then + if netstat -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}" || ss -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}"; then print_success "后端端口监听正常 (${BACKEND_PORT})" else print_error "后端端口监听异常" @@ -1574,11 +2755,22 @@ health_check() { fi # 检查Nginx - if systemctl is-active --quiet nginx; then - print_success "Nginx服务运行正常" + if [[ -d /www/server/nginx ]]; then + # 宝塔面板:检查进程 + if pgrep -x nginx > /dev/null; then + print_success "Nginx服务运行正常" + else + print_error "Nginx服务异常" + return 1 + fi else - print_error "Nginx服务异常" - return 1 + # 标准Nginx:使用systemctl检查 + if systemctl is-active --quiet nginx; then + print_success "Nginx服务运行正常" + else + print_error "Nginx服务异常" + return 1 + fi fi # 检查数据库 @@ -1671,15 +2863,13 @@ print_completion() { # SSL续期提示 if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then - echo -e "${YELLOW}SSL证书:${NC}" - case $SSL_METHOD in - 1) - echo " 自动续期: 已配置Certbot自动续期" - ;; - 2|3|4) - echo " 自动续期: 已配置acme.sh自动续期" - ;; - esac + echo -e "${YELLOW}SSL证书自动续期:${NC}" + echo " - 方式: acme.sh cron任务" + echo " - 频率: 每天自动检查" + echo " - 时机: 证书到期前30天自动续期" + echo " - 检查任务: crontab -l | grep acme" + echo " - 查看证书: ~/.acme.sh/acme.sh --list" + echo " - 手动续期: ~/.acme.sh/acme.sh --renew -d $DOMAIN --force" echo "" fi @@ -2342,12 +3532,13 @@ main() { echo -e "${GREEN}[1]${NC} 安装/部署 玩玩云" echo -e "${BLUE}[2]${NC} 更新/升级 玩玩云" echo -e "${YELLOW}[3]${NC} 修复/重新配置 玩玩云" - echo -e "${RED}[4]${NC} 卸载 玩玩云" + echo -e "${PURPLE}[4]${NC} SSL证书管理(安装/续签/更换证书)" + echo -e "${RED}[5]${NC} 卸载 玩玩云" echo -e "${GRAY}[0]${NC} 退出脚本" echo "" while true; do - read -p "请输入选项 [0-4]: " mode_choice < /dev/tty + read -p "请输入选项 [0-5]: " mode_choice < /dev/tty case $mode_choice in 1) print_success "已选择: 安装模式" @@ -2367,6 +3558,12 @@ main() { exit 0 ;; 4) + print_info "切换到SSL证书管理模式..." + echo "" + ssl_main + exit 0 + ;; + 5) print_info "切换到卸载模式..." echo "" uninstall_main @@ -2391,8 +3588,9 @@ main() { echo -e "${YELLOW}提示:${NC}" echo " 安装: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh" echo " 更新: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --update" - echo " 卸载: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --uninstall" echo " 修复: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --repair" + echo " SSL管理: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --ssl" + echo " 卸载: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --uninstall" echo "" sleep 2 fi @@ -2434,11 +3632,14 @@ main() { # 打包上传工具 build_upload_tool - # 部署SSL证书 + # 先配置基础HTTP Nginx(SSL证书申请需要) + configure_nginx_http_first + + # 部署SSL证书(需要HTTP server block进行验证) deploy_ssl - # 配置Nginx - configure_nginx + # 根据SSL结果配置最终Nginx + configure_nginx_final # 启动后端服务 start_backend_service @@ -2775,6 +3976,463 @@ repair_main() { # 完成提示 print_repair_completion } + +################################################################################ +# SSL证书管理功能 +################################################################################ + +print_ssl_banner() { + clear + echo -e "${GREEN}" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ 🔐 SSL证书管理模式 ║" + echo "║ ║" + echo "║ SSL Certificate Manager ║" + echo "║ ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +confirm_ssl_operation() { + print_ssl_banner + + echo -e "${YELLOW}" + echo "本脚本将执行以下操作:" + echo "" + echo "【SSL证书管理】" + echo " ✓ 检测现有域名配置" + echo " ✓ 选择SSL证书部署方案" + echo " ✓ 申请/更换/续签证书" + echo " ✓ 更新Nginx HTTPS配置" + echo " ✓ 重载服务" + echo "" + echo "【将会保留】" + echo " ✓ 数据库文件(用户数据)" + echo " ✓ 用户上传的文件" + echo " ✓ 后端配置文件(.env)" + echo " ✓ 现有HTTP配置" + echo -e "${NC}" + echo "" + + print_info "适用场景: 初次配置HTTPS、更换证书方案、证书续签" + echo "" + + read -p "确定要继续吗? (y/n): " confirm < /dev/tty + + if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + print_info "已取消操作" + exit 0 + fi + + echo "" +} + +ssl_check_project() { + print_step "检查项目是否已安装..." + + if [[ ! -d "$PROJECT_DIR" ]]; then + print_error "项目未安装: $PROJECT_DIR" + print_info "请先运行安装命令: bash install.sh" + exit 1 + fi + + if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then + print_error "项目目录不完整" + exit 1 + fi + + print_success "项目已安装: $PROJECT_DIR" + echo "" +} + +ssl_load_existing_config() { + print_step "读取现有配置..." + + # 从.env读取后端端口 + if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then + BACKEND_PORT=$(grep "^PORT=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2 || echo "40001") + print_success "后端端口: $BACKEND_PORT" + else + BACKEND_PORT="40001" + print_warning ".env文件不存在,使用默认端口: $BACKEND_PORT" + fi + + # 检查现有nginx配置 + local nginx_conf="" + if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then + nginx_conf="/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" + elif [[ -f "/etc/nginx/conf.d/${PROJECT_NAME}.conf" ]]; then + nginx_conf="/etc/nginx/conf.d/${PROJECT_NAME}.conf" + elif [[ -f "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" ]]; then + nginx_conf="/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" + fi + + if [[ -n "$nginx_conf" ]]; then + # 读取HTTP端口 + EXISTING_HTTP_PORT=$(grep "listen" "$nginx_conf" | grep -v "ssl" | grep -v "#" | head -1 | awk '{print $2}' | tr -d ';' || echo "80") + HTTP_PORT=${EXISTING_HTTP_PORT:-80} + + # 检查是否有HTTPS配置 + if grep -q "listen.*ssl" "$nginx_conf"; then + EXISTING_HTTPS_PORT=$(grep "listen.*ssl" "$nginx_conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "443") + HTTPS_PORT=${EXISTING_HTTPS_PORT:-443} + print_info "检测到现有HTTPS配置,端口: $HTTPS_PORT" + else + HTTPS_PORT="443" + print_info "未检测到HTTPS配置,将使用默认端口: 443" + fi + + # 读取域名 + SERVER_NAME=$(grep "server_name" "$nginx_conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "") + if [[ -n "$SERVER_NAME" ]] && [[ "$SERVER_NAME" != "_" ]] && [[ "$SERVER_NAME" != "localhost" ]]; then + DOMAIN="$SERVER_NAME" + USE_DOMAIN=true + print_success "检测到域名: $DOMAIN" + else + USE_DOMAIN=false + print_warning "未检测到域名配置" + fi + + print_success "HTTP端口: $HTTP_PORT" + else + print_error "未找到Nginx配置文件" + exit 1 + fi + + echo "" +} + +ssl_configure_domain() { + print_step "配置域名" + echo "" + + # 如果已有域名,询问是否使用 + if [[ "$USE_DOMAIN" == "true" ]] && [[ -n "$DOMAIN" ]]; then + print_info "检测到现有域名: $DOMAIN" + read -p "是否使用此域名? (y/n): " use_existing < /dev/tty + + if [[ "$use_existing" == "y" || "$use_existing" == "Y" ]]; then + print_success "使用现有域名: $DOMAIN" + echo "" + return 0 + fi + fi + + # 输入新域名 + while true; do + read -p "请输入您的域名 (例如: wwy.example.com): " DOMAIN < /dev/tty + if [[ -z "$DOMAIN" ]]; then + print_error "域名不能为空" + continue + fi + + # 验证域名格式 + if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then + print_error "域名格式不正确" + continue + fi + + # 验证域名解析 + print_info "正在验证域名解析..." + DOMAIN_IP=$(dig +short "$DOMAIN" 2>/dev/null | tail -n1 || nslookup "$DOMAIN" 2>/dev/null | grep -A1 "Name:" | tail -1 | awk '{print $2}') + PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "") + + if [[ -n "$DOMAIN_IP" ]] && [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then + print_success "域名已正确解析到当前服务器IP" + USE_DOMAIN=true + break + else + print_warning "域名未解析到当前服务器IP" + print_info "域名解析IP: ${DOMAIN_IP:-未解析}" + print_info "当前服务器IP: $PUBLIC_IP" + read -p "是否继续? (y/n): " continue_choice < /dev/tty + if [[ "$continue_choice" == "y" || "$continue_choice" == "Y" ]]; then + USE_DOMAIN=true + break + fi + fi + done + + echo "" +} + +ssl_choose_method() { + print_step "选择SSL证书部署方式" + echo "" + echo -e "${YELLOW}【推荐方案】${NC}" + echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)" + echo " - 最稳定可靠,支持自动续期" + echo "" + echo -e "${YELLOW}【备选方案】${NC}" + echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt" + echo " - 纯Shell脚本,更轻量级" + echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL" + echo " - Let's Encrypt的免费替代品" + echo -e "${GREEN}[5]${NC} acme.sh + Buypass" + echo " - 挪威免费CA,有效期180天" + echo "" + echo -e "${YELLOW}【云服务商证书】${NC}" + echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)" + echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)" + echo "" + echo -e "${YELLOW}【其他选项】${NC}" + echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)" + echo -e "${GREEN}[8]${NC} 移除HTTPS配置 (改回HTTP)" + echo -e "${GREEN}[0]${NC} 取消操作" + echo "" + + while true; do + read -p "请输入选项 [0-8]: " ssl_choice < /dev/tty + case $ssl_choice in + 1|2|3|4|5|6|7) + SSL_METHOD=$ssl_choice + break + ;; + 8) + SSL_METHOD=$ssl_choice + print_warning "将移除HTTPS配置,改回HTTP模式" + read -p "确定要继续吗? (y/n): " confirm_remove < /dev/tty + if [[ "$confirm_remove" == "y" || "$confirm_remove" == "Y" ]]; then + break + fi + ;; + 0) + print_info "已取消操作" + exit 0 + ;; + *) + print_error "无效选项,请重新选择" + ;; + esac + done + echo "" +} + +ssl_deploy_certificate() { + print_step "部署SSL证书..." + + # 如果选择移除HTTPS + if [[ "$SSL_METHOD" == "8" ]]; then + print_info "将移除HTTPS配置..." + # 配置为HTTP模式 + configure_nginx_http + return 0 + fi + + # 部署证书 + deploy_ssl + + # 检查证书是否部署成功 + if [[ -f "/etc/nginx/ssl/${DOMAIN}.crt" ]] && [[ -f "/etc/nginx/ssl/${DOMAIN}.key" ]]; then + print_success "证书文件已部署" + else + print_warning "证书文件未找到,将使用HTTP配置" + SSL_METHOD="8" + fi +} + +ssl_update_nginx_config() { + print_step "更新Nginx配置..." + + if [[ "$SSL_METHOD" == "8" ]]; then + # HTTP配置 + configure_nginx_http + else + # HTTPS配置 + configure_nginx_https + fi + + # 测试nginx配置 + if ! nginx -t 2>&1; then + print_error "Nginx配置测试失败" + print_info "请检查配置文件" + return 1 + fi + + print_success "Nginx配置已更新" + echo "" +} + +ssl_reload_services() { + print_step "重载服务..." + + # 重载Nginx + if [[ -d /www/server/nginx ]]; then + # 宝塔面板 + print_info "宝塔环境,重载Nginx..." + if [[ -f /www/server/nginx/sbin/nginx ]]; then + /www/server/nginx/sbin/nginx -s reload 2>/dev/null + if [[ $? -eq 0 ]]; then + print_success "Nginx已重载" + else + /www/server/nginx/sbin/nginx 2>/dev/null + print_success "Nginx已启动" + fi + fi + systemctl reload nginx 2>/dev/null || true + else + # 标准Nginx + systemctl reload nginx + print_success "Nginx已重载" + fi + + # 重启后端服务(更新PUBLIC_PORT配置) + if command -v pm2 &> /dev/null; then + if pm2 list | grep -q "${PROJECT_NAME}-backend"; then + pm2 restart ${PROJECT_NAME}-backend + print_success "后端服务已重启" + fi + fi + + echo "" +} + +ssl_verify_deployment() { + print_step "验证部署..." + + # 检查Nginx + if [[ -d /www/server/nginx ]]; then + if pgrep -x nginx > /dev/null; then + print_success "Nginx运行正常" + else + print_error "Nginx未运行" + fi + else + if systemctl is-active --quiet nginx; then + print_success "Nginx运行正常" + else + print_error "Nginx未运行" + fi + fi + + # 检查SSL证书 + if [[ "$SSL_METHOD" != "8" ]]; then + if [[ -f "/etc/nginx/ssl/${DOMAIN}.crt" ]]; then + print_success "SSL证书已部署: /etc/nginx/ssl/${DOMAIN}.crt" + + # 显示证书信息 + CERT_EXPIRY=$(openssl x509 -enddate -noout -in "/etc/nginx/ssl/${DOMAIN}.crt" 2>/dev/null | cut -d= -f2) + if [[ -n "$CERT_EXPIRY" ]]; then + print_info "证书有效期至: $CERT_EXPIRY" + fi + else + print_warning "SSL证书文件未找到" + fi + fi + + echo "" +} + +print_ssl_completion() { + clear + echo -e "${GREEN}" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ ✓ SSL配置完成! ║" + echo "║ ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo -e "${NC}" + echo "" + + # 显示访问地址 + if [[ "$SSL_METHOD" == "8" ]]; then + if [[ "$HTTP_PORT" == "80" ]]; then + echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}" + else + echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}" + fi + echo -e "${YELLOW}模式:${NC} HTTP" + else + if [[ "$HTTPS_PORT" == "443" ]]; then + echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}" + else + echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}" + fi + echo -e "${YELLOW}模式:${NC} HTTPS" + + # SSL信息 + echo "" + echo -e "${YELLOW}SSL证书:${NC}" + case $SSL_METHOD in + 1) + echo " 方案: Certbot (Let's Encrypt)" + echo " 续期: 自动续期已配置" + ;; + 2) + echo " 方案: acme.sh + Let's Encrypt" + echo " 续期: 自动续期已配置" + ;; + 3) + echo " 方案: acme.sh + ZeroSSL" + echo " 续期: 自动续期已配置" + ;; + 4) + echo " 方案: acme.sh + Buypass" + echo " 续期: 自动续期已配置" + ;; + 7) + echo " 方案: 手动上传证书" + echo " 续期: 需手动更新证书文件" + ;; + esac + fi + echo "" + + echo -e "${YELLOW}常用命令:${NC}" + echo " 查看证书信息: openssl x509 -text -noout -in /etc/nginx/ssl/${DOMAIN}.crt" + echo " 测试HTTPS: curl -I https://${DOMAIN}" + echo " 查看Nginx日志: tail -f /var/log/nginx/error.log" + echo " 重新配置SSL: bash install.sh --ssl" + echo "" + + echo -e "${GREEN}SSL配置完成!${NC}" + echo "" +} + +ssl_main() { + # 检查root权限 + check_root + + # 检测操作系统 + detect_os + + # 确认操作 + confirm_ssl_operation + + # 检查项目 + ssl_check_project + + # 读取现有配置 + ssl_load_existing_config + + # 配置域名 + if [[ "$USE_DOMAIN" != "true" ]] || [[ -z "$DOMAIN" ]]; then + ssl_configure_domain + fi + + # 选择SSL方案 + ssl_choose_method + + # 先配置基础HTTP Nginx(SSL验证需要) + configure_nginx_http_first + + # 部署SSL证书 + ssl_deploy_certificate + + # 更新Nginx配置 + ssl_update_nginx_config + + # 重载服务 + ssl_reload_services + + # 验证部署 + ssl_verify_deployment + + # 完成提示 + print_ssl_completion +} + # 执行主流程 if [[ "$MODE" == "uninstall" ]]; then uninstall_main @@ -2782,6 +4440,8 @@ elif [[ "$MODE" == "update" ]]; then update_main elif [[ "$MODE" == "repair" ]]; then repair_main +elif [[ "$MODE" == "ssl" ]]; then + ssl_main else main fi