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).
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: prepare_chrome_profile
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "prepare_chrome_profile --src <user-data-dir> --dst <user-data-dir> [--keep <ext_id>]... [--force]"
|
||||
description: "Clona un user-data-dir de Chrome/Chromium creando un perfil de scraping limpio: conserva solo las extensiones de una lista blanca (por defecto uBlock Origin Lite) y excluye caché, locks y sesiones antiguas."
|
||||
tags: [chrome, browser, profile, scraping, extensions, navegator]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/browser/prepare_chrome_profile.sh"
|
||||
params:
|
||||
- name: --src
|
||||
desc: "user-data-dir origen con un perfil Chrome/Chromium ya configurado (debe existir --src/Default)"
|
||||
- name: --dst
|
||||
desc: "Ruta de destino del nuevo perfil; no debe existir salvo que se pase --force"
|
||||
- name: --keep
|
||||
desc: "ID de extensión Chrome a conservar (repetible). Si no se pasa ninguno el default es ddkjiahejlhfcafbddmgiahcphecmpfh (uBlock Origin Lite)"
|
||||
- name: --force
|
||||
desc: "Borra --dst si existe antes de recrearlo. Sin este flag la función aborta si --dst ya existe"
|
||||
output: "JSON en stdout: {dst, kept: [id...], removed: [id...]}. Exit 0 en éxito."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source $HOME/fn_registry/bash/functions/browser/prepare_chrome_profile.sh
|
||||
|
||||
prepare_chrome_profile \
|
||||
--src "$HOME/.config/chromium" \
|
||||
--dst "$HOME/.local/share/web_scraping/chrome-profile"
|
||||
|
||||
# Con extensión adicional conservada
|
||||
prepare_chrome_profile \
|
||||
--src "$HOME/.config/chromium" \
|
||||
--dst "$HOME/.local/share/web_scraping/chrome-profile" \
|
||||
--keep "ddkjiahejlhfcafbddmgiahcphecmpfh" \
|
||||
--keep "cjpalhdlnbpafiamejdnhcphjbkeiagm" \
|
||||
--force
|
||||
|
||||
# Salida esperada (ejemplo):
|
||||
# {"dst":"/home/enmanuel/.local/share/web_scraping/chrome-profile","kept":["ddkjiahejlhfcafbddmgiahcphecmpfh"],"removed":["abcdefghijklmnopabcdefghijklmnop","dark-reader-id"]}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Úsala antes de lanzar una sesión de scraping/automatización para partir de un perfil aislado: con uBlock Origin Lite activo (menos anuncios/trackers = DOM más limpio, respuestas más rápidas) pero sin extensiones que interfieren (Dark Reader muta colores del DOM, NoScript bloquea JS, OneTab modifica tabs). También sirve para aislar sesiones de diferentes proyectos de scraping sin contaminar el perfil personal.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Chrome debe estar CERRADO sobre `--src`** antes de ejecutar. Los archivos SQLite (`Cookies`, `History`, `Login Data`, etc.) estarán bloqueados si Chrome está abierto, y `rsync` copiará versiones inconsistentes. Verificar con `pgrep -x chromium` o `pgrep -x chrome`.
|
||||
- **HMAC de Secure Preferences**: el archivo `Local State` contiene la semilla HMAC que Chrome usa para verificar `Preferences` y `Secure Preferences`. Si no se copia (o se copia entre máquinas distintas con distinto binding), Chrome puede invalidar las extensiones al arrancar y resetear configuraciones. La función copia `Local State` automáticamente, pero la copia entre máquinas puede seguir produciendo resets de extensiones — esto es comportamiento esperado de Chrome, no un bug de esta función.
|
||||
- **Purga de referencias en Preferences**: tras borrar las carpetas de extensiones fuera de la whitelist, la función también elimina con `python3` las entradas `extensions.settings.<id>` de `Default/Preferences` y `Default/Secure Preferences`, los IDs de `extensions.pinned_extensions` y las claves `protection.macs.extensions.settings.<id>`. Sin esta limpieza Chrome detecta las entradas en Preferences (con `from_webstore`/install_source) y **vuelve a descargar la extensión del Web Store al arrancar**, deshaciendo el filtrado (caso real: Dark Reader reaparece y oscurece páginas rompiendo screenshots). Si `python3` falla al procesar un Preferences concreto se emite un warning a stderr pero la función no aborta — el borrado de carpetas ya es el efecto principal.
|
||||
- **`--force` borra `--dst` completamente**: si `--dst` es un perfil con datos que quieres conservar, no uses `--force` sin antes hacer backup.
|
||||
- **Extensiones instaladas desde Web Store vs unpacked**: esta función opera sobre la carpeta `Extensions/` física. Las extensiones instaladas desde la Web Store tienen IDs de 32 caracteres en minúsculas. Las extensiones unpacked (`--load-extension`) no viven en `Extensions/` y no se ven afectadas.
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Código | Significado |
|
||||
|--------|------------|
|
||||
| 0 | Éxito |
|
||||
| 1 | Argumento inválido o `--src/Default` no existe |
|
||||
| 2 | `--dst` ya existe y no se pasó `--force` |
|
||||
| 3 | `--src` y `--dst` resuelven al mismo path real |
|
||||
| 4 | Error durante `rsync` |
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user