chore: initial sync

This commit is contained in:
fn-registry agent
2026-06-02 21:50:21 +02:00
parent 97dc5a35cf
commit c4c8e53915
5 changed files with 560 additions and 20 deletions
+104
View File
@@ -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();
}
});
+15
View File
@@ -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": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "web_proxy: activar/desactivar captura",
"default_popup": "popup.html"
}
}
+200
View File
@@ -0,0 +1,200 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
--accent: #7c3aed;
--green: #16a34a;
--bg: #ffffff;
--fg: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1f2937;
--fg: #f3f4f6;
--muted: #9ca3af;
--border: #374151;
}
}
body {
font-family: system-ui, sans-serif;
background: var(--bg);
color: var(--fg);
width: 300px;
margin: 0;
padding: 14px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
h1 {
font-size: 14px;
margin: 0;
}
.switch {
position: relative;
display: inline-block;
width: 46px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #9ca3af;
border-radius: 26px;
transition: 0.2s;
}
.slider::before {
content: "";
position: absolute;
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background: #fff;
border-radius: 50%;
transition: 0.2s;
}
input:checked + .slider {
background: var(--green);
}
input:checked + .slider::before {
transform: translateX(20px);
}
.status {
font-size: 12px;
color: var(--muted);
margin-bottom: 10px;
}
ul {
list-style: none;
margin: 0 0 10px;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
}
li:last-child {
border-bottom: none;
}
li .meta {
flex: 1;
min-width: 0;
}
li .name {
font-size: 13px;
font-weight: 600;
}
li .addr {
font-size: 11px;
color: var(--muted);
font-family: monospace;
}
li .del {
background: none;
border: none;
color: var(--muted);
cursor: pointer;
font-size: 14px;
}
details {
font-size: 12px;
}
summary {
cursor: pointer;
color: var(--accent);
margin-bottom: 8px;
}
.form-row {
display: flex;
gap: 6px;
margin-bottom: 6px;
}
input[type="text"],
input[type="number"],
select {
font-size: 12px;
padding: 5px 6px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--fg);
width: 100%;
box-sizing: border-box;
}
button.add {
width: 100%;
padding: 7px;
background: var(--accent);
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.hint {
font-size: 11px;
color: var(--muted);
margin-top: 10px;
line-height: 1.4;
}
</style>
</head>
<body>
<header>
<h1>web_proxy</h1>
<label class="switch">
<input type="checkbox" id="toggle" />
<span class="slider"></span>
</label>
</header>
<div class="status" id="status">Captura desactivada</div>
<ul id="proxy-list"></ul>
<details>
<summary>Anadir proxy</summary>
<div class="form-row">
<input type="text" id="f-name" placeholder="Nombre" />
</div>
<div class="form-row">
<select id="f-scheme">
<option value="http">http</option>
<option value="https">https</option>
<option value="socks5">socks5</option>
<option value="socks4">socks4</option>
</select>
<input type="text" id="f-host" placeholder="host (127.0.0.1)" />
<input type="number" id="f-port" placeholder="puerto" />
</div>
<button class="add" id="f-add">Anadir</button>
</details>
<div class="hint">
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).
</div>
<script src="popup.js"></script>
</body>
</html>
+131
View File
@@ -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();
+110 -20
View File
@@ -45,6 +45,7 @@ SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
DEFAULT_PORT=8080 DEFAULT_PORT=8080
DEFAULT_OUT="$HOME/captures" DEFAULT_OUT="$HOME/captures"
DEFAULT_ROTATE=20 DEFAULT_ROTATE=20
DEFAULT_WEB_PORT=8081
MITMDUMP_BIN="$(command -v mitmdump 2>/dev/null || echo "$HOME/.local/bin/mitmdump")" 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")" MITMWEB_BIN="$(command -v mitmweb 2>/dev/null || echo "$HOME/.local/bin/mitmweb")"
@@ -70,14 +71,24 @@ conf_get() {
} }
conf_write() { conf_write() {
local port="$1" out="$2" rotate="$3" local port="$1" out="$2" rotate="$3" web_port="${4:-}" web_pass="${5:-}"
cat > "$CONFFILE" <<EOF cat > "$CONFFILE" <<EOF
PORT=$port PORT=$port
OUT=$out OUT=$out
ROTATE=$rotate ROTATE=$rotate
WEB_PORT=$web_port
WEB_PASS=$web_pass
EOF EOF
} }
# URL base de la UI de registros en vivo (sin token; la auth es por password).
web_ui_url() {
local web_port
web_port="$(conf_get WEB_PORT "")"
[[ -z "$web_port" ]] && return 1
printf 'http://127.0.0.1:%s/' "$web_port"
}
# PID del proxy manual, si vive. Imprime el PID o nada. # PID del proxy manual, si vive. Imprime el PID o nada.
running_pid() { running_pid() {
[[ -f "$PIDFILE" ]] || return 1 [[ -f "$PIDFILE" ]] || return 1
@@ -188,6 +199,14 @@ cmd_status() {
info " servicio: no instalado" info " servicio: no instalado"
fi fi
local web_port web_pass
web_port="$(conf_get WEB_PORT "")"
web_pass="$(conf_get WEB_PASS "")"
if [[ -n "$web_port" ]]; then
ok " UI viva: http://127.0.0.1:$web_port (registros en tiempo real)"
[[ -n "$web_pass" ]] && info " UI login: password $web_pass (deja el usuario vacio)"
fi
if [[ -d "$out" ]]; then if [[ -d "$out" ]]; then
local n size local n size
n="$(find "$out" -maxdepth 1 -name 'traffic-*.mitm' 2>/dev/null | wc -l)" n="$(find "$out" -maxdepth 1 -name 'traffic-*.mitm' 2>/dev/null | wc -l)"
@@ -206,12 +225,14 @@ cmd_status() {
} }
cmd_browser() { cmd_browser() {
local url="" proxy_port local url="" proxy_port show_ui="yes" web_port
proxy_port="$(conf_get PORT "$DEFAULT_PORT")" proxy_port="$(conf_get PORT "$DEFAULT_PORT")"
web_port="$(conf_get WEB_PORT "")"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--url) url="$2"; shift 2 ;; --url) url="$2"; shift 2 ;;
--port) proxy_port="$2"; shift 2 ;; --port) proxy_port="$2"; shift 2 ;;
--no-ui) show_ui="no"; shift ;;
*) err "browser: flag desconocido: $1"; return 2 ;; *) err "browser: flag desconocido: $1"; return 2 ;;
esac esac
done done
@@ -222,11 +243,42 @@ cmd_browser() {
fi fi
local args=(--proxy "http://127.0.0.1:${proxy_port}" --profile "$WEB_PROXY_HOME/chromium-profile") 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. # Primera pestaña: la UI de registros en vivo (si el servicio corre en modo
[[ -n "$url" ]] && args+=(--url "$url") # 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[@]}" 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. # Resuelve la lista de archivos a consultar: por defecto la ultima captura.
resolve_capture_files() { resolve_capture_files() {
local out scope="$1" local out scope="$1"
@@ -308,25 +360,49 @@ cmd_ca() {
info " ya usa --ignore-certificate-errors si no instalas el CA." info " ya usa --ignore-certificate-errors si no instalas el CA."
} }
# Genera e instala el unit systemd --user. El servicio corre mitmdump en # Genera e instala el unit systemd --user. El servicio corre en foreground
# foreground (systemd gestiona el proceso) con Restart=always. # (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() { 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")" port="$(conf_get PORT "$DEFAULT_PORT")"
out="$(conf_get OUT "$DEFAULT_OUT")" out="$(conf_get OUT "$DEFAULT_OUT")"
rotate="$(conf_get ROTATE "$DEFAULT_ROTATE")" 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 while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--port) port="$2"; shift 2 ;; --port) port="$2"; shift 2 ;;
--out) out="$2"; shift 2 ;; --out) out="$2"; shift 2 ;;
--rotate-min) rotate="$2"; shift 2 ;; --rotate-min) rotate="$2"; shift 2 ;;
--linger) enable_linger="yes"; shift ;; --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 ;; *) err "install-service: flag desconocido: $1"; return 2 ;;
esac esac
done 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/urandom 2>/dev/null | head -c 10)"
[[ -z "$web_pass" ]] && web_pass="webproxy"
fi
if [[ ! -x "$MITMDUMP_BIN" ]]; then local bin="$MITMDUMP_BIN" exec_line
err "mitmdump no encontrado. Instala con: uv tool install mitmproxy" 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 return 1
fi fi
@@ -337,7 +413,9 @@ cmd_install_service() {
fi fi
mkdir -p "$SYSTEMD_USER_DIR" "$out" 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" <<EOF cat > "$SYSTEMD_USER_DIR/$SERVICE_NAME" <<EOF
[Unit] [Unit]
@@ -346,7 +424,7 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
ExecStart=$MITMDUMP_BIN -s $ADDON_PATH --set rotate_min=$rotate --set capture_dir=$out --listen-port $port ExecStart=$exec_line
Restart=always Restart=always
RestartSec=2 RestartSec=2
# Restart=always (no on-failure): un SIGTERM limpio es exit success y # Restart=always (no on-failure): un SIGTERM limpio es exit success y
@@ -357,7 +435,11 @@ WantedBy=default.target
EOF EOF
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now "$SERVICE_NAME" systemctl --user enable "$SERVICE_NAME"
# restart (no enable --now): si el servicio ya estaba activo, --now NO lo
# reinicia y seguiria corriendo con el unit viejo (password/puerto previos).
# restart fuerza recarga del unit actual, arrancando si estaba parado.
systemctl --user restart "$SERVICE_NAME"
if [[ "$enable_linger" == "yes" ]]; then if [[ "$enable_linger" == "yes" ]]; then
loginctl enable-linger "$USER" 2>/dev/null \ loginctl enable-linger "$USER" 2>/dev/null \
@@ -369,6 +451,10 @@ EOF
if service_active; then if service_active; then
ok "Servicio instalado y ACTIVO en 127.0.0.1:$port." ok "Servicio instalado y ACTIVO en 127.0.0.1:$port."
info " capturas -> $out (rotacion cada ${rotate} min)" 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 " logs -> web_proxy logs"
info " navegador -> web_proxy browser" info " navegador -> web_proxy browser"
[[ "$enable_linger" == "no" ]] && info " persistir tras logout -> web_proxy install-service --linger" [[ "$enable_linger" == "no" ]] && info " persistir tras logout -> web_proxy install-service --linger"
@@ -409,13 +495,16 @@ Proxy:
status Estado: proxy, servicio, capturas, CA status Estado: proxy, servicio, capturas, CA
Servicio (siempre activo, systemd --user): 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 Instala + arranca como servicio
--web: UI de registros en vivo (mitmweb, estilo Burp)
stop-service / uninstall-service Para / desinstala el servicio stop-service / uninstall-service Para / desinstala el servicio
logs [N] Ultimas N lineas de log logs [N] Ultimas N lineas de log
Navegacion: 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) ca Instrucciones para confiar en el CA (HTTPS)
Consultar capturas: Consultar capturas:
@@ -448,6 +537,7 @@ main() {
restart) cmd_restart "$@" ;; restart) cmd_restart "$@" ;;
status) cmd_status "$@" ;; status) cmd_status "$@" ;;
browser) cmd_browser "$@" ;; browser) cmd_browser "$@" ;;
ui) cmd_ui "$@" ;;
query) cmd_query "$@" ;; query) cmd_query "$@" ;;
har) cmd_har "$@" ;; har) cmd_har "$@" ;;
inspect) cmd_inspect "$@" ;; inspect) cmd_inspect "$@" ;;