├── domain_http.conf ├── install.sh ├── default_system ├── domain_proxy.conf ├── README.md ├── domain_html.conf ├── domain_php.conf ├── domain_flarum.conf ├── donain_www.conf ├── nginx.conf ├── .github └── workflows │ └── main.yml ├── LICENSE └── ng.sh /domain_http.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name domain; 4 | 5 | location /.well-known/acme-challenge/ { 6 | root /var/www/acme; # 這是 certbot 要寫入的 webroot 路徑 7 | } 8 | } -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | install_path="/usr/local/bin/site" 4 | run_cmd="site" 5 | 6 | echo "正在下載腳本..." 7 | wget -qO "$install_path" https://raw.githubusercontent.com/gebu8f8/site_sh/refs/heads/main/ng.sh || { 8 | echo "下載失敗,請檢查網址或網路狀態。" 9 | exit 1 10 | } 11 | 12 | chmod +x "$install_path" 13 | 14 | 15 | echo 16 | echo "腳本已成功安裝!" 17 | echo "請輸入 '$run_cmd' 啟動面板。" 18 | 19 | read -n 1 -s -r -p "按任意鍵立即啟動..." key 20 | echo 21 | "site" 22 | -------------------------------------------------------------------------------- /default_system: -------------------------------------------------------------------------------- 1 | # By 科技lion 2 | # 製作成nginx系統版 by gebu8f. 3 | 4 | 5 | server { 6 | listen 80 default_server; 7 | listen [::]:80 default_server; 8 | listen 443 ssl default_server; 9 | listen [::]:443 ssl default_server; 10 | 11 | server_name _; 12 | 13 | # SSL 证书配置 14 | 15 | ssl_certificate /home/web/cert/default_server.crt; 16 | ssl_certificate_key /home/web/cert/default_server.key; 17 | 18 | # 返回 444 状态码以丢弃无效请求 19 | 20 | return 444; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /domain_proxy.conf: -------------------------------------------------------------------------------- 1 | # HTTP 轉 HTTPS + ACME 驗證 2 | server { 3 | listen 80; 4 | 5 | server_name domain; 6 | 7 | location /.well-known/acme-challenge/ { 8 | root /var/www/acme; # 與 certbot --webroot 對應 9 | try_files $uri =404; 10 | } 11 | 12 | location / { 13 | return 301 https://$host$request_uri; 14 | } 15 | } 16 | 17 | # HTTPS 反向代理主站點 18 | server { 19 | listen 443 ssl; 20 | listen [::]:443 ssl; 21 | 22 | listen 443 quic; 23 | listen [::]:443 quic; 24 | 25 | http2 on; 26 | http3 on; 27 | server_name domain; 28 | 29 | # SSL 憑證 30 | ssl_certificate /etc/letsencrypt/live/main/fullchain.pem; 31 | ssl_certificate_key /etc/letsencrypt/live/main/privkey.pem; 32 | 33 | # 安全 Header 34 | add_header X-Frame-Options SAMEORIGIN; 35 | add_header X-Content-Type-Options nosniff; 36 | add_header Referrer-Policy no-referrer-when-downgrade; 37 | 38 | # Gzip(可選) 39 | gzip on; 40 | gzip_types text/plain text/css application/json application/javascript application/xml; 41 | gzip_min_length 1024; 42 | 43 | # Proxy 設定 44 | location / { 45 | proxy_pass host:port; 46 | proxy_http_version 1.1; 47 | proxy_set_header Host $host; 48 | proxy_set_header X-Real-IP $remote_addr; 49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 50 | proxy_set_header X-Forwarded-Proto $scheme; 51 | proxy_set_header Upgrade $http_upgrade; 52 | proxy_set_header Connection "upgrade"; 53 | proxy_read_timeout 60s; 54 | } 55 | 56 | # 防止訪問隱藏檔案 57 | location ~ /\. { 58 | deny all; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nginx/Caddy 全自動建站工具(支援 Certbot + WordPress)By gebu8f 2 | 3 | # 站點管理器所整合的所有腳本 4 | 資料庫管理器:https://github.com/gebu8f8/db_sh 5 | 6 | 防火牆管理器:https://github.com/gebu8f8/firewall_sh 7 | 8 | Docker管理器: 9 | https://github.com/gebu8f8/docker_sh 10 | 11 | # 注意 ! 12 | - 本腳本採取選擇性設計原則: 13 | - 僅加入經過測試、穩定、必要的功能。 14 | - 對於可能影響系統相容性或造成混亂的需求,將不予加入,並在必要時說明理由。 15 | - 穩定性優先於功能多樣性,是本腳本的核心理念。 16 | 17 | # 🔐 授權 License 18 | 19 | 本專案以 [Apache License 2.0](./LICENSE) 授權,您可以自由使用、修改、商用或學術用途,條件如下: 20 | 21 | - **請保留原始作者署名(標註我)** 22 | - **若有修改,請明確標註變更** 23 | - **禁止移除授權資訊後冒充原作者** 24 | - **不提供任何保固或技術責任** 25 | 26 | 違反者我將保留依法提出 DMCA 下架的權利。 27 | 28 | # 介紹 29 | 30 | 這是一套純本地部署(非 Docker)的 Nginx/Caddy + SSL + WordPress 自動化建站腳本,專為 VPS 多系統環境設計,支援 **Debian / CentOS / Alpine Linux** 三大主流系統,讓你一鍵完成完整建站流程。 31 | 32 | # 📌 備註 33 | 34 | 我目前已將專案主力倉庫搬遷至 GitHub,原本長期維護於 GitLab(提交數已累積超過 200 次以上),目前此 GitHub 倉庫屬於新建立版本,因此提交紀錄較少屬正常情況。 35 | 36 | 🔗 原始 GitLab 倉庫:https://gitlab.com/gebu8f/sh 37 | 38 | 🔗 GitHub 倉庫:https://github.com/gebu8f8/site_sh 39 | 40 | --- 41 | 42 | ## 特點亮點 43 | 44 | ### ✅ 本地版非 Docker,更穩定可控 45 | 與部分大佬的 Docker 方案不同,本專案專注於本地安裝,**無容器依賴、無封裝黑盒**,配置與系統高度整合,便於排錯與維護。 46 | 47 | ### ✅ 跨三大主流系統自動適配 48 | 自動偵測系統,根據環境自動採用: 49 | - apt(Debian/Ubuntu) 50 | - yum / dnf(CentOS/RHEL) 51 | - apk(Alpine) 52 | 53 | ### ✅ 支援多家 CA 與 DNS / HTTP 驗證 54 | - 憑證機構選擇: 55 | - Let's Encrypt 56 | - ZeroSSL 57 | - Google Trust Services 58 | - 驗證方式: 59 | - Cloudflare DNS(API Token 驗證) 60 | - HTTP(Webroot / nginx 模組) 61 | 62 | ### ✅ WordPress 一鍵部署 + 自動資料庫建立 63 | - 自動建立資料庫與帳號密碼 64 | - 可保留語言選擇頁面(非全自動跳過) 65 | - Nginx 配置自動完成 66 | 67 | ### ✅ 全面錯誤處理與修復 68 | - 權限修復(避免 500 錯誤) 69 | - fastcgi socket 錯誤預防 70 | - certbot 自動續簽 + nginx reload 71 | - 自動開放 / 關閉 firewall(ufw / iptables / firewalld) 72 | 73 | --- 74 | 75 | ## 初次運行時需要下方指令,接下來可用site呼叫 76 | 77 | ## 安裝與使用 78 | ``` 79 | bash <(curl -sL https://sh.gebu8f.com/site) 80 | ``` 81 | 之後即可用site使用之 82 | -------------------------------------------------------------------------------- /domain_html.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name domain; 5 | 6 | location /.well-known/acme-challenge/ { 7 | root /var/www/acme; 8 | try_files $uri $uri/ =404; 9 | } 10 | 11 | location / { 12 | return 301 https://$host$request_uri; 13 | } 14 | } 15 | 16 | server { 17 | listen 443 ssl ; 18 | listen [::]:443 ssl ; 19 | 20 | listen 443 quic ; 21 | listen [::]:443 quic ; 22 | 23 | http2 on; 24 | http3 on; 25 | 26 | server_name domain; 27 | 28 | ssl_certificate /etc/letsencrypt/live/main/fullchain.pem; 29 | ssl_certificate_key /etc/letsencrypt/live/main/privkey.pem; 30 | 31 | root /var/www/domain; 32 | index index.html index.htm; 33 | 34 | # HTTP/3 標頭提示 35 | add_header Alt-Svc 'h3=":443"; ma=86400'; 36 | add_header QUIC-Status $http3; 37 | 38 | # Gzip 壓縮 39 | gzip on; 40 | gzip_comp_level 5; 41 | gzip_min_length 256; 42 | gzip_proxied any; 43 | gzip_disable "MSIE [1-6]\."; 44 | gzip_buffers 16 8k; 45 | gzip_http_version 1.1; 46 | gzip_static on; 47 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 48 | 49 | # 限制請求體積 50 | client_max_body_size 128M; 51 | 52 | # 圖片、資源長效緩存 53 | location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|webp|woff2?)$ { 54 | expires 30d; 55 | add_header Cache-Control "public, max-age=2592000"; 56 | access_log off; 57 | } 58 | 59 | # 防盜鏈 60 | location ~* \.(png|jpg|gif)$ { 61 | valid_referers none blocked domain; 62 | if ($invalid_referer) { 63 | return 403; 64 | } 65 | } 66 | 67 | location / { 68 | try_files $uri $uri/ /index.html; 69 | } 70 | 71 | # 禁止存取隱藏文件 72 | location ~ /\. { 73 | deny all; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /domain_php.conf: -------------------------------------------------------------------------------- 1 | # HTTPS 服務(HTTP/3 + HTTP/2 + TLSv1.3) 2 | server { 3 | listen 443 ssl; 4 | listen [::]:443 ssl; 5 | 6 | listen 443 quic; 7 | listen [::]:443 quic; 8 | 9 | http2 on; 10 | http3 on; 11 | 12 | server_name domain; 13 | root /var/www/domain; 14 | index index.php index.html; 15 | 16 | ssl_certificate /etc/letsencrypt/live/main/fullchain.pem; 17 | ssl_certificate_key /etc/letsencrypt/live/main/privkey.pem; 18 | # HTTP/3 標頭提示 19 | add_header Alt-Svc 'h3=":443"; ma=86400'; 20 | add_header QUIC-Status $http3; 21 | 22 | # Gzip 壓縮 23 | gzip on; 24 | gzip_comp_level 5; 25 | gzip_min_length 256; 26 | gzip_proxied any; 27 | gzip_types 28 | text/plain 29 | text/css 30 | text/xml 31 | application/xml 32 | application/json 33 | application/javascript 34 | application/rss+xml 35 | application/atom+xml 36 | image/svg+xml 37 | font/ttf 38 | font/otf 39 | application/font-woff 40 | application/font-woff2; 41 | client_max_body_size 128M; 42 | # 圖片緩存設定 43 | location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|css|js|woff2?)$ { 44 | expires 30d; 45 | access_log off; 46 | add_header Cache-Control "public, max-age=2592000"; 47 | } 48 | # 防盜鏈 49 | location ~* \.(png|jpg|gif)$ { 50 | valid_referers none blocked domain; 51 | if ($invalid_referer) { 52 | return 403; 53 | } 54 | } 55 | 56 | # 主網站邏輯 57 | location / { 58 | try_files $uri $uri/ /index.php?$args; 59 | } 60 | 61 | # PHP 支援 62 | location ~ \.php$ { 63 | include fastcgi_params; 64 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 65 | fastcgi_index index.php; 66 | fastcgi_pass unix:/run/php/php-fpm.sock; 67 | fastcgi_buffering on; 68 | fastcgi_buffers 16 16k; 69 | fastcgi_buffer_size 32k; 70 | } 71 | 72 | # 禁止隱藏文件 73 | location ~ /\. { 74 | deny all; 75 | } 76 | } 77 | 78 | # HTTP 導向 HTTPS 79 | server { 80 | listen 80; 81 | server_name domain; 82 | 83 | location /.well-known/acme-challenge/ { 84 | root /var/www/acme; 85 | try_files $uri $uri/ =404; 86 | } 87 | 88 | location / { 89 | return 301 https://$host$request_uri; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /domain_flarum.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | listen [::]:443 ssl; 4 | listen 443 quic; 5 | listen [::]:443 quic; 6 | http2 on; 7 | http3 on; 8 | 9 | server_name domain; 10 | root /var/www/domain; 11 | index index.php; 12 | 13 | ssl_certificate /etc/letsencrypt/live/main/fullchain.pem; 14 | ssl_certificate_key /etc/letsencrypt/live/main/privkey.pem; 15 | 16 | add_header Alt-Svc 'h3=":443"; ma=86400'; 17 | add_header QUIC-Status $http3; 18 | 19 | gzip on; 20 | gzip_comp_level 5; 21 | gzip_min_length 256; 22 | gzip_proxied any; 23 | gzip_disable "MSIE [1-6]\."; 24 | gzip_buffers 16 8k; 25 | gzip_http_version 1.1; 26 | gzip_static on; 27 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 28 | 29 | client_max_body_size 128M; 30 | 31 | # 圖片緩存 32 | location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|css|js|woff2?)$ { 33 | expires 30d; 34 | access_log off; 35 | add_header Cache-Control "public, max-age=2592000"; 36 | } 37 | 38 | # 防盜鏈 39 | location ~* \.(png|jpg|gif)$ { 40 | valid_referers none blocked domain; 41 | if ($invalid_referer) { 42 | return 403; 43 | } 44 | } 45 | 46 | # Flarum 路由支持 47 | location / { 48 | try_files $uri $uri/ /index.php?$query_string; 49 | } 50 | 51 | # 可選 - 支援 Flarum API 單獨處理 52 | location /api { 53 | try_files $uri $uri/ /index.php?$query_string; 54 | } 55 | 56 | # 禁止訪問敏感資料 57 | location ~* ^/(composer\.json|config\.php|flarum|storage|vendor)/ { 58 | deny all; 59 | return 404; 60 | } 61 | 62 | location ~ \.php$ { 63 | include fastcgi_params; 64 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 65 | fastcgi_index index.php; 66 | fastcgi_pass unix:/run/php/php-fpm.sock; 67 | fastcgi_buffering on; 68 | fastcgi_buffers 16 16k; 69 | fastcgi_buffer_size 32k; 70 | } 71 | 72 | location ~ /\. { 73 | deny all; 74 | } 75 | } 76 | 77 | server { 78 | listen 80; 79 | server_name domain; 80 | 81 | location /.well-known/acme-challenge/ { 82 | root /var/www/acme; 83 | try_files $uri $uri/ =404; 84 | } 85 | 86 | location / { 87 | return 301 https://$host$request_uri; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /donain_www.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | listen [::]:443 ssl; 4 | listen 443 quic; 5 | listen [::]:443 quic; 6 | http2 on; 7 | http3 on; 8 | 9 | server_name www.domain domain; 10 | root /var/www/www.domain; 11 | index index.php index.html; 12 | 13 | ssl_certificate /etc/letsencrypt/live/main/fullchain.pem; 14 | ssl_certificate_key /etc/letsencrypt/live/main/privkey.pem; 15 | 16 | # TLS/QUIC 優化 17 | ssl_session_timeout 10m; 18 | ssl_protocols TLSv1.3; 19 | ssl_prefer_server_ciphers on; 20 | ssl_ciphers HIGH:!aNULL:!MD5; 21 | ssl_session_cache shared:SSL:50m; 22 | 23 | # HTTP/3 標頭提示 24 | add_header Alt-Svc 'h3=":443"; ma=86400'; 25 | add_header QUIC-Status $http3; 26 | 27 | # Gzip 壓縮 28 | gzip on; 29 | gzip_comp_level 5; 30 | gzip_min_length 256; 31 | gzip_proxied any; 32 | gzip_disable "MSIE [1-6]\."; 33 | gzip_buffers 16 8k; 34 | gzip_http_version 1.1; 35 | gzip_static on; 36 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 37 | 38 | # 限制請求體積 39 | client_max_body_size 128M; 40 | 41 | # 如果訪問的是非 www 域名,重導向 42 | if ($host = "gebu8f.com") { 43 | return 301 https://www.domain$request_uri; 44 | } 45 | 46 | # 圖片緩存設定 47 | location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|css|js|woff2?)$ { 48 | expires 30d; 49 | access_log off; 50 | add_header Cache-Control "public, max-age=2592000"; 51 | } 52 | 53 | # 防盜鏈 54 | location ~* \.(png|jpg|gif)$ { 55 | valid_referers none blocked www.domain domain; 56 | if ($invalid_referer) { 57 | return 403; 58 | } 59 | } 60 | 61 | # 主網站邏輯(含 token 判斷) 62 | location / { 63 | try_files $uri $uri/ /index.php?$args; 64 | } 65 | 66 | # PHP 支援 67 | location ~ \.php$ { 68 | include fastcgi_params; 69 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 70 | fastcgi_index index.php; 71 | fastcgi_pass unix:/run/php/php-fpm.sock; 72 | fastcgi_buffering on; 73 | fastcgi_buffers 16 16k; 74 | fastcgi_buffer_size 32k; 75 | } 76 | 77 | # 禁止存取隱藏文件 78 | location ~ /\. { 79 | deny all; 80 | } 81 | } 82 | 83 | # 非 www HTTP 轉 HTTPS 84 | server { 85 | listen 80; 86 | server_name domain; 87 | return 301 https://www.domain$request_uri; 88 | } 89 | 90 | # www HTTP 轉 HTTPS(並保留 ACME 支援) 91 | server { 92 | listen 80; 93 | server_name www.domain; 94 | 95 | location /.well-known/acme-challenge/ { 96 | root /var/www/www.domain; 97 | try_files $uri $uri/ =404; 98 | } 99 | 100 | location / { 101 | return 301 https://$host$request_uri; 102 | } 103 | } -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes auto; 4 | 5 | error_log logs/error.log; 6 | error_log logs/error.log notice; 7 | error_log logs/error.log info; 8 | 9 | #pid /run/nginx.pid; 10 | 11 | 12 | events { 13 | worker_connections 1024; 14 | } 15 | 16 | 17 | http { 18 | set_real_ip_from 0.0.0.0/0; 19 | 20 | # 通用的 real_ip_header 優先順序 21 | real_ip_header X-Forwarded-For; 22 | 23 | # 如用 Cloudflare,會自動在最前面給出真實 IP 24 | real_ip_recursive on; 25 | 26 | include mime.types; 27 | default_type application/octet-stream; 28 | include /etc/nginx/conf.d/*.conf; 29 | 30 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 31 | '$status $body_bytes_sent "$http_referer" ' 32 | '"$http_user_agent" "$http_x_forwarded_for"'; 33 | 34 | access_log logs/access.log main; 35 | 36 | sendfile on; 37 | #tcp_nopush on; 38 | 39 | #keepalive_timeout 0; 40 | keepalive_timeout 65; 41 | 42 | gzip on; 43 | 44 | server { 45 | 46 | 47 | #error_page 404 /404.html; 48 | 49 | # redirect server error pages to the static page /50x.html 50 | # 51 | error_page 500 502 503 504 /50x.html; 52 | location = /50x.html { 53 | root html; 54 | } 55 | 56 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 57 | # 58 | #location ~ \.php$ { 59 | # proxy_pass http://127.0.0.1; 60 | #} 61 | 62 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 63 | # 64 | #location ~ \.php$ { 65 | # root html; 66 | # fastcgi_pass 127.0.0.1:9000; 67 | # fastcgi_index index.php; 68 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 69 | # include fastcgi_params; 70 | #} 71 | 72 | # deny access to .htaccess files, if Apache's document root 73 | # concurs with nginx's one 74 | # 75 | #location ~ /\.ht { 76 | # deny all; 77 | #} 78 | } 79 | 80 | 81 | # another virtual host using mix of IP-, name-, and port-based configuration 82 | # 83 | #server { 84 | # listen 8000; 85 | # listen somename:8080; 86 | # server_name somename alias another.alias; 87 | 88 | # location / { 89 | # root html; 90 | # index index.html index.htm; 91 | # } 92 | #} 93 | 94 | 95 | # HTTPS server 96 | # 97 | #server { 98 | # listen 443 ssl; 99 | # server_name localhost; 100 | 101 | # ssl_certificate cert.pem; 102 | # ssl_certificate_key cert.key; 103 | 104 | # ssl_session_cache shared:SSL:1m; 105 | # ssl_session_timeout 5m; 106 | 107 | # ssl_ciphers HIGH:!aNULL:!MD5; 108 | # ssl_prefer_server_ciphers on; 109 | 110 | # location / { 111 | # root html; 112 | # index index.html index.htm; 113 | # } 114 | #} 115 | 116 | } 117 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Generate Release Note 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release_note: 10 | runs-on: ubuntu-latest 11 | 12 | env: 13 | AI_API_URL: ${{ secrets.AI_API_URL }} 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Check if ng.sh was modified in this push 21 | id: check_ngsh_commit 22 | run: | 23 | if git rev-parse HEAD~1 >/dev/null 2>&1; then 24 | # 檢查 HEAD~1 → HEAD 是否變更 ng.sh 25 | if git diff --name-only HEAD~1 HEAD | grep -q '^ng\.sh$'; then 26 | echo "changed_ngsh=true" >> $GITHUB_ENV 27 | echo "本次 push 有修改 ng.sh" 28 | else 29 | echo "changed_ngsh=false" >> $GITHUB_ENV 30 | echo "本次 push 未修改 ng.sh,後續流程將跳過" 31 | fi 32 | else 33 | # 若 repo 僅有一個 commit 34 | if git ls-tree --name-only HEAD | grep -q '^ng\.sh$'; then 35 | echo "changed_ngsh=true" >> $GITHUB_ENV 36 | echo "首次 commit 包含 ng.sh" 37 | else 38 | echo "changed_ngsh=false" >> $GITHUB_ENV 39 | echo "首次 commit 未包含 ng.sh,後續流程將跳過" 40 | fi 41 | fi 42 | 43 | - name: Read version from ng.sh 44 | if: env.changed_ngsh == 'true' 45 | id: read_version 46 | run: | 47 | raw_version=$(grep '^version=' ng.sh | cut -d'=' -f2 | tr -d '"' | tr -d "'") 48 | echo "抓到原始版本: $raw_version" 49 | 50 | if [[ "$raw_version" =~ ^v ]]; then 51 | version_final="$raw_version" 52 | else 53 | version_final="v$raw_version" 54 | fi 55 | 56 | echo "version_final=$version_final" >> $GITHUB_OUTPUT 57 | 58 | - name: Check if tag already exists 59 | if: env.changed_ngsh == 'true' 60 | id: check_tag 61 | run: | 62 | git fetch --tags 63 | if git tag --list | grep -q "^${{ steps.read_version.outputs.version_final }}$"; then 64 | echo "此版本已存在,不重複執行。" 65 | echo "skip_whole_workflow=true" >> $GITHUB_ENV 66 | else 67 | echo "skip_whole_workflow=false" >> $GITHUB_ENV 68 | fi 69 | 70 | - name: Generate git diff 71 | if: env.changed_ngsh == 'true' && env.skip_whole_workflow != 'true' 72 | id: diff 73 | run: | 74 | git fetch origin main 75 | 76 | if git rev-parse HEAD~1 >/dev/null 2>&1; then 77 | if git diff --quiet HEAD~1 HEAD -- ng.sh; then 78 | echo "diff_text=(未變更 ng.sh)" >> $GITHUB_ENV 79 | echo "skip_ai=true" >> $GITHUB_ENV 80 | else 81 | diff_text=$(git diff HEAD~1 HEAD -- ng.sh | grep -vE '^[+-]version=') 82 | echo "diff_text<> $GITHUB_ENV 83 | echo "$diff_text" >> $GITHUB_ENV 84 | echo "EOF" >> $GITHUB_ENV 85 | echo "skip_ai=false" >> $GITHUB_ENV 86 | fi 87 | else 88 | echo "首次 commit,無法產生 diff" 89 | echo "diff_text=(首次 commit)" >> $GITHUB_ENV 90 | echo "skip_ai=true" >> $GITHUB_ENV 91 | fi 92 | 93 | - name: Call custom AI API 94 | if: env.changed_ngsh == 'true' && env.skip_whole_workflow != 'true' && env.skip_ai == 'false' 95 | run: | 96 | diff_json=$(jq -Rs . <<< "$diff_text") 97 | 98 | json_payload=$(jq -n \ 99 | --arg system_msg "請用繁體中文,根據以下 diff 寫出 changelog 條列,不要開場白、不要說明文字、也不要提到版本更新,也不要提到 diff,僅條列變更項目,簡短扼要。條列時請使用 - 作為項目前綴符號。" \ 100 | --arg user_msg "$diff_text" \ 101 | '{ 102 | model: "deepseek/DeepSeek-V3-0324", 103 | messages: [ 104 | {"role": "system", "content": $system_msg}, 105 | {"role": "user", "content": $user_msg} 106 | ] 107 | }') 108 | 109 | curl -s -X POST $AI_API_URL \ 110 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 111 | -H "Content-Type: application/json" \ 112 | -d "$json_payload" > response.json 113 | 114 | summary=$(jq -r '.choices[0].message.content // "AI 回傳空結果"' response.json) 115 | 116 | echo "summary<> $GITHUB_ENV 117 | echo "$summary" >> $GITHUB_ENV 118 | echo "EOF" >> $GITHUB_ENV 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | 122 | - name: Show Release Note 123 | if: env.changed_ngsh == 'true' && env.skip_whole_workflow != 'true' 124 | env: 125 | SUMMARY_CONTENT: ${{ env.summary }} 126 | run: | 127 | echo "最終版本號: ${{ steps.read_version.outputs.version_final }}" 128 | echo "$SUMMARY_CONTENT" 129 | 130 | - name: Create git tag 131 | if: env.changed_ngsh == 'true' && env.skip_whole_workflow != 'true' && env.skip_ai == 'false' 132 | run: | 133 | git tag ${{ steps.read_version.outputs.version_final }} 134 | git push origin ${{ steps.read_version.outputs.version_final }} 135 | 136 | - name: Create GitHub Release 137 | if: env.changed_ngsh == 'true' && env.skip_whole_workflow != 'true' && env.skip_ai == 'false' 138 | uses: softprops/action-gh-release@v2 139 | with: 140 | tag_name: ${{ steps.read_version.outputs.version_final }} 141 | name: ${{ steps.read_version.outputs.version_final }} 142 | body: ${{ env.summary }} 143 | env: 144 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 145 | 146 | - name: Send Telegram Message 147 | continue-on-error: true 148 | if: env.changed_ngsh == 'true' && env.skip_whole_workflow != 'true' && env.skip_ai == 'false' 149 | env: 150 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 151 | TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} 152 | FINAL_VERSION: ${{ steps.read_version.outputs.version_final }} 153 | SUMMARY_CONTENT: ${{ env.summary }} 154 | run: | 155 | # 組合訊息,在最前面加上固定的標題和一個換行 156 | MESSAGE_BODY="【站點管理器】 157 | $FINAL_VERSION 158 | 159 | $SUMMARY_CONTENT" 160 | 161 | # 使用 curl 的 --data-urlencode 來安全地傳遞文字 162 | curl -s -X POST "https://api.telegram.org/bot$TG_BOT_TOKEN/sendMessage" \ 163 | --data-urlencode "chat_id=$TG_CHAT_ID" \ 164 | --data-urlencode "text=$MESSAGE_BODY" \ 165 | --data-urlencode "parse_mode=Markdown" 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 gebu8f 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ng.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 檢查是否以root權限運行 4 | if [ "$(id -u)" -ne 0 ]; then 5 | echo "此腳本需要root權限運行" 6 | if command -v sudo >/dev/null 2>&1; then 7 | exec sudo "$0" "$@" 8 | else 9 | install_sudo_cmd="" 10 | if command -v apt >/dev/null 2>&1; then 11 | install_sudo_cmd="apt-get update && apt-get install -y sudo" 12 | elif command -v yum >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then 13 | install_sudo_cmd="yum install -y sudo" 14 | elif command -v apk >/dev/null 2>&1; then 15 | install_sudo_cmd="apk add sudo" 16 | else 17 | echo "無sudo指令" 18 | sleep 1 19 | exit 1 20 | fi 21 | su -c "$install_sudo_cmd" 22 | if [ $? -eq 0 ] && command -v sudo >/dev/null 2>&1; then 23 | echo "sudo指令已經安裝成功,請等下輸入您的密碼" 24 | exec sudo "$0" "$@" 25 | fi 26 | fi 27 | fi 28 | 29 | # 版本 30 | version="8.0.4" 31 | 32 | 33 | # 顏色定義 34 | RED="\033[1;31m" # ❌ 錯誤用紅色 35 | GREEN="\033[1;32m" # ✅ 成功用綠色 36 | YELLOW='\033[1;33m' # ⚠️ 警告用黃色 37 | CYAN="\033[1;36m" # ℹ️ 一般提示用青色 38 | RESET='\033[0m' # 清除顏色 39 | 40 | 41 | phpini_path()( 42 | local php_var=$(check_php_version) 43 | local php_ini 44 | if [ "$system" -eq 1 ]; then 45 | php_ini="/etc/php/$php_var/fpm/php.ini" 46 | else 47 | php_ini=$(php -i | grep "Loaded Configuration File" | awk '{print $5}') 48 | fi 49 | echo $php_ini 50 | ) 51 | 52 | check_system(){ 53 | if command -v apt >/dev/null 2>&1; then 54 | system=1 55 | elif command -v yum >/dev/null 2>&1; then 56 | system=2 57 | if grep -q -Ei "release 7|release 8" /etc/redhat-release; then 58 | echo -e "${RED}不支援 CentOS 7 或 CentOS 8,請升級至 9 系列 (Rocky/Alma/CentOS Stream)${RESET}" 59 | exit 1 60 | fi 61 | elif command -v apk >/dev/null 2>&1; then 62 | system=3 63 | else 64 | echo -e "${RED}不支援的系統。${RESET}" >&2 65 | exit 1 66 | fi 67 | } 68 | 69 | check_and_start_service() { 70 | if command -v openresty >/dev/null 2>&1; then 71 | local service_name=openresty 72 | elif command -v nginx >/dev/null 2>&1; then 73 | local service_name=nginx 74 | fi 75 | 76 | if service "$service_name" status >/dev/null 2>&1; then 77 | service "$service_name" start 78 | fi 79 | } 80 | 81 | check_web_environment() { 82 | use_my_app=false 83 | port_in_use=false 84 | ss -tln | grep -qE ':(80|443)\s' && port_in_use=true 85 | # 有安裝 nginx 或 openresty 即可啟用 86 | if command -v nginx >/dev/null 2>&1 || command -v openresty >/dev/null 2>&1 || command -v caddy >/dev/null 2>&1; then 87 | use_my_app=true 88 | fi 89 | } 90 | 91 | check_cert() { 92 | local domain="$1" 93 | local cert_dir="/etc/letsencrypt/live" 94 | 95 | [ $caddy -eq 1 ] && return 0 96 | 97 | # 計算網域層級 98 | IFS='.' read -ra domain_parts <<< "$domain" 99 | local level=${#domain_parts[@]} 100 | 101 | if [ "$level" -ge 6 ]; then 102 | echo -e ${RED} "網域層級過多($level),請檢查輸入是否正確。${RESET}" >&2 103 | return 1 104 | fi 105 | 106 | # 掃描所有憑證資料夾,逐一分析 SAN 107 | for dir in "$cert_dir"/*; do 108 | [ -d "$dir" ] || continue 109 | local cert_path="$dir/fullchain.pem" 110 | 111 | if [ -f "$cert_path" ]; then 112 | local san_list=$(openssl x509 -in "$cert_path" -noout -ext subjectAltName 2>/dev/null | \ 113 | grep -oE 'DNS:[^,]+' | sed 's/DNS://g') 114 | 115 | for san in $san_list; do 116 | if [[ "$san" == "$domain" ]] || [[ "$san" == "*.${domain#*.}" ]]; then 117 | echo "$(basename "$dir")" 118 | return 0 119 | fi 120 | done 121 | fi 122 | done 123 | 124 | echo -e "${YELLOW}未找到包含 $domain 的有效憑證${RESET}" >&2 125 | return 1 126 | } 127 | 128 | check_app(){ 129 | declare -A pkg_map=( 130 | ["wget"]="wget" 131 | ["jq"]="jq" 132 | ["nano"]="nano" 133 | ["ss"]="iproute2" 134 | ["openssl"]="openssl" 135 | ) 136 | if [ $system -eq 2 ]; then 137 | if ! [ -f /etc/fedora-release ]; then 138 | if ! yum repolist enabled | grep -q "epel"; then 139 | yum install -y epel-release 140 | fi 141 | fi 142 | fi 143 | for cmd in "${!pkg_map[@]}"; do 144 | if ! command -v "$cmd" >/dev/null 2>&1; then 145 | pkg="${pkg_map[$cmd]}" 146 | case "$system" in 147 | 1) apt update -qq && apt install -y "$pkg" ;; 148 | 2) yum update && yum install -y "$pkg" ;; 149 | esac 150 | fi 151 | done 152 | if ! command -v lsb_release &>/dev/null; then 153 | case $system in 154 | 1) 155 | apt update && apt install -y lsb-release 156 | ;; 157 | 2) 158 | dnf install -y lsb-release 159 | ;; 160 | esac 161 | fi 162 | if ! command -v dig &>/dev/null; then 163 | case $system in 164 | 1) 165 | apt update && apt install -y dnsutils 166 | ;; 167 | 2) 168 | yum install -y bind-utils 169 | ;; 170 | 3) 171 | apk add bind-tools 172 | ;; 173 | esac 174 | fi 175 | } 176 | 177 | check_webserver_install(){ 178 | if [[ $use_my_app = false && $port_in_use = false ]]; then 179 | while true; do 180 | clear 181 | echo "=========站點管理器之安裝網站伺服器==========" 182 | echo "1. 安裝nginx(支援HTTP3)" 183 | echo "2. 安裝Openresy(支援LUA)" 184 | echo "3. 安裝caddy server (個人站適用)" 185 | read -p "請選擇安裝的伺服器[1-3,預設為2]" choice 186 | choice=${choice:-2} 187 | case $choice in 188 | 1) 189 | install_web_server nginx 190 | break 191 | ;; 192 | 2) 193 | if [ $system == 1 ]; then 194 | local codename 195 | local os=$(lsb_release -is | tr '[:upper:]' '[:lower:]') 196 | if [[ $os == kali ]]; then 197 | codename=bookworm 198 | else 199 | codename=$(grep -Po 'VERSION="[0-9]+ \(\K[^)]+' /etc/os-release) 200 | local codename_ver=$(grep -Po '(?<=VERSION_ID=")[0-9]+' /etc/os-release) 201 | fi 202 | if [ $codename_ver -gt 12 ]; then 203 | codename=bookworm 204 | fi 205 | if ! curl -sf "https://openresty.org/package/debian/dists/${codename}/" >/dev/null; then 206 | echo -e "${RED}官方倉庫尚未支援 ${codename}${RESET}" 207 | sleep 2 208 | else 209 | install_web_server openresty 210 | break 211 | fi 212 | elif [ $system == 3 ]; then 213 | if curl -sf https://openresty.org/package/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/main >/dev/null; then 214 | install_web_server openresty 215 | else 216 | install_web_server openresty compile 217 | break 218 | fi 219 | fi 220 | ;; 221 | 3) 222 | if [ $system -eq 3 ]; then 223 | echo -e " 224 | ${YELLOW}官方倉庫尚未支援${RESET}" 225 | sleep 1 226 | fi 227 | install_web_server caddy 228 | break 229 | esac 230 | done 231 | fi 232 | } 233 | 234 | check_web_server(){ 235 | openresty=0 236 | nginx=0 237 | caddy=0 238 | if command -v openresty >/dev/null 2>&1; then 239 | openresty=1 240 | elif command -v nginx >/dev/null 2>&1; then 241 | nginx=1 242 | elif command -v caddy >/dev/null 2>&1; then 243 | caddy=1 244 | fi 245 | } 246 | 247 | detect_nginx_conf_paths(){ 248 | local command="" 249 | if [ $openresty -eq 1 ]; then 250 | command=openresty 251 | elif [ $nginx -eq 1 ]; then 252 | command=nginx 253 | fi 254 | # 執行 nginx/openresty -t 並提取配置文件路徑 255 | # 從第二行提取,移除 "nginx: configuration file " 和 " test" 之後的內容 256 | local conf_path=$($command -t 2>&1 | sed -n '2s/^nginx: configuration file \(.*\) test.*$/\1/p') 257 | 258 | echo "$conf_path" 259 | } 260 | # WordPress備份 261 | # 回傳 wp/flarum/unknown 262 | 263 | detect_site_type() { 264 | local web_root="$1" 265 | if [[ -f "$web_root/wp-config.php" ]]; then 266 | echo "wp" 267 | elif [[ -f "$web_root/config.php" && -d "$web_root/vendor/flarum" ]]; then 268 | echo "flarum" 269 | else 270 | echo "unknown" 271 | fi 272 | } 273 | 274 | # 多站型備份主函式,$1=wp/flarum,$2=domain 275 | backup_site_type() { 276 | local type="$1" 277 | local domain="$2" 278 | local web_root="/var/www/$domain" 279 | local backup_dir="/opt/backups/$domain" 280 | local timestamp=$(date +"%Y%m%d-%H%M%S") 281 | local backup_file="$backup_dir/backup-$timestamp.tar.gz" 282 | mkdir -p "$backup_dir" 283 | 284 | # --- 資料庫備份 (DB Backup) --- 285 | local db_name="" 286 | local dba_export_dir="/root/mysql_backups" 287 | 288 | if [[ "$type" == "wp" ]]; then 289 | local wp_config="$web_root/wp-config.php" 290 | db_name=$(awk -F"'" '/DB_NAME/{print $4}' "$wp_config") 291 | elif [[ "$type" == "flarum" ]]; then 292 | local config="$web_root/config.php" 293 | db_name=$(php -r "\$c = include '$config'; echo \$c['database']['database'] ?? '';") 294 | else 295 | echo -e "${RED}不支援的站點類型:$type${RESET}" 296 | sleep 1 297 | return 1 298 | fi 299 | if [[ -z "$db_name" ]]; then 300 | echo -e "${RED}無法從設定檔中讀取到資料庫名稱!${RESET}" 301 | sleep 1 302 | return 1 303 | fi 304 | if ! command -v dba >/dev/null 2>&1; then 305 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 306 | fi 307 | 308 | latest_sql_export=$(dba mysql export "$db_name" "$dba_export_dir") 309 | if [ $? -ne 0 ]; then 310 | echo -e "${RED}使用 'dba' 工具備份資料庫失敗!${RESET}" 311 | sleep 1 312 | return 1 313 | fi 314 | if [[ ! -f "$latest_sql_export" ]]; then 315 | echo -e "${RED}資料庫備份指令執行成功,但在預期目錄中找不到 SQL 檔案!(${dba_export_dir})${RESET}" 316 | sleep 1 317 | return 1 318 | fi 319 | 320 | echo -e "${GREEN}資料庫已成功匯出至:$latest_sql_export${RESET}" 321 | if ! cp "$latest_sql_export" "$web_root/"; then 322 | echo -e "${RED}無法複製 SQL 備份檔案,打包中止!${RESET}" 323 | rm -f "$latest_sql_export" # 清理 dba 生成的原始 sql 324 | sleep 1 325 | return 1 326 | fi 327 | tar -czf "$backup_file" -C "$web_root" . 328 | 329 | rm -f "$web_root/$(basename "$latest_sql_export")" 330 | rm -f "$latest_sql_export" 331 | 332 | echo -e "${GREEN}備份完成!檔案位置:$backup_file${RESET}" 333 | } 334 | 335 | backup_site() { 336 | local conf_dir=$(detect_conf_path) 337 | # 取得所有 .conf 338 | local raw_files=("$conf_dir"/*.conf) 339 | local site_files=() 340 | 341 | # 過濾掉 basename 不含 "." 的 342 | for f in "${raw_files[@]}"; do 343 | local name=$(basename "$f" .conf) 344 | if [[ "$name" == *.* ]]; then 345 | site_files+=("$f") 346 | fi 347 | done 348 | 349 | if [ ${#site_files[@]} -eq 0 ]; then 350 | echo -e "${YELLOW}目前沒有任何可備份的站點。${RESET}" 351 | return 0 352 | fi 353 | 354 | echo "請選擇要備份的站點:" 355 | local idx=1 356 | for f in "${site_files[@]}"; do 357 | local name=$(basename "$f" .conf) 358 | echo " $idx) $name" 359 | idx=$((idx + 1)) 360 | done 361 | 362 | echo 363 | read -p "請輸入數字:" choice 364 | 365 | # 非數字 366 | if ! [[ "$choice" =~ ^[0-9]+$ ]]; then 367 | echo -e "${RED}無效的選擇。${RESET}" 368 | sleep 1 369 | return 1 370 | fi 371 | 372 | local max=${#site_files[@]} 373 | if (( choice < 1 || choice > max )); then 374 | echo -e "${RED}選擇超出範圍。${RESET}" 375 | sleep 1 376 | return 1 377 | fi 378 | 379 | local conf_file="${site_files[$((choice - 1))]}" 380 | local domain=$(basename "$conf_file" .conf) 381 | local web_root="/var/www/$domain" 382 | local backup_dir="/opt/wp_backups/$domain" 383 | mkdir -p "$backup_dir" 384 | 385 | local type=$(detect_site_type "$web_root") 386 | 387 | if [[ "$type" == "unknown" ]]; then 388 | echo -e "${RED}不支援的站點類型,取消備份。${RESET}" 389 | sleep 1 390 | return 1 391 | fi 392 | 393 | backup_site_type "$type" "$domain" || return 1 394 | echo -e "${GREEN}備份作業完成${RESET}" 395 | } 396 | 397 | clean_ssl_session_cache() { 398 | [ $caddy -eq 1 ] && return 0 399 | local paths=$(detect_nginx_conf_paths) 400 | if [ -f "$paths" ]; then 401 | # 先計算未註解的 ssl_session_cache 行數 402 | local count_before count_after 403 | count_before=$(grep -E '^[[:space:]]*ssl_session_cache' "$paths" | wc -l) 404 | # 刪除未註解的 ssl_session_cache 行(前面不能有 # 和任意空白) 405 | sed -i '/^[[:space:]]*ssl_session_cache[[:space:]]/d' "$paths" 406 | count_after=$(grep -E '^[[:space:]]*ssl_session_cache' "$paths" | wc -l) 407 | if [ "$count_before" -gt "$count_after" ]; then 408 | echo -e "${GREEN}已清除 $file 中的 ssl_session_cache 設定${RESET}" 409 | fi 410 | fi 411 | } 412 | 413 | check_http3_support() { 414 | support_http3=false 415 | # 找出 nginx 或 openresty 的執行檔 416 | nginx_bin="" 417 | if command -v openresty >/dev/null 2>&1; then 418 | nginx_bin=$(command -v openresty) 419 | elif command -v nginx >/dev/null 2>&1; then 420 | nginx_bin=$(command -v nginx) 421 | fi 422 | 423 | # 沒有 nginx/openresty 就直接 return 424 | [ -z "$nginx_bin" ] && return 425 | 426 | # 嘗試從版本資訊中看是否支援 http_v3_module 427 | if "$nginx_bin" -V 2>&1 | grep -q -- '--with-http_v3_module'; then 428 | support_http3=true 429 | echo "$support_http3" 430 | return 431 | fi 432 | echo "$support_http3" 433 | } 434 | 435 | check_nginx(){ 436 | check_web_environment 437 | if [[ $use_my_app = false && $port_in_use = true ]]; then 438 | echo -e "${RED}偵測到您的系統已安裝其他 Web Server,或 80/443 端口已被佔用。${RESET}" 439 | echo -e "${YELLOW}請手動停止或解除安裝相關服務,例如 apache、Caddy 或其他佔用程式。${RESET}" 440 | read -n1 -r -p "請處理完畢後再繼續,按任意鍵結束..." _ 441 | return 1 442 | elif [[ $use_my_app = false && $port_in_use = false ]]; then 443 | check_web_server 444 | else 445 | echo -e "${YELLOW}您已成功安裝,不用重複安裝${RESET}" 446 | sleep 1 447 | fi 448 | } 449 | 450 | check_certbot(){ 451 | [ $caddy -eq 1 ] && return 0 452 | if command -v certbot >/dev/null 2>&1; then 453 | return 0 454 | fi 455 | case $system in 456 | 1) 457 | apt update 458 | apt install -y snapd 459 | snap install core && snap refresh core 460 | snap install --classic certbot 461 | ln -sf /snap/bin/certbot /usr/bin/certbot 462 | snap set certbot trust-plugin-with-root=ok 463 | snap install certbot-dns-cloudflare 464 | ;; 465 | 2) 466 | dnf install -y python3-pip gcc libffi-devel python3-devel 467 | python3 -m pip install --upgrade pip 468 | python3 -m pip install --upgrade certbot certbot-nginx certbot-dns-cloudflare certbot-dns-gcore --root-user-action=ignore 469 | ln -sf /usr/local/bin/certbot /usr/bin/certbot 470 | ;; 471 | 3) 472 | apk update 473 | apk add python3 py3-pip py3-virtualenv gcc musl-dev libffi-dev openssl-dev 474 | python3 -m pip install --upgrade pip 475 | python3 -m pip install certbot certbot-nginx certbot-dns-cloudflare certbot-dns-gcore --break-system-packages 476 | ln -sf /usr/local/bin/certbot /usr/bin/certbot 477 | ;; 478 | esac 479 | } 480 | 481 | check_php(){ 482 | if ! command -v php >/dev/null 2>&1; then 483 | php_install 484 | sleep 5 485 | php_fix 486 | fi 487 | } 488 | 489 | check_flarum_supported_php() { 490 | local versions 491 | local valid_versions=() 492 | local base_url="https://github.com/flarum/installation-packages/raw/main/packages/v1.x" 493 | 494 | case $system in 495 | 1) # Debian/Ubuntu 496 | versions=$(apt-cache search ^php[0-9.]+$ | grep -oP '^php\K[0-9.]+' | awk -F. '$1 >= 8 {print}' | sort -Vr) 497 | ;; 498 | 2) # CentOS 499 | versions=$(yum module list php | grep -E '^php\s+(remi-)?8\.[0-9]+' | awk '{print $2}' | sed 's/remi-//' | sort -Vu | xargs) 500 | ;; 501 | 3) # Alpine 502 | versions=$(apk search -x php[0-9]* | grep -oE 'php[0-9]+' | sed 's/php//' | sort -u | awk '{printf "8.%d\n", $1 - 80}' | sort -Vr) 503 | ;; 504 | esac 505 | 506 | for ver in $versions; do 507 | url="$base_url/flarum-v1.x-no-public-dir-php$ver.zip" 508 | if curl -s -I "$url" | grep -q '^HTTP/.* 302'; then 509 | valid_versions+=("$ver") 510 | fi 511 | done 512 | 513 | if [[ ${#valid_versions[@]} -eq 0 ]]; then 514 | echo "${RED}沒有任何版本符合 Flarum 安裝包${RESET}" 515 | return 1 516 | fi 517 | 518 | echo "${valid_versions[*]}" 519 | } 520 | 521 | 522 | create_directories() { 523 | [ $caddy -eq 1 ] && return 0 524 | mkdir -p /home/web/ 525 | mkdir -p /home/web/cert 526 | mkdir -p /etc/nginx/conf.d/ 527 | mkdir -p /etc/nginx/logs 528 | touch /etc/nginx/logs/error.log 529 | touch /etc/nginx/logs/access.log 530 | } 531 | chown_set(){ 532 | local ngx_user=$(get_web_run_user) 533 | case $system in 534 | 1|2) 535 | mkdir -p /run/php 536 | chown -R $ngx_user:$ngx_user /run/php 537 | chmod 755 /run/php 538 | ;; 539 | 3) 540 | mkdir -p /run/php 541 | chown $ngx_user:$ngx_user /run/php 542 | chmod 755 /run/php 543 | rc-service php-fpm83 restart 544 | ;; 545 | esac 546 | } 547 | 548 | check_no_ngx(){ 549 | check_web_environment 550 | if [[ $use_my_app != true ]]; then 551 | echo -e "${RED}您好,您現在使用其他web server 無法使用此功能${RESET}" 552 | read -p "操作完成,請按任意鍵..." -n1 553 | return 1 554 | fi 555 | } 556 | 557 | check_php_version() { 558 | case "$system" in 559 | 1) 560 | if command -v php >/dev/null 2>&1; then 561 | phpver=$(php -v | head -n1 | grep -oP '\d+\.\d+') 562 | echo "$phpver" 563 | else 564 | echo -e "${RED}PHP 尚未安裝。${RESET}" >&2 565 | return 1 566 | fi 567 | ;; 568 | 2) 569 | if command -v php >/dev/null 2>&1; then 570 | phpver=$(php -v | head -n1 | grep -oP '\d+\.\d+') 571 | echo "$phpver" # ex 8.3 572 | else 573 | echo -e "${RED}PHP 尚未安裝。${RESET}" >&2 574 | return 1 575 | fi 576 | ;; 577 | 3) 578 | if command -v php >/dev/null 2>&1; then 579 | local rawver=$(php -v | head -n1 | grep -oE '[0-9]+\.[0-9]+') # 使用 -E(延伸正規表示式) 580 | alpver=$(echo "$rawver" | tr -d '.') 581 | echo "$alpver" #出現83 582 | else 583 | echo -e "${RED}PHP 尚未安裝。${RESET}" >&2 584 | return 1 585 | fi 586 | ;; 587 | esac 588 | } 589 | 590 | check_php_ext_available() { 591 | local ext_name="$1" 592 | local phpver="$2" # e.g., "8.2" 593 | local shortver=$(echo "$phpver" | tr -d '.') 594 | 595 | case "$system" in 596 | 1) # Debian / Ubuntu (APT) 597 | apt-cache show "php$phpver-$ext_name" &>/dev/null && return 0 598 | ;; 599 | 600 | 2) # CentOS / RHEL / AlmaLinux / Rocky (YUM + Remi) 601 | yum --quiet list available "php-$ext_name" &>/dev/null && return 0 602 | yum --quiet list available "php-pecl-$ext_name" &>/dev/null && return 0 603 | ;; 604 | 605 | 3) # Alpine (APK) 606 | apk search "php$shortver-$ext_name" | grep -q "^php$shortver-$ext_name" && return 0 607 | ;; 608 | esac 609 | 610 | return 1 611 | } 612 | cf_cert_autogen() ( 613 | key_file="/ssl_ca/.cf_origin.key" 614 | enc_file="/ssl_ca/.cf_origin.enc" 615 | 616 | echo "===== Cloudflare Origin 憑證自動申請器 =====" 617 | echo "感謝NS論壇之bananapork提供的cf文檔" 618 | 619 | # 1. 檢查加密檔案 620 | if [ ! -f "$key_file" ] || [ ! -f "$enc_file" ]; then 621 | echo -e "${YELLOW}尚未設定帳號資訊,請輸入:${RESET}" 622 | read -p "Cloudflare 登入信箱: " cf_email 623 | read -p "Global API Key(將加密儲存): " -s cf_key 624 | echo 625 | 626 | mkdir -p "$(dirname "$key_file")" 627 | head -c 32 /dev/urandom > "$key_file" 628 | chmod 600 "$key_file" 629 | 630 | echo "$cf_email:$cf_key" | openssl enc -aes-256-cbc -pbkdf2 -salt -pass file:"$key_file" -out "$enc_file" 631 | chmod 600 "$enc_file" 632 | echo -e "${GREEN}Cloudflare 認證資料已加密儲存${RESET}" 633 | fi 634 | 635 | # 2. 解密帳號資訊 636 | cf_cred=$(openssl enc -d -aes-256-cbc -pbkdf2 -pass file:"$key_file" -in "$enc_file") 637 | cf_email="$(echo "$cf_cred" | cut -d':' -f1)" 638 | cf_api_key="$(echo "$cf_cred" | cut -d':' -f2)" 639 | 640 | # 3. 讀取用戶輸入的任何子域名 641 | while true; do 642 | read -p "請輸入你擁有的主域名(如 xxx.eu.org 或 xxx.com): " input_domain 643 | if [[ "$input_domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then 644 | break 645 | else 646 | echo -e "${YELLOW}請輸入正確格式的域名(不可含 http/https/空格)${RESET}" 647 | fi 648 | done 649 | response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones" \ 650 | -H "X-Auth-Email: $cf_email" \ 651 | -H "X-Auth-Key: $cf_api_key" \ 652 | -H "Content-Type: application/json") 653 | all_zones=$(echo "$response" | jq -r '.result[].name') 654 | base_domain="" 655 | for zone in $all_zones; do 656 | if [[ "$input_domain" == *"$zone" ]]; then 657 | base_domain="$zone" 658 | break 659 | fi 660 | done 661 | if [ -z "$base_domain" ]; then 662 | echo -e "${RED}找不到與 $input_domain 對應的根域名,請確認該域名是否在你帳號內託管。${RESET}" 663 | sleep 1 664 | return 1 665 | fi 666 | le_dir="/etc/letsencrypt/live/$base_domain" 667 | mkdir -p "$le_dir" 668 | cd "$le_dir" 669 | 670 | openssl req -new -newkey rsa:2048 -nodes \ 671 | -keyout privkey.pem \ 672 | -out domain.csr \ 673 | -subj "/CN=$base_domain" 674 | csr_content=$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' domain.csr) 675 | response=$(curl -s -X POST https://api.cloudflare.com/client/v4/certificates \ 676 | -H "Content-Type: application/json" \ 677 | -H "X-Auth-Email: $cf_email" \ 678 | -H "X-Auth-Key: $cf_api_key" \ 679 | -d "{ 680 | \"hostnames\": [\"$base_domain\", \"*.$base_domain\"], 681 | \"requested_validity\": 5475, 682 | \"request_type\": \"origin-rsa\", 683 | \"csr\": \"$csr_content\" 684 | }") 685 | 686 | if echo "$response" | grep -q '"success":true'; then 687 | echo "$response" | jq -r '.result.certificate' > cert.pem 688 | cat cert.pem > fullchain.pem 689 | local cert_id=$(echo "$response" | jq -r '.result.id') 690 | echo "$cert_id" > cf_cert_id.txt 691 | echo -e "${GREEN}成功!憑證已儲存於:$le_dir${RESET}" 692 | echo "- cert.pem" 693 | echo "- fullchain.pem" 694 | echo "- privkey.pem" 695 | else 696 | echo -e "${RED}憑證申請失敗,錯誤如下:${RESET}" 697 | echo "$response" | jq 698 | sleep 9 699 | fi 700 | ) 701 | 702 | cf_cert_revoke() ( 703 | input_domain="$1" 704 | key_file="/ssl_ca/.cf_origin.key" 705 | enc_file="/ssl_ca/.cf_origin.enc" 706 | cf_cred="" 707 | cf_api_key="" 708 | cf_email="" 709 | 710 | 711 | echo "===== Cloudflare Origin 憑證吊銷器 =====" 712 | 713 | if [ ! -f "$key_file" ] || [ ! -f "$enc_file" ]; then 714 | echo -e "${RED}尚未設定 Cloudflare 認證資料,請先執行申請功能${RESET}" 715 | sleep 1 716 | return 1 717 | fi 718 | 719 | # 解密認證資料 720 | cf_cred=$(openssl enc -d -aes-256-cbc -pbkdf2 -pass file:"$key_file" -in "$enc_file") 721 | cf_email="$(echo "$cf_cred" | cut -d':' -f1)" 722 | cf_api_key="$(echo "$cf_cred" | cut -d':' -f2)" 723 | 724 | # 輸入主域名 725 | if [ -z "$input_domain" ]; then 726 | while true; do 727 | read -p "請輸入你想吊銷憑證的主域名(如 example.com): " input_domain 728 | if [[ "$input_domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then 729 | break 730 | else 731 | echo -e "${YELLOW} 請輸入正確格式的域名${RESET}" 732 | fi 733 | done 734 | fi 735 | 736 | le_dir="/etc/letsencrypt/live/$input_domain" 737 | cert_id_file="$le_dir/cf_cert_id.txt" 738 | 739 | if [ ! -f "$cert_id_file" ]; then 740 | echo -e "${RED} 找不到本地憑證 ID ($cert_id_file),無法吊銷${RESET}" 741 | sleep 1 742 | return 1 743 | fi 744 | certificate_id=$(cat "$cert_id_file") 745 | 746 | read -p "確定要吊銷 Cloudflare Origin 憑證 ID [$certificate_id] 嗎?(y/N): " confirm 747 | if [[ "$confirm" =~ ^[Yy]$ ]]; then 748 | revoke_response=$(curl -s -X DELETE "https://api.cloudflare.com/client/v4/certificates/$certificate_id" \ 749 | -H "X-Auth-Email: $cf_email" \ 750 | -H "X-Auth-Key: $cf_api_key" \ 751 | -H "Content-Type: application/json") 752 | 753 | if echo "$revoke_response" | grep -q '"success":true'; then 754 | echo -e "${GREEN}Cloudflare Origin 憑證已成功吊銷${RESET}" 755 | rm -f "$le_dir/cert.pem" "$le_dir/fullchain.pem" "$le_dir/privkey.pem" "$cert_id_file" 756 | else 757 | echo -e "${RED}吊銷失敗,回傳如下:${RESET}" 758 | echo "$revoke_response" | jq 759 | fi 760 | fi 761 | ) 762 | 763 | change_wp_admin_username() { 764 | local domain="$1" 765 | local site_path="/var/www/$domain" 766 | 767 | # 確認 WordPress 路徑 768 | if [ ! -f "$site_path/wp-config.php" ]; then 769 | echo -e "${RED}找不到 WordPress 安裝路徑:$site_path${RESET}" 770 | return 1 771 | fi 772 | 773 | # 取得管理員用戶名列表 774 | mapfile -t admins < <(wp --skip-plugins --skip-themes --allow-root --path="$site_path" user list --role=administrator --field=user_login) 775 | 776 | if [ ${#admins[@]} -eq 0 ]; then 777 | echo -e "${RED}沒有找到管理員用戶${RESET}" 778 | return 1 779 | fi 780 | 781 | local selected_admin="" 782 | if [ ${#admins[@]} -eq 1 ]; then 783 | selected_admin="${admins[0]}" 784 | echo "只有一個管理員用戶:$selected_admin" 785 | else 786 | echo "請選擇要修改的管理員用戶:" 787 | select admin in "${admins[@]}"; do 788 | if [ -n "$admin" ]; then 789 | selected_admin="$admin" 790 | break 791 | else 792 | echo "請輸入有效選項" 793 | fi 794 | done 795 | fi 796 | 797 | read -p "請輸入新的管理員使用者名稱:" new_username 798 | if [ -z "$new_username" ]; then 799 | echo -e "${RED}新用戶名不可為空,取消修改${RESET}" 800 | return 1 801 | fi 802 | 803 | # 確認新用戶名是否已存在 804 | if wp --skip-plugins --skip-themes --allow-root --path="$site_path" user get "$new_username" >/dev/null 2>&1; then 805 | echo -e "${RED} 新用戶名已存在,請換一個${RESET}" 806 | return 1 807 | fi 808 | 809 | # 用 SQL 方式修改用戶名(因為 wp-cli 沒有直接修改用戶名指令) 810 | local sql="UPDATE wp_users SET user_login='${new_username}' WHERE user_login='${selected_admin}';" 811 | wp --skip-plugins --skip-themes --allow-root --path="$site_path" db query "$sql" 812 | 813 | echo -e "${GREEN}管理員使用者名稱已從 '$selected_admin' 修改為 '$new_username'${RESET}" 814 | } 815 | 816 | change_wp_admin_password() { 817 | local domain="$1" 818 | local site_path="/var/www/$domain" 819 | 820 | # 確認 WordPress 路徑 821 | if [ ! -f "$site_path/wp-config.php" ]; then 822 | echo "${RED}找不到 WordPress 安裝路徑:$site_path${RESET}" 823 | return 1 824 | fi 825 | 826 | # 取得管理員用戶名列表 827 | mapfile -t admins < <(wp --skip-plugins --skip-themes --allow-root --path="$site_path" user list --role=administrator --field=user_login) 828 | 829 | if [ ${#admins[@]} -eq 0 ]; then 830 | echo -e "${RED}沒有找到管理員用戶${RESET}" 831 | return 1 832 | fi 833 | 834 | local selected_admin="" 835 | if [ ${#admins[@]} -eq 1 ]; then 836 | selected_admin="${admins[0]}" 837 | echo "只有一個管理員用戶:$selected_admin" 838 | else 839 | echo "請選擇要修改密碼的管理員用戶:" 840 | select admin in "${admins[@]}"; do 841 | if [ -n "$admin" ]; then 842 | selected_admin="$admin" 843 | break 844 | else 845 | echo "請輸入有效選項" 846 | fi 847 | done 848 | fi 849 | 850 | # 輸入新密碼(隱藏輸入) 851 | read -s -p "請輸入新的密碼:" new_password 852 | echo 853 | if [ -z "$new_password" ]; then 854 | echo -e "${RED} 密碼不可為空,取消修改${RESET}" 855 | return 1 856 | fi 857 | 858 | read -s -p "請再輸入一次新的密碼以確認:" confirm_password 859 | echo 860 | if [ "$new_password" != "$confirm_password" ]; then 861 | echo -e "${RED}兩次輸入的密碼不一致,取消修改${RESET}" 862 | return 1 863 | fi 864 | 865 | # 修改密碼 866 | wp --skip-plugins --skip-themes --allow-root --path="$site_path" user update "$selected_admin" --user_pass="$new_password" --skip-email 867 | 868 | if [ $? -eq 0 ]; then 869 | echo -e "${GREEN}管理員 '$selected_admin' 的密碼已更新成功${RESET}" 870 | else 871 | echo -e "${RED}密碼更新失敗${RESET}" 872 | return 1 873 | fi 874 | } 875 | 876 | 877 | clean_nginx_ssl_config() { 878 | conf_path=$(detect_conf_path) 879 | 880 | # 遍歷所有 conf 檔 881 | find "$conf_path" -type f -name "*.conf" | while read -r file; do 882 | 883 | # 刪掉常見 SSL/TLS 設定行 884 | sed -i \ 885 | -e '/^\s*ssl_protocols/d' \ 886 | -e '/^\s*ssl_ciphers/d' \ 887 | -e '/^\s*ssl_prefer_server_ciphers/d' \ 888 | -e '/^\s*ssl_session_cache/d' \ 889 | -e '/^\s*ssl_session_timeout/d' \ 890 | "$file" 891 | done 892 | restart_webserver 893 | } 894 | 895 | default(){ 896 | local mode=$1 897 | local detect_conf_path=$(detect_conf_path) 898 | local ngx_conf=$(detect_nginx_conf_paths) 899 | create_directories 900 | generate_ssl_cert 901 | case "$system" in 902 | 1|2) 903 | if [ $mode == openresty ]; then 904 | rm -f $ngx_conf 905 | wget -O $ngx_conf https://gitlab.com/gebu8f/sh/-/raw/main/nginx/nginx.conf 906 | id -u nginx &>/dev/null || useradd -r -s /sbin/nologin -M nginx 907 | fi 908 | if [ $mode == caddy ]; then 909 | rm -f /etc/caddy/Caddyfile 910 | wget -O /etc/caddy/Caddyfile https://gitlab.com/gebu8f/sh/-/raw/main/nginx/caddy/Caddyfile 911 | mkdir -p /etc/caddy/conf.d 912 | else 913 | rm -f $detect_conf_path/default.conf $detect_conf_path/default 914 | wget -O /etc/nginx/conf.d/default.conf https://gitlab.com/gebu8f/sh/-/raw/main/nginx/default_system 915 | fi 916 | restart_webserver 917 | ;; 918 | 3) 919 | if [ $mode == openresty ]; then 920 | id -u nginx &>/dev/null || adduser -D -H -s /sbin/nologin nginx 921 | fi 922 | # download default 923 | rm -f $detect_conf_path/default.conf 924 | rm -f $ngx_conf 925 | wget -O $ngx_conf https://gitlab.com/gebu8f/sh/-/raw/main/nginx/nginx.conf 926 | wget -O /etc/nginx/conf.d/default.conf https://gitlab.com/gebu8f/sh/-/raw/main/nginx/default_system 927 | restart_webserver 928 | ;; 929 | esac 930 | } 931 | 932 | detect_conf_path() { 933 | local conf="" 934 | local default_conf_dir="" 935 | if command -v openresty >/dev/null 2>&1 || command -v nginx >/dev/null 2>&1; then 936 | conf=$(detect_nginx_conf_paths) 937 | elif command -v caddy >/dev/null 2>&1; then 938 | conf="/etc/caddy/Caddyfile" 939 | fi 940 | if command -v caddy >/dev/null 2>&1; then 941 | # 找出有 import 且含 * 的行 942 | local import_line 943 | import_line=$(grep -E '^[[:space:]]*import[[:space:]]+/' "$conf" | grep '\*' | head -n 1) 944 | 945 | if [[ -n "$import_line" ]]; then 946 | local path 947 | path=$(echo "$import_line" | sed -E 's/^[[:space:]]*import[[:space:]]+([^*]+)\*.*/\1/') 948 | path="${path%/}" 949 | 950 | echo "$path" 951 | return 0 952 | fi 953 | 954 | default_conf_dir="/etc/caddy/conf.d" 955 | mkdir -p "$default_conf_dir" 956 | 957 | # 插入 import 行到 Caddyfile 最後一行 958 | echo "" >> "$conf" 959 | echo "import ${default_conf_dir}/*" >> "$conf" 960 | 961 | # 重啟 Caddy 962 | restart_webserver 963 | echo "$default_conf_dir" 964 | return 0 965 | fi 966 | # 搜尋 include *.conf 967 | local search_regex='^[[:space:]]*include[[:space:]]+([^;]*\*[^;]*);' 968 | local included_path=$(sed -E -n "s/${search_regex}/\1/p" "$conf" | head -n 1) 969 | 970 | if [[ -n "$included_path" ]]; then 971 | local target_dir 972 | target_dir=$(dirname "$included_path") 973 | mkdir -p "$target_dir" 974 | echo "$target_dir" 975 | return 0 976 | fi 977 | 978 | if command -v openresty >/dev/null 2>&1; then 979 | default_conf_dir="/usr/local/openresty/nginx/conf/conf.d" 980 | else 981 | default_conf_dir="/etc/nginx/conf.d" 982 | fi 983 | 984 | mkdir -p "$default_conf_dir" 985 | 986 | # 若 nginx.conf 中沒有 include conf.d → 自動插入 987 | if ! grep -qE "include[[:space:]]+${default_conf_dir}/\*\.conf;" "$conf"; then 988 | sed -i "/^http[[:space:]]*{/,/^}/ { 989 | /^}/i\\ include ${default_conf_dir}/*.conf; 990 | }" "$conf" 2>/dev/null 991 | 992 | restart_webserver 993 | fi 994 | 995 | echo "$default_conf_dir" 996 | return 0 997 | } 998 | detect_sites() { 999 | local app_type="$1" 1000 | local base_dir="/var/www" 1001 | 1002 | for dir in "$base_dir"/*; do 1003 | [ ! -d "$dir" ] && continue 1004 | 1005 | case "$app_type" in 1006 | WordPress) 1007 | if [ -f "$dir/wp-config.php" ]; then 1008 | echo "$(basename "$dir")" 1009 | fi 1010 | ;; 1011 | Flarum) 1012 | if [ -f "$dir/flarum" ] || [ -f "$dir/site.php" ] || [ -d "$dir/vendor/flarum" ]; then 1013 | echo "$(basename "$dir")" 1014 | fi 1015 | ;; 1016 | esac 1017 | done 1018 | } 1019 | 1020 | detect_sites_menu() { 1021 | local app_type="$1" 1022 | local base_dir="/var/www" 1023 | local sites=() 1024 | for dir in "$base_dir"/*; do 1025 | [ ! -d "$dir" ] && continue 1026 | 1027 | case "$app_type" in 1028 | WordPress) 1029 | [ -f "$dir/wp-config.php" ] && sites+=("$(basename "$dir")") 1030 | ;; 1031 | Flarum) 1032 | [ -f "$dir/flarum" ] || [ -f "$dir/site.php" ] || [ -d "$dir/vendor/flarum" ] && \ 1033 | sites+=("$(basename "$dir")") 1034 | ;; 1035 | *) 1036 | echo -e "${RED}暫不支援偵測此應用:$app_type${RESET}" >&2 1037 | return 1 1038 | ;; 1039 | esac 1040 | done 1041 | 1042 | if [ ${#sites[@]} -eq 0 ]; then 1043 | echo -e "${RED}未偵測到任何 $app_type 網站${RESET}" >&2 1044 | return 1 1045 | fi 1046 | 1047 | if ! [ -t 0 ]; then 1048 | echo -e "${RED}非交互式環境,無法使用選單${RESET}" >&2 1049 | return 1 1050 | fi 1051 | 1052 | echo "請選擇欲操作的 $app_type 網站:" >&2 1053 | select site in "${sites[@]}"; do 1054 | if [ -n "$site" ]; then 1055 | echo "$site" 1056 | return 0 1057 | else 1058 | echo -e "${YELLOW}請輸入有效的編號${RESET}" >&2 1059 | fi 1060 | done 1061 | } 1062 | 1063 | deploy_or_remove_theme() { 1064 | local action="$1" # install or remove 1065 | local domain="$2" # 網址 (如 aa.com) 1066 | 1067 | local site_path="/var/www/$domain" 1068 | local wp_theme_dir="$site_path/wp-content/themes" 1069 | local wp_cli="wp --skip-plugins --skip-themes --allow-root" 1070 | 1071 | # 確保 wp-cli 存在 1072 | if ! command -v wp >/dev/null 2>&1; then 1073 | echo -e "${RED}找不到 wp-cli,可先執行 install_wp_cli${RESET}" 1074 | return 1 1075 | fi 1076 | 1077 | # 確保路徑存在 1078 | if [ ! -d "$wp_theme_dir" ]; then 1079 | echo -e "${RED}找不到 WordPress themes 目錄:$wp_theme_dir${RESET}" 1080 | return 1 1081 | fi 1082 | 1083 | case "$action" in 1084 | install) 1085 | read -p "請輸入主題名稱或下載 URL:" theme_input 1086 | if [ -z "$theme_input" ]; then 1087 | echo -e "${RED}未輸入任何主題名稱或 URL,取消安裝${RESET}" 1088 | return 1 1089 | fi 1090 | 1091 | if [[ "$theme_input" =~ ^https?:// ]]; then 1092 | # 是網址,先下載 1093 | tmp_file="/tmp/theme_download.$(date +%s)" 1094 | echo -e "${CYAN}正在下載主題:$theme_input${RESET}" 1095 | curl -L "$theme_input" -o "$tmp_file" || { 1096 | echo -e "${RED}無法下載 $theme_input${RESET}" 1097 | return 1 1098 | } 1099 | 1100 | # 解壓縮 1101 | case "$theme_input" in 1102 | *.zip) 1103 | unzip -q "$tmp_file" -d "$wp_theme_dir" || { 1104 | echo -e "${RED}解壓縮失敗${RESET}" 1105 | rm -f "$tmp_file" 1106 | return 1 1107 | } 1108 | ;; 1109 | *.tar.gz|*.tgz) 1110 | tar -xzf "$tmp_file" -C "$wp_theme_dir" || { 1111 | echo -e "${RED}解壓縮失敗${RESET}" 1112 | rm -f "$tmp_file" 1113 | return 1 1114 | } 1115 | ;; 1116 | *.tar) 1117 | tar -xf "$tmp_file" -C "$wp_theme_dir" || { 1118 | echo -e "${RED}解壓縮失敗${RESET}" 1119 | rm -f "$tmp_file" 1120 | return 1 1121 | } 1122 | ;; 1123 | *) 1124 | echo -e "${RED}不支援的壓縮格式:$theme_input${RESET}" 1125 | rm -f "$tmp_file" 1126 | return 1 1127 | ;; 1128 | esac 1129 | 1130 | echo -e "${GREEN}主題已部署到 $wp_theme_dir${RESET}" 1131 | rm -f "$tmp_file" 1132 | 1133 | else 1134 | # 非網址 → 當作主題名稱 → wp-cli 搜尋 1135 | echo -e "${CYAN}正在搜尋主題:$theme_input${RESET}" 1136 | 1137 | mapfile -t themes < <( 1138 | $wp_cli --path="$site_path" theme search "$theme_input" --per-page=10 --format=json \ 1139 | | jq -r '.[] | "\(.name)|\(.slug)"' 1140 | ) 1141 | 1142 | if [ ${#themes[@]} -eq 0 ]; then 1143 | echo -e "${RED}找不到任何與 \"$theme_input\" 相關的主題${RESET}" 1144 | return 1 1145 | fi 1146 | 1147 | local options=() 1148 | local slugs=() 1149 | 1150 | for entry in "${themes[@]}"; do 1151 | name="${entry%%|*}" 1152 | slug="${entry##*|}" 1153 | [ -n "$slug" ] && options+=("$name (slug: $slug)") && slugs+=("$slug") 1154 | done 1155 | 1156 | echo "請選擇要安裝的主題:" 1157 | select opt in "${options[@]}"; do 1158 | if [ -n "$opt" ]; then 1159 | idx=$((REPLY - 1)) 1160 | slug="${slugs[$idx]}" 1161 | echo -e "${CYAN}正在安裝主題:$slug${RESET}" 1162 | $wp_cli --path="$site_path" theme install "$slug" --activate 1163 | echo -e "${GREEN}已安裝並啟用主題:$slug${RESET}" 1164 | return 0 1165 | else 1166 | echo -e "${RED}無效的選項,請重新選擇${RESET}" 1167 | fi 1168 | done 1169 | fi 1170 | ;; 1171 | 1172 | remove) 1173 | echo "正在偵測已安裝的主題..." 1174 | 1175 | mapfile -t themes < <( 1176 | $wp_cli --path="$site_path" theme list --status=active,inactive --format=json \ 1177 | | jq -r '.[] | "\(.name)|\(.status)|\(.slug)"' 1178 | ) 1179 | 1180 | if [ ${#themes[@]} -eq 0 ]; then 1181 | echo -e "${YELLOW}尚未安裝任何主題${RESET}" 1182 | return 0 1183 | fi 1184 | 1185 | local options=() 1186 | local slugs=() 1187 | 1188 | for theme in "${themes[@]}"; do 1189 | name=$(echo "$theme" | cut -d'|' -f1) 1190 | status=$(echo "$theme" | cut -d'|' -f2) 1191 | slug=$(echo "$theme" | cut -d'|' -f3) 1192 | 1193 | options+=("$name [$status]") 1194 | slugs+=("$slug") 1195 | done 1196 | 1197 | echo "請選擇要移除的主題:" 1198 | select opt in "${options[@]}"; do 1199 | if [ -n "$opt" ]; then 1200 | idx=$((REPLY - 1)) 1201 | slug="${slugs[$idx]}" 1202 | 1203 | echo -e "${CYAN}正在移除主題:$slug${RESET}" 1204 | $wp_cli --path="$site_path" theme delete "$slug" 1205 | echo -e "${GREEN}已移除主題:$slug${RESET}" 1206 | return 0 1207 | else 1208 | echo -e "${RED}無效的選項,請重新選擇${RESET}" 1209 | fi 1210 | done 1211 | ;; 1212 | 1213 | *) 1214 | echo -e "${RED}不支援的操作:$action${RESET}" 1215 | return 1 1216 | ;; 1217 | esac 1218 | } 1219 | 1220 | flarum_setup() { 1221 | local php_var=$(check_php_version) 1222 | local supported_php_versions=$(check_flarum_supported_php) 1223 | local max_supported_php=$(echo "$supported_php_versions" | tr ' ' '\n' | sort -V | tail -n1) 1224 | local ngx_user=$(get_web_run_user) 1225 | local php_ini=$(phpini_path) 1226 | 1227 | # 判斷 PHP 是否高於支援版本 1228 | if [ "$(printf '%s\n' "$php_var" "$max_supported_php" | sort -V | tail -n1)" != "$php_var" ]; then 1229 | echo -e "${YELLOW}您目前使用的 PHP 版本是 $php_var,但 Flarum 僅建議使用到 $max_supported_php。${RESET}" 1230 | read -p "是否仍要繼續安裝?(y/N):" confirm 1231 | [[ "$confirm" =~ ^[Yy]$ ]] || return 1 1232 | fi 1233 | # 根據是否支援決定使用哪個 zip 檔 1234 | if echo "$supported_php_versions" | grep -qw "$php_var"; then 1235 | local download_phpver="$php_var" 1236 | else 1237 | echo -e "${YELLOW}您選擇的 PHP 版本不在 Flarum 支援列表,因為實測發現很不穩定,故禁止安裝${RESET}" 1238 | sleep 3 1239 | return 1 1240 | fi 1241 | 1242 | if ! command -v dba >/dev/null 2>&1; then 1243 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 1244 | fi 1245 | if ! command -v mysql >/dev/null 2>&1 && ! command -v mariadb >/dev/null 2>&1; then 1246 | dba mysql install true 1247 | fi 1248 | 1249 | if ! command -v composer &>/dev/null; then 1250 | echo "正在安裝 Composer..." 1251 | curl -sS https://getcomposer.org/installer | php 1252 | mv composer.phar /usr/local/bin/composer 1253 | fi 1254 | 1255 | read -p "請輸入您的Flarum網址(例如 bbs.example.com):" domain 1256 | 1257 | # 自動申請 SSL(若不存在) 1258 | check_cert "$domain" || { 1259 | echo "未偵測到 Let's Encrypt 憑證,嘗試自動申請..." 1260 | if ssl_apply "$domain"; then 1261 | echo "申請成功,重新驗證憑證..." 1262 | check_cert "$domain" || { 1263 | echo "申請成功但仍無法驗證憑證,中止建立站點" 1264 | return 1 1265 | } 1266 | else 1267 | echo "SSL 申請失敗,中止建立站點" 1268 | return 1 1269 | fi 1270 | } 1271 | 1272 | local db_name="flarum_${domain//./_}" 1273 | local db_user="${db_name}_user" 1274 | local db_pass=$(dba mysql add $db_name $db_user false) 1275 | 1276 | # 下載 Flarum 1277 | mkdir -p /var/www/$domain 1278 | wget "https://github.com/flarum/installation-packages/raw/main/packages/v1.x/flarum-v1.x-no-public-dir-php$download_phpver.zip" -O /tmp/flarum.zip 1279 | mkdir -p /tmp/flarum 1280 | unzip /tmp/flarum.zip -d /tmp/flarum 1281 | cp -a /tmp/flarum/. /var/www/$domain/ 1282 | rm -rf /tmp/flarum.zip /tmp/flarum 1283 | cd "/var/www/$domain" 1284 | 1285 | export COMPOSER_ALLOW_SUPERUSER=1 1286 | composer install --no-dev --no-interaction 1287 | composer require --no-interaction flarum-lang/chinese-traditional 1288 | composer require --no-interaction flarum-lang/chinese-simplified 1289 | php flarum cache:clear 1290 | echo "已安裝繁體與簡體中文語系,可至 Flarum 後台 Extensions 啟用。" 1291 | 1292 | chown -R $ngx_user:$ngx_user "/var/www/$domain" 1293 | setup_site "$domain" flarum 1294 | 1295 | if grep -qE '^[[:space:]]*opcache\.revalidate_freq[[:space:]]*=' "$php_ini"; then 1296 | local current_revalidate_freq 1297 | current_revalidate_freq=$(grep -E '^[[:space:]]*opcache\.revalidate_freq[[:space:]]*=' "$php_ini" | \ 1298 | awk -F= '{gsub(/[[:space:]]/,"",$2); print $2}') 1299 | 1300 | if [ "$current_revalidate_freq" = "0" ]; then 1301 | sed -i 's/^[[:space:]]*opcache\.revalidate_freq[[:space:]]*=.*/opcache.revalidate_freq=1/' "$php_ini" 1302 | fi 1303 | fi 1304 | 1305 | # 檢查並處理 opcache.validate_timestamps 1306 | if grep -qE '^[[:space:]]*opcache\.validate_timestamps[[:space:]]*=' "$php_ini"; then 1307 | local current_validate_timestamps 1308 | current_validate_timestamps=$(grep -E '^[[:space:]]*opcache\.validate_timestamps[[:space:]]*=' "$php_ini" | \ 1309 | awk -F= '{gsub(/[[:space:]]/,"",$2); print $2}') 1310 | 1311 | if [ "$current_validate_timestamps" = "0" ]; then 1312 | sed -i 's/^[[:space:]]*opcache\.validate_timestamps[[:space:]]*=.*/opcache.validate_timestamps=2/' "$php_ini" 1313 | fi 1314 | fi 1315 | 1316 | case $system in 1317 | 1) 1318 | service php$php_var-fpm restart 1319 | ;; 1320 | 2) 1321 | service php-fpm restart 1322 | ;; 1323 | 3) 1324 | service php$php_var-fpm restart 1325 | ;; 1326 | esac 1327 | 1328 | echo "===== Flarum 資訊 =====" 1329 | echo "網址:https://$domain" 1330 | echo "資料庫名稱:$db_name" 1331 | echo "資料庫用戶:$db_user" 1332 | echo "資料庫密碼:$db_pass" 1333 | echo "請在安裝介面輸入以上資訊完成安裝。" 1334 | echo "=======================" 1335 | } 1336 | 1337 | flarum_extensions() { 1338 | read -p "請輸入 Flarum 網址(例如 bbs.example.com):" flarum_domain 1339 | 1340 | local site_path="/var/www/$flarum_domain" 1341 | if [ ! -f "$site_path/config.php" ]; then 1342 | echo "此站點並非 Flarum 網站(缺少 config.php)。" 1343 | return 1 1344 | fi 1345 | 1346 | echo "已偵測為 Flarum 網站:$flarum_domain" 1347 | echo "選擇操作:" 1348 | echo "1) 安裝擴展" 1349 | echo "2) 移除擴展" 1350 | read -p "請選擇操作(預設 1):" action 1351 | action="${action:-1}" 1352 | 1353 | read -p "請輸入擴展套件名稱(例如 flarum-lang/chinese-traditional):" ext_name 1354 | 1355 | cd "$site_path" 1356 | 1357 | if [ "$action" = "1" ]; then 1358 | export COMPOSER_ALLOW_SUPERUSER=1 1359 | composer require --no-interaction "$ext_name" 1360 | php flarum cache:clear 1361 | echo "擴展已安裝並清除快取。請至後台啟用擴展。" 1362 | elif [ "$action" = "2" ]; then 1363 | export COMPOSER_ALLOW_SUPERUSER=1 1364 | composer remove --no-interaction "$ext_name" 1365 | php flarum cache:clear 1366 | echo "擴展已移除並清除快取。" 1367 | else 1368 | echo "無效選項。" 1369 | fi 1370 | } 1371 | 1372 | 1373 | generate_ssl_cert(){ 1374 | [ $caddy -eq 1 ] && return 0 1375 | openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ 1376 | -keyout /home/web/cert/default_server.key \ 1377 | -out /home/web/cert/default_server.crt \ 1378 | -days 5475 \ 1379 | -subj "/C=US/ST=State/L=City/O=Organization/OU=Organizational Unit/CN=Common Name" 1380 | } 1381 | 1382 | get_web_run_user() { 1383 | # --- 1. 偵測 Nginx --- 1384 | local nginx_conf=$(detect_nginx_conf_paths) 1385 | 1386 | if [ -n "$nginx_conf" ]; then 1387 | # 讀取 user 行,抓第一個 user 名稱,去掉分號 1388 | local user 1389 | user=$(grep -E '^\s*user\s+' "$nginx_conf" | head -1 | awk '{print $2}' | sed 's/;//') 1390 | if [ -z "$user" ]; then 1391 | user="nobody" 1392 | fi 1393 | echo "$user" 1394 | return 0 1395 | fi 1396 | 1397 | # --- 2. 偵測 Caddy --- 1398 | local pid=$(ss -ltnp 2>/dev/null | awk '/:(80|443)/ && /users:/{gsub(/.*pid=/,""); gsub(/,.*$/,""); print $NF; exit}') 1399 | 1400 | if [ -n "$pid" ]; then 1401 | local user 1402 | user=$(awk '/^Uid:/ {print $2}' "/proc/$pid/status") 1403 | if [ -n "$user" ]; then 1404 | user=$(getent passwd "$user" | cut -d: -f1) 1405 | echo "$user" 1406 | else 1407 | return 1 1408 | fi 1409 | return 0 1410 | fi 1411 | } 1412 | 1413 | html_sites(){ 1414 | local ngx_user=$(get_web_run_user) 1415 | read -p "請輸入網址:" domain 1416 | check_cert "$domain" || { 1417 | echo "未偵測到 Let's Encrypt 憑證,嘗試自動申請..." 1418 | if ssl_apply "$domain"; then 1419 | echo "申請成功,重新驗證憑證..." 1420 | check_cert "$domain" || { 1421 | echo "申請成功但仍無法驗證憑證,中止建立站點" 1422 | return 1 1423 | } 1424 | else 1425 | echo "SSL 申請失敗,中止建立站點" 1426 | return 1 1427 | fi 1428 | } 1429 | mkdir -p /var/www/$domain 1430 | local confirm 1431 | read -p "是否自訂html?(Y/n)" confirm 1432 | confirm=${confirm,,} 1433 | if [[ $confirm == y || $confirm == "" ]]; then 1434 | nano /var/www/$domain/index.html 1435 | else 1436 | echo "

歡迎來到 $domain

" > /var/www/$domain/index.html 1437 | fi 1438 | chown -R $ngx_user:$ngx_user /var/www/$domain 1439 | setup_site "$domain" html 1440 | echo "已建立 $domain 之html站點。" 1441 | } 1442 | httpguard_setup()( 1443 | [ $caddy -eq 1 ] && return 0 1444 | check_php 1445 | case $system in 1446 | 1|2) 1447 | if ! command -v openresty &>/dev/null; then 1448 | echo -e "${RED}未偵測到 openresty 指令${RESET}" 1449 | read -p "操作完成,請按任意鍵繼續..." -n1 1450 | return 1 1451 | fi 1452 | local guard_dir="/usr/local/openresty/nginx/conf/HttpGuard" 1453 | ;; 1454 | 3) 1455 | if ! command -v nginx &>/dev/null; then 1456 | echo -e "${RED}未偵測到 nginx 指令${RESET}" 1457 | read -p "操作完成,請按任意鍵繼續..." -n1 1458 | return 1 1459 | fi 1460 | if ! nginx -V 2>&1 | grep -iq lua; then 1461 | echo -e "${RED}您的 Nginx 不支援 Lua 模組,無法使用 HttpGuard。${RESET}" 1462 | read -p "操作完成,請按任意鍵繼續..." -n1 1463 | return 1 1464 | fi 1465 | local guard_dir="/etc/nginx/HttpGuard" 1466 | ;; 1467 | esac 1468 | local ngx_conf=$(detect_nginx_conf_paths) 1469 | if [ -d "$guard_dir" ]; then 1470 | echo "HttpGuard 已安裝,進入管理選單..." 1471 | menu_httpguard 1472 | return 0 1473 | fi 1474 | local marker="HttpGuard/init.lua" 1475 | 1476 | # === 若尚未安裝則執行安裝 === 1477 | echo "下載 HttpGuard..." 1478 | 1479 | case $system in 1480 | 1|2) 1481 | local HttpGuard_download_path="/usr/local/openresty/nginx/conf/HttpGuard.zip" 1482 | local http_path="/usr/local/openresty/nginx/conf/HttpGuard" 1483 | ;; 1484 | 3) 1485 | local HttpGuard_download_path="/etc/nginx/HttpGuard.zip" 1486 | local http_path="/etc/nginx/HttpGuard" 1487 | ;; 1488 | esac 1489 | wget -O $HttpGuard_download_path https://files.gebu8f.com/site/HttpGuard.zip || { 1490 | echo "下載失敗" 1491 | return 1 1492 | } 1493 | 1494 | unzip -o "$HttpGuard_download_path" -d /etc/nginx 1495 | if [ $system = 3 ]; then 1496 | sed -i "s|^baseDir *=.*|baseDir = '/etc/nginx/HttpGuard/'|" /etc/nginx/HttpGuard/config.lua 1497 | local ss_path=$(command -v ss 2>/dev/null) 1498 | if [ -n "$ss_path" ]; then 1499 | sed -i "s|ssCommand *= *\"[^\"]*\"|ssCommand = \"$ss_path\"|" /etc/nginx/HttpGuard/config.lua 1500 | fi 1501 | fi 1502 | rm $HttpGuard_download_path 1503 | echo "正在生成動態配置文件..." 1504 | cd $http_path/captcha/ 1505 | php getImg.php 1506 | 1507 | chown -R nginx:nginx $http_path 1508 | if [[ $system == 1 || $system == 2 ]]; then 1509 | sed -i '/http {/a \ 1510 | lua_package_path "/usr/local/openresty/lualib/?.lua;/usr/local/openresty/nginx/conf/HttpGuard/?.lua;;";\n\ 1511 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;";\n\ 1512 | lua_shared_dict guard_dict 100m;\n\ 1513 | lua_shared_dict dict_captcha 128m;\n\ 1514 | init_by_lua_file /usr/local/openresty/nginx/conf/HttpGuard/init.lua;\n\ 1515 | access_by_lua_file /usr/local/openresty/nginx/conf/HttpGuard/runtime.lua;\n\ 1516 | lua_max_running_timers 1;' "$ngx_conf" 1517 | else 1518 | sed -i '/http {/a \ 1519 | lua_package_path "/usr/local/share/lua/5.1/?.lua;/etc/nginx/HttpGuard/?.lua;;";\n\ 1520 | lua_package_cpath "/usr/local/lib/lua/5.1/?.so;;";\n\ 1521 | lua_shared_dict guard_dict 100m;\n\ 1522 | lua_shared_dict dict_captcha 128m;\n\ 1523 | init_by_lua_file /etc/nginx/HttpGuard/init.lua;\n\ 1524 | access_by_lua_file /etc/nginx/HttpGuard/runtime.lua;\n\ 1525 | lua_max_running_timers 1;' "$ngx_conf" 1526 | fi 1527 | 1528 | if nginx -t; then 1529 | restart_webserver 1530 | echo "HttpGuard 安裝完成" 1531 | menu_httpguard 1532 | else 1533 | echo "安裝失敗.." 1534 | return 1 1535 | fi 1536 | ) 1537 | 1538 | install_wp_plugin_with_search_or_url() { 1539 | local domain="$1" 1540 | local site_path="/var/www/$domain" 1541 | local plugin_dir="$site_path/wp-content/plugins" 1542 | 1543 | read -p "請輸入插件關鍵字 或 ZIP 下載網址: " input 1544 | [ -z "$input" ] && echo -e "${RED}未輸入內容${RESET}" && return 1 1545 | 1546 | # --------------------------------------------------- 1547 | # 如果是 ZIP 下載網址 1548 | # --------------------------------------------------- 1549 | if [[ "$input" =~ ^https?://.*\.zip$ ]]; then 1550 | echo "偵測到為 ZIP 插件連結,開始下載..." 1551 | tmp_file="/tmp/plugin_$$.zip" 1552 | 1553 | if ! wget -qO "$tmp_file" "$input"; then 1554 | echo -e "${RED}下載失敗${RESET}" 1555 | return 1 1556 | fi 1557 | 1558 | if ! unzip -t "$tmp_file" >/dev/null 2>&1; then 1559 | echo -e "${RED}下載的檔案不是有效的 ZIP 壓縮檔${RESET}" 1560 | rm -f "$tmp_file" 1561 | return 1 1562 | fi 1563 | 1564 | unzip -q "$tmp_file" -d "$plugin_dir" || { 1565 | echo -e "${RED}解壓失敗${RESET}" 1566 | rm -f "$tmp_file" 1567 | return 1 1568 | } 1569 | rm -f "$tmp_file" 1570 | echo -e "${GREEN}插件已解壓至:$plugin_dir${RESET}" 1571 | 1572 | plugin_slug=$(ls -1 "$plugin_dir" | head -n 1) 1573 | if [ -n "$plugin_slug" ]; then 1574 | echo -e "${GREEN}正在嘗試啟用插件...${RESET}" 1575 | wp --skip-plugins --skip-themes --allow-root --path="$site_path" plugin activate "$plugin_slug" 2>/dev/null \ 1576 | && echo -e "${GREEN}已啟用插件:$plugin_slug${RESET}" \ 1577 | || echo -e "${YELLOW}無法自動啟用,請手動啟用插件${RESET}" 1578 | else 1579 | echo -e "${YELLOW}無法偵測插件目錄,請手動啟用插件${RESET}" 1580 | fi 1581 | return 0 1582 | fi 1583 | 1584 | # --------------------------------------------------- 1585 | # 插件關鍵字搜尋(使用 JSON 以避免 CSV 問題) 1586 | # --------------------------------------------------- 1587 | echo "正在搜尋包含 \"$input\" 的插件..." 1588 | 1589 | mapfile -t plugins < <( 1590 | wp --allow-root --path="$site_path" plugin search "$input" --per-page=10 --format=json | jq -r '.[] | "\(.name)|\(.slug)"' 1591 | ) 1592 | 1593 | if [ ${#plugins[@]} -eq 0 ]; then 1594 | echo -e "${RED}找不到任何相關插件${RESET}" 1595 | return 1 1596 | fi 1597 | 1598 | local options=() 1599 | local slugs=() 1600 | 1601 | for entry in "${plugins[@]}"; do 1602 | name="${entry%%|*}" 1603 | slug="${entry##*|}" 1604 | [ -n "$slug" ] && options+=("$name (slug: $slug)") && slugs+=("$slug") 1605 | done 1606 | 1607 | if [ ${#options[@]} -eq 0 ]; then 1608 | echo -e "${RED}找不到任何有效插件${RESET}" 1609 | return 1 1610 | fi 1611 | 1612 | echo "請選擇欲安裝的插件:" 1613 | select opt in "${options[@]}"; do 1614 | if [ -n "$opt" ]; then 1615 | idx=$((REPLY - 1)) 1616 | slug="${slugs[$idx]}" 1617 | echo -e "${CYAN}開始安裝插件:$slug${RESET}" 1618 | wp --allow-root --path="$site_path" plugin install "$slug" --activate 1619 | return 1620 | else 1621 | echo -e "${YELLOW}無效的選項,請重新選擇${RESET}" 1622 | fi 1623 | done 1624 | } 1625 | 1626 | install_web_server(){ 1627 | local mode=$1 1628 | local other=$2 1629 | local os=$(lsb_release -is | tr '[:upper:]' '[:lower:]') 1630 | local codename=$(lsb_release -sc) 1631 | 1632 | if [ $mode == openresty ]; then 1633 | case "$system" in 1634 | 1) 1635 | apt update 1636 | apt install -y curl gnupg2 ca-certificates lsb-release 1637 | curl -s https://openresty.org/package/pubkey.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/openresty.gpg 1638 | if [[ $os == "debian" ]]; then 1639 | if [[ $codename == "trixie" ]]; then 1640 | codename="bookworm" 1641 | fi 1642 | echo "deb https://openresty.org/package/debian $codename openresty" | tee /etc/apt/sources.list.d/openresty.list 1643 | elif [[ $os == "kali" ]]; then 1644 | codename="bookworm" 1645 | echo "deb https://openresty.org/package/debian $codename openresty" | tee /etc/apt/sources.list.d/openresty.list 1646 | elif [[ $os == "ubuntu" ]]; then 1647 | echo "deb https://openresty.org/package/ubuntu $codename openresty" | tee /etc/apt/sources.list.d/openresty.list 1648 | fi 1649 | apt update 1650 | apt install openresty -y 1651 | rm -rf /etc/nginx 1652 | ln -sf /usr/local/openresty/nginx/sbin/nginx /usr/sbin/nginx 1653 | ln -sf /usr/local/openresty/nginx/conf /etc/nginx 1654 | mkdir -p /etc/nginx/conf.d 1655 | systemctl enable openresty 1656 | ;; 1657 | 2) 1658 | yum update 1659 | yum install -y yum-utils 1660 | yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo 1661 | yum update 1662 | yum install -y openresty --nogpgcheck 1663 | rm -rf /etc/nginx 1664 | ln -sf /usr/local/openresty/nginx/sbin/nginx /usr/sbin/nginx 1665 | ln -sf /usr/local/openresty/nginx/conf /etc/nginx 1666 | mkdir -p /etc/nginx/conf.d 1667 | systemctl enable openresty 1668 | ;; 1669 | 3) 1670 | if [[ $other == compile ]]; then 1671 | apk add build-base perl pcre2-dev openssl-dev zlib-dev curl git 1672 | ver=$(curl -s https://openresty.org/en/download.html | grep -m 1 -Eo 'openresty-[0-9.]+\.tar\.gz' | sed 's/^openresty-\(.*\)\.tar\.gz$/\1/' | head -n 1) 1673 | if [ -z $ver ]; then 1674 | echo "${RED} 無法辨識版本${RESET}" 1675 | sleep 1 1676 | return 1 1677 | fi 1678 | mkdir -p /usr/local/src/ && cd /usr/local/src/ 1679 | curl -O https://openresty.org/download/openresty-$ver.tar.gz 1680 | tar -xzvf openresty-$ver.tar.gz 1681 | rm openresty-$ver.tar.gz 1682 | cd openresty-$ver/ 1683 | ./configure --prefix=/usr/local/openresty \ 1684 | --with-compat \ 1685 | --with-threads \ 1686 | --with-pcre-jit \ 1687 | --with-http_ssl_module \ 1688 | --with-http_v2_module \ 1689 | --with-http_v3_module \ 1690 | --with-http_realip_module \ 1691 | --with-http_stub_status_module \ 1692 | --with-http_gzip_static_module \ 1693 | --with-http_gunzip_module \ 1694 | --with-stream \ 1695 | --with-stream_ssl_module \ 1696 | --with-stream_ssl_preread_module 1697 | make -j$(nproc) 1698 | make install 1699 | ln -s /usr/local/openresty/bin/openresty /usr/local/bin/openresty 1700 | ln -s /usr/local/openresty/nginx/sbin/nginx /usr/local/bin/nginx 1701 | ln -s /usr/local/openresty/bin/resty /usr/local/bin/resty 1702 | cat > /etc/init.d/openresty << 'EOF' 1703 | #!/sbin/openrc-run 1704 | 1705 | name="openresty" 1706 | description="OpenResty Web Platform" 1707 | 1708 | command="/usr/local/openresty/nginx/sbin/nginx" 1709 | command_args="-p /usr/local/openresty/nginx/ -c /usr/local/openresty/nginx/conf/nginx.conf" 1710 | pidfile="/usr/local/openresty/nginx/logs/nginx.pid" 1711 | 1712 | depend() { 1713 | need net 1714 | use dns logger 1715 | } 1716 | 1717 | start_pre() { 1718 | checkpath --directory --mode 0755 /usr/local/openresty/nginx/logs 1719 | } 1720 | 1721 | reload() { 1722 | ebegin "Reloading $name" 1723 | if [ -f "$pidfile" ]; then 1724 | start-stop-daemon --signal HUP --pidfile "$pidfile" 1725 | eend $? 1726 | else 1727 | eend 1 "PID file not found, is $name running?" 1728 | fi 1729 | } 1730 | EOF 1731 | chmod +x /etc/init.d/openresty 1732 | rc-update add openresty default 1733 | rc-service openresty start 1734 | else 1735 | apk update 1736 | apk add --no-cache pcre openssl curl gnupg 1737 | curl -O https://openresty.org/package/admin@openresty.com-5ea678a6.rsa.pub 1738 | mv admin@openresty.com-5ea678a6.rsa.pub /etc/apk/keys/ 1739 | echo "https://openresty.org/package/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/main" \ 1740 | | tee -a /etc/apk/repositories 1741 | apk update 1742 | apk add --no-cache openresty 1743 | ln -sf /usr/local/openresty/nginx/sbin/nginx /usr/sbin/nginx 1744 | ln -sf /usr/local/openresty/nginx/conf /etc/nginx 1745 | mkdir -p /etc/nginx/conf.d 1746 | rc-update add openresty default 1747 | fi 1748 | ;; 1749 | esac 1750 | check_web_server 1751 | default $mode 1752 | elif [ $mode == nginx ]; then 1753 | case $system in 1754 | 1) 1755 | apt update 1756 | apt install -y curl gnupg2 ca-certificates lsb-release 1757 | curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg 1758 | if [[ $os == "debian" ]]; then 1759 | echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/debian $codename nginx" | tee /etc/apt/sources.list.d/nginx.list 1760 | elif [[ $os == "ubuntu" ]]; then 1761 | echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/ubuntu $codename nginx" | tee /etc/apt/sources.list.d/nginx.list 1762 | elif [[ $os == "kali" ]]; then 1763 | echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/debian bookworm nginx" | tee /etc/apt/sources.list.d/nginx.list 1764 | fi 1765 | apt update 1766 | apt install nginx -y 1767 | systemctl enable nginx 1768 | ;; 1769 | 2) 1770 | tee /etc/yum.repos.d/nginx.repo << 'EOF' 1771 | [nginx-stable] 1772 | name=nginx stable repo 1773 | baseurl=https://nginx.org/packages/centos/$releasever/$basearch/ 1774 | gpgcheck=1 1775 | enabled=1 1776 | gpgkey=https://nginx.org/keys/nginx_signing.key 1777 | module_hotfixes=true 1778 | EOF 1779 | yum install nginx -y 1780 | systemctl enable nginx 1781 | ;; 1782 | 3) 1783 | wget -O /tmp/nginx_signing.rsa.pub https://nginx.org/keys/nginx_signing.rsa.pub 1784 | mv /tmp/nginx_signing.rsa.pub /etc/apk/keys/ 1785 | echo "https://nginx.org/packages/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/main" | tee -a /etc/apk/repositories 1786 | apk update 1787 | apk add nginx 1788 | rc-update add nginx default 1789 | ;; 1790 | esac 1791 | check_web_server 1792 | default $mode 1793 | elif [ $mode == caddy ]; then 1794 | case $system in 1795 | 1) 1796 | apt install -y debian-keyring debian-archive-keyring apt-transport-https 1797 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 1798 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list 1799 | chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg 1800 | chmod o+r /etc/apt/sources.list.d/caddy-stable.list 1801 | apt update 1802 | apt install caddy 1803 | systemctl enable caddy 1804 | ;; 1805 | 2) 1806 | if [ -f /etc/fedora-release ]; then 1807 | dnf install -y dnf5-plugins 1808 | dnf copr enable @caddy/caddy 1809 | dnf install -y caddy 1810 | else 1811 | dnf install -y dnf-plugins-core 1812 | dnf copr enable @caddy/caddy 1813 | dnf install -y caddy 1814 | fi 1815 | systemctl enable caddy 1816 | esac 1817 | check_web_server 1818 | default $mode 1819 | fi 1820 | } 1821 | 1822 | install_wpcli_if_needed() { 1823 | if ! command -v wp >/dev/null 2>&1; then 1824 | echo "尚未安裝 WP-CLI,開始下載安裝..." 1825 | curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar || { 1826 | echo "下載失敗,請檢查網路!" 1827 | return 1 1828 | } 1829 | chmod +x wp-cli.phar 1830 | mv wp-cli.phar /usr/local/bin/wp 1831 | echo "安裝完成,版本:$(wp --allow-root --version | head -n1)" 1832 | fi 1833 | } 1834 | 1835 | php_install() { 1836 | case $system in 1837 | 1) 1838 | local os=$(lsb_release -is | tr '[:upper:]' '[:lower:]') 1839 | local codename=$(lsb_release -sc) 1840 | apt update 1841 | apt install -y software-properties-common ca-certificates lsb-release gnupg curl 1842 | 1843 | if [[ $os == "kali" ]]; then 1844 | # Kali rolling 沒有對應的 Sury 名稱,強制指定為 bookworm 1845 | codename="bookworm" 1846 | curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/ondrej_php.gpg 1847 | echo "deb https://packages.sury.org/php/ $codename main" > /etc/apt/sources.list.d/php.list 1848 | elif [[ $os == "debian" ]]; then 1849 | curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/ondrej_php.gpg 1850 | echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list 1851 | elif [[ $os == "ubuntu" ]]; then 1852 | add-apt-repository -y ppa:ondrej/php 1853 | fi 1854 | 1855 | apt update 1856 | 1857 | echo -e "${CYAN}偵測可用 PHP 版本...${RESET}" 1858 | local flarum_php_var=$(check_flarum_supported_php) 1859 | local versions=$(apt-cache search ^php[0-9.]+$ | grep -oP '^php\K[0-9.]+' | sort -Vu | awk -F. '$1>=8 {print}') 1860 | if [[ -z "$versions" ]]; then 1861 | echo -e "${RED}無法取得 PHP 版本列表,請檢查倉庫是否正常。${RESET}" 1862 | return 1 1863 | fi 1864 | 1865 | echo -e "${YELLOW}可用 PHP 版本如下(僅列出 8.0 以上):${GREEN}$(echo "$versions" | xargs)${RESET}" 1866 | echo -e "${CYAN}您好,如果您要使用 flarum 的話,這是它現在支援建議的版本,請留意:${GREEN}${flarum_php_var}${RESET}" 1867 | read -p "請輸入要安裝的 PHP 版本(例如 8.3)[預設8.3]: " phpver 1868 | phpver=${phpver:-8.3} 1869 | if ! echo "$versions" | grep -qx "$phpver"; then 1870 | echo -e "${RED}無效版本號:$phpver${RESET}" 1871 | return 1 1872 | fi 1873 | 1874 | apt install -y php$phpver php$phpver-fpm php$phpver-mysql php$phpver-curl php$phpver-gd \ 1875 | php$phpver-xml php$phpver-mbstring php$phpver-zip php$phpver-intl php$phpver-bcmath php$phpver-imagick unzip redis 1876 | 1877 | systemctl enable --now php$phpver-fpm 1878 | ;; 1879 | 1880 | 2) 1881 | yum update -y 1882 | yum install -y epel-release 1883 | yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm 1884 | yum update -y 1885 | yum module reset php -y 1886 | 1887 | local flarum_php_var=$(check_flarum_supported_php) 1888 | 1889 | local php_versions=$(yum module list php | grep -E '^php\s+(remi-)?8\.[0-9]+' | awk '{print $2}' | sed 's/remi-//' | sort -Vu | xargs) 1890 | 1891 | if [[ -z "$php_versions" ]]; then 1892 | echo -e "${RED}無法偵測可用 PHP 模組版本。${RESET}" 1893 | return 1 1894 | fi 1895 | 1896 | echo -e "${YELLOW}可用 PHP 版本如下(僅列出 8.0 以上):${GREEN}$(echo "$php_versions" | xargs)${RESET}" 1897 | echo -e "${CYAN}您好,如果您要使用 flarum 的話,這是它現在支援建議的版本,請留意:${GREEN}${flarum_php_var}${RESET}" 1898 | read -p "請輸入要安裝的 PHP 版本(例如 8.3)[預設8.3]: " phpver 1899 | phpver=${phpver:-8.3} 1900 | 1901 | if [[ ! " $php_versions " =~ " $phpver " ]]; then 1902 | echo -e "${RED}無效版本號:$phpver${RESET}" 1903 | return 1 1904 | fi 1905 | 1906 | yum module reset php -y 1907 | yum module enable php:remi-$phpver -y 1908 | yum install -y php php-fpm php-mysqlnd php-curl php-gd php-xml php-mbstring php-zip php-intl php-bcmath php-pecl-imagick unzip redis 1909 | 1910 | systemctl enable --now php-fpm 1911 | ;; 1912 | 1913 | 3) 1914 | echo "@edgecommunity http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories 1915 | apk update 1916 | 1917 | local candidates=$(apk search -x php[0-9]* | grep -oE 'php[0-9]{2}' | sort -u) 1918 | 1919 | # 擷取可用版本 1920 | local available_versions="" 1921 | 1922 | local flarum_php_var=$(check_flarum_supported_php) 1923 | 1924 | for c in $candidates; do 1925 | if apk info "$c" >/dev/null 2>&1; then 1926 | short=${c#php} 1927 | [[ "$short" -ge 80 ]] && available_versions+=$'\n'"8.${short:1}" 1928 | fi 1929 | done 1930 | 1931 | # 過濾 80 以下版本 1932 | local filtered_versions=$(echo "$available_versions" | sort -Vu) 1933 | 1934 | echo -e "${YELLOW}可用 PHP 版本如下(僅列出 8.0 以上):${GREEN}$(echo "$filtered_versions" | xargs)${RESET}" 1935 | 1936 | echo -e "${CYAN}您好,如果您要使用 flarum 的話,這是它現在支援建議的版本,請留意:${GREEN}${flarum_php_var}${RESET}" 1937 | 1938 | read -p "請輸入要安裝的 PHP 版本(例如 8.3)[預設8.3]: " phpver 1939 | phpver=${phpver:-8.3} 1940 | 1941 | if ! echo "$phpver" | grep -qE '^8\.[0-9]+$'; then 1942 | echo -e "${RED}請輸入有效的 PHP 8.x 版本${RESET}" 1943 | return 1 1944 | fi 1945 | 1946 | local shortver=$(echo "$phpver" | tr -d '.') 1947 | 1948 | if ! echo "$available_versions" | grep -q "^8\.${shortver:1}$"; then 1949 | echo -e "${RED}Edge 倉庫中找不到 php$shortver,請確認版本是否正確${RESET}" 1950 | return 1 1951 | fi 1952 | 1953 | if ! apk add --simulate php$shortver>/dev/null 2>&1; then 1954 | echo -e "${RED}您好,您的php版本$phpver無法安裝${RESET}" 1955 | return 1 1956 | fi 1957 | 1958 | apk add php$shortver php$shortver-fpm php$shortver-mysqli php$shortver-curl \ 1959 | php$shortver-gd php$shortver-xml php$shortver-mbstring php$shortver-zip \ 1960 | php$shortver-intl php$shortver-bcmath php$shortver-pecl-imagick php$shortver-phar unzip redis || { 1961 | echo -e "${RED}安裝失敗,請確認版本是否存在於 Edge 社群源。${RESET}" 1962 | return 1 1963 | } 1964 | 1965 | ln -sf /usr/bin/php$shortver /usr/bin/php 1966 | ln -sf /usr/sbin/php$shortver-fpm /usr/sbin/php-fpm 1967 | rc-service php-fpm$shortver start 1968 | rc-update add php-fpm$shortver default 1969 | ;; 1970 | esac 1971 | } 1972 | 1973 | 1974 | php_fix(){ 1975 | local php_var=$(check_php_version) 1976 | local ngx_user=$(get_web_run_user) 1977 | 1978 | if [ $system -eq 1 ]; then # Debian/Ubuntu 1979 | sed -i -r "s|^;?(user\s*=\s*).*|\1$ngx_user|" /etc/php/$php_var/fpm/pool.d/www.conf 1980 | sed -i -r "s|^;?(group\s*=\s*).*|\1$ngx_user|" /etc/php/$php_var/fpm/pool.d/www.conf 1981 | sed -i -r "s|^;?(listen.owner\s*=\s*).*|\1$ngx_user|" /etc/php/$php_var/fpm/pool.d/www.conf 1982 | sed -i -r "s|^;?(listen.group\s*=\s*).*|\1$ngx_user|" /etc/php/$php_var/fpm/pool.d/www.conf 1983 | sed -i -r "s|^;?(listen.mode\s*=\s*).*|\10660|" /etc/php/$php_var/fpm/pool.d/www.conf 1984 | sed -i -r "s|^;?(listen\s*=\s*).*|\1/run/php/php-fpm.sock|" /etc/php/$php_var/fpm/pool.d/www.conf 1985 | chown_set 1986 | systemctl restart php$php_var-fpm 1987 | 1988 | elif [ $system -eq 2 ]; then # CentOS/RHEL 1989 | sed -i "s|^user *=.*|user = $ngx_user|" /etc/php-fpm.d/www.conf 1990 | sed -i "s|^group *=.*|group = $ngx_user|" /etc/php-fpm.d/www.conf 1991 | sed -i "s|^listen.owner *=.*|listen.owner = $ngx_user|" /etc/php-fpm.d/www.conf 1992 | sed -i "s|^listen.group *=.*|listen.group = $ngx_user|" /etc/php-fpm.d/www.conf 1993 | sed -i "s|^listen =.*|listen = /run/php/php-fpm.sock|" /etc/php-fpm.d/www.conf 1994 | sed -i "s|^listen.mode *=.*|listen.mode = 0660|" /etc/php-fpm.d/www.conf 1995 | chown_set 1996 | systemctl restart php-fpm 1997 | 1998 | elif [ $system -eq 3 ]; then # Alpine 1999 | sed -i "s/^user =.*/user = $ngx_user/" /etc/php$php_var/php-fpm.d/www.conf 2000 | sed -i "s/^group =.*/group = $ngx_user/" /etc/php$php_var/php-fpm.d/www.conf 2001 | sed -i "s|^listen =.*|listen = /run/php/php-fpm.sock|" /etc/php$php_var/php-fpm.d/www.conf 2002 | sed -i "s/^;listen.owner =.*/listen.owner = $ngx_user/" /etc/php$php_var/php-fpm.d/www.conf 2003 | sed -i "s/^;listen.group =.*/listen.group = $ngx_user/" /etc/php$php_var/php-fpm.d/www.conf 2004 | sed -i "s/^;listen.mode =.*/listen.mode = 0660/" /etc/php$php_var/php-fpm.d/www.conf 2005 | chown_set 2006 | rc-service php-fpm$php_var restart 2007 | fi 2008 | } 2009 | 2010 | 2011 | php_switch_version() { 2012 | case $system in 2013 | 1) 2014 | oldver=$(check_php_version) 2015 | local versions=$(apt-cache search ^php[0-9.]+$ | grep -oP '^php\K[0-9.]+' | sort -Vu | awk -F. '$1>=8 {print}') 2016 | ;; 2017 | 2) 2018 | oldver=$(check_php_version) 2019 | local versions=$(yum module list php | grep -E '^php\s+(remi-)?8\.[0-9]+' | awk '{print $2}' | sed 's/remi-//' | sort -Vu | xargs) 2020 | ;; 2021 | 3) 2022 | local oldver=$(php -v | head -n1 | grep -oE '[0-9]+\.[0-9]+') # 8.3 2023 | local candidates=$(apk search -x php[0-9]* | grep -oE 'php[0-9]{2}' | sort -u) 2024 | 2025 | # 擷取可用版本 2026 | local available_versions="" 2027 | 2028 | for c in $candidates; do 2029 | if apk info "$c" >/dev/null 2>&1; then 2030 | short=${c#php} 2031 | [[ "$short" -ge 80 ]] && available_versions+=$'\n'"8.${short:1}" 2032 | fi 2033 | done 2034 | 2035 | # 過濾 80 以下版本 2036 | local versions=$(echo "$available_versions" | sort -Vu) 2037 | ;; 2038 | esac 2039 | 2040 | 2041 | echo "目前安裝的 PHP 版本為:$oldver" 2042 | echo "可升級/降級版本:$versions" 2043 | read -p "請輸入要升級/降級的 PHP 版本(例如 8.3)[預設與目前相同]: " newver 2044 | newver=${newver:-$oldver} 2045 | shortold=$(echo "$oldver" | tr -d '.') 2046 | shortnew=$(echo "$newver" | tr -d '.') 2047 | 2048 | echo "準備擷取舊版已安裝擴充模組..." 2049 | case $system in 2050 | 1) 2051 | mapfile -t exts < <(dpkg -l | grep "^ii php$oldver-" | awk '{print $2}' | grep -oP "(?<=php$oldver-).*" | grep -vE '^(fpm|cli|common)$') 2052 | ;; 2053 | 2) 2054 | mapfile -t exts < <( 2055 | rpm -qa | grep "^php-" | 2056 | grep -vE '^php-(cli|fpm|common|[0-9]+\.[0-9]+)' | 2057 | sed -E 's/^php-pecl-//; s/^php-//' | 2058 | sed -E 's/(-im[0-9]+)?-[0-9].*$//' | 2059 | sort -u 2060 | ) 2061 | ;; 2062 | 3) 2063 | mapfile -t exts < <(apk info | grep "^php$shortold-" | sed "s/php$shortold-//" | grep -vE '^(fpm|cli|common)$') 2064 | ;; 2065 | esac 2066 | 2067 | echo -e "${CYAN}已偵測的擴充模組:${exts[*]:-無}${RESET}" 2068 | 2069 | case $system in 2070 | 3) 2071 | echo "偵測是否能順利安裝..." 2072 | if ! apk add --simulate php$shortnew>/dev/null 2>&1; then 2073 | echo "您好,您的php版本$phpver無法安裝" 2074 | return 1 2075 | fi 2076 | ;; 2077 | esac 2078 | 2079 | echo -e "${CYAN}停止 PHP 與 Web 服務...${RESET}" 2080 | case $system in 2081 | 1) 2082 | systemctl stop php$oldver-fpm 2>/dev/null 2083 | systemctl disable php$oldver-fpm 2>/dev/null 2084 | systemctl stop nginx 2>/dev/null 2085 | systemctl stop openresty 2>/dev/null 2086 | ;; 2087 | 2) 2088 | systemctl stop php-fpm 2>/dev/null 2089 | systemctl disable php-fpm 2>/dev/null 2090 | systemctl stop nginx 2>/dev/null 2091 | systemctl stop openresty 2>/dev/null 2092 | ;; 2093 | 3) 2094 | rc-service php-fpm$shortold stop 2>/dev/null 2095 | rc-update del php-fpm$shortold 2096 | rc-service nginx stop 2>/dev/null 2097 | ;; 2098 | esac 2099 | 2100 | echo -e "${CYAN}移除舊版 PHP...${RESET}" 2101 | case $system in 2102 | 1) 2103 | apt purge -y php$oldver* ;; 2104 | 2) 2105 | yum module reset php -y 2106 | mapfile -t php_packages < <(rpm -qa | grep "^php-" | awk '{print $1}') 2107 | if [[ ${#php_packages[@]} -eq 0 ]]; then 2108 | echo -e "${YELLOW}未發現任何 PHP 套件可移除。${RESET}" 2109 | else 2110 | echo -e "${CYAN}即將移除下列 PHP 套件:${RESET}" 2111 | printf ' - %s\n' "${php_packages[@]}" 2112 | yum remove -y --noautoremove "${php_packages[@]}" 2113 | fi 2114 | ;; 2115 | 3) 2116 | apk del php$shortold* ;; 2117 | esac 2118 | 2119 | echo -e "${CYAN}安裝新版 PHP:$newver${RESET}" 2120 | case $system in 2121 | 1) 2122 | apt install php$newver php$newver-fpm -y 2123 | ;; 2124 | 2) 2125 | yum module enable php:remi-$newver -y 2126 | yum install php php-fpm -y 2127 | ;; 2128 | 3) 2129 | apk add php$shortnew php$shortnew-fpm 2130 | ;; 2131 | esac 2132 | 2133 | echo -e "${CYAN}重新安裝擴充模組...${RESET}" 2134 | for ext in "${exts[@]}"; do 2135 | echo " - 重新安裝模組:$ext" 2136 | case $system in 2137 | 1) apt install -y php$newver-$ext ;; 2138 | 2) yum install -y php-$ext ;; 2139 | 3) apk add php$shortnew-$ext ;; 2140 | esac 2141 | done 2142 | 2143 | case $system in 2144 | 1) 2145 | systemctl enable php$newver-fpm 2146 | systemctl restart php$newver-fpm 2147 | systemctl start openresty 2148 | ;; 2149 | 2) 2150 | systemctl enable php-fpm 2151 | systemctl restart php-fpm 2152 | systemctl start openresty 2153 | ;; 2154 | 3) 2155 | rc-update add php-fpm$shortnew default 2156 | rc-service php-fpm$shortnew restart 2157 | rc-service nginx start 2158 | ;; 2159 | esac 2160 | sleep 5 2161 | php_fix 2162 | 2163 | echo -e "${GREEN}PHP 升級/降級完成(從 $oldver → $newver)${RESET}" 2164 | } 2165 | 2166 | 2167 | php_tune_upload_limit() { 2168 | local php_var=$(check_php_version) 2169 | if ! command -v php >/dev/null 2>&1; then 2170 | echo "未偵測到 PHP,請先安裝 PHP 後再使用此功能。" 2171 | return 1 2172 | fi 2173 | 2174 | php_ini=$(phpini_path) 2175 | 2176 | echo "目前使用的 php.ini:$php_ini" 2177 | read -p "請輸入最大上傳大小(例如 64M、100M、1G,預設 64M):" max_upload 2178 | max_upload="${max_upload:-64M}" 2179 | 2180 | # 將 max_upload 轉成 MB 數值(單位大小推算) 2181 | unit=$(echo "$max_upload" | grep -oEi '[MG]' | tr '[:lower:]' '[:upper:]') 2182 | value=$(echo "$max_upload" | grep -oE '^[0-9]+') 2183 | 2184 | if [ "$unit" == "G" ]; then 2185 | post_size="$((value * 2))G" 2186 | elif [ "$unit" == "M" ]; then 2187 | post_size="$((value * 2))M" 2188 | else 2189 | echo "格式錯誤,請輸入例如 64M 或 1G" 2190 | return 1 2191 | fi 2192 | 2193 | # 固定設定 memory_limit 為 1536M(1.5GB) 2194 | memory_limit="1536M" 2195 | 2196 | # 修改 php.ini 內容 2197 | sed -i "s/^\s*upload_max_filesize\s*=.*/upload_max_filesize = $max_upload/" "$php_ini" 2198 | sed -i "s/^\s*post_max_size\s*=.*/post_max_size = $post_size/" "$php_ini" 2199 | sed -i "s/^\s*memory_limit\s*=.*/memory_limit = $memory_limit/" "$php_ini" 2200 | 2201 | echo -e "${GREEN}已設定:${RESET}" 2202 | echo " - upload_max_filesize = $max_upload" 2203 | echo " - post_max_size = $post_size" 2204 | echo " - memory_limit = $memory_limit" 2205 | 2206 | # 重啟 php-fpm 2207 | if [ $system -eq 1 ]; then 2208 | systemctl restart php$php_var-fpm 2209 | elif [ $system -eq 2 ]; then 2210 | systemctl restart php-fpm 2211 | elif [ $system -eq 3 ]; then 2212 | rc-service php-fpm$php_var restart 2213 | fi 2214 | 2215 | echo -e "${GREEN}PHP FPM 已重新啟動${RESET}" 2216 | } 2217 | 2218 | php_install_extensions() { 2219 | local php_var=$(check_php_version) 2220 | 2221 | read -p "請輸入要安裝的 PHP 擴展名稱(如:gd、mbstring、curl、intl、zip、imagick 等): " ext_name 2222 | if [ -z "$ext_name" ]; then 2223 | echo "未輸入擴展名稱,中止操作。" 2224 | return 1 2225 | fi 2226 | 2227 | echo -en "${CYAN}檢查 PHP 擴展:$ext_name ... ${RESET}" 2228 | if php -m | grep -Fxiq -- "$ext_name"; then 2229 | echo -e "${GREEN}已安裝${RESET}" 2230 | return 0 2231 | fi 2232 | 2233 | if ! check_php_ext_available "$ext_name" "$php_var"; then 2234 | echo -e "${RED}擴展 $ext_name 不存在於倉庫,無法安裝${RESET}" 2235 | return 1 2236 | fi 2237 | 2238 | echo "倉庫中找到 $ext_name,開始安裝..." 2239 | 2240 | case $system in 2241 | 1) 2242 | apt update 2243 | apt install -y php$php_var-$ext_name 2244 | systemctl restart php$php_var-fpm 2245 | ;; 2246 | 2) 2247 | yum install -y php-$ext_name || yum install -y php-pecl-$ext_name 2248 | systemctl restart php-fpm 2249 | ;; 2250 | 3) 2251 | apk update 2252 | apk add php$php_var-$ext_name 2253 | rc-service php-fpm$php_var restart 2254 | ;; 2255 | *) 2256 | echo "不支援的系統類型。" 2257 | return 1 2258 | ;; 2259 | esac 2260 | 2261 | if php -m | grep -Fxiq -- "$ext_name"; then 2262 | echo -e "${GREEN}PHP 擴展 $ext_name 安裝成功。${RESET}" 2263 | else 2264 | echo -e "${RED}PHP 擴展 $ext_name 安裝失敗,請檢查錯誤訊息。${RESET}" 2265 | return 1 2266 | fi 2267 | } 2268 | 2269 | 2270 | 2271 | reverse_proxy(){ 2272 | read -p "請輸入網址(格式:(example.com)):" domain 2273 | read -p "請輸入反向代理網址(如果是容器,則不用填,預設127.0.0.1):" target_url 2274 | read -p "請輸入反向代理網址的端口號:" target_port 2275 | echo "正在檢查輸入的網址..." 2276 | if ! [[ "$target_port" =~ ^[0-9]+$ ]] || [ "$target_port" -lt 1 ] || [ "$target_port" -gt 65535 ]; then 2277 | echo "端口號必須在1到65535之間。" 2278 | return 1 2279 | fi 2280 | read -p "請輸入反向代理的http(s)(如果是容器的話預設是http):" target_protocol 2281 | target_url=${target_url:-127.0.0.1} 2282 | target_protocol=${target_protocol:-http} 2283 | check_cert "$domain" || { 2284 | echo "未偵測到 Let's Encrypt 憑證,嘗試自動申請..." 2285 | if ssl_apply "$domain"; then 2286 | echo "申請成功,重新驗證憑證..." 2287 | check_cert "$domain" || { 2288 | echo "申請成功但仍無法驗證憑證,中止建立站點" 2289 | return 1 2290 | } 2291 | else 2292 | echo "SSL 申請失敗,中止建立站點" 2293 | return 1 2294 | fi 2295 | } 2296 | setup_site "$domain" proxy "$target_url" "$target_protocol" "$target_port" 2297 | echo "已建立 $domain 反向代理站點。" 2298 | } 2299 | 2300 | restart_webserver() { 2301 | if [ "$openresty" -eq "1" ]; then 2302 | service openresty restart 2303 | elif [ "$nginx" -eq "1" ]; then 2304 | service nginx restart 2305 | elif [ "$caddy" -eq "1" ]; then 2306 | service caddy restart 2307 | fi 2308 | } 2309 | 2310 | remove_wp_plugin_with_menu() { 2311 | local domain="$1" 2312 | local site_path="/var/www/$domain" 2313 | local plugin_dir="$site_path/wp-content/plugins" 2314 | 2315 | echo -e "${CYAN}正在偵測已安裝的插件...${RESET}" 2316 | 2317 | # 只抓目錄 (真正的 plugins) 2318 | mapfile -t plugin_folders < <( 2319 | find "$plugin_dir" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" 2320 | ) 2321 | 2322 | if [ ${#plugin_folders[@]} -eq 0 ]; then 2323 | echo -e "${YELLOW}此網站沒有安裝任何插件${RESET}" 2324 | return 0 2325 | fi 2326 | 2327 | local options=() 2328 | for folder in "${plugin_folders[@]}"; do 2329 | status=$(wp --skip-plugins --skip-themes --allow-root --path="$site_path" plugin get "$folder" --field=status 2>/dev/null) 2330 | if [ -n "$status" ]; then 2331 | options+=("$folder [$status]") 2332 | else 2333 | options+=("$folder [unknown]") 2334 | fi 2335 | if [ "$status" = "dropin" ]; then 2336 | continue 2337 | fi 2338 | done 2339 | 2340 | echo "請選擇要移除的插件:" 2341 | select opt in "${options[@]}"; do 2342 | if [ -n "$opt" ]; then 2343 | slug=$(echo "$opt" | awk '{print $1}') 2344 | echo -e "${CYAN}正在移除插件:$slug${RESET}" 2345 | wp --skip-plugins --skip-themes --allow-root --path="$site_path" plugin deactivate "$slug" 2346 | wp --skip-plugins --skip-themes --allow-root --path="$site_path" plugin delete "$slug" 2347 | echo -e "${GREEN}插件已刪除:$slug${RESET}" 2348 | return 2349 | else 2350 | echo -e "${RED}無效的選項,請重新選擇${RESET}" 2351 | fi 2352 | done 2353 | } 2354 | 2355 | # 只列出有自動備份排程的網站,讓用戶選擇移除 2356 | remove_site_backup_cron() { 2357 | echo "============【 移除網站自動備份排程 】============" 2358 | local crontab_lines 2359 | crontab_lines=$(crontab -l 2>/dev/null | grep '/var/www/' || true) 2360 | if [[ -z "$crontab_lines" ]]; then 2361 | echo -e "${RED}目前沒有任何網站有自動備份排程。${RESET}" 2362 | return 1 2363 | fi 2364 | # 從 crontab 取唯一網站 2365 | local sites=() 2366 | while read -r line; do 2367 | site=$(echo "$line" | grep -o '/var/www/[^ ]*' | awk -F/ '{print $4}') 2368 | [[ -n "$site" ]] && sites+=("$site") 2369 | done <<< "$(echo "$crontab_lines" | sort | uniq)" 2370 | # 去重 2371 | local uniq_sites=() 2372 | local seen="" 2373 | for s in "${sites[@]}"; do 2374 | [[ "$seen" =~ " $s " ]] || uniq_sites+=("$s") 2375 | seen+=" $s " 2376 | done 2377 | if [[ ${#uniq_sites[@]} -eq 0 ]]; then 2378 | echo -e "${RED}沒有偵測到任何網站有自動備份排程。${RESET}" 2379 | return 1 2380 | fi 2381 | echo "可移除排程的網站:" 2382 | local i=1 2383 | for site in "${uniq_sites[@]}"; do 2384 | echo " [$i] $site" 2385 | ((i++)) 2386 | done 2387 | read -p "請輸入要移除排程的網站編號:" idx 2388 | if [[ ! "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#uniq_sites[@]} )); then 2389 | echo -e "${RED}輸入無效,取消操作。${RESET}" 2390 | return 1 2391 | fi 2392 | local domain="${uniq_sites[$((idx-1))]}" 2393 | crontab -l 2>/dev/null | grep -v "/var/www/$domain" | crontab - 2394 | echo -e "${GREEN}已移除 $domain 的自動備份排程(不影響現有備份檔案)。${RESET}" 2395 | } 2396 | 2397 | 2398 | 2399 | reset_wp_site() { 2400 | local domain="$1" 2401 | local path="/var/www/$domain" 2402 | local wp_cli="wp --skip-plugins --skip-themes --allow-root" 2403 | 2404 | # 檢查該路徑是否是 WordPress 2405 | if [ ! -f "$path/wp-config.php" ]; then 2406 | echo -e "${RED}$domain 不是 WordPress 網站!${RESET}" 2407 | return 1 2408 | fi 2409 | 2410 | echo -e "${CYAN}正在對 $domain 執行 WordPress 緊急重置...${RESET}" 2411 | 2412 | # 停用全部外掛 2413 | $wp_cli plugin deactivate --all --path="$path" || \ 2414 | echo -e "${YELLOW}停用外掛失敗。${RESET}" 2415 | 2416 | # 嘗試找預設主題 2417 | default_theme=$($wp_cli theme list --path="$path" --status=inactive --field=name | grep -E '^twenty' | head -n 1) 2418 | 2419 | if [ -z "$default_theme" ]; then 2420 | echo -e "${YELLOW}未發現預設佈景主題,嘗試安裝 Twenty Twenty-Four...${RESET}" 2421 | $wp_cli theme install twentytwentyfour --path="$path" 2422 | default_theme="twentytwentyfour" 2423 | fi 2424 | 2425 | $wp_cli theme activate "$default_theme" --path="$path" || \ 2426 | echo -e "${YELLOW}切換佈景主題失敗。${RESET}" 2427 | 2428 | echo -e "${GREEN}$domain 已完成緊急重置。可嘗試重新登入後台。${RESET}" 2429 | } 2430 | 2431 | 2432 | restore_site_files() { 2433 | local mode="$1" 2434 | local domain="$2" 2435 | 2436 | local default_backup_dir="/opt/wp_backups/$domain" 2437 | local backup_dir="" 2438 | local archive="" 2439 | 2440 | # 讓使用者輸入或確認備份檔案所在的目錄 2441 | read -p "請輸入備份檔案所在目錄 [預設: $default_backup_dir]: " backup_dir 2442 | backup_dir="${backup_dir:-$default_backup_dir}" 2443 | 2444 | # 檢查目錄是否存在 2445 | if [[ ! -d "$backup_dir" ]]; then 2446 | echo -e "${RED}目錄不存在: $backup_dir${RESET}" >&2 2447 | return 1 2448 | fi 2449 | 2450 | # 使用 mapfile 讀取所有 .tar.gz 和 .zip 檔案,並按時間倒序排序 2451 | mapfile -t backup_files < <(find "$backup_dir" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.zip" \) -printf "%T@ %p\n" | sort -nr | cut -d' ' -f2-) 2452 | 2453 | # 檢查是否有找到備份檔 2454 | if [ ${#backup_files[@]} -eq 0 ]; then 2455 | echo -e "${YELLOW}在 '$backup_dir' 目錄中找不到任何 .tar.gz 或 .zip 備份檔。${RESET}" >&2 2456 | return 1 2457 | fi 2458 | 2459 | # 列出檔案讓使用者選擇 2460 | echo -e "${CYAN}在 '$backup_dir' 中找到以下備份檔:${RESET}" 2461 | for i in "${!backup_files[@]}"; do 2462 | printf "%3d) %s\n" "$((i + 1))" "$(basename "${backup_files[$i]}")" 2463 | done 2464 | read -p "請選擇要還原的檔案編號: " choice 2465 | 2466 | # 驗證選擇 2467 | if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#backup_files[@]} )); then 2468 | echo -e "${RED}無效的選擇。${RESET}" >&2 2469 | return 1 2470 | fi 2471 | archive="${backup_files[$((choice - 1))]}" 2472 | # === 後續流程不變 === 2473 | local dest_dir="/var/www/$domain" 2474 | if [[ -d "$dest_dir" ]]; then 2475 | read -p "目錄已存在,是否清空目錄後還原?(y/N): " yn 2476 | case "$yn" in 2477 | [Yy]*) 2478 | echo "正在清空 $dest_dir ..." 2479 | rm -rf "${dest_dir:?}"/* 2480 | ;; 2481 | *) 2482 | echo "已取消還原。" 2483 | return 0 2484 | ;; 2485 | esac 2486 | fi 2487 | 2488 | mkdir -p "$dest_dir" 2489 | 2490 | echo -e "${CYAN}正在解壓 $archive ...${RESET}" 2491 | if [[ "$archive" == *.tar.gz ]]; then 2492 | tar -xzf "$archive" -C "$dest_dir" 2493 | elif [[ "$archive" == *.zip ]]; then 2494 | unzip -q "$archive" -d "$dest_dir" 2495 | else 2496 | echo -e "${RED}不支援的壓縮格式${RESET}" 2497 | return 1 2498 | fi 2499 | 2500 | # 檢查解壓是否成功 (一個簡單的檢查方法是看目錄是否仍為空) 2501 | if [ -z "$(ls -A "$dest_dir")" ]; then 2502 | echo -e "${RED}解壓縮失敗或壓縮檔為空!${RESET}" 2503 | return 1 2504 | fi 2505 | 2506 | set_site_permissions "$mode" "$dest_dir" 2507 | 2508 | echo -e "${GREEN}[$mode] 檔案還原完成!${RESET}" 2509 | 2510 | # 根據 system 呼叫不同的 DB restore 2511 | case "$mode" in 2512 | wp) 2513 | echo -e "${CYAN}WordPress 檔案已還原,繼續執行 WordPress 資料庫還原...${RESET}" 2514 | restore_site_db "$mode" "$domain" 2515 | ;; 2516 | flarum) 2517 | echo -e "${CYAN}Flarum 檔案已還原,繼續執行 Flarum 資料庫還原...${RESET}" 2518 | restore_site_db "$mode" "$domain" 2519 | ;; 2520 | *) 2521 | echo -e "${YELLOW}尚未支援系統:$mode${RESET}" 2522 | ;; 2523 | esac 2524 | } 2525 | 2526 | restore_site_db() { 2527 | local type="$1" 2528 | local domain="$2" 2529 | local site_path="/var/www/$domain" 2530 | local db_name="" 2531 | local db_user="" 2532 | local db_pass="" 2533 | local sql_to_restore="" 2534 | 2535 | # --- 步驟 1: 從設定檔讀取資料庫憑證 (不變) --- 2536 | if [[ "$type" == "wp" ]]; then 2537 | local config="$site_path/wp-config.php" 2538 | db_name=$(awk -F"'" '/DB_NAME/{print $4}' "$config") 2539 | db_user=$(awk -F"'" '/DB_USER/{print $4}' "$config") 2540 | db_pass=$(awk -F"'" '/DB_PASSWORD/{print $4}' "$config") 2541 | elif [[ "$type" == "flarum" ]]; then 2542 | local config="$site_path/config.php" 2543 | db_name=$(php -r "\$c = include '$config'; echo \$c['database']['database'] ?? '';") 2544 | db_user=$(php -r "\$c = include '$config'; echo \$c['database']['username'] ?? '';") 2545 | db_pass=$(php -r "\$c = include '$config'; echo \$c['database']['password'] ?? '';") 2546 | fi 2547 | 2548 | if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then 2549 | echo -e "${RED}錯誤:無法從設定檔中完整讀取資料庫憑證。${RESET}" >&2 2550 | return 1 2551 | fi 2552 | 2553 | # 確保 dba 工具存在 2554 | if ! command -v dba >/dev/null 2>&1; then 2555 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 2556 | fi 2557 | 2558 | # --- 步驟 2: 智慧地尋找並讓使用者選擇 SQL 檔案 (核心修改) --- 2559 | local search_dir="$site_path" 2560 | mapfile -t sql_files < <(find "$search_dir" -maxdepth 1 -type f -name "*.sql" -printf "%T@ %p\n" | sort -nr | cut -d' ' -f2-) 2561 | 2562 | # 如果在網站根目錄找不到,則詢問使用者 2563 | if [ ${#sql_files[@]} -eq 0 ] || [[ ! -f "${sql_files[0]}" ]]; then 2564 | echo -e "${YELLOW}在網站根目錄 '$site_path' 中未找到 .sql 檔案。${RESET}" 2565 | local default_sql_dir="/root/mysql_backups" 2566 | read -p "請輸入 .sql 檔案所在的目錄 [預設: $default_sql_dir]: " search_dir 2567 | search_dir="${search_dir:-$default_sql_dir}" 2568 | 2569 | if [[ ! -d "$search_dir" ]]; then 2570 | echo -e "${RED}目錄不存在: $search_dir${RESET}" >&2 2571 | return 1 2572 | fi 2573 | # 重新在使用者指定的目錄中搜尋 2574 | mapfile -t sql_files < <(find "$search_dir" -maxdepth 1 -type f -name "*.sql" -printf "%T@ %p\n" | sort -nr | cut -d' ' -f2-) 2575 | fi 2576 | 2577 | # 檢查最終是否找到檔案 2578 | if [ ${#sql_files[@]} -eq 0 ] || [[ ! -f "${sql_files[0]}" ]]; then 2579 | echo -e "${RED}錯誤:在 '$search_dir' 目錄中仍然找不到任何 .sql 檔案。${RESET}" >&2 2580 | return 1 2581 | fi 2582 | 2583 | # 讓使用者從找到的檔案列表中選擇 2584 | if [ ${#sql_files[@]} -gt 1 ]; then 2585 | echo -e "${CYAN}在 '$search_dir' 中找到以下 SQL 檔案:${RESET}" 2586 | for i in "${!sql_files[@]}"; do 2587 | printf "%3d) %s\n" "$((i + 1))" "$(basename "${sql_files[$i]}")" 2588 | done 2589 | read -p "請選擇要還原的檔案編號: " choice 2590 | 2591 | if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#sql_files[@]} )); then 2592 | echo -e "${RED}無效的選擇。${RESET}" >&2 2593 | return 1 2594 | fi 2595 | sql_to_restore="${sql_files[$((choice - 1))]}" 2596 | else 2597 | # 如果只有一個檔案,就自動選擇,無需使用者操作 2598 | sql_to_restore="${sql_files[0]}" 2599 | echo -e "${CYAN}找到唯一的 SQL 備份檔:$(basename "$sql_to_restore"),將自動進行還原...${RESET}" 2600 | fi 2601 | 2602 | echo "您選擇了: $(basename "$sql_to_restore")" 2603 | 2604 | # --- 步驟 3: 果斷執行還原 --- 2605 | echo "正在將 '$(basename "$sql_to_restore")' 匯入至資料庫 '$db_name'..." 2606 | 2607 | if dba mysql import "$db_name" "$db_user" "$db_pass" "$sql_to_restore"; then 2608 | echo -e "${GREEN}資料庫還原成功!${RESET}" 2609 | # 只有當 SQL 檔案在網站根目錄時才刪除,避免誤刪 /root/mysql_backups 中的檔案 2610 | if [[ "$(dirname "$sql_to_restore")" == "$site_path" ]]; then 2611 | echo "正在清理已還原的 SQL 檔案..." 2612 | rm -f "$sql_to_restore" 2613 | fi 2614 | return 0 2615 | else 2616 | echo -e "${RED}使用 'dba' 工具還原資料庫失敗!請檢查 'dba' 工具的輸出訊息。${RESET}" >&2 2617 | return 1 2618 | fi 2619 | } 2620 | 2621 | set_site_permissions() { 2622 | local mode="$1" 2623 | local dest_dir="$2" 2624 | 2625 | local ngx_user=$(get_web_run_user) 2626 | 2627 | echo -e "${CYAN}設定檔案擁有者為:$owner${RESET}" 2628 | chown -R $ngx_user:$ngx_user "$dest_dir" 2629 | 2630 | echo -e "${CYAN}套用預設檔案/資料夾權限...${RESET}" 2631 | find "$dest_dir" -type d -exec chmod 755 {} + 2632 | find "$dest_dir" -type f -exec chmod 644 {} + 2633 | 2634 | case "$mode" in 2635 | wp) 2636 | mkdir -p "$dest_dir/wp-content/uploads" "$dest_dir/wp-content/cache" 2637 | chown -R $ngx_user:$ngx_user "$dest_dir/wp-content" 2638 | find "$dest_dir/wp-content" -type d -exec chmod 775 {} + 2639 | find "$dest_dir/wp-content" -type d -exec chmod g+s {} + 2640 | [ -f "$dest_dir/wp-config.php" ] && chmod 640 "$dest_dir/wp-config.php" 2641 | ;; 2642 | flarum) 2643 | mkdir -p "$dest_dir/storage" "$dest_dir/public/assets" 2644 | chown -R $ngx_user:$ngx_user "$dest_dir/storage" "$dest_dir/public/assets" 2645 | find "$dest_dir/storage" "$dest_dir/public/assets" -type d -exec chmod 775 {} + 2646 | find "$dest_dir/storage" "$dest_dir/public/assets" -type d -exec chmod g+s {} + 2647 | ;; 2648 | esac 2649 | } 2650 | 2651 | 2652 | 2653 | setup_site_http2(){ 2654 | local domain=$1 2655 | local http3=$(check_http3_support) 2656 | 2657 | local conf_file=$(detect_conf_path)/$domain.conf 2658 | 2659 | if [[ "$http3" != "true" ]]; then 2660 | if command -v nginx >/dev/null 2>&1; then 2661 | local ngx_ver=$(nginx -v 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') 2662 | elif command -v openresty >/dev/null 2>&1; then 2663 | local ngx_ver=$(openresty -v 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') 2664 | fi 2665 | if [ "$(printf '%s\n' "$ngx_ver" "1.25.1" | sort -V | head -n1)" != "1.25.1" ]; then 2666 | sed -i -e '/http2 on/d' "$conf_file" 2667 | # 把 listen 443 ssl; 變成 listen 443 ssl http2; 2668 | sed -i -E 's/(listen\s+443\s+ssl)(;)/\1 http2\2/' "$conf_file" 2669 | sed -i -E 's/(listen\s+\[::\]:443\s+ssl)(;)/\1 http2\2/' "$conf_file" 2670 | fi 2671 | # 刪除所有 HTTP/3 + QUIC 相關設定 2672 | sed -i \ 2673 | -e '/listen.*quic/d' \ 2674 | -e '/http3 on/d' \ 2675 | -e '/http2 on/d' \ 2676 | -e '/Alt-Svc/d' \ 2677 | -e '/QUIC-Status/d' \ 2678 | "$conf_file" 2679 | 2680 | 2681 | echo -e "${GREEN}已刪除 $conf_file 中所有 HTTP/3 / QUIC 相關配置,並啟用 HTTP/2${RESET}" 2682 | fi 2683 | } 2684 | 2685 | 2686 | setup_site() { 2687 | local domain=$1 2688 | local type=$2 2689 | local domain_cert=$(check_cert "$domain" | tail -n 1 | tr -d '\r\n') 2690 | local escaped_cert=$(printf '%s' "$domain_cert" | sed 's/[&/\]/\\&/g') # 取得主域名或泛域名作為憑證目錄 2691 | local conf_file=$(detect_conf_path)/$domain.conf 2692 | clean_ssl_session_cache 2693 | 2694 | if [ $caddy -eq 1 ]; then 2695 | case $system in 2696 | 1|2) 2697 | case $type in 2698 | html|php|flarum) 2699 | local conf_url="https://gitlab.com/gebu8f/sh/-/raw/main/nginx/caddy/domain_${type}.conf" 2700 | wget -qO "$conf_file" "$conf_url" 2701 | sed -i -e "s|domain|$domain|g" "$conf_file" 2702 | restart_webserver 2703 | ;; 2704 | proxy) 2705 | local target_url=$3 2706 | local target_protocol=$4 2707 | local target_port=$5 2708 | wget -O "$conf_file" https://gitlab.com/gebu8f/sh/-/raw/main/nginx/caddy/domain_proxy.conf 2709 | sed -i "s|reverse_proxy host:port|reverse_proxy $target_protocol://$target_url:$target_port|g" "$conf_file" 2710 | sed -i -e "s|domain|$domain|g" "$conf_file" 2711 | restart_webserver 2712 | ;; 2713 | *) 2714 | echo "不支援的類型: $type"; return 1;; 2715 | esac 2716 | ;; 2717 | esac 2718 | return $? 2719 | fi 2720 | case $system in 2721 | 1|2|3) 2722 | case $type in 2723 | html|php|www|flarum|phpmyadmin) 2724 | local conf_url="https://gitlab.com/gebu8f/sh/-/raw/main/nginx/domain_${type}.conf" 2725 | wget -O "$conf_file" "$conf_url" 2726 | sed -i -e "s|domain|$domain|g" \ 2727 | -e "s|main|$escaped_cert|g" \ 2728 | "$conf_file" 2729 | setup_site_http2 "$domain" 2730 | if [ $openresty -eq 1 ]; then 2731 | openresty -t || { 2732 | echo "openresty 測試失敗,請檢查配置" 2733 | return 1 2734 | } 2735 | elif [ $nginx -eq 1 ]; then 2736 | nginx -t || { 2737 | echo "nginx 測試失敗,請檢查配置" 2738 | return 1 2739 | } 2740 | fi 2741 | restart_webserver 2742 | ;; 2743 | proxy) 2744 | local target_url=$3 2745 | local target_protocol=$4 2746 | local target_port=$5 2747 | wget -O "$conf_file" https://gitlab.com/gebu8f/sh/-/raw/main/nginx/domain_proxy.conf 2748 | sed -i "s|proxy_pass host:port;|proxy_pass $target_protocol://$target_url:$target_port;|g" "$conf_file" 2749 | sed -i -e "s|domain|$domain|g" \ 2750 | -e "s|main|$escaped_cert|g" \ 2751 | "$conf_file" 2752 | setup_site_http2 "$domain" 2753 | if [ $openresty -eq 1 ]; then 2754 | openresty -t || { 2755 | echo "openresty 測試失敗,請檢查配置" 2756 | return 1 2757 | } 2758 | elif [ $nginx -eq 1 ]; then 2759 | nginx -t || { 2760 | echo "nginx 測試失敗,請檢查配置" 2761 | return 1 2762 | } 2763 | fi 2764 | restart_webserver 2765 | ;; 2766 | *) 2767 | echo "不支援的類型: $type"; return 1;; 2768 | esac 2769 | ;; 2770 | esac 2771 | } 2772 | 2773 | show_registered_cas() { 2774 | echo "===== 已註冊憑證機構郵箱如下 =====" 2775 | local config_file="/ssl_ca/.ssl_ca_emails" 2776 | 2777 | # 先確保設定檔存在,避免迴圈內重複檢查 2778 | if [ ! -f "$config_file" ]; then 2779 | echo "設定檔 $config_file 不存在。" 2780 | echo "===================================" 2781 | return 1 2782 | fi 2783 | 2784 | for ca in letsencrypt zerossl google; do 2785 | email=$(awk -v section="[$ca]" ' 2786 | # 當找到我們正在尋找的區塊時,設定 found=1,然後讀取下一行 2787 | $0 == section { found=1; next } 2788 | 2789 | # *** 修正點:使用正確的正則表達式來匹配任何區塊標題 *** 2790 | # 如果在我們的區塊內讀到下一個區塊標題,就代表我們的區塊結束了 2791 | /^\[.*\]$/ { found=0 } 2792 | 2793 | # 如果 found=1 且該行是以 email= 開頭,就印出值並退出 2794 | found && /^email=/ { print substr($0,7); exit } 2795 | ' "$config_file" 2>/dev/null) 2796 | 2797 | if [ -n "$email" ]; then 2798 | echo "$ca:$email" 2799 | else 2800 | echo "$ca:未註冊" 2801 | fi 2802 | done 2803 | echo "===================================" 2804 | } 2805 | 2806 | 2807 | select_ca() ( 2808 | mkdir -p /ssl_ca 2809 | show_registered_cas 2810 | echo "請選擇你要註冊的憑證簽發機構:" 2811 | echo "1. Let's Encrypt (預設)" 2812 | echo "2. ZeroSSL" 2813 | echo "3. Google Trust Services" 2814 | echo "0. 返回" 2815 | read -rp "選擇 [0-3]: " ca_choice 2816 | 2817 | case "$ca_choice" in 2818 | 0) 2819 | return 0 2820 | ;; 2821 | 2) 2822 | echo "請先註冊zeroSSL帳號" 2823 | echo "接著到這個網址生成EAB Credentials for ACME Clients:https://app.zerossl.com/developer" 2824 | read -p "您的EAB KID:" eab_kid 2825 | read -p "您的EAB HMAC Key" eab_key 2826 | read -p "您的郵箱:" zero_email 2827 | certbot register \ 2828 | --email $zero_email \ 2829 | --no-eff-email \ 2830 | --agree-tos \ 2831 | --non-interactive \ 2832 | --server "https://acme.zerossl.com/v2/DV90" \ 2833 | --eab-kid "$eab_kid" \ 2834 | --eab-hmac-key "$eab_key" 2835 | set_ca_email "zerossl" "$zero_email" 2836 | ;; 2837 | 3) 2838 | echo "首先你需要有一個google帳號" 2839 | echo "打開此網址並啟用api,請記得選一個專案:https://console.cloud.google.com/apis/library/publicca.googleapis.com" 2840 | echo "打開Cloud Shell 並輸入:gcloud beta publicca external-account-keys create" 2841 | read -p "請輸入keyId:" goog_id 2842 | read -p "請輸入Key:" goog_eab_key 2843 | read -p "請輸入您註冊的郵箱" goog_email 2844 | certbot register \ 2845 | --email "$goog_email" \ 2846 | --no-eff-email \ 2847 | --agree-tos \ 2848 | --non-interactive \ 2849 | --server "https://dv.acme-v02.api.pki.goog/directory" \ 2850 | --eab-kid "$goog_id" \ 2851 | --eab-hmac-key "$goog_eab_key" 2852 | set_ca_email "google" "$goog_email" 2853 | ;; 2854 | *) 2855 | read -p "請輸入您的郵箱:" le_email 2856 | certbot register \ 2857 | --email "$le_email" \ 2858 | --no-eff-email \ 2859 | --non-interactive \ 2860 | --agree-tos \ 2861 | --server "https://acme-v02.api.letsencrypt.org/directory" 2862 | set_ca_email "letsencrypt" "$le_email" 2863 | ;; 2864 | esac 2865 | ) 2866 | set_ca_email() { 2867 | local ca_name="$1" 2868 | local email="$2" 2869 | local config_file="/ssl_ca/.ssl_ca_emails" 2870 | local temp_file=$(mktemp) 2871 | 2872 | mkdir -p /ssl_ca 2873 | # 確保檔案存在,如果不存在則建立一個空的 2874 | touch "$config_file" 2875 | 2876 | # 使用 awk 來安全地移除舊的區塊。 2877 | # 邏輯:設置一個 'skip' 標記。當遇到目標 [ca_name] 時開始跳過, 2878 | # 當遇到下一個 [section] 時停止跳過。 2879 | awk -v ca="[$ca_name]" ' 2880 | BEGIN { skip=0 } 2881 | $0 == ca { skip=1; next } 2882 | /^\[.*\]$/ { skip=0 } 2883 | !skip { print } 2884 | ' "$config_file" > "$temp_file" 2885 | 2886 | # 將新的 CA 資訊追加到臨時檔案的末尾 2887 | # 初始模板中所有 email 都為空,所以不需要特殊處理初始情況 2888 | echo -e "[$ca_name]\nemail=$email\n" >> "$temp_file" 2889 | 2890 | # 用處理過的新檔案覆蓋舊檔案 2891 | mv "$temp_file" "$config_file" 2892 | } 2893 | 2894 | show_cert_status() ( 2895 | # 在子 Shell 中執行 2896 | check_web_environment 2897 | if [[ $use_my_app != true ]]; then 2898 | echo -e "===== 站點憑證狀態 =====" 2899 | echo -e "${RED}您好,您現在使用其他 web server 無法使用站點憑證狀態之功能${RESET}" 2900 | return 0 2901 | fi 2902 | 2903 | echo -e "===== 站點憑證狀態 =====" 2904 | 2905 | if (( BASH_VERSINFO[0] < 4 )); then 2906 | echo "錯誤:此腳本需要 Bash 4.0 或更高版本才能使用關聯陣列。" >&2 2907 | return 1 2908 | fi 2909 | 2910 | # --- 快取相關設定 --- 2911 | local CACHE_DIR="/var/cache/site_manager" 2912 | local NGINX_CACHE_FILE="$CACHE_DIR/nginx_domains.cache" 2913 | mkdir -p "$CACHE_DIR" 2914 | 2915 | local nginx_conf_paths=$(detect_conf_path) 2916 | 2917 | # --- 1. Nginx 配置解析 (帶智慧快取) --- 2918 | declare -A domain_to_cert_path 2919 | 2920 | local nginx_last_mod=0 2921 | [ -d "$nginx_conf_paths" ] && nginx_last_mod=$(stat -c %Y "$nginx_conf_paths") 2922 | local cache_last_mod=0 2923 | [ -f "$NGINX_CACHE_FILE" ] && cache_last_mod=$(stat -c %Y "$NGINX_CACHE_FILE") 2924 | 2925 | # 判斷是否讀取快取 2926 | if (( cache_last_mod > nginx_last_mod )); then 2927 | while IFS='|' read -r domain cert_path; do 2928 | # [修復重點 1] 絕對防禦:如果讀到空行或 domain 是空的,立刻跳過 2929 | [[ -z "$domain" || -z "$cert_path" ]] && continue 2930 | domain_to_cert_path["$domain"]="$cert_path" 2931 | done < "$NGINX_CACHE_FILE" 2932 | else 2933 | local server_configs 2934 | # 嘗試解析,並過濾掉錯誤訊息 2935 | server_configs=$(awk '/server_name/,/ssl_certificate /' "$nginx_conf_paths"/*.conf 2>/dev/null | grep -E "server_name|ssl_certificate ") 2936 | 2937 | local current_domains="" 2938 | > "$NGINX_CACHE_FILE" 2939 | 2940 | # 如果 server_configs 是空的,這裡就不會執行,自然安全 2941 | if [[ -n "$server_configs" ]]; then 2942 | while IFS= read -r line; do 2943 | if [[ $line =~ server_name ]]; then 2944 | current_domains=$(echo "$line" | sed -e 's/server_name//' -e 's/;//' | xargs) 2945 | elif [[ $line =~ ssl_certificate && -n "$current_domains" ]]; then 2946 | local cert_path 2947 | cert_path=$(echo "$line" | awk '{print $2}' | sed 's/;//') 2948 | for domain in $current_domains; do 2949 | # [修復重點 2] 只有當 domain 真的是有內容時才寫入陣列 2950 | if [[ "$cert_path" == /etc/letsencrypt/live/* && -n "$domain" ]]; then 2951 | domain_to_cert_path["$domain"]="$cert_path" 2952 | echo "$domain|$cert_path" >> "$NGINX_CACHE_FILE" 2953 | fi 2954 | done 2955 | current_domains="" 2956 | fi 2957 | done <<< "$server_configs" 2958 | fi 2959 | fi 2960 | 2961 | # --- [修復重點 3] 關鍵檢查:如果上面跑完,陣列裡什麼都沒有,直接結束 --- 2962 | # 這樣就不會去跑下面的迴圈,避免對空陣列操作報錯 2963 | if [ ${#domain_to_cert_path[@]} -eq 0 ]; then 2964 | echo -e "${GREEN}目前沒有偵測到任何使用 Let's Encrypt 的域名。${RESET}" 2965 | return 0 2966 | fi 2967 | 2968 | # --- 2. 處理憑證資訊 (帶記憶體內 openssl 快取) --- 2969 | declare -A cert_cache 2970 | declare -A domain_data 2971 | 2972 | local nginx_domains 2973 | # 因為上面已經檢查過陣列長度不為0,這裡 mapfile 就不會出錯 2974 | mapfile -t nginx_domains < <(printf "%s\n" "${!domain_to_cert_path[@]}" | sort -u) 2975 | 2976 | for domain in "${nginx_domains[@]}"; do 2977 | # 再次確認 domain 非空 (雙重保險) 2978 | [[ -z "$domain" ]] && continue 2979 | 2980 | local cert_path="${domain_to_cert_path[$domain]}" 2981 | local cert_name=$(basename "$(dirname "$cert_path")") 2982 | local note="" 2983 | local end_date="" 2984 | 2985 | # 避免 cert_path 為空導致 cert_cache 報錯 2986 | if [[ -z "$cert_path" ]]; then continue; fi 2987 | 2988 | if [[ -n "${cert_cache[$cert_path]}" ]]; then 2989 | IFS='|' read -r end_date note <<< "${cert_cache[$cert_path]}" 2990 | else 2991 | if [[ -f "$cert_path" ]]; then 2992 | local cert_info 2993 | cert_info=$(openssl x509 -in "$cert_path" -noout -enddate -issuer 2>/dev/null) 2994 | 2995 | if [[ -n "$cert_info" ]]; then 2996 | local end_date_raw=$(echo "$cert_info" | grep 'notAfter' | cut -d= -f2) 2997 | end_date=$([[ -n "$end_date_raw" ]] && date -d "$end_date_raw" +"%Y-%m-%d" || echo "無效日期") 2998 | 2999 | local issuer 3000 | issuer=$(echo "$cert_info" | grep 'issuer' | sed 's/issuer=//') 3001 | if [[ ${issuer,,} == *cloudflare* ]]; then 3002 | note="CF 原始憑證" 3003 | fi 3004 | 3005 | cert_cache["$cert_path"]="$end_date|$note" 3006 | else 3007 | end_date="讀取失敗" 3008 | fi 3009 | else 3010 | end_date="檔案不存在" 3011 | fi 3012 | [[ -z "${cert_cache[$cert_path]}" ]] && cert_cache["$cert_path"]="$end_date|$note" 3013 | fi 3014 | domain_data["$domain"]="$end_date|$cert_name|$note" 3015 | done 3016 | 3017 | # --- 3. 渲染輸出 --- 3018 | display_width() { 3019 | local str="$1"; local width=0; local i=0 3020 | while [ $i -lt ${#str} ]; do 3021 | local char="${str:$i:1}" 3022 | if [[ $(printf "%d" "'$char") -gt 127 ]] 2>/dev/null; then width=$((width + 2)); else width=$((width + 1)); fi 3023 | i=$((i + 1)); done; echo $width 3024 | } 3025 | pad_left() { 3026 | local text="$1"; local max_width="$2" 3027 | local current_width=$(display_width "$text"); local padding=$((max_width - current_width)) 3028 | printf "%s%*s" "$text" $padding "" 3029 | } 3030 | 3031 | local headers=("域名" "到期日" "憑證資料夾" "備註") 3032 | local -a max_widths=() 3033 | for header in "${headers[@]}"; do max_widths+=($(display_width "$header")); done 3034 | 3035 | local -a data_rows 3036 | for domain in "${nginx_domains[@]}"; do 3037 | [[ -z "$domain" ]] && continue 3038 | IFS='|' read -r end_date cert_name note <<< "${domain_data[$domain]}" 3039 | if [ -z "$end_date" ]; then 3040 | end_date="無憑證"; cert_name="-"; note="" 3041 | fi 3042 | data_rows+=("$domain|$end_date|$cert_name|$note") 3043 | local -a current_row_data=("$domain" "$end_date" "$cert_name" "$note") 3044 | for i in "${!max_widths[@]}"; do 3045 | local current_width=$(display_width "${current_row_data[$i]}"); 3046 | if [[ $current_width -gt ${max_widths[$i]} ]]; then max_widths[$i]=$current_width; fi 3047 | done 3048 | done 3049 | 3050 | # 顯示表頭 3051 | for i in "${!headers[@]}"; do 3052 | pad_left "${headers[$i]}" "${max_widths[$i]}"; 3053 | if [[ $i -lt $((${#headers[@]} - 1)) ]]; then printf " | "; fi; 3054 | done; printf "\n" 3055 | 3056 | local total_width=0 3057 | for i in "${!max_widths[@]}"; do 3058 | total_width=$((total_width + max_widths[i])); 3059 | if [[ $i -lt $((${#headers[@]} - 1)) ]]; then total_width=$((total_width + 3)); fi; 3060 | done; printf '%.0s-' $(seq 1 $total_width); printf "\n" 3061 | 3062 | # 顯示資料 3063 | for row in "${data_rows[@]}"; do 3064 | IFS='|' read -r domain date cert note <<< "$row" 3065 | local -a fields=("$domain" "$date" "$cert" "$note") 3066 | for i in "${!fields[@]}"; do 3067 | pad_left "${fields[$i]}" "${max_widths[$i]}"; 3068 | if [[ $i -lt $((${#headers[@]} - 1)) ]]; then printf " | "; fi; 3069 | done; printf "\n" 3070 | done 3071 | ) 3072 | 3073 | show_domain_status_caddy() ( 3074 | echo "===== Caddy 站點域名列表 =====" 3075 | 3076 | local CADDY_CONF_MAIN="/etc/caddy/Caddyfile" 3077 | local CADDY_CONF_DIR=$(detect_conf_path) 3078 | 3079 | if [[ ! -f "$CADDY_CONF_MAIN" ]]; then 3080 | echo "未找到 Caddy 主配置:$CADDY_CONF_MAIN" 3081 | return 1 3082 | fi 3083 | 3084 | declare -A domain_set 3085 | 3086 | # --- 解析 domain {...} 格式 --- 3087 | parse_domains() { 3088 | local file="$1" 3089 | while IFS= read -r line; do 3090 | # 僅匹配: 3091 | # example.com { 3092 | # sub.domain.net { 3093 | if [[ "$line" =~ ^([A-Za-z0-9._-]+)\ \{$ ]]; then 3094 | domain_set["${BASH_REMATCH[1]}"]=1 3095 | fi 3096 | done < "$file" 3097 | } 3098 | 3099 | # 主 Caddyfile 3100 | parse_domains "$CADDY_CONF_MAIN" 3101 | 3102 | # import conf.d/*.caddy 3103 | if [[ -d "$CADDY_CONF_DIR" ]]; then 3104 | while IFS= read -r f; do 3105 | parse_domains "$f" 3106 | done < <(find "$CADDY_CONF_DIR" -maxdepth 1 -type f -name "*.conf") 3107 | fi 3108 | 3109 | # --- 輸出 --- 3110 | 3111 | if [[ ${#domain_set[@]} -eq 0 ]]; then 3112 | echo "找不到任何域名。" 3113 | return 0 3114 | fi 3115 | 3116 | echo "域名" 3117 | echo "----" 3118 | 3119 | for domain in "${!domain_set[@]}"; do 3120 | echo "$domain" 3121 | done | sort 3122 | ) 3123 | 3124 | 3125 | show_httpguard_status(){ 3126 | 3127 | get_module_state() { 3128 | # 自動偵測 config.lua 路徑 3129 | if [ -f "/usr/local/openresty/nginx/conf/HttpGuard/config.lua" ]; then 3130 | config_file="/usr/local/openresty/nginx/conf/HttpGuard/config.lua" 3131 | elif [ -f "/etc/nginx/HttpGuard/config.lua" ]; then 3132 | config_file="/etc/nginx/HttpGuard/config.lua" 3133 | else 3134 | echo "錯誤:HttpGuard/config.lua 未找到。請確認安裝目錄或文件路徑。" 3135 | return 1 3136 | fi 3137 | local module_name=$1 3138 | export LANG=en_US.UTF-8 3139 | export LC_ALL=en_US.UTF-8 3140 | grep -E "^\s*${module_name}\s*=" "$config_file" | \ 3141 | grep -oE 'state\s*=\s*["'\''][^"'\'']*["'\'']' | \ 3142 | sed -E 's/.*state\s*=\s*["'\''](.*)["'\'']/\1/' | \ 3143 | head -n 1 3144 | } 3145 | 3146 | echo "--- HttpGuard 主動防禦與自動開啟狀態 ---" 3147 | 3148 | redirect_state=$(get_module_state "redirectModules") 3149 | jsjump_state=$(get_module_state "JsJumpModules") 3150 | cookie_state=$(get_module_state "cookieModules") 3151 | auto_enable_state=$(get_module_state "autoEnable") 3152 | 3153 | echo -e "${CYAN}主動防禦 (302 Redirect Modules) 狀態: ${redirect_state:-未找到} ${RESET}" 3154 | echo -e "${CYAN}主動防禦 (JS Jump Modules) 狀態: ${jsjump_state:-未找到} ${RESET}" 3155 | echo -e "${CYAN}主動防禦 (Cookie Modules) 狀態: ${cookie_state:-未找到} ${RESET}" 3156 | echo -e "${CYAN}自動開啟主動防禦 狀態: ${auto_enable_state:-未找到} ${RESET}" 3157 | echo "-------------------------------------" 3158 | } 3159 | 3160 | 3161 | show_php() { 3162 | local wp_root="/var/www" 3163 | echo "===== 已安裝 PHP 網站列表 =====" 3164 | printf "%-20s | %-10s\n" "網址" "備註" 3165 | echo "-------------------------------------------" 3166 | 3167 | # 使用 find 命令高效地找出所有符合基本條件的候選目錄 3168 | # -mindepth 1 -maxdepth 1: 只搜尋 /var/www 的下一層,不深入 3169 | # -type d: 只尋找目錄 3170 | # -name '*.*': 目錄名稱必須包含 . (初步過濾) 3171 | # -print0: 使用 NULL 字元分隔結果,處理包含空格等特殊字元的目錄名 3172 | find "$wp_root" -mindepth 1 -maxdepth 1 -type d -name '*.*' -print0 | \ 3173 | while IFS= read -r -d '' site_dir; do 3174 | # find 已經幫我們完成了初步篩選,現在只需對候選目錄進行深度檢查 3175 | 3176 | # 必須有 index.php 才處理 3177 | if [[ ! -f "$site_dir/index.php" ]]; then 3178 | continue 3179 | fi 3180 | 3181 | # basename 已不再是瓶頸,可以安全使用 3182 | local site_name 3183 | site_name=$(basename "$site_dir") 3184 | 3185 | local remark="PHP網站" 3186 | 3187 | # 應用識別邏輯,這部分是無法避免的檢查,且原始寫法已相當優化 (利用 || 短路特性) 3188 | if [[ -f "$site_dir/wp-config.php" ]]; then 3189 | remark="WordPress" 3190 | elif [[ -f "$site_dir/public/assets/forum.js" ]] || grep -qi "flarum" "$site_dir/index.php" 2>/dev/null; then 3191 | remark="Flarum" 3192 | elif [[ -f "$site_dir/usr/index.php" ]] || grep -qi "Typecho" "$site_dir/index.php" 2>/dev/null; then 3193 | remark="Typecho" 3194 | fi 3195 | 3196 | printf "%-20s | %-10s\n" "$site_name" "$remark" 3197 | done 3198 | } 3199 | 3200 | ssl_apply() ( 3201 | update_certbot 3202 | mkdir -p /ssl_ca 3203 | 3204 | # 將常用變數宣告在最前面 3205 | local domains="$1" 3206 | local selected_ca selected_email server_url auth_method 3207 | local cred_file reload_cmd 3208 | local -A ca_emails 3209 | local -a domain_args certbot_args # 使用陣列來動態建立 certbot 命令 3210 | local needs_auto_renew=0 # 標記是否需要加入自動續訂 3211 | if [ -z "$domains" ]; then 3212 | read -p "請輸入您的域名(只能用空白鍵分隔):" domains 3213 | fi 3214 | 3215 | # 將域名字串轉換為 certbot 的 -d 參數陣列 3216 | IFS=$' ,\n' read -ra domain_array <<< "$domains" 3217 | for d in "${domain_array[@]}"; do 3218 | domain_args+=("-d" "$d") 3219 | done 3220 | # (此段邏輯已相當清晰,僅微調) 3221 | local current_ca_config="/ssl_ca/.ssl_ca_emails" 3222 | if [ -f "$current_ca_config" ]; then 3223 | local current_ca="" 3224 | while IFS="=" read -r key val; do 3225 | if [[ $key =~ ^\[(.*)\]$ ]]; then 3226 | current_ca="${BASH_REMATCH[1]}" 3227 | elif [[ -n "$current_ca" && $key == "email" && -n "$val" ]]; then 3228 | ca_emails["$current_ca"]="$val" 3229 | fi 3230 | done < "$current_ca_config" 3231 | fi 3232 | 3233 | local ca_options=() 3234 | for ca in letsencrypt zerossl google; do 3235 | [ -n "${ca_emails[$ca]}" ] && ca_options+=("$ca") 3236 | done 3237 | 3238 | if [ ${#ca_options[@]} -eq 0 ]; then 3239 | echo "尚未註冊任何憑證簽發機構,請直接輸入電子郵件。" 3240 | selected_ca="letsencrypt" 3241 | read -p "請輸入電子郵件:" selected_email 3242 | certbot register --email "$selected_email" --non-interactive --agree-tos --no-eff-email --server "https://acme-v02.api.letsencrypt.org/directory" 3243 | set_ca_email "letsencrypt" "$selected_email" 3244 | elif [ ${#ca_options[@]} -eq 1 ]; then 3245 | selected_ca="${ca_options[0]}" 3246 | echo "自動選擇已註冊的 CA:$selected_ca(${ca_emails[$selected_ca]})" 3247 | selected_email="${ca_emails[$selected_ca]}" 3248 | else 3249 | echo "偵測到以下已註冊的 CA:" 3250 | for i in "${!ca_options[@]}"; do 3251 | echo "$((i+1))) ${ca_options[$i]}(${ca_emails[${ca_options[$i]}]})" 3252 | done 3253 | read -p "請選擇您要使用的 CA [1-${#ca_options[@]}](預設 1):" choice 3254 | selected_ca="${ca_options[$((choice-1))]}" 3255 | selected_email="${ca_emails[$selected_ca]}" 3256 | fi 3257 | case "$selected_ca" in 3258 | zerossl) 3259 | server_url="https://acme.zerossl.com/v2/DV90" 3260 | ;; 3261 | google) 3262 | server_url="https://dv.acme-v02.api.pki.goog/directory" 3263 | ;; 3264 | *) 3265 | server_url="https://acme-v02.api.letsencrypt.org/directory" 3266 | ;; 3267 | esac 3268 | 3269 | certbot_args=( 3270 | certonly 3271 | --email "$selected_email" 3272 | --agree-tos 3273 | --key-type rsa 3274 | --server "$server_url" 3275 | --non-interactive 3276 | "${domain_args[@]}" 3277 | ) 3278 | 3279 | echo "選擇驗證方式:" 3280 | echo "1) DNS (Cloudflare) " 3281 | echo "2) DNS (其他供應商) " 3282 | echo "3) DNS (CNAME橋接)" 3283 | echo "4) HTTP" 3284 | read -p "選擇 [1-4](預設 3):" auth_method 3285 | auth_method="${auth_method:-3}" 3286 | 3287 | case "$auth_method" in 3288 | 1) # DNS (Cloudflare) 3289 | cred_file="/ssl_ca/cloudflare/cloudflare.ini" 3290 | if [ ! -f "$cred_file" ]; then 3291 | mkdir -p /ssl_ca/cloudflare 3292 | read -s -p "請輸入您的 Cloudflare API Token (非Global API Key):" cf_token 3293 | echo "dns_cloudflare_api_token = $cf_token" > "$cred_file" 3294 | chmod 600 "$cred_file" 3295 | fi 3296 | certbot_args+=( 3297 | --dns-cloudflare 3298 | --dns-cloudflare-credentials "$cred_file" 3299 | --dns-cloudflare-propagation-seconds 60 3300 | ) 3301 | needs_auto_renew=1 3302 | ;; 3303 | 2) # DNS (其他供應商) 3304 | read -p "此DNS驗證方式不支援自動續訂,是否繼續? (y/n)" continue_choice 3305 | [[ ! "$continue_choice" =~ ^[Yy]$ ]] && { echo "已取消操作。"; return 1; } 3306 | local temp_args=() 3307 | for arg in "${certbot_args[@]}"; do 3308 | if [[ "$arg" != "--non-interactive" ]]; then 3309 | temp_args+=("$arg") 3310 | fi 3311 | done 3312 | # 用過濾後的陣列覆蓋原始陣列 3313 | certbot_args=("${temp_args[@]}") 3314 | certbot_args+=(--manual --preferred-challenges "dns-01") 3315 | ;; 3316 | 3) # DNS (CNAME橋接) 3317 | local base_domain="" 3318 | if [ "${#domain_array[@]}" -eq 1 ]; then 3319 | base_domain="${domain_array[0]/\*./}" 3320 | elif [ "${#domain_array[@]}" -eq 2 ] && [[ "${domain_array[0]}" == "*."*"${domain_array[1]}" || "${domain_array[1]}" == "*."*"${domain_array[0]}" ]]; then 3321 | base_domain=$([ ${#domain_array[0]} -lt ${#domain_array[1]} ] && echo "${domain_array[0]}" || echo "${domain_array[1]}") 3322 | else 3323 | echo -e "${RED}域名數量或組合無效。此模式僅支援單一域名或一對匹配的根域名與萬用字元域名。${RESET}" >&2; return 1 3324 | fi 3325 | 3326 | local RANDOM_PART=$(head /dev/urandom | tr -dc a-z0-9 | head -c 10) 3327 | local CNAME_TARGET="$RANDOM_PART.ssl.gebu8f.de" 3328 | 3329 | echo "請新增 CNAME 記錄: _acme-challenge.$base_domain CNAME $CNAME_TARGET" 3330 | read -p "新增完成後請按任意鍵繼續..." -n1 3331 | 3332 | local success=0 3333 | for i in {1..12}; do 3334 | echo -n "第 $i 次檢查中... " 3335 | cname=$(dig +short CNAME "_acme-challenge.$base_domain" @1.1.1.1 | sed 's/\.$//') 3336 | if [ "$cname" = "$CNAME_TARGET" ]; then 3337 | echo -e "${GREEN}驗證成功!${RESET}"; success=1; break 3338 | fi 3339 | sleep 10 3340 | done 3341 | [ $success -ne 1 ] && { echo -e "${RED}驗證失敗。${RESET}" >&2; return 1; } 3342 | 3343 | mkdir -p "/opt/certbot-hook" 3344 | [ ! -f /opt/certbot-hook/cf-hook.sh ] && wget -q -O /opt/certbot-hook/cf-hook.sh https://files.gebu8f.com/files/cf-hook.sh && chmod +x /opt/certbot-hook/cf-hook.sh 3345 | 3346 | certbot_args+=( 3347 | --manual 3348 | --preferred-challenges "dns-01" 3349 | --reuse-key 3350 | --manual-auth-hook "/opt/certbot-hook/cf-hook.sh add_TXT $CNAME_TARGET" 3351 | --manual-cleanup-hook "/opt/certbot-hook/cf-hook.sh del_TXT" 3352 | ) 3353 | needs_auto_renew=1 3354 | ;; 3355 | 4) # HTTP 3356 | [[ "$domains" =~ \*\. ]] && echo "HTTP驗證不支援萬用字元域名。" >&2 && sleep 1; return 1 3357 | if [ "$selected_ca" = "google" ]; then 3358 | echo "Google CA 不支援 HTTP 驗證。" >&2; return 1 3359 | fi 3360 | [ ! -f /opt/certbot-hook/open_port.sh ] && wget -q -O /opt/certbot-hook/open_port.sh https://gitlab.com/gebu8f/sh/-/raw/main/nginx/open_port.sh && chmod +x /opt/certbot-hook/open_port.sh 3361 | /opt/certbot-hook/open_port.sh add 80 3362 | local detect_conf_path=$(detect_conf_path) 3363 | mkdir -p /var/www/acme 3364 | wget -O "$detect_conf_path/acme.conf" https://gitlab.com/gebu8f/sh/-/raw/main/nginx/domain_http.conf 3365 | sed -i "s|domain|$domains|g" "$detect_conf_path/acme.conf" 3366 | restart_webserver 3367 | certbot_args+=(--webroot --webroot-path /var/www/acme) 3368 | # 執行後的清理工作 3369 | trap 'rm -f "$detect_conf_path/acme.conf"; /opt/certbot-hook/open_port.sh del 80; restart_webserver' RETURN 3370 | needs_auto_renew=2 3371 | ;; 3372 | *) 3373 | echo "無效的選擇。" >&2 3374 | return 1 3375 | ;; 3376 | esac 3377 | if ! certbot "${certbot_args[@]}"; then 3378 | echo -e "${RED}SSL 憑證申請失敗。${RESET}" >&2 3379 | return 1 3380 | fi 3381 | echo -e "${GREEN}SSL 憑證申請成功!${RESET}" 3382 | if [ "$needs_auto_renew" -gt 0 ]; then 3383 | local cron_command="certbot renew --quiet" 3384 | if [ "$needs_auto_renew" -eq 2 ]; then # 處理 HTTP 驗證的 hook 3385 | mkdir -p /opt/certbot-hook 3386 | [ ! -f /opt/certbot-hook/certbot_post.sh] && wget -q -O /opt/certbot-hook/certbot_post.sh https://gitlab.com/gebu8f/sh/-/raw/main/nginx/certbot_post.sh && chmod +x /opt/certbot-hook/certbot_post.sh 3387 | cron_command+=" --pre-hook \"/opt/certbot-hook/certbot_post.sh add\" --post-hook \"/opt/certbot-hook/certbot_post.sh del\"" 3388 | fi 3389 | if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then 3390 | (crontab -l 2>/dev/null; echo "0 3 * * * $cron_command") | crontab - 3391 | echo "已加入自動續訂任務。" 3392 | fi 3393 | # 啟用 crond 服務 3394 | case $system in 3395 | 1) systemctl enable --now cron ;; 3396 | 2) systemctl enable --now crond ;; 3397 | 3) rc-update add crond default && rc-service crond start ;; 3398 | esac 3399 | fi 3400 | ) 3401 | 3402 | toggle_httpguard_module() { 3403 | local module_name=$1 3404 | local current_state=$2 3405 | local config_file 3406 | 3407 | case $system in 3408 | 1|2) 3409 | config_file="/usr/local/openresty/nginx/conf/HttpGuard/config.lua" 3410 | ;; 3411 | 3) 3412 | config_file="/etc/nginx/HttpGuard/config.lua" 3413 | ;; 3414 | esac 3415 | 3416 | if [ ! -f "$config_file" ]; then 3417 | echo "錯誤:HttpGuard/config.lua 未找到。請確認安裝目錄或文件路徑。" 3418 | return 1 3419 | fi 3420 | 3421 | local new_state="" 3422 | if [ "$current_state" = "On" ]; then 3423 | new_state="Off" 3424 | elif [ "$current_state" = "Off" ]; then 3425 | new_state="On" 3426 | else 3427 | echo "錯誤:無法識別的當前狀態 '$current_state'。" 3428 | return 1 3429 | fi 3430 | 3431 | echo "正在將模組 [$module_name] 的狀態從 [$current_state] 切換為 [$new_state]..." 3432 | 3433 | # 使用 sed 替換 config.lua 中的狀態 3434 | # 這裡使用一個更精確的 regex,確保只替換指定模組的 state 值 3435 | sed -i "/^\s*${module_name}\s*=/ s/state\s*=\s*\"[^\"]*\"/state = \"$new_state\"/" "$config_file" 3436 | 3437 | if [ $? -eq 0 ]; then 3438 | echo -e "${GREEN}模組 [$module_name] 狀態已更新為 [$new_state]。${RESET}" 3439 | echo "正在重啟 Nginx/OpenResty 以應用變更..." 3440 | restart_webserver 3441 | if [ $? -eq 0 ]; then 3442 | echo -e "${GREEN}Nginx/OpenResty 已重啟成功。${RESET}" 3443 | else 3444 | echo -e "${RED}Nginx/OpenResty 重啟失敗,請手動檢查配置。${RESET}" 3445 | fi 3446 | else 3447 | echo -e "${RED}更新模組 [$module_name] 狀態失敗。${RESET}" 3448 | fi 3449 | } 3450 | 3451 | update_certbot(){ 3452 | case $system in 3453 | 1) 3454 | snap refresh certbot > /dev/null 2>&1 3455 | ;; 3456 | 2) 3457 | python3 -m pip install --upgrade certbot certbot-nginx certbot-dns-cloudflare --break-system-packages > /dev/null 2>&1 3458 | ;; 3459 | 3) 3460 | python3 -m pip install --upgrade certbot certbot-nginx certbot-dns-cloudflare --break-system-packages > /dev/null 2>&1 3461 | ;; 3462 | esac 3463 | } 3464 | update_script() { 3465 | local download_url="https://gitlab.com/gebu8f/sh/-/raw/main/nginx/ng.sh" 3466 | local temp_path="/tmp/ng.sh" 3467 | local current_script="/usr/local/bin/site" 3468 | local current_path="$0" 3469 | 3470 | echo "正在檢查更新..." 3471 | wget -q "$download_url" -O "$temp_path" 3472 | if [ $? -ne 0 ]; then 3473 | echo -e "${RED}無法下載最新版本,請檢查網路連線。${RESET}" 3474 | return 3475 | fi 3476 | 3477 | # 比較檔案差異 3478 | if [ -f "$current_script" ]; then 3479 | if diff "$current_script" "$temp_path" >/dev/null; then 3480 | echo -e "${GREEN}腳本已是最新版本,無需更新。${RESET}" 3481 | rm -f "$temp_path" 3482 | return 3483 | fi 3484 | echo "正在更新..." 3485 | cp "$temp_path" "$current_script" && chmod +x "$current_script" 3486 | if [ $? -eq 0 ]; then 3487 | echo -e "${GREEN}更新成功!將自動重新啟動腳本以套用變更...${RESET}" 3488 | sleep 1 3489 | exec "$current_script" 3490 | else 3491 | echo -e "${RED}更新失敗,請確認權限。${RESET}" 3492 | fi 3493 | else 3494 | # 非 /usr/local/bin 執行時 fallback 為當前檔案路徑 3495 | if diff "$current_path" "$temp_path" >/dev/null; then 3496 | echo -e "${GREEN}腳本已是最新版本,無需更新。${RESET}" 3497 | rm -f "$temp_path" 3498 | return 3499 | fi 3500 | echo "檢測到新版本,正在更新..." 3501 | cp "$temp_path" "$current_path" && chmod +x "$current_path" 3502 | if [ $? -eq 0 ]; then 3503 | echo -e "${GREEN}更新成功!將自動重新啟動腳本以套用變更...${RESET}" 3504 | sleep 1 3505 | exec "$current_path" 3506 | else 3507 | echo -e "${RED}更新失敗,請確認權限。${RESET}" 3508 | fi 3509 | fi 3510 | 3511 | rm -f "$temp_path" 3512 | } 3513 | 3514 | uninstall_webserver(){ 3515 | check_web_server 3516 | if [ $openresty -eq 1 ]; then 3517 | case $system in 3518 | 1|2) 3519 | systemctl disable openresty 3520 | ;; 3521 | 3) 3522 | rc-update del openresty default 3523 | ;; 3524 | esac 3525 | service openresty stop 3526 | case $system in 3527 | 1) apt purge -y openresty ;; 3528 | 2) yum remove -y openresty ;; 3529 | 3) apk del openresty ;; 3530 | esac 3531 | pkill -f openresty 3532 | pkill -f nginx 3533 | unlink /etc/nginx 3534 | unlink /usr/sbin/nginx 3535 | rm -rf /etc/nginx 3536 | elif [ $nginx -eq 1 ]; then 3537 | case $system in 3538 | 1|2) 3539 | systemctl disable nginx 3540 | ;; 3541 | 3) 3542 | rc-update del nginx default 3543 | ;; 3544 | esac 3545 | service nginx stop 3546 | case $system in 3547 | 1) apt purge -y nginx* ;; 3548 | 2) yum remove -y nginx ;; 3549 | 3) 3550 | apk del nginx 3551 | rm -rf /etc/init.d/nginx 3552 | ;; 3553 | esac 3554 | rm -rf /etc/nginx 3555 | local nginx_path=$(command -v nginx) 3556 | if [ -n $nginx_path ]; then 3557 | rm -rf $nginx_path 3558 | fi 3559 | pkill -f nginx 3560 | elif [ $caddy -eq 1 ]; then 3561 | case $system in 3562 | 1) apt purge -y caddy ;; 3563 | 2) yum remove -y caddy ;; 3564 | esac 3565 | rm -rf /etc/caddy 3566 | fi 3567 | exit 0 3568 | } 3569 | 3570 | 3571 | 3572 | wordpress_site() { 3573 | local MY_IP=$(curl -s https://api64.ipify.org) 3574 | local HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time 3 https://wordpress.org) 3575 | local ngx_user=$(get_web_run_user) 3576 | 3577 | if [[ "$HTTP_CODE" == "200" ]]; then 3578 | echo "您的IP地址支持訪問 WordPress。" 3579 | else 3580 | echo "您的IP地址不支持訪問 WordPress。" 3581 | # 如果IP看起來像IPv6格式(簡單判斷包含冒號) 3582 | if [[ "$MY_IP" == *:* ]]; then 3583 | echo "您目前是 IPv6,請使用 WARP 等方式將流量轉為 IPv4 以正常訪問 WordPress。" 3584 | fi 3585 | return 1 3586 | fi 3587 | if ! command -v dba >/dev/null 2>&1; then 3588 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 3589 | fi 3590 | if ! command -v mysql >/dev/null 2>&1 && ! command -v mariadb >/dev/null 2>&1; then 3591 | dba mysql install true 3592 | fi 3593 | read -p "請輸入您的 WordPress 網址(例如 wp.example.com):" domain 3594 | 3595 | # 自動申請 SSL(若不存在) 3596 | check_cert "$domain" || { 3597 | echo "未偵測到 Let Encrypt 憑證,嘗試自動申請..." 3598 | if ssl_apply "$domain"; then 3599 | echo "申請成功,重新驗證憑證..." 3600 | check_cert "$domain" || { 3601 | echo "申請成功但仍無法驗證憑證,中止建立站點" 3602 | return 1 3603 | } 3604 | else 3605 | echo "SSL 申請失敗,中止建立站點" 3606 | return 1 3607 | fi 3608 | } 3609 | 3610 | 3611 | read -p "是否還原現有的wp文件?(Y/N): " restore_file 3612 | restore_file=${restore_file,,} 3613 | if [[ $restore_file == "y" || $restore_file == "" ]]; then 3614 | restore_site_files wp "$domain" 3615 | return 0 3616 | fi 3617 | # 下載 WordPress 並部署 3618 | mkdir -p "/var/www/$domain" 3619 | curl -L https://wordpress.org/latest.zip -o /tmp/wordpress.zip 3620 | unzip /tmp/wordpress.zip -d /tmp 3621 | mv /tmp/wordpress/* "/var/www/$domain/" 3622 | 3623 | local db_name="wp_${domain//./_}" 3624 | local db_user="${db_name}_user" 3625 | local db_pass=$(dba mysql add $db_name $db_user false) 3626 | 3627 | # 設定 wp-config.php 3628 | cp "/var/www/$domain/wp-config-sample.php" "/var/www/$domain/wp-config.php" 3629 | sed -i "s/database_name_here/$db_name/" "/var/www/$domain/wp-config.php" 3630 | sed -i "s/username_here/$db_user/" "/var/www/$domain/wp-config.php" 3631 | sed -i "s/password_here/$db_pass/" "/var/www/$domain/wp-config.php" 3632 | sed -i "s/localhost/localhost/" "/var/www/$domain/wp-config.php" 3633 | # 設定權限 3634 | chown -R $ngx_user:$ngx_user "/var/www/$domain" 3635 | setup_site "$domain" php 3636 | read -p "是否要導入現有 SQL 資料?(Y/N): " import_sql 3637 | import_sql=${import_sql,,} 3638 | if [[ $import_sql == "y" || $import_sql == "" ]]; then 3639 | restore_site_db wp $domain 3640 | return 0 3641 | fi 3642 | echo "WordPress 網站 $domain 建立完成!請瀏覽 https://$domain 開始安裝流程。" 3643 | } 3644 | 3645 | 3646 | self_signed_certificate()( 3647 | read -p "請輸入您的域名或 IP 地址 (Common Name):" domain 3648 | 3649 | 3650 | mkdir -p /etc/letsencrypt/live/$domain 3651 | 3652 | # 設置憑證和金鑰的完整路徑 3653 | CERT_DIR="/etc/letsencrypt/live/$domain" 3654 | KEY_FILE="$CERT_DIR/privkey.pem" 3655 | CERT_FILE="$CERT_DIR/fullchain.pem" 3656 | 3657 | # 3. 檢查目錄是否已存在憑證,避免覆蓋 3658 | if [ -f "$KEY_FILE" ] && [ -f "$CERT_FILE" ]; then 3659 | read -p "警告:憑證已存在於 $domain,是否要覆蓋?(y/N)[預設:n] " response 3660 | response=${response,,} 3661 | if ! [[ "$response" =~ ^(y|yes)$ ]]; then 3662 | echo "操作已取消。" 3663 | return 0 3664 | fi 3665 | fi 3666 | 3667 | 3668 | openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ 3669 | -keyout "$KEY_FILE" \ 3670 | -out "$CERT_FILE" \ 3671 | -subj "/C=GB/ST=Globe/L=Globe/O=gebu8f7/OU=IT/CN=$domain" 3672 | 3673 | # --- 5. 完成與提示 --- 3674 | 3675 | if [ $? -eq 0 ]; then 3676 | echo -e "${GREEN}自簽名憑證生成成功!${RESET}" 3677 | echo " 域名/IP (CN): $domain" 3678 | echo " 私鑰路徑: $KEY_FILE" 3679 | echo " 憑證路徑: $CERT_FILE" 3680 | 3681 | chmod 700 "$KEY_FILE" 3682 | fi 3683 | ) 3684 | 3685 | 3686 | # 菜單 3687 | 3688 | menu_httpguard(){ 3689 | clear 3690 | echo "HttpGuard管理" 3691 | echo "-------------------" 3692 | show_httpguard_status 3693 | echo "-------------------" 3694 | echo "1. 開啟/關閉 302 重定向 (redirectModules)" 3695 | echo "2. 開啟/關閉 JS 跳轉 (JsJumpModules)" 3696 | echo "3. 開啟/關閉 Cookie 認證 (cookieModules)" 3697 | echo "4. 開啟/關閉 自動開啟主動防禦 (autoEnable)" 3698 | echo "5. 卸載 HttpGuard" 3699 | echo "0. 退出" 3700 | echo -n -e "\033[1;33m請選擇操作 [0-5]: \033[0m" 3701 | read -r choice 3702 | case $choice in 3703 | 1) 3704 | local current_state=$(get_module_state "redirectModules") 3705 | toggle_httpguard_module "redirectModules" "$current_state" 3706 | read -p "操作完成,請按任意鍵繼續..." -n1 3707 | ;; 3708 | 2) 3709 | local current_state=$(get_module_state "JsJumpModules") 3710 | toggle_httpguard_module "JsJumpModules" "$current_state" 3711 | read -p "操作完成,請按任意鍵繼續..." -n1 3712 | ;; 3713 | 3) 3714 | local current_state=$(get_module_state "cookieModules") 3715 | toggle_httpguard_module "cookieModules" "$current_state" 3716 | read -p "操作完成,請按任意鍵繼續..." -n1 3717 | ;; 3718 | 4) 3719 | local current_state=$(get_module_state "autoEnable") 3720 | toggle_httpguard_module "autoEnable" "$current_state" 3721 | read -p "操作完成,請按任意鍵繼續..." -n1 3722 | ;; 3723 | 5) 3724 | sed -i '/HttpGuard\/init.lua\|HttpGuard\/runtime.lua\|lua_package_path\|lua_package_cpath\|lua_shared_dict guard_dict\|lua_shared_dict dict_captcha\|lua_max_running_timers/d' "$ngx_conf" 3725 | rm -rf "/etc/nginx/HttpGuard" 3726 | restart_webserver 3727 | echo "HttpGuard 卸載完成。" 3728 | read -p "操作完成,請按任意鍵繼續..." -n1 3729 | ;; 3730 | 0) 3731 | return 0 3732 | ;; 3733 | *) 3734 | echo "無效的選擇,請重新輸入。" 3735 | read -p "操作完成,請按任意鍵繼續..." -n1 3736 | ;; 3737 | esac 3738 | } 3739 | 3740 | menu_add_sites(){ 3741 | clear 3742 | echo "新增站點" 3743 | echo "-------------------" 3744 | echo "1. 添加站點(HTML)" 3745 | echo "" 3746 | echo "2. 反向代理" 3747 | echo "-------------------" 3748 | echo "0. 退出" 3749 | echo -n -e "\033[1;33m請選擇操作 [0-2]: \033[0m" 3750 | read -r choice 3751 | case $choice in 3752 | 1) 3753 | html_sites 3754 | ;; 3755 | 2) 3756 | reverse_proxy 3757 | ;; 3758 | 0) 3759 | return 0 3760 | ;; 3761 | *) 3762 | echo "無效選擇。" 3763 | esac 3764 | } 3765 | 3766 | menu_del_sites() { 3767 | local conf_dir=$(detect_conf_path) 3768 | 3769 | # 取得所有 .conf 3770 | local raw_files=("$conf_dir"/*.conf) 3771 | local site_files=() 3772 | 3773 | # 過濾掉 basename 不含 "." 的 3774 | for f in "${raw_files[@]}"; do 3775 | local name=$(basename "$f" .conf) 3776 | if [[ "$name" == *.* ]]; then 3777 | site_files+=("$f") 3778 | fi 3779 | done 3780 | 3781 | if [ ${#site_files[@]} -eq 0 ]; then 3782 | echo "目前沒有任何可刪除的站點。" 3783 | return 1 3784 | fi 3785 | 3786 | echo "請選擇要刪除的站點:" 3787 | local idx=1 3788 | for f in "${site_files[@]}"; do 3789 | local name=$(basename "$f" .conf) 3790 | echo " $idx) $name" 3791 | idx=$((idx + 1)) 3792 | done 3793 | 3794 | echo 3795 | read -p "請輸入數字:" choice 3796 | 3797 | # 非數字 3798 | if ! [[ "$choice" =~ ^[0-9]+$ ]]; then 3799 | echo "無效的選擇。" 3800 | return 1 3801 | fi 3802 | 3803 | local max=${#site_files[@]} 3804 | if (( choice < 1 || choice > max )); then 3805 | echo "選擇超出範圍。" 3806 | return 1 3807 | fi 3808 | 3809 | local conf_file="${site_files[$((choice - 1))]}" 3810 | local domain=$(basename "$conf_file" .conf) 3811 | 3812 | read -p "確定要刪除站點 [$domain] 嗎?(Y/n) " confirm 3813 | confirm=${confirm,,} 3814 | 3815 | if [[ "$confirm" != "y" && "$confirm" != "" ]]; then 3816 | return 0 3817 | fi 3818 | 3819 | local site_type=$(detect_site_type "/var/www/$domain") 3820 | 3821 | # SSL 吊銷 3822 | menu_ssl_revoke "$domain" || { 3823 | echo "吊銷 SSL 失敗,停止操作。" 3824 | return 1 3825 | } 3826 | 3827 | # 刪除配置與網站資料夾 3828 | rm -rf "$conf_file" 3829 | rm -rf "/var/www/$domain" 3830 | 3831 | # 刪除資料庫 3832 | if [ $site_type = wp ]; then 3833 | db_name="wp_${domain//./_}" 3834 | elif [ $site_type = flarum ]; then 3835 | db_name="flarum_${domain//./_}" 3836 | fi 3837 | 3838 | if [ $site_type != "unknown" ]; then 3839 | if ! command -v dba >/dev/null 2>&1; then 3840 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 3841 | fi 3842 | dba mysql del "$db_name" --force 3843 | fi 3844 | 3845 | restart_webserver 3846 | echo -e "${GREEN}已刪除站點:$domain${RESET}" 3847 | } 3848 | 3849 | menu_ssl_apply() { 3850 | [ $caddy -eq 1 ] && return 0 3851 | echo "SSL 申請" 3852 | echo "-------------------" 3853 | echo "1. Certbot(Let's Encrypt、ZeroSSL、Google) 憑證" 3854 | echo "" 3855 | echo "2. Cloudflare 原始憑證" 3856 | echo "" 3857 | echo "3. 自簽名" 3858 | echo "-------------------" 3859 | echo "0. 返回" 3860 | read -p "請選擇: " ssl_choice 3861 | case "$ssl_choice" in 3862 | 1) 3863 | ssl_apply 3864 | ;; 3865 | 2) 3866 | cf_cert_autogen 3867 | ;; 3868 | 3) 3869 | self_signed_certificate 3870 | ;; 3871 | 0) return ;; 3872 | esac 3873 | } 3874 | 3875 | menu_ssl_revoke() { 3876 | local cert_dir="/etc/letsencrypt/live" 3877 | local domain="$1" 3878 | 3879 | if [ -z "$domain" ]; then 3880 | local conf_dir 3881 | conf_dir=$(detect_conf_path) 3882 | 3883 | local raw_files=("$conf_dir"/*.conf) 3884 | local domain_list=() 3885 | local item 3886 | 3887 | # 從 conf 抓所有 server_name 3888 | for f in "${raw_files[@]}"; do 3889 | while read -r item; do 3890 | domain_list+=("$item") 3891 | done < <(grep -hoP 'server_name\s+\K[^;]+' "$f" 2>/dev/null | tr -s ' ' '\n') 3892 | done 3893 | 3894 | # 去重 3895 | readarray -t domain_list < <(printf "%s\n" "${domain_list[@]}" | sort -u) 3896 | 3897 | if [[ ${#domain_list[@]} -eq 0 ]]; then 3898 | echo -e "${RED}找不到任何 server_name,無法提供選項。${RESET}" 3899 | return 1 3900 | fi 3901 | 3902 | echo "請選擇要吊銷的域名:" 3903 | local idx=1 3904 | for d in "${domain_list[@]}"; do 3905 | echo " $idx) $d" 3906 | ((idx++)) 3907 | done 3908 | 3909 | echo 3910 | read -p "輸入數字:" choice 3911 | 3912 | if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#domain_list[@]} )); then 3913 | echo -e "${RED}無效選擇${RESET}" 3914 | return 1 3915 | fi 3916 | 3917 | domain="${domain_list[$((choice - 1))]}" 3918 | fi 3919 | local cert_info 3920 | if ! cert_info=$(check_cert "$domain"); then 3921 | echo -e "${RED}憑證檢查失敗: $cert_info${RESET}" 3922 | return 1 3923 | fi 3924 | 3925 | local cert_path="/etc/letsencrypt/live/$cert_info/cert.pem" 3926 | if [ ! -f "$cert_path" ]; then 3927 | echo -e "${RED}找不到憑證檔案: $cert_path${RESET}" 3928 | return 1 3929 | fi 3930 | 3931 | openssl x509 -in "$cert_path" -noout -text | grep -A1 "Subject Alternative Name" 3932 | echo 3933 | echo "確定要吊銷憑證 [$domain] 嗎?(y/n)" 3934 | read -p "選擇:" confirm 3935 | [[ "$confirm" != "y" ]] && echo "已取消。" && return 0 3936 | 3937 | if openssl x509 -in "$cert_path" -noout -subject | grep -i -q "CloudFlare Origin Certificate"; then 3938 | cf_cert_revoke "$cert_info" || return 1 3939 | return 0 3940 | fi 3941 | 3942 | check_certbot 3943 | update_certbot 3944 | 3945 | certbot revoke --cert-path "$cert_path" --non-interactive --quiet && echo "已吊銷憑證" 3946 | 3947 | echo 3948 | echo "是否刪除憑證檔案 [$cert_info]?(y/n)" 3949 | read -p "選擇:" delete_choice 3950 | 3951 | if [[ "$delete_choice" == "y" ]]; then 3952 | rm -rf "$cert_dir/$cert_info" 3953 | rm -rf "/etc/letsencrypt/archive/$cert_info" 3954 | rm -f "/etc/letsencrypt/renewal/$cert_info.conf" 3955 | if [ -z "$(find "$cert_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)" ]; then 3956 | if crontab -l 2>/dev/null | grep -q "certbot renew"; then 3957 | crontab -l 2>/dev/null | grep -v "certbot renew" | crontab - 3958 | fi 3959 | fi 3960 | fi 3961 | } 3962 | menu_wp(){ 3963 | while true; do 3964 | clear 3965 | echo "WordPress站點" 3966 | echo "-------------------" 3967 | detect_sites WordPress 3968 | echo "-------------------" 3969 | echo "WordPress管理" 3970 | echo -e "${YELLOW}1. 部署WordPress站點${RESET}" 3971 | echo "" 3972 | echo "2. 安裝插件 3. 移除插件" 3973 | echo "" 3974 | echo "4. 部署主題 5. 移除主題" 3975 | echo "" 3976 | echo "6. 修改管理員帳號 7. 修改管理員密碼" 3977 | echo "" 3978 | echo -e "${YELLOW}8. 修復網站崩潰(禁用所有插件和恢復預設主題,慎用)${RESET}" 3979 | echo "" 3980 | echo "0. 返回" 3981 | echo -n -e "\033[1;33m請選擇操作 [0-10]: \033[0m" 3982 | read -r choice 3983 | case $choice in 3984 | 0) 3985 | break 3986 | ;; 3987 | 1) 3988 | wordpress_site 3989 | ;; 3990 | 2) 3991 | install_wpcli_if_needed 3992 | local domain=$(detect_sites_menu WordPress) 3993 | install_wp_plugin_with_search_or_url $domain 3994 | read -p "操作完成,請按任意鍵繼續..." -n1 3995 | ;; 3996 | 3) 3997 | install_wpcli_if_needed 3998 | local domain=$(detect_sites_menu WordPress) 3999 | remove_wp_plugin_with_menu $domain 4000 | read -p "操作完成,請按任意鍵繼續..." -n1 4001 | ;; 4002 | 4) 4003 | install_wpcli_if_needed 4004 | local domain=$(detect_sites_menu WordPress) 4005 | deploy_or_remove_theme install $domain 4006 | read -p "操作完成,請按任意鍵繼續..." -n1 4007 | ;; 4008 | 5) 4009 | install_wpcli_if_needed 4010 | local domain=$(detect_sites_menu WordPress) 4011 | deploy_or_remove_theme remove $domain 4012 | read -p "操作完成,請按任意鍵繼續..." -n1 4013 | ;; 4014 | 6) 4015 | install_wpcli_if_needed 4016 | local domain=$(detect_sites_menu WordPress) 4017 | change_wp_admin_username $domain 4018 | read -p "操作完成,請按任意鍵繼續..." -n1 4019 | ;; 4020 | 7) 4021 | install_wpcli_if_needed 4022 | local domain=$(detect_sites_menu WordPress) 4023 | change_wp_admin_password $domain 4024 | read -p "操作完成,請按任意鍵繼續..." -n1 4025 | ;; 4026 | 8) 4027 | install_wpcli_if_needed 4028 | local domain=$(detect_sites_menu WordPress) 4029 | reset_wp_site $domain 4030 | read -p "操作完成,請按任意鍵繼續..." -n1 4031 | ;; 4032 | esac 4033 | done 4034 | } 4035 | 4036 | menu_restore_site() { 4037 | local domain site_type choice 4038 | 4039 | echo "--- 網站還原工具 ---" 4040 | 4041 | # === 步驟 1: 從 Nginx 設定檔選擇域名 (邏輯內聯) === 4042 | local conf_dir 4043 | conf_dir=$(detect_conf_path) 4044 | if [ $? -ne 0 ]; then # 假設 detect_conf_path 失敗會回傳非 0 4045 | echo -e "${RED}無法偵測到 Nginx 設定檔目錄。${RESET}" >&2 4046 | sleep 1 4047 | return 1 4048 | fi 4049 | 4050 | # 使用 mapfile 結合 find 來安全地處理檔名和過濾 4051 | mapfile -t site_files < <(find "$conf_dir" -maxdepth 1 -type f -name "*.conf" -exec bash -c 'name=$(basename "$0" .conf); [[ "$name" == *.* ]]' {} \; -print) 4052 | 4053 | if [ ${#site_files[@]} -eq 0 ]; then 4054 | echo -e "${YELLOW}目前 Nginx 設定中沒有任何可還原的站點。${RESET}" >&2 4055 | sleep 1 4056 | return 1 4057 | fi 4058 | 4059 | echo "請選擇要還原的站點:" 4060 | local idx=1 4061 | for f in "${site_files[@]}"; do 4062 | local name 4063 | name=$(basename "$f" .conf) 4064 | printf "%3d) %s\n" "$idx" "$name" 4065 | idx=$((idx + 1)) 4066 | done 4067 | 4068 | read -p "請輸入數字:" choice 4069 | 4070 | if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#site_files[@]} )); then 4071 | echo -e "${RED}無效的選擇。${RESET}" >&2 4072 | sleep 1 4073 | return 1 4074 | fi 4075 | domain=$(basename "${site_files[$((choice - 1))]}" .conf) 4076 | echo -e "${CYAN}您選擇了站點: $domain${RESET}" 4077 | echo 4078 | 4079 | # === 步驟 2: 選擇網站類型 (邏輯內聯) === 4080 | echo "請選擇 '$domain' 的網站類型:" 4081 | echo " 1) WordPress" 4082 | echo " 2) Flarum" 4083 | read -p "請輸入數字:" choice 4084 | 4085 | case "$choice" in 4086 | 1) site_type="wp" ;; 4087 | 2) site_type="flarum" ;; 4088 | *) 4089 | echo -e "${RED}無效的選擇。${RESET}" >&2 4090 | sleep 1 4091 | return 1 4092 | ;; 4093 | esac 4094 | echo -e "${CYAN}您選擇了類型: $site_type${RESET}" 4095 | echo 4096 | 4097 | # === 步驟 3: 選擇要執行的操作 === 4098 | echo "請選擇要對 '$domain' ($site_type) 執行的操作:" 4099 | echo " 1) 還原文件 (包含資料庫)" 4100 | echo " 2) 僅還原資料庫" 4101 | echo " 0) 返回" 4102 | read -p "請輸入數字 [0-2]: " choice 4103 | 4104 | case "$choice" in 4105 | 1) 4106 | restore_site_files "$site_type" "$domain" 4107 | ;; 4108 | 2) 4109 | restore_site_db "$site_type" "$domain" 4110 | ;; 4111 | 0) 4112 | # 返回 4113 | return 0 4114 | ;; 4115 | *) 4116 | echo -e "${RED}無效的選擇。${RESET}" >&2 4117 | sleep 1 4118 | return 1 4119 | ;; 4120 | esac 4121 | } 4122 | 4123 | menu_php() { 4124 | while true; do 4125 | clear 4126 | show_php 4127 | echo "-------------------" 4128 | echo "PHP管理" 4129 | echo "" 4130 | echo "1. 安裝php 2. 升級/降級php" 4131 | echo "" 4132 | echo "3. 新增普通PHP站點 4. WordPress管理" 4133 | echo "" 4134 | echo "5. 部署flarum站點" 4135 | echo "" 4136 | echo "6. 設定php上傳大小值 7. 安裝php擴展" 4137 | echo "" 4138 | echo "8. 安裝Flarum擴展 9. 管理HttpGuard" 4139 | echo 4140 | echo "10. 備份網站 11. 還原網站 " 4141 | echo "" 4142 | echo "r. PHP一鍵配置(設定www配置文件至我腳本可用之狀態)" 4143 | echo "-------------------" 4144 | echo "0. 返回" 4145 | echo -n -e "\033[1;33m請選擇操作 [0-11]: \033[0m" 4146 | read -r choice 4147 | case $choice in 4148 | 1) 4149 | clear 4150 | php_install 4151 | sleep 5 4152 | php_fix 4153 | read -p "操作完成,請按任意鍵繼續..." -n1 4154 | ;; 4155 | 2) 4156 | clear 4157 | check_php 4158 | php_switch_version 4159 | read -p "操作完成,請按任意鍵繼續..." -n1 4160 | ;; 4161 | 3) 4162 | clear 4163 | check_php 4164 | local ngx_user=$(get_web_run_user) 4165 | read -p "請輸入您的域名:" domain 4166 | check_cert "$domain" || { 4167 | echo "未偵測到 Let's Encrypt 憑證,嘗試自動申請..." 4168 | if ssl_apply "$domain"; then 4169 | echo "申請成功,重新驗證憑證..." 4170 | check_cert "$domain" || { 4171 | echo "申請成功但仍無法驗證憑證,中止建立站點" 4172 | return 1 4173 | } 4174 | else 4175 | echo "SSL 申請失敗,中止建立站點" 4176 | return 1 4177 | fi 4178 | } 4179 | mkdir -p /var/www/$domain 4180 | read -p "是否自訂index.php文件(Y/n)?" confirm 4181 | confirm=${confirm,,} 4182 | if [[ "$confirm" == "y" || "$confirm" == "" ]]; then 4183 | nano /var/www/$domain/index.php 4184 | else 4185 | echo "" > "/var/www/$domain/index.php" 4186 | fi 4187 | chown -R $ngx_user:$ngx_user "/var/www/$domain" 4188 | setup_site "$domain" php 4189 | read -p "操作完成,請按任意鍵繼續..." -n1 4190 | ;; 4191 | 4) 4192 | clear 4193 | check_php 4194 | menu_wp 4195 | ;; 4196 | 5) 4197 | clear 4198 | check_php 4199 | flarum_setup 4200 | read -p "按任意鍵繼續..." -n1 4201 | ;; 4202 | 6) 4203 | clear 4204 | check_php 4205 | php_tune_upload_limit 4206 | read -p "操作完成,請按任意鍵繼續..." -n1 4207 | ;; 4208 | 7) 4209 | check_php 4210 | php_install_extensions 4211 | read -p "操作完成,請按任意鍵繼續..." -n1 4212 | ;; 4213 | 8) 4214 | check_php 4215 | flarum_extensions 4216 | read -p "操作完成,請按任意鍵繼續..." -n1 4217 | ;; 4218 | 9) 4219 | httpguard_setup 4220 | ;; 4221 | 10) 4222 | backup_site 4223 | read -p "操作完成,請按任意鍵繼續..." -n1 4224 | ;; 4225 | 11) 4226 | menu_restore_site 4227 | read -p "操作完成,請按任意鍵繼續..." -n1 4228 | ;; 4229 | r) 4230 | php_fix 4231 | ;; 4232 | 0) 4233 | break 4234 | ;; 4235 | *) 4236 | echo "無效的選擇,請重新輸入。" 4237 | ;; 4238 | esac 4239 | done 4240 | } 4241 | 4242 | #主菜單 4243 | show_menu_caddy(){ 4244 | while true; do 4245 | conf_file="" 4246 | domain="" 4247 | clear 4248 | show_domain_status_caddy 4249 | echo "-------------------" 4250 | echo "站點管理器" 4251 | echo "" 4252 | echo -e "${YELLOW}r. 解除安裝 Caddy${RESET}" 4253 | echo "" 4254 | echo "1. 新增站點 2. 刪除站點" 4255 | echo "" 4256 | echo "3. PHP 管理 4. MYSQL安裝及管理" 4257 | echo "" 4258 | echo "5. Docker安裝及管理" 4259 | echo "" 4260 | echo "u. 更新腳本 0. 離開" 4261 | echo "-------------------" 4262 | echo -n -e "\033[1;33m請選擇操作 [1-5 / i u 0]: \033[0m" 4263 | read -r choice 4264 | case $choice in 4265 | 1) 4266 | check_no_ngx || continue 4267 | menu_add_sites 4268 | read -p "操作完成,請按任意鍵繼續..." -n1 4269 | ;; 4270 | 2) 4271 | check_no_ngx || continue 4272 | menu_del_sites 4273 | read -p "操作完成,請按任意鍵繼續..." -n1 4274 | ;; 4275 | 3) 4276 | check_no_ngx || continue 4277 | menu_php 4278 | ;; 4279 | 4) 4280 | if ! command -v dba >/dev/null 2>&1; then 4281 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 4282 | fi 4283 | if ! command -v mysql >/dev/null 2>&1 && ! command -v mariadb >/dev/null 2>&1; then 4284 | dba mysql install 4285 | else 4286 | dba mysql 4287 | fi 4288 | ;; 4289 | 5) 4290 | if ! command -v d >/dev/null 2>&1; then 4291 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/docker/install.sh) 4292 | else 4293 | d 4294 | fi 4295 | ;; 4296 | 0) 4297 | exit 0 4298 | ;; 4299 | u) 4300 | clear 4301 | echo "更新腳本" 4302 | echo "------------------------" 4303 | update_script 4304 | ;; 4305 | r) 4306 | uninstall_webserver 4307 | ;; 4308 | *) 4309 | echo "無效選擇。" 4310 | esac 4311 | done 4312 | } 4313 | 4314 | show_menu_nginx(){ 4315 | while true; do 4316 | conf_file="" 4317 | domain="" 4318 | clear 4319 | show_cert_status 4320 | echo "-------------------" 4321 | echo "站點管理器" 4322 | echo "" 4323 | echo -e "${YELLOW}i. 安裝 Nginx / OpenResty r. 解除安裝 Nginx / OpenResty${RESET}" 4324 | echo "" 4325 | echo "1. 新增站點 2. 刪除站點" 4326 | echo "" 4327 | echo "3. 申請 SSL 證書 4. 刪除 SSL 證書" 4328 | echo "" 4329 | echo "5. 切換 Certbot 廠商 6. PHP 管理" 4330 | echo "" 4331 | echo "7. 修復Cloudflare 525錯誤 8. MYSQL安裝及管理" 4332 | echo "" 4333 | echo "9. Docker安裝及管理" 4334 | echo "" 4335 | echo "u. 更新腳本 0. 離開" 4336 | echo "-------------------" 4337 | echo -n -e "\033[1;33m請選擇操作 [1-9 / i u 0]: \033[0m" 4338 | read -r choice 4339 | case $choice in 4340 | i) 4341 | check_web_environment 4342 | check_nginx 4343 | check_web_server 4344 | ;; 4345 | 1) 4346 | check_no_ngx || continue 4347 | menu_add_sites 4348 | read -p "操作完成,請按任意鍵繼續..." -n1 4349 | ;; 4350 | 2) 4351 | check_no_ngx || continue 4352 | menu_del_sites 4353 | read -p "操作完成,請按任意鍵繼續..." -n1 4354 | ;; 4355 | 3) 4356 | menu_ssl_apply 4357 | read -p "操作完成,請按任意鍵繼續..." -n1 4358 | ;; 4359 | 4) 4360 | menu_ssl_revoke 4361 | read -p "操作完成,請按任意鍵繼續..." -n1 4362 | ;; 4363 | 5) 4364 | check_certbot 4365 | update_certbot 4366 | select_ca 4367 | ;; 4368 | 6) 4369 | check_no_ngx || continue 4370 | menu_php 4371 | ;; 4372 | 7) 4373 | clean_nginx_ssl_config 4374 | ;; 4375 | 8) 4376 | if ! command -v dba >/dev/null 2>&1; then 4377 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/db/dba.sh) install_script 4378 | fi 4379 | if ! command -v mysql >/dev/null 2>&1 && ! command -v mariadb >/dev/null 2>&1; then 4380 | dba mysql install 4381 | else 4382 | dba mysql 4383 | fi 4384 | ;; 4385 | 9) 4386 | if ! command -v d >/dev/null 2>&1; then 4387 | bash <(curl -sL https://gitlab.com/gebu8f/sh/-/raw/main/docker/install.sh) 4388 | else 4389 | d 4390 | fi 4391 | ;; 4392 | 0) 4393 | exit 0 4394 | ;; 4395 | u) 4396 | clear 4397 | echo "更新腳本" 4398 | echo "------------------------" 4399 | update_script 4400 | ;; 4401 | r) 4402 | uninstall_webserver 4403 | ;; 4404 | *) 4405 | echo "無效選擇。" 4406 | esac 4407 | done 4408 | } 4409 | case "$1" in 4410 | --version|-V) 4411 | echo "站點管理器版本 $version" 4412 | exit 0 4413 | ;; 4414 | esac 4415 | 4416 | # 只有不是 --version 或 -V 才會執行以下初始化 4417 | check_system 4418 | check_app 4419 | check_web_environment 4420 | check_webserver_install 4421 | check_and_start_service 4422 | check_web_server 4423 | 4424 | case "$1" in 4425 | setup) 4426 | domain="$2" 4427 | site_type="$3" 4428 | 4429 | if [[ -z "$domain" || -z "$site_type" ]]; then 4430 | exit 1 4431 | fi 4432 | case "$site_type" in 4433 | html|flarum|php) 4434 | setup_site "$domain" $site_type 4435 | ;; 4436 | proxy) 4437 | target_url="$4" 4438 | target_proto="$5" 4439 | target_port="$6" 4440 | 4441 | if [[ -z "$target_url" || -z "$target_proto" || -z "$target_port" ]]; then 4442 | echo "proxy 類型需要提供 target_url protocol port" 4443 | exit 1 4444 | fi 4445 | 4446 | # 自動申請 SSL(若不存在) 4447 | check_cert "$domain" || { 4448 | echo "未偵測到 Let Encrypt 憑證,嘗試自動申請..." 4449 | if ssl_apply "$domain"; then 4450 | echo "申請成功,重新驗證憑證..." 4451 | check_cert "$domain" || { 4452 | echo "申請成功但仍無法驗證憑證,中止建立站點" 4453 | return 1 4454 | } 4455 | else 4456 | echo "SSL 申請失敗,中止建立站點" 4457 | return 1 4458 | fi 4459 | } 4460 | 4461 | setup_site "$domain" proxy "$target_url" "$target_proto" "$target_port" 4462 | exit 0 4463 | ;; 4464 | esac 4465 | ;; 4466 | del) 4467 | domain="$2" 4468 | menu_del_sites "$domain" 4469 | exit 0 4470 | ;; 4471 | api) 4472 | if [[ "$2" == "search" && "$3" == "proxy_domain" ]]; then 4473 | target="$4" 4474 | conf_file=$(detect_conf_path)/ 4475 | if [ $caddy -eq 1 ]; then 4476 | find $conf_file -type f -name "*.conf" | while read -r file; do 4477 | if grep -qE "reverse_proxy\s+(http|https)://.*$target" "$file"; then 4478 | awk -v tgt="$target" ' 4479 | /^[^ \t\n#]/ { 4480 | sub(/ \{$/, ""); 4481 | current_domain = $1; 4482 | } 4483 | $0 ~ "reverse_proxy.*" tgt { 4484 | if (current_domain != "") print current_domain; 4485 | } 4486 | ' "$file" 4487 | fi 4488 | done | sort | uniq 4489 | else 4490 | find $conf_file -type f -name "*.conf" | while read -r file; do 4491 | if grep -qE "proxy_pass\\s+(http|https)://$target" "$file"; then 4492 | grep -E "^\\s*server_name\\s+" "$file" | awk '{for(i=2;i<=NF;i++) print $i}' | sed 's/;$//' 4493 | fi 4494 | done | sort | uniq 4495 | fi 4496 | fi 4497 | exit 0 4498 | ;; 4499 | esac 4500 | if [[ $openresty -eq 1 || $nginx -eq 1 ]]; then 4501 | show_menu_nginx 4502 | elif [ $caddy -eq 1 ]; then 4503 | show_menu_caddy 4504 | fi --------------------------------------------------------------------------------