// Página de opciones del proxy por contenedor. // // DOS CAPAS, IGUAL QUE EN `fondo.js`: lo que la distro deja de fábrica llega por // `storage.managed` (política de empresa, `3rdparty.Extensions` en `distribution/policies.json`) y // es de SÓLO LECTURA para esta página; lo que el usuario decide se guarda en `storage.local` y pisa // a lo anterior contenedor por contenedor. «Volver a los de fábrica» no reescribe valores: BORRA la // capa del usuario, que es lo único que deja el resultado igual a un perfil recién creado. const TIPOS = [ ["direct", "Directo (sin proxy)"], ["socks", "SOCKS5"], ["socks4", "SOCKS4"], ["http", "HTTP"], ["https", "HTTPS"], ]; const filas = document.getElementById("filas"); const vacio = document.getElementById("vacio"); const estado = document.getElementById("estado"); function decir(msg, error) { estado.textContent = msg; estado.style.color = error ? "#c15728" : ""; } async function leerCapa(area, nombre) { try { const r = await browser.storage[area].get("proxies"); return (r && r.proxies) || {}; } catch (e) { // `storage.managed` LANZA cuando no hay política; no es un fallo, es la ausencia de la capa. console.info(`atuq/proxy: sin capa ${nombre} (${e})`); return {}; } } function fila(identidad, deFabrica, delUsuario) { const valor = delUsuario || deFabrica || { type: "direct" }; const tr = document.createElement("tr"); tr.dataset.nombre = identidad.name; const tdNombre = document.createElement("td"); const cont = document.createElement("div"); cont.className = "nombre"; const punto = document.createElement("span"); punto.className = "punto"; // `colorCode` viene del propio contenedor: el punto de esta tabla y la raya de la pestaña son el // mismo color porque salen del mismo sitio, no porque los hayamos igualado a ojo. punto.style.background = identidad.colorCode || "#888"; cont.append(punto, document.createTextNode(identidad.name)); if (deFabrica && !delUsuario) { const et = document.createElement("span"); et.className = "fabrica"; et.textContent = "de fábrica"; et.title = "Viene de la política de la distro; si lo cambiás, tu valor manda."; cont.append(et); } tdNombre.append(cont); const tdTipo = document.createElement("td"); const sel = document.createElement("select"); for (const [v, etiqueta] of TIPOS) { const o = document.createElement("option"); o.value = v; o.textContent = etiqueta; sel.append(o); } sel.value = TIPOS.some(([v]) => v === valor.type) ? valor.type : "direct"; tdTipo.append(sel); const tdHost = document.createElement("td"); const host = document.createElement("input"); host.type = "text"; host.className = "host"; host.placeholder = "127.0.0.1"; host.value = valor.host || ""; tdHost.append(host); const tdPort = document.createElement("td"); const port = document.createElement("input"); port.type = "number"; port.className = "port"; port.min = "1"; port.max = "65535"; port.placeholder = "1080"; port.value = valor.port || ""; tdPort.append(port); const tdDns = document.createElement("td"); tdDns.className = "dns"; const dns = document.createElement("input"); dns.type = "checkbox"; // Prendido por defecto y no apagado: sin esto el navegador resuelve el nombre por su cuenta antes // de hablar con el proxy y la consulta DNS sale por la línea que se quería evitar. Es la fuga // clásica de esta configuración, así que el default es el seguro. dns.checked = valor.proxyDNS !== false; dns.title = "Resolver los nombres en el proxy y no en esta máquina. Sólo aplica a SOCKS."; tdDns.append(dns); const sincronizar = () => { const directo = sel.value === "direct"; const socks = sel.value === "socks" || sel.value === "socks4"; host.disabled = directo; port.disabled = directo; dns.disabled = !socks; }; sel.addEventListener("change", sincronizar); sincronizar(); tr.append(tdNombre, tdTipo, tdHost, tdPort, tdDns); tr.leer = () => { const tipo = sel.value; if (tipo === "direct") return { type: "direct" }; const p = Number(port.value); if (!host.value.trim()) throw new Error(`«${identidad.name}»: falta el servidor`); if (!Number.isInteger(p) || p < 1 || p > 65535) { throw new Error(`«${identidad.name}»: el puerto tiene que estar entre 1 y 65535`); } const info = { type: tipo, host: host.value.trim(), port: p }; if (tipo === "socks" || tipo === "socks4") info.proxyDNS = dns.checked; return info; }; return tr; } async function pintar() { filas.textContent = ""; // ── «NO HAY CONTENEDORES» Y «NO HAY API» SE PARECEN Y NO SON LO MISMO ──────────────────────── // Medido, no supuesto: abriendo esta página mientras el arranque de las extensiones todavía corre // —pasa en un arranque en frío, y pasó de verdad en la prueba headless— el global `browser` aún // no está inyectado. Sin este corte, cada `await` de abajo tira `ReferenceError`, los catch lo // tragan y la página termina diciendo «No hay contenedores en este perfil»: un mensaje FALSO que // manda a mirar al sitio equivocado. Un ausente tiene que fallar ruidoso. if (typeof browser === "undefined" || !browser.contextualIdentities) { vacio.hidden = true; decir("Las APIs de la extensión todavía no están listas — recargá la página en un momento.", true); return; } const [deFabrica, delUsuario] = await Promise.all([ leerCapa("managed", "de fábrica"), leerCapa("local", "del usuario"), ]); let identidades = []; try { identidades = await browser.contextualIdentities.query({}); } catch (e) { decir(`No pude listar los contenedores: ${e}`, true); } vacio.hidden = identidades.length > 0; for (const id of identidades) { filas.append(fila(id, deFabrica[id.name], delUsuario[id.name])); } // Un proxy configurado para un contenedor que no existe es la causa número uno de «lo configuré y // no pasa nada» — casi siempre porque el contenedor se renombró. Se dice acá, no en la consola. const nombres = new Set(identidades.map(i => i.name)); const huerfanos = [...new Set([...Object.keys(deFabrica), ...Object.keys(delUsuario)])] .filter(n => !nombres.has(n)); if (huerfanos.length) { decir(`Configurados sin contenedor que les corresponda: ${huerfanos.join(", ")}`, true); } } document.getElementById("guardar").addEventListener("click", async () => { const proxies = {}; try { for (const tr of filas.children) proxies[tr.dataset.nombre] = tr.leer(); } catch (e) { // No se guarda NADA si una fila está mal. Guardar las buenas y saltar la rota dejaría al // usuario creyendo que guardó, con un contenedor saliendo por donde no quería. decir(e.message, true); return; } await browser.storage.local.set({ proxies }); decir("Guardado. Vale para las peticiones nuevas, sin reiniciar."); }); document.getElementById("restablecer").addEventListener("click", async () => { await browser.storage.local.remove("proxies"); await pintar(); decir("Listo: quedó lo que trae la distro de fábrica."); }); pintar();