Files
fn_registry/bash/functions/browser/prepare_chrome_profile.sh
Egutierrez ccfa5bc78b feat(browser): funciones anti-deteccion + perfiles para web_scraping
Funciones nuevas del dominio browser (grupo navegator):
- cdp_move_mouse_human / cdp_click_human: movimiento de raton con curva
  de Bezier cubica, easing y micro-jitter para imitar comportamiento
  humano y reducir deteccion de automatizacion.
- cdp_wait_idle: espera network-idle contando requests en vuelo via
  eventos CDP Network.*; inmune a extensiones que mutan el DOM
  (Dark Reader, uBlock) y a animaciones JS.
- list_chrome_profiles: lista perfiles de un user-data-dir (extensiones,
  nombre legible, preferencias).
- prepare_chrome_profile (bash): clona un user-data-dir conservando solo
  una whitelist de extensiones (default uBlock Origin Lite).

Modificadas:
- chrome_launch: Linux-first (chromium/google-chrome/brave antes que
  chrome.exe), KeepExtensions y Setpgid para matar el arbol con cdp_close.
- cdp_close: kill por grupo de proceso.

Todas con tests verdes (go test ./functions/browser ok).
2026-06-05 16:25:11 +02:00

224 lines
7.7 KiB
Bash

#!/usr/bin/env bash
# prepare_chrome_profile — clona un user-data-dir de Chrome/Chromium conservando solo
# las extensiones de una lista blanca. Sirve para perfiles de scraping limpios.
set -euo pipefail
# ── defaults ──────────────────────────────────────────────────────────────────
_SRC=""
_DST=""
_FORCE=0
# uBlock Origin Lite por defecto
_KEEP=()
_DEFAULT_EXT="ddkjiahejlhfcafbddmgiahcphecmpfh"
# ── parse args ────────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: prepare_chrome_profile --src <user-data-dir> --dst <user-data-dir> \
[--keep <ext_id>]... [--force]
--src user-data-dir origen (ej. $HOME/.config/chromium)
--dst user-data-dir destino a crear
--keep ID de extensión a conservar (repetible). Default: uBlock Origin Lite
--force si --dst existe, lo borra y recrea; sin flag aborta si existe
Exit codes:
0 éxito
1 error de argumento o validación
2 --dst ya existe y no se pasó --force
3 --src igual a --dst (mismo path real)
4 error de copia/rsync
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--src) _SRC="$2"; shift 2 ;;
--dst) _DST="$2"; shift 2 ;;
--keep) _KEEP+=("$2"); shift 2 ;;
--force) _FORCE=1; shift ;;
-h|--help) _usage ;;
*) echo "prepare_chrome_profile: argumento desconocido: $1" >&2; _usage ;;
esac
done
# ── validaciones básicas ──────────────────────────────────────────────────────
if [[ -z "$_SRC" || -z "$_DST" ]]; then
echo "prepare_chrome_profile: --src y --dst son obligatorios" >&2
exit 1
fi
if [[ ! -d "$_SRC/Default" ]]; then
echo "prepare_chrome_profile: $_SRC/Default no existe; ¿es un user-data-dir válido?" >&2
exit 1
fi
# Resolver paths reales para comparar (evitar borrar src cuando src==dst)
_SRC_REAL="$(realpath "$_SRC")"
_DST_REAL="$(realpath -m "$_DST")" # -m: no requiere que exista
if [[ "$_SRC_REAL" == "$_DST_REAL" ]]; then
echo "prepare_chrome_profile: --src y --dst resuelven al mismo path: $_SRC_REAL" >&2
exit 3
fi
# También rechazar si --dst es prefijo de --src (evitar borrar el origen)
if [[ "$_SRC_REAL" == "$_DST_REAL"/* ]]; then
echo "prepare_chrome_profile: --src está dentro de --dst; operación peligrosa, abortando" >&2
exit 3
fi
# ── lista blanca de extensiones ───────────────────────────────────────────────
if [[ ${#_KEEP[@]} -eq 0 ]]; then
_KEEP=("$_DEFAULT_EXT")
fi
# ── gestionar destino ─────────────────────────────────────────────────────────
if [[ -d "$_DST" ]]; then
if [[ $_FORCE -eq 1 ]]; then
rm -rf "$_DST"
else
echo "prepare_chrome_profile: $_DST ya existe; usa --force para sobreescribir" >&2
exit 2
fi
fi
mkdir -p "$_DST/Default"
# ── copiar Local State (HMAC seed para Secure Preferences) ────────────────────
if [[ -f "$_SRC/Local State" ]]; then
cp "$_SRC/Local State" "$_DST/Local State"
fi
# ── rsync del perfil Default excluyendo caché y locks ─────────────────────────
rsync -a \
--exclude='Cache/' \
--exclude='Code Cache/' \
--exclude='GPUCache/' \
--exclude='Dawn Cache/' \
--exclude='DawnGraphiteCache/' \
--exclude='DawnWebGPUCache/' \
--exclude='Service Worker/CacheStorage/' \
--exclude='Service Worker/ScriptCache/' \
--exclude='Singleton*' \
--exclude='*.lock' \
--exclude='lockfile' \
--exclude='Sessions/' \
--exclude='Session Storage/' \
--exclude='Current Session' \
--exclude='Current Tabs' \
--exclude='Last Session' \
--exclude='Last Tabs' \
"$_SRC/Default/" "$_DST/Default/" || {
echo "prepare_chrome_profile: rsync falló (exit $?)" >&2
exit 4
}
# ── eliminar extensiones fuera de la lista blanca ────────────────────────────
_EXT_DIR="$_DST/Default/Extensions"
_removed=()
_kept=()
if [[ -d "$_EXT_DIR" ]]; then
while IFS= read -r -d '' ext_path; do
ext_id="$(basename "$ext_path")"
# Conservar siempre la carpeta Temp (usada por Chrome durante installs)
if [[ "$ext_id" == "Temp" ]]; then
continue
fi
# Comprobar si está en la lista blanca
_in_keep=0
for keep_id in "${_KEEP[@]}"; do
if [[ "$ext_id" == "$keep_id" ]]; then
_in_keep=1
break
fi
done
if [[ $_in_keep -eq 1 ]]; then
_kept+=("$ext_id")
else
rm -rf "$ext_path"
_removed+=("$ext_id")
fi
done < <(find "$_EXT_DIR" -mindepth 1 -maxdepth 1 -type d -print0)
fi
# ── purgar referencias a extensiones eliminadas en Preferences ───────────────
# Chrome re-descarga del Web Store cualquier extensión que aparezca en
# extensions.settings aunque su carpeta haya sido borrada. Editamos el JSON
# con python3 para evitar ese comportamiento.
if [[ ${#_removed[@]} -gt 0 ]]; then
# Construir lista Python de IDs eliminados
_py_ids_list=""
for _id in "${_removed[@]}"; do
_py_ids_list+="\"${_id}\","
done
_py_ids_list="[${_py_ids_list%,}]"
for _prefs_file in "$_DST/Default/Preferences" "$_DST/Default/Secure Preferences"; do
if [[ -f "$_prefs_file" ]]; then
python3 - "$_prefs_file" "$_py_ids_list" <<'PY' || \
echo "prepare_chrome_profile: advertencia — no se pudieron purgar refs en $(basename "$_prefs_file")" >&2
import sys, json
prefs_path = sys.argv[1]
removed_ids = json.loads(sys.argv[2])
with open(prefs_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 1. extensions.settings.<id>
ext_settings = data.get("extensions", {}).get("settings", {})
for ext_id in removed_ids:
ext_settings.pop(ext_id, None)
# 2. extensions.pinned_extensions (lista de IDs)
pinned = data.get("extensions", {}).get("pinned_extensions", None)
if isinstance(pinned, list):
data["extensions"]["pinned_extensions"] = [
pid for pid in pinned if pid not in removed_ids
]
# 3. protection.macs.extensions.settings.<id> (Secure Preferences)
try:
mac_ext = data["protection"]["macs"]["extensions"]["settings"]
for ext_id in removed_ids:
mac_ext.pop(ext_id, None)
except (KeyError, TypeError):
pass
with open(prefs_path, "w", encoding="utf-8") as f:
json.dump(data, f, separators=(",", ":"))
PY
fi
done
fi
# ── emitir resultado JSON ─────────────────────────────────────────────────────
_json_array() {
# Convierte array bash en JSON array de strings
local arr=("$@")
local out="["
local first=1
for item in "${arr[@]}"; do
if [[ $first -eq 1 ]]; then
out+="\"$item\""
first=0
else
out+=",\"$item\""
fi
done
out+="]"
echo "$out"
}
_kept_json="$(_json_array "${_kept[@]+"${_kept[@]}"}")"
_removed_json="$(_json_array "${_removed[@]+"${_removed[@]}"}")"
printf '{"dst":"%s","kept":%s,"removed":%s}\n' \
"$_DST_REAL" \
"$_kept_json" \
"$_removed_json"