DISCLAIMER: no seré responsable si alguien decide seguir mis pasos y se carga su aspiradora. En principio todo lo que cuento debería ser seguro, pero por motivos obvios no me puedo responsabilizar de lo que hagan otras personas, sólo de lo que haga yo.
Hay que reconocer que cuando las cosas se complican, se complican de verdad. No se si lo que ha pasado demuestra que el programador que lo hizo es un genio y metió una sutil protección antihacking, o un chapuzas que hizo un código que funciona de milagro.
Me explico: decidí hacer una prueba rápida de la conexión de la aspiradora con un servidor mío, así que escribí un miniservidor con Python3 que escuchaba en el puerto 80, y que cuando recibía una petición a alguno de los documentos a los que responde el de bona robots (/baole-web/common/sumbitClearTime.do y /baole-web/common/getToken.do), anotaba los valores que pasaban y respondía lo que la aspiradora esperaba.
Pero claro, la aspiradora se empeña en conectarse a bl-app-eu.robotbona.com o a bl-im-eu.robotbona.com, así que tenía que engañarla para que se conectase a mi ordenador y no al servidor chino. La solución consistió en dos pasos:
- primero, editar el fichero /etc/hosts de la Raspberry Pi que usaba como Access Point para conectar la aspiradora, y definir ahí esos dos dominios (además de robotbona.com, por si acaso), de manera que apuntasen a mi ordenador. Si la IP del ordenador fuese 192.168.0.89, habría que añadir:
192.168.0.89 bl-app-eu.robotbona.com 192.168.0.89 bl-im-eu.robotbona.com 192.168.0.89 robotbona.com
- relanzar dnsmasq en dicha Raspberry Pi con sudo systemctl restart dnsmasq, para que lea los cambios.
Y con esto, cuando la aspiradora se conecte a dicha red y pida la dirección IP de esos dominios, éste le devolverá la de mi equipo, con lo que lo usará como servidor en lugar del real. Por supuesto, cuando termine todo esto habrá que montar el servidor en la Raspberry y que todo se conecte a la WiFi normal, pero eso ya lo haré en el último capítulo.
Ahora bien, como utilicé el módulo http.server, mi programa no implementaba la parte del puerto 20008 (donde realmente se reciben todos los comandos), y como para ello tenía que hacer más cosas, decidí, de momento, no complicarme la vida para una simple prueba, y limitarme lanzar un netcat en dicho puerto en un terminal. Así, cuando la aspiradora se conectase, vería por la pantalla lo que ésta enviase.
Así pues, apagué y encendí la aspiradora, la puse en modo emparejamiento, la emparejé desde mi ordenador, ésta apagó su Access Point, se conectó a la WiFi, hizo la petición sumbitClearTime.do, mi servidor respondió, hizo luego la petición getToken.do, mi servidor respondió también… y volvió a pedir getToken.do… Y otra vez lo volvió a pedir… Y el netcat sin inmutarse.
Algo raro ocurría, porque después de pedir getToken.do, netcat debería mostrar una conexión, pero ahí no ocurría nada. Decidí probar a quitar la redirección en el DNS por si acaso le había pasado algo a la aspiradora, de manera que se conectase al servidor real, y entonces funcionó de nuevo.
Esto era muy raro, así que volví a configurar mi DNS y lancé el tcpdump en la Raspberry para capturar el tráfico que se intercambiaba entre la aspiradora y mi servidor, y ver qué podía estar pasando. Tenía que ser algo en la segunda orden, porque la primera sí funcionaba bien.
Comparé las respuestas de mi servidor simulado con las del servidor real y eran idénticas: los mismos campos, el mismo orden… ¡Espera, había una diferencia sutil entre los JSON que yo enviaba y los que enviaba el servidor! En mi caso, en getToken.do utilizaba las funciones de python para convertir de un diccionario a JSON, y éste lo estaba embelleciendo añadiendo espacios detrás de las comas, de los dos puntos, etc. Sin embargo, en sumbitClearTime.do generaba la cadena «tal cual», por lo que no tenía espacios, igual que la que enviaba el servidor. Aunque un JSON debería funcionar igual con ellos que sin ellos, probablemente quien escribió el código que lo analizaba en la aspiradora podría haber hecho la chapuza de esperarlo de una determinada manera (o bien exigirlo así para detectar hackeos), así que eliminé ese trozo de código y generé la salida «a pelo», juntando cachos de código. Ahora sí funcionaría.
Volví a lanzar todo, encendí la aspiradora, me senté a ver en pantalla las conexiones al servidor… y volvió a fallar otra vez. Había alguna otra diferencia. ¿Pero cual?
Volví a revisar todo, y me di cuenta de que mi código estaba enviando una línea con el nombre del servidor. En concreto, el campo Server valía BaseHTTP/0.6 Python/3.8.3, mientras que el servidor real no enviaba dicho campo.
El problema es que el módulo http.server no permite eliminar ese campo… ¿Como resolverlo? La solución consitió en sobrescribir el método send_header del objeto, de manera que ignorase dicho campo. Esto funciona porque el módulo la usa para añadir sus campos propios. Así pues, añadí esto al servidor:
def send_header(self, token, data): if token != 'Server': super().send_header(token, data)
¡Y con esto por fin funcio…! No, seguía fallando. No era eso tampoco.
Hmm… En el código de error estoy enviando, después del 200, el texto OK, mientras que su servidor no envía nada, sólo hay un espacio detrás del 200 y ya luego el retorno de carro. ¡Seguro que es eso!
¡Agh, tampoco! No lo entiendo, estoy enviando exactamente lo mismo que su servidor: tanto las cabeceras como el contenido es idéntico. ¿Por qué no funciona? Si abro el wireshark y comparo las capturas reales con las mías a nivel de byte… son iguales. ¿Qué demonios está pasando? Es exactamente lo mismo, con la única excepción de…
No…
No habrán sido capaces…
No pueden haberlo hecho…
Lo fueron. Resulta que su servidor envía en un solo paquete la cabecera HTTP y el primer bloque de los datos en formato chunk, y en un paquete diferente el segundo y último bloque de los datos chunk; o sea, esto va en un paquete IP:
HTTP/1.1 200 \r\n Date: Sat, 27 Jun 2020 16:38:08 GMT\r\n Content-Type: application/json;charset=UTF-8\r\n Transfer-Encoding: chunked\r\n Connection: close\r\n Set-Cookie: SERVERID=abcdefghijklmnopqrstuvwxyz12345|1234567890|1234567890;Path=/\r\n \r\n 2b\r\n {"msg":"ok","result":"0","version":"1.0.0"}\r\n
Y esto va en el siguiente paquete IP:
0\r\n \r\n
Sin embargo, mi servidor enviaba en un paquete la cabecera HTTP, y el resto en un paquete diferente. O sea, esto iba en un paquete:
HTTP/1.1 200 \r\n Date: Sat, 27 Jun 2020 16:38:08 GMT\r\n Content-Type: application/json;charset=UTF-8\r\n Transfer-Encoding: chunked\r\n Connection: close\r\n Set-Cookie: SERVERID=abcdefghijklmnopqrstuvwxyz12345|1234567890|1234567890;Path=/\r\n \r\n
Y esto en el siguiente:
2b\r\n {"msg":"ok","result":"0","version":"1.0.0"}\r\n 0\r\n \r\n
Obviamente, en condiciones normales eso daría igual, pues TCP es un protocolo orientado a byte y se supone que da igual que un trozo viaje en un paquete IP y otro en el siguiente, pero es que esa era la única diferencia que quedaba, y efectivamente, en cuanto cambié el código para que lo emitiese de esa manera, por fin funcionó y se conectó al puerto 20008. ¿Será algún tipo de protección antihacking, o simplemente que, a nivel de programación del microcontrolador hicieron alguna chapuza? Sin analizar el firmware no puedo saberlo, y la verdad, es una tarea que, en este momento, no me llama nada. Pero da igual, porque lo importante es que…