Files
feadulta/scripts/deploy_php83_compat_step1.sh

283 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Paso 1 del upgrade PHP 8.3 en feadulta.com (issue #46):
# subir 5 ficheros parcheados + limpieza malware, manteniendo PHP 7.4.
#
# Modos:
# --dry-run imprime lo que haría sin tocar nada (default)
# --apply ejecuta el despliegue real
# --rollback BACKUP_DIR restaura desde un backup pre-step1 concreto
#
# Pre-flight estricto: aborta si los 5 ficheros remotos no coinciden con el
# hash esperado (el que verificamos hoy 2026-05-25). Si en prod cambió algo
# entre planning y ejecución, hay que rehacer la planificación.
set -euo pipefail
# ─── Config ────────────────────────────────────────────────────────────────
SSH_USER="feadulta"
SSH_HOST="134.0.10.170"
SSH_PASS='6Rm2qOF@eundwpda'
REMOTE_WEB_ROOT="/web"
LOCAL_SRC_ROOT="/home/rafa/joomla-migration/joomla-php83"
BACKUP_ROOT="/home/rafa/joomla-migration/backup/prod-20260525-php83-compat"
# Mapa: ruta_relativa | hash_local_esperado | hash_remoto_esperado_actual
# (verificado 2026-05-25 con md5sum local + ssh + tar -xzO del backup)
declare -a FILES=(
"modules/mod_featcats/helper.php|01ae5ad40e13abdcd5852897786d3733|744922888ae533b090eb34effe3bb469"
"templates/fe_adulta_1/functions.php|06b2c26a618dbceefa5d8d5f0293c2cf|6318edec84cdf7a6b84a1d07f063c6c0"
"templates/fe_adulta_1/index.php|dc318909b9ae5976e5c4f01c06c35f66|9793dfa3c880eba37ad5ad35e6988705"
"modules/mod_k2_filter/helper.php|0d9767a0d8d67aa420baefe38382a87c|8530a4e70043973fd2d625c6f8de6ce9"
"modules/mod_k2_filter/tmpl/Default/template.php|a62a39dacafa0748528268b9f04aa844|008465147acdeb442eca6f64311fb23d"
)
# URLs de smoke test (deben devolver HTTP 200 antes y después del cambio)
declare -a SMOKE_URLS=(
"https://www.feadulta.com/"
"https://www.feadulta.com/es/"
"https://www.feadulta.com/es/ayuda.html"
"https://www.feadulta.com/es/quienessomos/colaboradores.html"
"https://www.feadulta.com/es/buscadoravanzado/itemlist/"
"https://www.feadulta.com/es/buscadoravanzado/itemlist/user/43-fraymarcos.html"
"https://www.feadulta.com/es/comentcol2.html"
)
# Patrones de spam que deben dar 0 hits en las respuestas tras el paso 1
SPAM_REGEX="(apuestadeportiva|vavada\.mobi|inkabet|betsafe|betcris|botbotbot)"
# IP del origen para bypass de Cloudflare (la web está detrás de CF managed challenge,
# las peticiones curl normales reciben 403). --resolve nos lleva directo al origen.
ORIGIN_IP="134.0.10.170"
SMOKE_UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
# ─── Utilidades ────────────────────────────────────────────────────────────
# Logs van a stderr; stdout queda libre para "datos" (p.ej. ruta del backup
# para capturar con $(...) sin contaminación). Bug detectado en revisión #46.
log() { printf '%s [%s] %s\n' "$(date +'%H:%M:%S')" "$1" "$2" >&2; }
info() { log "INFO" "$1"; }
warn() { log "WARN" "$1"; }
err() { log "ERR " "$1"; }
ok() { log "OK " "$1"; }
ssh_run() {
SSHPASS="$SSH_PASS" sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 \
"$SSH_USER@$SSH_HOST" "$@"
}
# El cPanel jail rechaza scp y sftp ("Connection closed"). Usamos cat-over-ssh,
# que sí funciona (cat está en /usr/bin/cat). Verificación 2026-05-25: download
# de un fichero conocido reproduce el mismo MD5; upload de bytes y verificación
# por re-lectura sin pérdidas.
scp_get() {
local remote="$1" local_target="$2"
SSHPASS="$SSH_PASS" sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 \
"$SSH_USER@$SSH_HOST" "cat '$remote'" > "$local_target"
}
scp_put() {
local local_src="$1" remote_target="$2"
SSHPASS="$SSH_PASS" sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 \
"$SSH_USER@$SSH_HOST" "cat > '$remote_target'" < "$local_src"
}
remote_md5() {
ssh_run "md5sum '$1' 2>/dev/null | cut -d' ' -f1"
}
# ─── Pre-flight checks ─────────────────────────────────────────────────────
preflight() {
info "Pre-flight: verificar hashes locales y remotos"
local fail=0
for entry in "${FILES[@]}"; do
IFS='|' read -r rel hl hr_expected <<<"$entry"
local local_path="$LOCAL_SRC_ROOT/$rel"
local remote_path="$REMOTE_WEB_ROOT/$rel"
if [[ ! -f "$local_path" ]]; then
err "Local no existe: $local_path"; fail=1; continue
fi
local hl_actual
hl_actual=$(md5sum "$local_path" | cut -d' ' -f1)
if [[ "$hl_actual" != "$hl" ]]; then
err "Local $rel: hash $hl_actual ≠ esperado $hl"; fail=1
else
ok "Local $rel coincide ($hl)"
fi
local hr_actual
hr_actual=$(remote_md5 "$remote_path")
if [[ -z "$hr_actual" ]]; then
err "Remoto no existe o vacío: $remote_path"; fail=1
elif [[ "$hr_actual" != "$hr_expected" ]]; then
err "Remoto $rel: hash $hr_actual ≠ esperado $hr_expected (algo cambió en prod, replanificar)"; fail=1
else
ok "Remoto $rel coincide ($hr_actual)"
fi
done
if [[ $fail -eq 1 ]]; then
err "Pre-flight FAIL → abortando"
exit 2
fi
ok "Pre-flight OK"
}
# ─── Backup pre-cambio (los 5 ficheros remotos) ────────────────────────────
backup_pre_step1() {
local ts; ts=$(date +'%Y%m%d-%H%M%S')
local dir="$BACKUP_ROOT/pre-step1-$ts"
if [[ "$MODE" == "dry-run" ]]; then
info "[dry-run] Crearía directorio: $dir"
info "[dry-run] Descargaría ${#FILES[@]} ficheros remotos a ese directorio + md5sums.txt + tar.gz"
echo "$dir" # imprime para usar después
return
fi
info "Creando backup pre-step1 en $dir"
mkdir -p "$dir"
for entry in "${FILES[@]}"; do
IFS='|' read -r rel _ hr_expected <<<"$entry"
local remote_path="$REMOTE_WEB_ROOT/$rel"
local local_target="$dir/$rel"
mkdir -p "$(dirname "$local_target")"
info " Descargando $remote_path"
scp_get "$remote_path" "$local_target"
if [[ ! -s "$local_target" ]]; then
err "Descarga vacía: $local_target — ABORT (revisar acceso SSH)"
exit 6
fi
local h_after
h_after=$(md5sum "$local_target" | cut -d' ' -f1)
if [[ "$h_after" != "$hr_expected" ]]; then
err "Backup $rel: md5 $h_after ≠ esperado $hr_expected — ABORT"
exit 6
fi
ok " Backup $rel verificado ($h_after)"
done
(cd "$dir" && find . -type f -name '*.php' -exec md5sum {} \; > md5sums.txt)
tar -czf "$dir.tar.gz" -C "$BACKUP_ROOT" "pre-step1-$ts"
ok "Backup pre-step1 creado: $dir"
ok "Backup tar.gz: $dir.tar.gz"
echo "$dir"
}
# ─── Subida de los 5 ficheros ──────────────────────────────────────────────
upload_files() {
for entry in "${FILES[@]}"; do
IFS='|' read -r rel hl _ <<<"$entry"
local local_path="$LOCAL_SRC_ROOT/$rel"
local remote_path="$REMOTE_WEB_ROOT/$rel"
if [[ "$MODE" == "dry-run" ]]; then
info "[dry-run] scp $local_path$remote_path"
continue
fi
info "Subiendo $rel"
scp_put "$local_path" "$remote_path"
local hr_after; hr_after=$(remote_md5 "$remote_path")
if [[ "$hr_after" != "$hl" ]]; then
err "Subida de $rel: hash remoto post-subida $hr_after ≠ local $hl"
err "ABORT — el fichero remoto no coincide con el local. Considerar rollback."
exit 3
fi
ok "Subido + verificado $rel ($hr_after)"
done
}
# ─── Smoke test HTTP ───────────────────────────────────────────────────────
# allow_spam=1 → solo validar HTTP code (uso post-rollback, donde los ficheros
# restaurados llevan las inyecciones spam originales y un spam>0 es esperado).
smoke_test() {
local allow_spam="${1:-0}"
if [[ "$allow_spam" -eq 1 ]]; then
info "Smoke test HTTP (post-rollback: solo validar HTTP, spam>0 esperado)"
else
info "Smoke test HTTP (HTTP 200/3xx + spam=0)"
fi
local fail=0
for url in "${SMOKE_URLS[@]}"; do
if [[ "$MODE" == "dry-run" ]]; then
info "[dry-run] curl $url + grep spam"
continue
fi
local code body_spam tmp
tmp=$(mktemp)
code=$(curl -ksL -A "$SMOKE_UA" --resolve "www.feadulta.com:443:$ORIGIN_IP" \
-o "$tmp" -w "%{http_code}" "$url" || echo "ERR")
body_spam=$(grep -cE "$SPAM_REGEX" "$tmp" || true)
rm -f "$tmp"
if [[ "$code" != "200" && "$code" != "301" && "$code" != "302" ]]; then
err " $url → HTTP $code"; fail=1
elif [[ "$allow_spam" -eq 0 && "$body_spam" -gt 0 ]]; then
err " $url → HTTP $code, $body_spam strings spam"; fail=1
else
ok " $url → HTTP $code, $body_spam spam"
fi
done
if [[ $fail -eq 1 ]]; then
err "Smoke test FAIL → considerar rollback manual con --rollback <backup-dir>"
exit 4
fi
ok "Smoke test OK"
}
# ─── Rollback desde backup ─────────────────────────────────────────────────
rollback() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
err "Backup dir no existe: $dir"; exit 5
fi
info "Rollback desde $dir"
for entry in "${FILES[@]}"; do
IFS='|' read -r rel _ _ <<<"$entry"
local local_src="$dir/$rel"
local remote_path="$REMOTE_WEB_ROOT/$rel"
if [[ ! -f "$local_src" ]]; then
err " No hay backup para $rel — saltando"; continue
fi
info " Restaurando $rel"
scp_put "$local_src" "$remote_path"
ok " Restaurado $rel"
done
ok "Rollback completado"
}
# ─── Main ──────────────────────────────────────────────────────────────────
MODE="dry-run"
ROLLBACK_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) MODE="dry-run"; shift ;;
--apply) MODE="apply"; shift ;;
--rollback) MODE="rollback"; ROLLBACK_DIR="${2:-}"; shift 2 ;;
-h|--help) sed -n '1,12p' "$0"; exit 0 ;;
*) err "Flag desconocido: $1"; exit 1 ;;
esac
done
info "Modo: $MODE"
if [[ "$MODE" == "rollback" ]]; then
if [[ -z "$ROLLBACK_DIR" ]]; then err "--rollback requiere ruta del backup"; exit 1; fi
rollback "$ROLLBACK_DIR"
smoke_test 1 # allow_spam=1: ficheros restaurados aún tienen las inyecciones
exit 0
fi
preflight
backup_dir=$(backup_pre_step1)
upload_files
smoke_test
ok "Paso 1 finalizado en modo: $MODE"
if [[ "$MODE" == "apply" ]]; then
ok "Backup pre-step1: $backup_dir"
ok "Para rollback: $0 --rollback $backup_dir"
fi