diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..73ab00a --- /dev/null +++ b/extension/background.js @@ -0,0 +1,104 @@ +// web_proxy toggle — service worker. +// +// Mantiene el estado del proxy en chrome.storage.local y lo aplica a la +// configuracion de red de Chromium con la API chrome.proxy. El popup solo +// escribe el estado; este worker reacciona a los cambios y reconfigura el +// proxy. Asi un unico punto aplica la configuracion (popup, arranque, +// instalacion). +// +// Modelo de estado (chrome.storage.local key "state"): +// { +// enabled: boolean, // captura ON/OFF +// activeProxy: string, // id del proxy activo +// proxies: [ +// { id, name, scheme, host, port } // proxy simple +// ] +// } +// +// Encadenacion de proxies (futuro): un proxy podra declarar +// `chain: [ {scheme,host,port}, ... ]` y se aplicara mediante un PAC script +// generado aqui. El modelo de lista ya lo permite sin migracion. + +const DEFAULT_STATE = { + enabled: false, + activeProxy: "capture", + proxies: [ + { + id: "capture", + name: "Captura web_proxy", + scheme: "http", + host: "127.0.0.1", + port: 8889, + }, + ], +}; + +async function getState() { + const r = await chrome.storage.local.get("state"); + return r.state || DEFAULT_STATE; +} + +async function setState(state) { + await chrome.storage.local.set({ state }); +} + +function setBadge(on) { + chrome.action.setBadgeText({ text: on ? "ON" : "" }); + chrome.action.setBadgeBackgroundColor({ color: on ? "#16a34a" : "#666666" }); +} + +// Aplica el proxy activo si la captura esta encendida; si no, limpia la +// configuracion para volver a la conexion directa del sistema. +async function applyProxy() { + const st = await getState(); + + if (!st.enabled) { + await chrome.proxy.settings.clear({ scope: "regular" }); + setBadge(false); + return; + } + + const p = + st.proxies.find((x) => x.id === st.activeProxy) || st.proxies[0] || null; + if (!p) { + await chrome.proxy.settings.clear({ scope: "regular" }); + setBadge(false); + return; + } + + // fixed_servers con un unico proxy. Sin "<-loopback>" en bypassList, de modo + // que el trafico a loopback (incluida la propia UI del proxy) NO se proxea y + // por tanto no se captura. El trafico a sitios externos si pasa por el proxy. + const config = { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: p.scheme || "http", + host: p.host, + port: Number(p.port), + }, + }, + }; + await chrome.proxy.settings.set({ value: config, scope: "regular" }); + setBadge(true); +} + +// Sembrar el estado por defecto la primera vez y aplicar. +chrome.runtime.onInstalled.addListener(async () => { + const r = await chrome.storage.local.get("state"); + if (!r.state) { + await setState(DEFAULT_STATE); + } + applyProxy(); +}); + +// Reaplicar al arrancar el navegador (la configuracion de proxy de la sesion +// no persiste entre arranques de Chromium). +chrome.runtime.onStartup.addListener(applyProxy); + +// El popup escribe el estado; aqui se reconfigura el proxy en consecuencia. +chrome.storage.onChanged.addListener((changes, area) => { + if (area === "local" && changes.state) { + applyProxy(); + } +}); diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..2812c1e --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,15 @@ +{ + "manifest_version": 3, + "name": "web_proxy toggle", + "version": "0.1.0", + "description": "Activa o desactiva la captura de trafico a traves del proxy de web_proxy con un clic. Gestiona varios proxies y deja preparada la futura encadenacion de proxies.", + "permissions": ["proxy", "storage"], + "host_permissions": [""], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_title": "web_proxy: activar/desactivar captura", + "default_popup": "popup.html" + } +} diff --git a/extension/popup.html b/extension/popup.html new file mode 100644 index 0000000..d26c25c --- /dev/null +++ b/extension/popup.html @@ -0,0 +1,200 @@ + + + + + + + + +
+

web_proxy

+ +
+
Captura desactivada
+ + + +
+ Anadir proxy +
+ +
+
+ + + +
+ +
+ +
+ El proxy activo se aplica a todo el navegador. El trafico a loopback no se + proxea (la UI de registros no se captura a si misma). +
+ + + + diff --git a/extension/popup.js b/extension/popup.js new file mode 100644 index 0000000..9b09f2e --- /dev/null +++ b/extension/popup.js @@ -0,0 +1,131 @@ +// web_proxy toggle — popup logic. +// +// Lee y escribe el estado en chrome.storage.local. El service worker +// (background.js) reacciona a cada cambio y reconfigura el proxy real, de modo +// que el popup nunca llama directamente a chrome.proxy. + +const DEFAULT_STATE = { + enabled: false, + activeProxy: "capture", + proxies: [ + { + id: "capture", + name: "Captura web_proxy", + scheme: "http", + host: "127.0.0.1", + port: 8889, + }, + ], +}; + +async function getState() { + const r = await chrome.storage.local.get("state"); + return r.state || structuredClone(DEFAULT_STATE); +} + +async function setState(state) { + await chrome.storage.local.set({ state }); +} + +function newId() { + return "p" + Math.random().toString(36).slice(2, 9); +} + +function render(state) { + const toggle = document.getElementById("toggle"); + const status = document.getElementById("status"); + toggle.checked = !!state.enabled; + + const active = state.proxies.find((p) => p.id === state.activeProxy); + if (state.enabled && active) { + status.textContent = `Capturando via ${active.host}:${active.port}`; + status.style.color = "var(--green)"; + } else { + status.textContent = "Captura desactivada"; + status.style.color = "var(--muted)"; + } + + const list = document.getElementById("proxy-list"); + list.innerHTML = ""; + for (const p of state.proxies) { + const li = document.createElement("li"); + + const radio = document.createElement("input"); + radio.type = "radio"; + radio.name = "active"; + radio.checked = p.id === state.activeProxy; + radio.addEventListener("change", async () => { + const s = await getState(); + s.activeProxy = p.id; + await setState(s); + render(s); + }); + + const meta = document.createElement("div"); + meta.className = "meta"; + const name = document.createElement("div"); + name.className = "name"; + name.textContent = p.name || p.id; + const addr = document.createElement("div"); + addr.className = "addr"; + addr.textContent = `${p.scheme}://${p.host}:${p.port}`; + meta.append(name, addr); + + li.append(radio, meta); + + // El proxy de captura por defecto no se puede borrar. + if (p.id !== "capture") { + const del = document.createElement("button"); + del.className = "del"; + del.textContent = "✕"; + del.title = "Eliminar"; + del.addEventListener("click", async () => { + const s = await getState(); + s.proxies = s.proxies.filter((x) => x.id !== p.id); + if (s.activeProxy === p.id) { + s.activeProxy = s.proxies[0] ? s.proxies[0].id : "capture"; + } + await setState(s); + render(s); + }); + li.append(del); + } + + list.append(li); + } +} + +async function init() { + const state = await getState(); + render(state); + + document.getElementById("toggle").addEventListener("change", async (e) => { + const s = await getState(); + s.enabled = e.target.checked; + await setState(s); + render(s); + }); + + document.getElementById("f-add").addEventListener("click", async () => { + const name = document.getElementById("f-name").value.trim(); + const scheme = document.getElementById("f-scheme").value; + const host = document.getElementById("f-host").value.trim(); + const port = parseInt(document.getElementById("f-port").value, 10); + if (!host || !port) return; + const s = await getState(); + s.proxies.push({ + id: newId(), + name: name || `${host}:${port}`, + scheme, + host, + port, + }); + await setState(s); + document.getElementById("f-name").value = ""; + document.getElementById("f-host").value = ""; + document.getElementById("f-port").value = ""; + render(s); + }); +} + +init(); diff --git a/web_proxy b/web_proxy index d4733ac..d06d5f1 100755 --- a/web_proxy +++ b/web_proxy @@ -45,6 +45,7 @@ SYSTEMD_USER_DIR="$HOME/.config/systemd/user" DEFAULT_PORT=8080 DEFAULT_OUT="$HOME/captures" DEFAULT_ROTATE=20 +DEFAULT_WEB_PORT=8081 MITMDUMP_BIN="$(command -v mitmdump 2>/dev/null || echo "$HOME/.local/bin/mitmdump")" MITMWEB_BIN="$(command -v mitmweb 2>/dev/null || echo "$HOME/.local/bin/mitmweb")" @@ -70,14 +71,24 @@ conf_get() { } conf_write() { - local port="$1" out="$2" rotate="$3" + local port="$1" out="$2" rotate="$3" web_port="${4:-}" web_pass="${5:-}" cat > "$CONFFILE" </dev/null | wc -l)" @@ -206,12 +225,14 @@ cmd_status() { } cmd_browser() { - local url="" proxy_port + local url="" proxy_port show_ui="yes" web_port proxy_port="$(conf_get PORT "$DEFAULT_PORT")" + web_port="$(conf_get WEB_PORT "")" while [[ $# -gt 0 ]]; do case "$1" in - --url) url="$2"; shift 2 ;; - --port) proxy_port="$2"; shift 2 ;; + --url) url="$2"; shift 2 ;; + --port) proxy_port="$2"; shift 2 ;; + --no-ui) show_ui="no"; shift ;; *) err "browser: flag desconocido: $1"; return 2 ;; esac done @@ -222,11 +243,42 @@ cmd_browser() { fi local args=(--proxy "http://127.0.0.1:${proxy_port}" --profile "$WEB_PROXY_HOME/chromium-profile") - # Si el CA esta instalado en el perfil, no forzamos --ignore-certificate-errors. - [[ -n "$url" ]] && args+=(--url "$url") + # Primera pestaña: la UI de registros en vivo (si el servicio corre en modo + # web). La UI es loopback y no se proxea (proxy-bypass de loopback), asi que + # carga directa. Las pestañas a sitios reales si pasan por el proxy y van + # apareciendo en la UI en tiempo real. + local ui_url="" + [[ "$show_ui" == "yes" && -n "$web_port" ]] && ui_url="http://127.0.0.1:${web_port}" + if [[ -n "$ui_url" ]]; then + args+=(--url "$ui_url") + [[ -n "$url" ]] && args+=(--extra "$url") + local web_pass + web_pass="$(conf_get WEB_PASS "")" + [[ -n "$web_pass" ]] && info "UI de registros: login con password '$web_pass' (usuario vacio). Se recuerda en este perfil." + elif [[ -n "$url" ]]; then + args+=(--url "$url") + fi bash "$BROWSER_FN" "${args[@]}" } +# Abre solo la UI de registros en vivo en el navegador por defecto del sistema. +cmd_ui() { + local web_port + web_port="$(conf_get WEB_PORT "")" + if [[ -z "$web_port" ]]; then + err "El servicio no corre en modo web. Reinstala con: web_proxy install-service --web" + return 1 + fi + local ui_url="http://127.0.0.1:${web_port}" + local web_pass + web_pass="$(conf_get WEB_PASS "")" + info "UI de registros en vivo: $ui_url" + [[ -n "$web_pass" ]] && info "login con password '$web_pass' (usuario vacio)" + if command -v xdg-open &>/dev/null; then + xdg-open "$ui_url" >/dev/null 2>&1 & + fi +} + # Resuelve la lista de archivos a consultar: por defecto la ultima captura. resolve_capture_files() { local out scope="$1" @@ -308,25 +360,49 @@ cmd_ca() { info " ya usa --ignore-certificate-errors si no instalas el CA." } -# Genera e instala el unit systemd --user. El servicio corre mitmdump en -# foreground (systemd gestiona el proceso) con Restart=always. +# Genera e instala el unit systemd --user. El servicio corre en foreground +# (systemd gestiona el proceso) con Restart=always. Con --web usa mitmweb, que +# expone una UI web en vivo (estilo Burp/ZAP) ademas de capturar a disco; sin +# --web usa mitmdump headless. cmd_install_service() { - local port out rotate enable_linger="no" + local port out rotate enable_linger="no" web="no" web_port web_pass port="$(conf_get PORT "$DEFAULT_PORT")" out="$(conf_get OUT "$DEFAULT_OUT")" rotate="$(conf_get ROTATE "$DEFAULT_ROTATE")" + web_port="$(conf_get WEB_PORT "$DEFAULT_WEB_PORT")" + web_pass="$(conf_get WEB_PASS "")" + [[ -n "$(conf_get WEB_PORT "")" ]] && web="yes" while [[ $# -gt 0 ]]; do case "$1" in - --port) port="$2"; shift 2 ;; - --out) out="$2"; shift 2 ;; - --rotate-min) rotate="$2"; shift 2 ;; - --linger) enable_linger="yes"; shift ;; + --port) port="$2"; shift 2 ;; + --out) out="$2"; shift 2 ;; + --rotate-min) rotate="$2"; shift 2 ;; + --web) web="yes"; shift ;; + --web-port) web="yes"; web_port="$2"; shift 2 ;; + --web-password) web="yes"; web_pass="$2"; shift 2 ;; + --headless) web="no"; shift ;; + --linger) enable_linger="yes"; shift ;; *) err "install-service: flag desconocido: $1"; return 2 ;; esac done + [[ "$web" == "yes" && -z "$web_port" ]] && web_port="$DEFAULT_WEB_PORT" + # Password estable para la UI (la auth por token de mitmweb cambia en cada + # arranque; un password fijo da URL estable y cookie persistente en el perfil + # del navegador). Se genera uno aleatorio la primera vez. + if [[ "$web" == "yes" && -z "$web_pass" ]]; then + web_pass="$(tr -dc 'a-z0-9' /dev/null | head -c 10)" + [[ -z "$web_pass" ]] && web_pass="webproxy" + fi - if [[ ! -x "$MITMDUMP_BIN" ]]; then - err "mitmdump no encontrado. Instala con: uv tool install mitmproxy" + local bin="$MITMDUMP_BIN" exec_line + if [[ "$web" == "yes" ]]; then + bin="$MITMWEB_BIN" + exec_line="$MITMWEB_BIN --no-web-open-browser --web-host 127.0.0.1 --web-port $web_port --set web_password=$web_pass -s $ADDON_PATH --set rotate_min=$rotate --set capture_dir=$out --set exclude_hosts=127.0.0.1:$web_port,localhost:$web_port --listen-port $port" + else + exec_line="$MITMDUMP_BIN -s $ADDON_PATH --set rotate_min=$rotate --set capture_dir=$out --listen-port $port" + fi + if [[ ! -x "$bin" ]]; then + err "$(basename "$bin") no encontrado. Instala con: uv tool install mitmproxy" return 1 fi @@ -337,7 +413,9 @@ cmd_install_service() { fi mkdir -p "$SYSTEMD_USER_DIR" "$out" - conf_write "$port" "$out" "$rotate" + conf_write "$port" "$out" "$rotate" \ + "$([[ "$web" == "yes" ]] && echo "$web_port")" \ + "$([[ "$web" == "yes" ]] && echo "$web_pass")" cat > "$SYSTEMD_USER_DIR/$SERVICE_NAME" </dev/null \ @@ -369,6 +451,10 @@ EOF if service_active; then ok "Servicio instalado y ACTIVO en 127.0.0.1:$port." info " capturas -> $out (rotacion cada ${rotate} min)" + if [[ "$web" == "yes" ]]; then + ok " UI viva -> http://127.0.0.1:$web_port (registros en tiempo real)" + info " UI login -> deja el usuario vacio, password: $web_pass" + fi info " logs -> web_proxy logs" info " navegador -> web_proxy browser" [[ "$enable_linger" == "no" ]] && info " persistir tras logout -> web_proxy install-service --linger" @@ -409,13 +495,16 @@ Proxy: status Estado: proxy, servicio, capturas, CA Servicio (siempre activo, systemd --user): - install-service [--port N] [--out DIR] [--rotate-min N] [--linger] + install-service [--port N] [--out DIR] [--rotate-min N] [--web] [--web-port N] [--linger] Instala + arranca como servicio + --web: UI de registros en vivo (mitmweb, estilo Burp) stop-service / uninstall-service Para / desinstala el servicio logs [N] Ultimas N lineas de log Navegacion: - browser [--url URL] [--port N] Lanza Chromium proxeado (perfil aislado) + browser [--url URL] [--port N] [--no-ui] Lanza Chromium proxeado (perfil aislado). + Abre la UI de registros en vivo como primera pestaña. + ui Abre solo la UI de registros en el navegador del sistema ca Instrucciones para confiar en el CA (HTTPS) Consultar capturas: @@ -448,6 +537,7 @@ main() { restart) cmd_restart "$@" ;; status) cmd_status "$@" ;; browser) cmd_browser "$@" ;; + ui) cmd_ui "$@" ;; query) cmd_query "$@" ;; har) cmd_har "$@" ;; inspect) cmd_inspect "$@" ;;