← bitácora

El embedder opina

Todo lo que es un navegador ya lo decide el motor

Servo renderiza la página, y para casi todo lo que hace un navegador esa es la respuesta completa. No lo es para la parte que un usuario llama el navegador y no la página: si una petición puede salir siquiera, con qué hoja de estilo se viste la página, qué significa una pestaña privada, quién contesta cuando un sitio pide una passkey. Eso es del shell, y hasta este otoño el shell no tenía manera de decir nada al respecto.

Ocho commits de tramuntana lo cambiaron, y todos tienen la misma forma. Cada uno coge una decisión que Servo tomaba por su cuenta y se la pasa al embedder — no como un flag de configuración que se lee una vez al arrancar, sino como una pregunta hecha en el momento en que la respuesta importa.

Un filtro antes de cada fetch

Un bloqueador de contenido tiene que vetar una petición antes de que llegue a la red. Servo ya tenía RequestInterceptor, que pregunta al embedder por un canal y puede sustituir una respuesta entera. Esa es la herramienta correcta para un puñado de peticiones y la equivocada para todas: una página con doscientos subrecursos pagaría doscientos viajes de ida y vuelta por canal para enterarse de que ciento noventa estaban bien.

RequestFilter es la contraparte barata. Es síncrono, contesta permitir o bloquear y nada más, y lo tiene el FetchContext en vez de alcanzarse por un canal. Se consulta en main_fetch después de HSTS y antes de la interceptación, que es el único sitio donde ve todas las peticiones al salir — incluido cada salto de una redirección, porque una redirección vuelve a pasar por main_fetch y por tanto se vuelve a preguntar. Una petición que el filtro bloquea no toca la red.

Colocarlo después de HSTS y no antes importa más de lo que parece. El filtro ve la URL que la petición va a usar de verdad, promovida a HTTPS si el host está en la lista, así que una lista de bloqueo escrita contra https:// no se salta en silencio el mismo host en http://.

Una cosa el filtro no la podía ver por sí solo. Cuando una petición le llega, un handshake de WebSocket tiene esquema HTTP y destino vacío, exactamente igual que una petición de fetch(). Son indistinguibles con los campos que recibe un filtro, y un shell que quiera permitir los sockets de un sitio mientras bloquea sus rastreadores necesita distinguirlos. FilteredRequest::is_websocket lleva el único campo que lo dice: el modo de la petición.

Hojas de estilo que se pueden cambiar en caliente

El filtrado cosmético de anuncios necesita una hoja de estilo distinta por página. UserContentManager solo podía añadir y quitar hojas de una en una, aplicadas al crear un pipeline, que está mal por dos lados: hay una ventana en la que las reglas de la página que se va y las de la que llega están vivas a la vez, y un documento que ya existe no se enteraba del cambio en absoluto.

SetUserStyleSheets reemplaza el conjunto entero de un gestor en una sola acción, así que esa ventana no existe. Y el cambio ahora llega a documentos vivos: el script thread lo propaga en vez de dejarlo para el siguiente pipeline.

Y luego el panic. Un Layout guarda las hojas de usuario de su WebView pero solo se las entrega al Stylist durante el primer reflow, así que antes de ese reflow el Stylist no tiene ninguna. Window::replace_user_stylesheets quitaba las hojas anteriores y añadía las nuevas, lo que en un WebView que todavía no había hecho reflow le pedía al Stylist que soltara una hoja que no había visto nunca:

stylesheet_set.rs:238: called Option::unwrap() on a None value

Un embedder haciendo lo obvio — pon la hoja de este sitio, luego navega — se lo comía siempre. El arreglo es que quitar sea una operación vacía antes del primer reflow, en vez de un índice en una lista que todavía está vacía.

Scripts elegidos según la página que carga

Los user scripts necesitaban las dos mismas cosas que las hojas de estilo, y una más. UserContentManager::set_scripts reemplaza el conjunto entero de golpe, y los documentos creados después ejecutan el nuevo conjunto.

La pieza extra es saber *cuándo* elegir. Un embedder que elige scripts por sitio tiene que decidir en el único momento en que el destino se conoce y el documento todavía no existe, que es request_navigation. Pero ese callback también salta para frames anidados, y cambiar el conjunto entero de scripts porque ha navegado un iframe de publicidad sería un error. NavigationRequest::is_for_main_frame separa una navegación del frame principal del WebView de una de un frame de dentro.

PUBLIC SESSION cookies · HTTP cache · HSTS credentials · site storage memory tier, bounded by bytes disk tier under Opts::cache_dir survives a restart TEMPORARY SESSION, ID THE EMBEDDER PICKS cookies · HTTP cache · HSTS credentials · site storage in memory only no disk tier gone on close_temporary_session WebViewBuilder::temporary_session puts one webview in a session of its own. A webview a page opens lands in the session of its opener, not the public one. nothing shared

Una pestaña privada, definida con precisión

WebViewBuilder::temporary_session pone un webview en una sesión propia, con un id que elige el embedder. Cookies, caché HTTP, lista HSTS, credenciales y almacenamiento de sitio viven en memoria, se comparten solo con los webviews de la misma sesión, y desaparecen cuando se llama a Servo::close_temporary_session y se cierra el último de sus webviews.

El detalle que lo hace usable es la herencia: un webview que abre una página está en la sesión de quien lo abrió. Sin esa regla, una pestaña privada que sigue un enlace con target=_blank filtra la página siguiente al tarro público, que es justo lo que una pestaña privada existe para evitar.

El shell como autenticador

navigator.credentials.create() y get() con un miembro publicKey llegan ahora al embedder como un PasskeyRequest por WebViewDelegate::request_passkey. El embedder es el autenticador: decide si la página puede hablar por la relying party que nombra, le pregunta al usuario y responde.

La propiedad de seguridad está en qué origen recibe. Al embedder se le da el origen que Servo conoce para el documento, no uno que diga la página, así que una página no puede pedir una credencial de otro haciéndose pasar por él. Una petición que se descarta, o que ningún delegate coge, se rechaza en vez de quedarse colgada.

Y una que el embedder tenía mal sobre sí mismo

Lo que un global hereda sobre ser un contexto seguro viene de quien lo crea: el padre de un frame, el dueño de un worker. Para un documento de nivel superior el valor heredado era el de la página desde cuyo enlace se había llegado, así que una página HTTPS abierta desde una HTTP — o desde una página del propio embedder — no se trataba como contexto seguro, y todo lo que depende de eso dejaba de funcionar sin decir por qué.

Una ventana de nivel superior se juzga ahora solo por su propia URL de creación, como dice la especificación. La página de inicio del propio shell era uno de los creadores que podían envenenar el juicio, que es un buen argumento para que el embedder no sea un caso especial.

Lo que esto no resuelve

El filtro es síncrono, que es lo que lo hace barato y también lo que lo limita: un embedder no puede ir a preguntarle a un servicio remoto si permite una petición sin bloquear el fetch mientras lo hace. Para eso el interceptor y su canal siguen siendo la única opción, y siguen siendo demasiado caros por subrecurso.

Las sesiones temporales tienen sus propios límites de recursos, y en rumb todavía no las usa nada — el shell no tiene una pestaña privada donde ponerlas. Lo mismo con el delegate de passkeys: la tubería llega al embedder, y el embedder ahora mismo rechaza todas las peticiones por no implementarlo.

Y nada de esto es aislamiento de sitios. Cada una de estas decisiones se sigue tomando en un proceso junto al renderer, que es el trigger que nombra el roadmap y que nada de aquí mueve.

MAIN_FETCH, ONE REQUEST HSTS RequestFilter synchronous RequestInterceptor a channel round trip network allow / block no channel, no response body blocked here and the network is never reached each redirect hop, asked again a WebSocket arrives with an HTTP scheme and an empty destination: only FilteredRequest::is_websocket, read off the request mode, tells it apart
La pregunta barata va primero: permitir o bloquear, sin canal y sin cuerpo de respuesta — y cada salto de redirección se vuelve a preguntar.