ccfa5bc78b
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).
224 lines
7.7 KiB
Bash
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"
|