← bitàcola

L'embedder hi diu la seva

Tot allò que és un navegador ja ho decideix el motor

Servo renderitza la pàgina, i per gairebé tot el que fa un navegador aquesta és la resposta sencera. No ho és per la part que un usuari anomena el navegador i no la pàgina: si una petició pot sortir ni que sigui, amb quin full d'estil es vesteix la pàgina, què vol dir una pestanya privada, qui contesta quan un lloc demana una passkey. Això és del shell, i fins aquesta tardor el shell no tenia manera de dir-hi res.

Vuit commits de tramuntana ho han canviat, i tots tenen la mateixa forma. Cadascun agafa una decisió que Servo prenia pel seu compte i la passa a l'embedder — no com un flag de configuració que es llegeix un cop en arrencar, sinó com una pregunta feta en el moment en què la resposta importa.

Un filtre abans de cada fetch

Un bloquejador de contingut ha de vetar una petició abans que arribi a la xarxa. Servo ja tenia RequestInterceptor, que pregunta a l'embedder per un canal i pot substituir una resposta sencera. Aquesta és l'eina correcta per a un grapat de peticions i la equivocada per a totes: una pàgina amb dos-cents subrecursos pagaria dos-cents viatges d'anada i tornada per canal per assabentar-se que cent noranta estaven bé.

RequestFilter és la contrapart barata. És síncron, contesta permetre o bloquejar i res més, i el té el FetchContext en lloc d'abastar-se per un canal. Es consulta a main_fetch després de HSTS i abans de la interceptació, que és l'únic lloc on veu totes les peticions quan surten — inclòs cada salt d'una redirecció, perquè una redirecció torna a passar per main_fetch i per tant es torna a preguntar. Una petició que el filtre bloqueja no toca la xarxa.

Col·locar-lo després de HSTS i no abans importa més del que sembla. El filtre veu la URL que la petició farà servir de debò, promoguda a HTTPS si l'amfitrió és a la llista, de manera que una llista de bloqueig escrita contra https:// no es salta en silenci el mateix amfitrió a http://.

Una cosa el filtre no la podia veure tot sol. Quan una petició li arriba, un handshake de WebSocket té esquema HTTP i destinació buida, exactament com una petició de fetch(). Són indistingibles amb els camps que rep un filtre, i un shell que vulgui permetre els sockets d'un lloc mentre bloqueja els seus rastrejadors els ha de distingir. FilteredRequest::is_websocket porta l'únic camp que ho diu: el mode de la petició.

Fulls d'estil que es poden canviar en calent

El filtratge cosmètic d'anuncis necessita un full d'estil diferent per pàgina. UserContentManager només podia afegir i treure fulls d'un en un, aplicats en crear un pipeline, cosa que està malament per dos costats: hi ha una finestra en què les regles de la pàgina que se'n va i les de la que arriba són vives alhora, i un document que ja existeix no se n'assabentava gens.

SetUserStyleSheets reemplaça el conjunt sencer d'un gestor en una sola acció, així que aquesta finestra no existeix. I el canvi ara arriba a documents vius: el script thread el propaga en lloc de deixar-lo per al pipeline següent.

I després el panic. Un Layout guarda els fulls d'usuari del seu WebView però només els lliura al Stylist durant el primer reflow, així que abans d'aquest reflow el Stylist no en té cap. Window::replace_user_stylesheets treia els fulls anteriors i afegia els nous, cosa que en un WebView que encara no havia fet reflow demanava al Stylist que deixés anar un full que no havia vist mai:

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

Un embedder fent el que és obvi — posa el full d'aquest lloc, després navega — se l'empassava sempre. L'arranjament és que treure sigui una operació buida abans del primer reflow, en lloc d'un índex en una llista que encara és buida.

Scripts triats segons la pàgina que carrega

Els user scripts necessitaven les dues mateixes coses que els fulls d'estil, i una més. UserContentManager::set_scripts reemplaça el conjunt sencer de cop, i els documents creats després executen el conjunt nou.

La peça extra és saber *quan* triar. Un embedder que tria scripts per lloc ha de decidir en l'únic moment en què la destinació es coneix i el document encara no existeix, que és request_navigation. Però aquest callback també salta per frames imbricats, i canviar el conjunt sencer de scripts perquè ha navegat un iframe de publicitat seria un error. NavigationRequest::is_for_main_frame separa una navegació del frame principal del WebView d'una d'un frame de dins.

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 pestanya privada, definida amb precisió

WebViewBuilder::temporary_session posa un webview en una sessió pròpia, amb un id que tria l'embedder. Galetes, memòria cau HTTP, llista HSTS, credencials i emmagatzematge de lloc viuen a la memòria, es comparteixen només amb els webviews de la mateixa sessió, i desapareixen quan es crida Servo::close_temporary_session i es tanca l'últim dels seus webviews.

El detall que ho fa usable és l'herència: un webview que obre una pàgina és a la sessió de qui l'ha obert. Sense aquesta regla, una pestanya privada que segueix un enllaç amb target=_blank filtra la pàgina següent al pot públic, que és justament el que una pestanya privada existeix per evitar.

El shell com a autenticador

navigator.credentials.create() i get() amb un membre publicKey arriben ara a l'embedder com un PasskeyRequest per WebViewDelegate::request_passkey. L'embedder és l'autenticador: decideix si la pàgina pot parlar per la relying party que anomena, ho pregunta a l'usuari i respon.

La propietat de seguretat és en quin origen rep. A l'embedder se li dóna l'origen que Servo coneix per al document, no un que digui la pàgina, així que una pàgina no pot demanar una credencial d'algú altre fent-se passar per ell. Una petició que es descarta, o que cap delegate no agafa, es rebutja en lloc de quedar-se penjada.

I una que l'embedder tenia malament sobre ell mateix

El que un global hereta sobre ser un context segur ve de qui el crea: el pare d'un frame, l'amo d'un worker. Per a un document de nivell superior el valor heretat era el de la pàgina des de l'enllaç de la qual s'hi havia arribat, així que una pàgina HTTPS oberta des d'una HTTP — o des d'una pàgina del mateix embedder — no es tractava com a context segur, i tot el que en depèn deixava de funcionar sense dir per què.

Una finestra de nivell superior es jutja ara només per la seva pròpia URL de creació, com diu l'especificació. La pàgina d'inici del mateix shell era un dels creadors que podien enverinar el judici, cosa que és un bon argument perquè l'embedder no sigui un cas especial.

El que això no resol

El filtre és síncron, cosa que el fa barat i també el limita: un embedder no pot anar a preguntar a un servei remot si permet una petició sense bloquejar el fetch mentre ho fa. Per a això l'interceptor i el seu canal continuen sent l'única opció, i continuen sent massa cars per subrecurs.

Les sessions temporals tenen els seus propis límits de recursos, i a rumb encara no les fa servir res — el shell no té cap pestanya privada on posar-les. El mateix amb el delegate de passkeys: la canonada arriba a l'embedder, i l'embedder ara mateix rebutja totes les peticions per no implementar-lo.

I res d'això no és aïllament de llocs. Cadascuna d'aquestes decisions es continua prenent en un procés al costat del renderer, que és el trigger que anomena el full de ruta i que res d'aquí no mou.

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 primer: permetre o bloquejar, sense canal i sense cos de resposta — i cada salt de redirecció es torna a preguntar.