L'articolo precedente lo avevo chiuso con una promessa che suonava bene: «la prossima versione stabile parte da qui». Qui era la v1.3.0, il redesign integrale, l'Electric Azure su nero profondo, l'interfaccia ricostruita da zero in tre giorni di febbre. L'app era diventata bella. Sembrava finita.
È esattamente quando un'applicazione sembra finita che dovresti diffidare di più.
Sono passate due settimane. In quelle due settimane non ho aggiunto nemmeno un bottone. Non ho cambiato un colore. Ho fatto una cosa diversa e molto meno divertente: mi sono seduto e ho cercato di immaginare tutti i modi in cui Runtime FeedDownloader Pro poteva farmi del male. Non a me che l'ho scritto. A chi lo usa.
Questo è il racconto di quella mattina. Perché è successo quasi tutto in una mattina sola: gli undici commit che vanno dalla v1.3.1 alla v1.3.11 portano timbri che vanno dalle otto alle dieci e mezza del 29 maggio. Una corsa, ma di un genere che non avevo ancora raccontato. La prima volta non stavo costruendo. Stavo cercando di scassinare quello che avevo costruito.
Il mestiere di cercarsi i guai

C'è una differenza profonda tra scrivere software e fare un audit di sicurezza, e ha a che fare con la testa più che con le mani.
Quando costruisci, pensi a cosa deve succedere. L'utente incolla l'indirizzo di un feed, l'app scarica la lista degli episodi, li mette in fila, li archivia. Pensi al percorso giusto e lo lastrichi per bene. Quando fai un audit, invece, devi pensare a cosa non deve succedere e proverà a succedere lo stesso. Devi smettere di essere l'autore e diventare l'ospite indesiderato. Devi guardare ogni porta che hai lasciato aperta per comodità e chiederti chi altro potrebbe passarci.
Un downloader di podcast sembra un programma innocuo. Prende dei file da internet e li salva su disco. Cosa vuoi che vada storto. Il problema è proprio lì, nella parola «internet»: l'app scarica da indirizzi che non decido io. Li decide il feed. E il feed lo sceglie chi lo pubblica, che non sono sempre persone gentili.
Quando ho cominciato a contare i punti deboli, li ho ordinati come fa un pronto soccorso: per gravità. Gravissimi, gravi, medi, lievi. Mi sono ritrovato davanti una lista più lunga di quanto sperassi. E sono partito dal piano più alto.
Le porte di casa tua
La cosa peggiore l'ho trovata subito, ed era anche la più elegante nella sua cattiveria.
L'app, prima di scaricare qualunque cosa, controllava l'indirizzo. Si chiama validateUrl, e il suo lavoro è dire: questo indirizzo punta a un sito pubblico, va bene, oppure punta a un posto che non dovrebbe toccare, fermati. I posti vietati sono quelli interni: il tuo router, la tua rete locale, gli indirizzi privati che vivono dentro casa tua e non dovrebbero mai essere raggiungibili da un feed di podcast.
Il controllo c'era. Funzionava. E non serviva a niente.
Serviva a niente per due motivi che ho dovuto guardare in faccia uno alla volta. Il primo: il controllo veniva fatto una volta sola, sull'indirizzo iniziale. Ma un server, quando lo interroghi, può risponderti «non sono qui, vai da quest'altra parte». È un redirect, una cosa normalissima del web. L'app seguiva il redirect senza ricontrollare dove la stava mandando. Un feed ostile poteva dire all'app: «scarica da questo indirizzo pulito», l'app validava l'indirizzo pulito, faceva la richiesta, e il server rispondeva «ah, in realtà vai qui», dove qui era l'indirizzo interno della rete cloud che custodisce le credenziali di una macchina. L'app, ubbidiente, ci andava.
Il secondo motivo era ancora più sottile, e ha un nome che sembra il titolo di un film: DNS rebinding. Il controllo guardava la stringa dell'indirizzo, il nome del dominio. Ma un nome di dominio è solo un'etichetta: dietro, c'è un numero, l'IP vero, e chi possiede il dominio può cambiarlo quando vuole. Un sito chiamato attacker.com può giurare di vivere su internet e in realtà puntare a 127.0.0.1, cioè al computer su cui gira l'app. Il filtro leggeva l'etichetta, vedeva un nome pubblico, e lasciava passare.
Tradotto: bastava un feed costruito apposta per convincere l'app a bussare alle porte di casa tua. Alla tua rete. Ai tuoi servizi interni. Una cassaforte per podcast che, con le parole giuste sussurrate da un file XML, andava a curiosare dove non doveva.
Una vulnerabilità non è un errore di battitura. È una porta che hai lasciato aperta convinto che nessuno conoscesse l'indirizzo.
La cura non poteva stare nella stringa, perché la stringa mente. Doveva stare più in basso, là dove l'app apre davvero la connessione. Ho scritto un nuovo strato, safeHttp, che si infila nel punto esatto in cui il programma traduce un nome in un numero e prova a collegarsi. A ogni connessione, iniziale o successiva, a ogni redirect, l'app adesso risolve il nome, guarda il numero vero, e se quel numero appartiene a casa tua, chiude tutto. Non c'è etichetta che tenga, perché il controllo avviene sul numero, non sul nome. Ho coperto tutti i travestimenti che un indirizzo può indossare: gli IP scritti in forma decimale per confondere, quelli mascherati dentro indirizzi di nuova generazione, le fasce riservate che quasi nessuno conosce a memoria. E gli stessi guanti li ho messi alla copertina degli episodi, l'immagine che finisce nei tag dei file MP3, che fino a quel momento veniva scaricata senza nessun controllo.
Questa è stata la v1.3.1. Una release che non cambia niente di visibile. Apri l'app e è identica a prima. È il tipo di lavoro che nessuno applaude, perché si vede solo quando manca.
Il motore che mentiva
Chiusa la porta più pericolosa, sono sceso nel motore. È la parte dell'app che scarica davvero i file, uno dopo l'altro, gestisce la coda, conta quanti ne sono andati a buon fine. Sembrava solido. Aveva qualche crepa.
La prima mi ha fatto quasi tenerezza, per quanto era stupida. Quando un episodio veniva tolto dal server, l'app riceveva la risposta «non esiste più», quel famoso 404 che tutti conosciamo. Avevo scritto il codice per gestirlo: se ricevi un 404, di' all'utente che l'episodio non c'è più e fermati. Solo che quel codice non veniva mai eseguito. La libreria che faccio le richieste, di sua iniziativa, trattava il 404 come un errore generico e lo buttava nel calderone dei fallimenti normali, quelli che si riprovano. Risultato: un episodio sparito dal server veniva richiesto tre volte di fila, inutilmente, per poi arrendersi senza mai dire la verità, cioè che non c'era niente da scaricare. Ho dovuto dire alla libreria di smettere di giudicare al posto mio e lasciarmi ispezionare ogni risposta. Adesso un 404 è un 404: niente tentativi, un messaggio onesto.
Poi c'era il pulsante Stop. Quando avviavi lo scaricamento di venti episodi e poi premevi Stop, l'app svuotava la coda di quelli in attesa. Ma quelli già partiti continuavano per conto loro, indifferenti, fino in fondo. Premere Stop e vedere i download proseguire è il genere di cosa che ti fa dubitare del programma intero. Adesso ogni scaricamento attivo ha una sua leva di emergenza, e lo Stop la tira per tutti, davvero.
C'è stato un bug che mi ha costretto a riscrivere da capo il modo in cui l'app conta i download in gruppo. Il problema era che non esiste un momento preciso in cui un «gruppo» comincia o finisce: l'app lo deduceva da una pausa, da una finestra di silenzio. Se ne arrivava uno nuovo mentre il gruppo precedente stava ancora chiudendo, i due si fondevano, i conti si mescolavano, e la notifica finale diceva numeri sbagliati. Peggio: se cliccavi un singolo episodio mentre un gruppo era in corso, quel click finiva nel conteggio del gruppo e poteva farlo chiudere prima del tempo. Ho riscritto tutta la contabilità in modo che ogni raffica di download sia un'isola con i suoi conti, le sue statistiche, la sua lista di fallimenti, e nessuno possa contaminare quella del vicino.
L'ultima crepa del motore non era nemmeno un errore di logica. Era una questione di fluidità. Durante i download, la lista degli episodi si ridisegnava interamente a ogni singolo aggiornamento di avanzamento, più volte al secondo, per ogni file in corso. Con cinque scaricamenti paralleli e una lista lunga, il programma sudava: scatti, ventola, CPU che saliva senza motivo. Ho fatto in modo che ogni riga si occupi solo dei propri affari. Quando un download avanza di un byte, adesso si aggiorna quella riga e basta, non tutta la pagina. Si vede e si sente: l'app respira.
Tra una correzione e l'altra c'è stato anche il lavoro meno raccontabile di tutti, quello sull'ossatura di Electron, il guscio dentro cui vive l'app. Ho aggiunto le regole che impediscono a un link malevolo dentro un feed di dirottare la finestra o di aprirne altre, e quella griglia di permessi che decide quali risorse il programma può caricare e da dove. Un'impalcatura invisibile che serve a fare in modo che, anche se un giorno qualcosa dovesse incrinarsi più in alto, i danni restino dentro un recinto.
Sei release, dalla v1.3.2 alla v1.3.6, per chiudere tutte le falle gravi. Nessuna di queste cambia una virgola di quello che vedi. Tutte cambiano quanto ti puoi fidare di quello che vedi.
La lima sulle cose piccole
Dopo i guai seri, restavano quelli minuti. I medi e i lievi. Li ho affrontati in tre release, e confesso che è stata la parte che mi è piaciuta di più, perché qui il lavoro torna a somigliare alla cura artigianale invece che alla bonifica.
C'era un feed che, se lo aprivi due volte di seguito, poteva scrivere due file diversi sullo stesso nome temporaneo e corromperli a vicenda; adesso ogni download «prenota» il suo posto e nessun altro può prenderglielo. C'era la verifica di integrità dei file, che accettava uno scarto fino all'uno per cento: su un file da cento megabyte voleva dire tollerare un megabyte mancante, un intero pezzo di episodio buttato via senza accorgersene. L'ho stretta a un decimo di punto, che è quanto basta per assorbire i capricci dell'ultimo byte e niente di più.
Poi le cose davvero piccole, quelle che però rivelano se un programma è stato scritto con attenzione o di fretta. Il pulsante per rimuovere un feed era invisibile e compariva solo passandoci sopra il mouse: chi naviga l'app da tastiera non lo trovava mai. Adesso compare anche quando ci arrivi con il tasto Tab. La logica per calcolare la durata di un episodio era scritta due volte, in due punti diversi, e i due calcoli non andavano d'accordo: uno teneva i secondi, l'altro li buttava. L'ho ridotta a una sola, condivisa. La cache dei feed cresceva in memoria senza fermarsi mai e non si svuotava nemmeno quando cancellavi un feed; adesso ha un tetto e dimentica le cose vecchie, come deve fare ogni memoria sana.
C'è un dettaglio quasi tenero in mezzo a tutto questo. Quando copiavi il link di un episodio dal pannello dei dettagli, l'app lo copiava davvero negli appunti ma non te lo diceva. Nessun riscontro, nessun lampo, niente. Cliccavi e non sapevi se avesse funzionato. Adesso un piccolo avviso conferma il gesto, tradotto in tutte e otto le lingue dell'app. È una di quelle cose che non chiede nessuno e che cambiano, di un grado, la sensazione di avere tra le mani uno strumento che ti risponde.
La copertina giusta
A un certo punto, finita la bonifica, mi sono concesso una versione che non riparava niente. Aggiungeva.
I file MP3 portano dentro di sé una copertina, l'immaginetta che vedi quando li apri nel lettore. Fino a quel momento l'app ci metteva sempre la copertina del podcast, quella generica, uguale per ogni episodio. Ma molti feed offrono una copertina specifica per la singola puntata: l'ospite di quel giorno, il tema di quell'episodio, una grafica dedicata. L'app la ignorava e schiacciava tutto sotto l'immagine di default.
Nella v1.3.10 ho ribaltato la priorità. Adesso, se l'episodio ha una sua copertina, è quella a finire nel file. Se non ce l'ha, o se per qualche ragione non si riesce a scaricarla, si torna a quella del podcast. Ogni episodio, dove possibile, tiene la propria faccia. È un dettaglio minuscolo per chi colleziona podcast e li riascolta a distanza di anni, ma è proprio per loro che ho scritto quest'app: per chi vuole che l'archivio assomigli alla cosa archiviata, non a una fila di icone tutte uguali.
Il messaggero che non sapeva consegnare
E poi è arrivata la v1.3.11, che è la mia preferita, perché ha smascherato un difetto che c'era da sempre e che fingeva di non esserci.
L'app ha un sistema di aggiornamento automatico. Dovrebbe accorgersi quando esce una versione nuova, scaricarla in silenzio e proporti di installarla. Sul mio altro progetto, il TelegramBot, ha sempre funzionato. Qui, no. E nessuno se n'era accorto, perché falliva senza fare rumore.
Il motivo, quando l'ho trovato, mi ha fatto ridere e arrabbiare insieme. Il nome del programma è «Runtime FeedDownloader Pro», con gli spazi. Quando si costruisce il pacchetto di installazione, gli spazi danno fastidio, e così vengono sostituiti. Solo che il sistema che prepara l'aggiornamento sostituiva gli spazi con dei trattini, mentre il sistema che ospita il file online sostituiva gli stessi spazi con dei punti. Due nomi per lo stesso file. Due nomi che non si incontravano mai. L'app si accorgeva benissimo che c'era una versione nuova, andava a cercare il file con il nome che conosceva lei, e trovava il vuoto. Vedeva l'aggiornamento. Non riusciva a prenderlo.
Un messaggero che sa che è arrivata una lettera per te, ma cerca la cassetta delle lettere a un indirizzo che non esiste. È bastato imporre un nome unico, senza spazi, identico da tutte le parti, e i due capi della corda si sono finalmente toccati.
Già che c'ero ho aggiunto la cosa che mancava per davvero: una notifica. Fino a ieri, anche quando l'aggiornamento funzionava, l'unico modo per saperlo era andare a frugare nelle impostazioni. Nessun avviso, nessun cenno. Adesso l'app te lo dice da sola, con una notifica di sistema, quando c'è qualcosa di nuovo da scaricare e quando è pronto da installare.
C'è un'ironia che non mi sfugge. Avevo chiuso l'articolo precedente dicendo che da quel punto l'app sapeva aggiornarsi da sola. Non era vero. Lo sapeva fare in teoria, e falliva in pratica, in silenzio, da sempre. A volte le promesse che fai a un software te le devi rimangiare, e va bene così: serve a ricordarti che «sembra finito» e «è finito» sono due frasi diverse.
Quello che resta
In una mattina ho pubblicato undici versioni. Nessuna di loro ha aggiunto un pixel a quell'interfaccia di cui andavo così fiero due settimane fa. Una sola, quella delle copertine, ha aggiunto qualcosa che si vede. Tutte le altre hanno tolto: hanno tolto modi di rompere l'app, di ingannarla, di farle fare quello che non deve.
È un lavoro strano, da raccontare. Quando ridisegni un'app hai qualcosa da mostrare alla fine della giornata, uno screenshot, un colore, un'animazione. Quando la metti in sicurezza, alla fine della giornata, l'app è identica a prima. Hai solo la consapevolezza, tua e di nessun altro, che adesso regge a una spinta che prima l'avrebbe fatta cadere.
Ho costruito una cassaforte per i podcast della gente. Mi sembrava bella, e lo è. Ma una cassaforte non si giudica da come è verniciata. Si giudica dalla serratura, e l'unico modo onesto di provare una serratura è cercare di scassinarla da soli, prima che lo faccia qualcun altro.
La prossima versione stabile parte da qui. E stavolta, prima di scriverlo, ci ho provato sul serio.
