#!/bin/bash # ============================================================ # OpenDance Studio Web 服务端 —— 外部访问诊断 # # 用途:服务"本机能 curl 通、外部却访问不了"时,一次性定位原因。 # # 用法(在装有本服务的服务器上执行,无需 root): # bash deploy/check-access.sh # 自动寻找 .env # bash deploy/check-access.sh /opt/opendance # # 只读诊断,不修改任何配置、不重启任何服务。 # ============================================================ set -uo pipefail APP_DIR="${1:-/opt/opendance}" PORT="" HOST_CFG="" PROBLEMS=0 ASSIMP_MISSING=0 c_red() { printf '\033[31m%s\033[0m\n' "$*"; } c_grn() { printf '\033[32m%s\033[0m\n' "$*"; } c_yel() { printf '\033[33m%s\033[0m\n' "$*"; } ok() { c_grn " ✅ $*"; } bad() { c_red " ❌ $*"; PROBLEMS=$((PROBLEMS + 1)); } warn() { c_yel " ⚠ $*"; } info() { printf ' %s\n' "$*"; } title() { printf '\n\033[1m%s\033[0m\n' "$*"; } # ---------- 1. 定位并读取 .env ---------- title "1. 定位配置(.env)" ENV_FILE="" for cand in "$APP_DIR/deploy/.env" "./deploy/.env" "./.env"; do if [ -f "$cand" ]; then ENV_FILE="$cand"; break; fi done if [ -z "$ENV_FILE" ]; then warn "未找到 .env(已尝试 $APP_DIR/deploy/.env、./deploy/.env、./.env)" info "将按默认值 PORT=3000、HOST=0.0.0.0 继续诊断" else ok "配置文件:$ENV_FILE" # 只提取需要的两个键,避免把口令等敏感值读进环境 PORT="$(grep -E '^[[:space:]]*PORT=' "$ENV_FILE" | tail -1 | cut -d= -f2- | tr -d '"'"'"' \r')" HOST_CFG="$(grep -E '^[[:space:]]*HOST=' "$ENV_FILE" | tail -1 | cut -d= -f2- | tr -d '"'"'"' \r')" fi PORT="${PORT:-3000}" HOST_CFG="${HOST_CFG:-0.0.0.0}" info "PORT = $PORT" info "HOST = $HOST_CFG" case "$HOST_CFG" in 127.0.0.1|localhost|::1) bad "HOST=$HOST_CFG 表示只监听本机回环,外部一定访问不到" info "修复:把 $ENV_FILE 里的 HOST 改为 0.0.0.0,然后重启服务" info " bash deploy/ctl.sh restart (或 systemctl restart opendance-api)" ;; 0.0.0.0|'*'|'') ok "HOST=$HOST_CFG 允许外部连接(是否真能访问还要看下面的监听与防火墙)" ;; *) warn "HOST=$HOST_CFG 只绑定某个具体网卡地址" info "若客户端不在该网段,请改为 0.0.0.0" ;; esac # ---------- 2. 实际监听端口 ---------- title "2. 实际监听状态" LISTEN_LINE="" if command -v ss >/dev/null 2>&1; then LISTEN_LINE="$(ss -tlnH 2>/dev/null | awk -v p=":$PORT" '$4 ~ p"$"')" elif command -v netstat >/dev/null 2>&1; then LISTEN_LINE="$(netstat -tln 2>/dev/null | awk -v p=":$PORT" '$4 ~ p"$"')" fi if [ -z "$LISTEN_LINE" ]; then if [ -z "$(command -v ss || command -v netstat)" ]; then warn "系统里没有 ss / netstat,跳过监听检查" else bad "没有任何进程监听 $PORT 端口 —— 服务可能没起来,或 PORT 与 .env 不一致" info "检查:bash deploy/ctl.sh status / bash deploy/ctl.sh logs" fi else printf '%s\n' "$LISTEN_LINE" | while IFS= read -r l; do info "$l"; done if printf '%s' "$LISTEN_LINE" | grep -qE '(^|[^0-9])127\.0\.0\.1:'"$PORT"; then if printf '%s' "$LISTEN_LINE" | grep -qE '(0\.0\.0\.0|\[::\]|\*):'"$PORT"; then ok "同时监听通配地址,外部可连" else bad "只监听 127.0.0.1:$PORT —— 这是「外部访问不到」最常见的原因" info "修复:$ENV_FILE 中 HOST=0.0.0.0,然后重启服务" fi else ok "监听了通配地址(0.0.0.0 / [::]),绑定层没问题" fi fi # ---------- 3. 本机回环自测 ---------- title "3. 本机自测(回环)" probe() { # $1=url if command -v curl >/dev/null 2>&1; then curl -sS -m 5 -o /dev/null -w '%{http_code}' "$1" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q -T 5 -O /dev/null -S "$1" 2>&1 | awk '/HTTP\//{code=$2} END{print code}' else printf '' fi } LOOP_CODE="$(probe "http://127.0.0.1:$PORT/health")" if [ "$LOOP_CODE" = "200" ]; then ok "http://127.0.0.1:$PORT/health → 200,服务本身正常" elif [ -z "$LOOP_CODE" ]; then warn "系统里没有 curl / wget,跳过 HTTP 自测" else bad "http://127.0.0.1:$PORT/health → $LOOP_CODE(期望 200)" info "503 通常表示数据库连不上;000/空表示端口没人应答" info "看日志:bash deploy/ctl.sh logs" fi # ---------- 4. 用对外 IP 自测 ---------- title "4. 本机自测(对外 IP)" LAN_IP="" if command -v ip >/dev/null 2>&1; then LAN_IP="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)" fi [ -z "$LAN_IP" ] && LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" if [ -z "$LAN_IP" ]; then warn "拿不到本机对外 IP,跳过" else info "本机对外 IP:$LAN_IP" EXT_CODE="$(probe "http://$LAN_IP:$PORT/health")" if [ "$EXT_CODE" = "200" ]; then ok "http://$LAN_IP:$PORT/health → 200,走网卡这一层是通的" info "那么外部访问不了多半是:防火墙 / 云安全组 / 客户端与服务器不在同一网段," info "或者你访问的是 80 / 443 端口(本项目不部署反向代理,见第 5 节)" elif [ -n "$EXT_CODE" ]; then bad "http://$LAN_IP:$PORT/health → $EXT_CODE" else warn "无法自测(缺 curl / wget)" fi fi # ---------- 5. 是否有 80/443 监听(反代才需要) ---------- title "5. 是否有人在监听 80 / 443(反向代理)" WEB_LISTEN="" if command -v ss >/dev/null 2>&1; then WEB_LISTEN="$(ss -tlnH 2>/dev/null | awk '$4 ~ /:(80|443)$/')" fi if [ -z "$WEB_LISTEN" ]; then warn "本机没有监听 80 / 443 的服务 —— 说明这台机器上没有反向代理" info "本项目按需求不部署反向代理:直接用 http://<服务器IP>:$PORT 访问后端业务服务" info "若要通过域名 / 80 / 443 访问,需由运维侧按 deploy/proxy/ 配置 IIS 或 nginx" else ok "检测到 80/443 监听(可能是运维侧的反向代理)" printf '%s\n' "$WEB_LISTEN" | while IFS= read -r l; do info "$l"; done info "请确认它的转发目标写的是本机 $PORT 端口,见 deploy/proxy/README.md" fi # ---------- 6. 防火墙 ---------- title "6. 防火墙" FW_FOUND=0 if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then FW_FOUND=1 info "firewalld 运行中" ZONES="$(firewall-cmd --get-active-zones 2>/dev/null | awk '/^[a-z]/{print $1}')" for z in $ZONES; do PORTS="$(firewall-cmd --zone="$z" --list-ports 2>/dev/null)" SVC="$(firewall-cmd --zone="$z" --list-services 2>/dev/null)" info " zone=$z ports=[$PORTS] services=[$SVC]" if printf '%s' "$PORTS" | grep -qE "(^|,| )$PORT/tcp(,| |$)"; then ok " $PORT/tcp 已在 zone=$z 放行" else bad " $PORT/tcp 未在 zone=$z 放行" info " 修复:firewall-cmd --permanent --add-port=$PORT/tcp && firewall-cmd --reload" fi done fi if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qi '^Status: active'; then FW_FOUND=1 info "ufw 运行中" ufw status 2>/dev/null | sed 's/^/ /' if ufw status 2>/dev/null | grep -qE "(^| )$PORT(/tcp)?( |$)"; then ok " $PORT 已在 ufw 规则中出现" else bad " ufw 中未见 $PORT 放行规则" info " 修复:ufw allow $PORT/tcp" fi fi if [ "$FW_FOUND" -eq 0 ]; then if command -v iptables >/dev/null 2>&1; then if iptables -S 2>/dev/null | grep -qE '(-P INPUT (DROP|REJECT)|--dport '"$PORT"')'; then info "iptables 规则(节选):" iptables -S 2>/dev/null | grep -E -- "--dport ($PORT|80|443)|-P INPUT" | sed 's/^/ /' if iptables -S 2>/dev/null | grep -qE -- "--dport $PORT .*-j ACCEPT"; then ok " iptables 中有 $PORT 的 ACCEPT 规则" elif iptables -S 2>/dev/null | grep -qE -- "--dport $PORT .*-j (DROP|REJECT)"; then bad " iptables 明确拒绝了 $PORT" else warn " iptables 中未见 $PORT 的放行规则(默认策略若为 DROP 则外部连不上)" info " 查询默认策略:iptables -S | grep '^-P INPUT'" fi else info "iptables 无相关丢弃规则(未检测到防火墙拦截迹象)" fi else info "未检测到 firewalld / ufw" fi info "注意:云服务器还需检查控制台的「安全组 / 网络 ACL」是否放行 $PORT" fi # ---------- 6.5 可选依赖 Assimp ---------- title "可选依赖 Assimp(.pmx/.fbx 模型转换、FBX/glTF 动作预处理)" if [ -f "$(dirname "$0")/assimp-lib.sh" ]; then # shellcheck source=deploy/assimp-lib.sh . "$(dirname "$0")/assimp-lib.sh" ASSIMP_PATH="$(opendance_assimp_resolve || true)" if [ -n "$ASSIMP_PATH" ]; then ok "Assimp 可用:$ASSIMP_PATH($("$ASSIMP_PATH" version 2>/dev/null | head -1))" else warn "未安装 Assimp:上传 .pmx / .fbx(或含它们的压缩包)会报转换失败" info "安装:$(opendance_assimp_install_cmd)" info "只上传 .glb / .vrm 模型与 .bvh / .vmd 动作则不受影响" ASSIMP_MISSING=1 fi else warn "未找到 deploy/assimp-lib.sh,跳过 Assimp 检查" fi # ---------- 7. 结论 ---------- title "结论" if [ "${ASSIMP_MISSING:-0}" = "1" ]; then c_yel " 提示:未安装 Assimp,.pmx / .fbx 模型转换与 FBX / glTF 动作预处理不可用(与访问无关)。" echo " 安装:$(opendance_assimp_install_cmd 2>/dev/null || echo '用发行版包管理器安装 assimp')" echo fi if [ "$PROBLEMS" -eq 0 ]; then c_grn " 未发现本机侧问题。" echo " 若仍访问不了,按顺序排查:" echo " 1) 客户端与服务器是否互通:ping <服务器IP>" echo " 2) 云控制台安全组 / 公司网络策略是否放行 $PORT" echo " 3) 你访问的端口是否写错(本项目服务在 $PORT,不在 80/443)" echo " 4) 若要通过域名访问,需运维侧按 deploy/proxy/ 配置反向代理" else c_red " 发现 $PROBLEMS 个问题,按上面的 ❌ 逐条修复后重试。" echo echo " 两种修复路径:" echo " A. 直连后端(本项目开箱即用,无需反代):" echo " 1) $ENV_FILE 里 HOST=0.0.0.0" echo " 2) 防火墙放行 $PORT/tcp" echo " 3) 重启服务后访问 http://<服务器IP>:$PORT" echo " B. 通过域名 / 80 / 443 访问(需运维侧配合):" echo " 按 deploy/proxy/README.md 配置 IIS 或 nginx,整体转发到 $PORT" fi echo exit 0