diff --git a/modules/boursorama/browser.py b/modules/boursorama/browser.py index 24249cc19c2dffffc82e601368ee76e3a31a072a..767e5fb7c9b9899dd176149f3650a5e1bb11d6e8 100644 --- a/modules/boursorama/browser.py +++ b/modules/boursorama/browser.py @@ -43,12 +43,12 @@ from weboob.tools.capabilities.bank.bank_transfer import sorted_transfers from .pages import ( - LoginPage, VirtKeyboardPage, AccountsPage, AsvPage, HistoryPage, AuthenticationPage, + VirtKeyboardPage, AccountsPage, AsvPage, HistoryPage, AuthenticationPage, MarketPage, LoanPage, SavingMarketPage, ErrorPage, IncidentPage, IbanPage, ProfilePage, ExpertPage, CardsNumberPage, CalendarPage, HomePage, PEPPage, TransferAccounts, TransferRecipients, TransferCharac, TransferConfirm, TransferSent, AddRecipientPage, StatusPage, CardHistoryPage, CardCalendarPage, CurrencyListPage, CurrencyConvertPage, - AccountsErrorPage, NoAccountPage, TransferMainPage, + AccountsErrorPage, NoAccountPage, TransferMainPage, PasswordPage, ) from .transfer_pages import TransferListPage, TransferInfoPage @@ -67,13 +67,13 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser): TWOFA_DURATION = 60 * 24 * 90 home = URL('/$', HomePage) - keyboard = URL('/connexion/clavier-virtuel\?_hinclude=300000', VirtKeyboardPage) + keyboard = URL(r'/connexion/clavier-virtuel\?_hinclude=1', VirtKeyboardPage) status = URL(r'/aide/messages/dashboard\?showza=0&_hinclude=1', StatusPage) calendar = URL('/compte/cav/.*/calendrier', CalendarPage) card_calendar = URL('https://api.boursorama.com/services/api/files/download.phtml.*', CardCalendarPage) error = URL('/connexion/compte-verrouille', '/infos-profil', ErrorPage) - login = URL('/connexion/', LoginPage) + login = URL(r'/connexion/saisie-mot-de-passe/', PasswordPage) accounts = URL('/dashboard/comptes\?_hinclude=300000', AccountsPage) accounts_error = URL('/dashboard/comptes\?_hinclude=300000', AccountsErrorPage) @@ -199,7 +199,7 @@ def handle_sms(self): def init_login(self): self.login.go() - self.page.login(self.username, self.password) + self.page.enter_password(self.username, self.password) if self.error.is_here(): raise BrowserIncorrectPassword() diff --git a/modules/boursorama/pages.py b/modules/boursorama/pages.py index 2c7f28e624e962775e8ff2602a43c1da92b69cb2..65b3a1b4110c6fe352bec890366d0832f359929a 100644 --- a/modules/boursorama/pages.py +++ b/modules/boursorama/pages.py @@ -19,13 +19,11 @@ from __future__ import unicode_literals -from base64 import b64decode import datetime from decimal import Decimal import re -from io import BytesIO from datetime import date -from PIL import Image +import hashlib from weboob.browser.pages import HTMLPage, LoggedPage, pagination, NextPage, FormNotFound, PartialHTMLPage, LoginPage, CsvPage, RawPage, JsonPage from weboob.browser.elements import ListElement, ItemElement, method, TableElement, SkipItem, DictElement @@ -160,74 +158,40 @@ class VirtKeyboardPage(HTMLPage): class BoursoramaVirtKeyboard(object): + # sha256 hexdigest of data in src of img symbols = { - '0': '0000110000001111110001110011100110000110111000011011100001101100000110111000011011100001100110000110011100111000111111000000110000', - '1': '0000110000000111000000111100000111110000011011000000001100000000110000000011000000001100000000110000000011000000001100000000110000', - '2': '0001111000011111110001100011100000000110000000011000000001100000001110000001110000001110000001110000001110000001111111110111111111', - '3': '0001111000011111111001100011100000000110000000011000000011100001111000000111110000000001100000000110111000111001111111100001110000', - '4': '0000011100000011110000001111000001101100000100110000110011000110001100011000110011111111101111111110000000111000000011000000001100', - '5': '1111111100111111110011100000001100000000110000000011111111001111111110010000011000000001100000000110111000111011111111000001110000', - '6': '0000111000001111111001110001000110000000011000000011101111001111111110111000011011100001100110000110011100111000111111000000111000', - '7': '0111111111011111111100000001100000000110000000111000000011000000011100000001100000000110000000110000000011000000011100000001100000', - '8': '0001111000011111111011100011101110000110011000011001111111000011111000011100111011100001101110000110111000111001111111100001111000', - '9': '0001110000011111110001100011101110000110110000011011100001100111111110001111011000000001100000000110001000110001111111000001110000', + '0': '86bda4bbc37e6cff7755be5a1ddae5322fe825ec7d0d447788e1d9ddf45599c7', + '1': 'e022e986a83bd9eef9ea740a628e983aae853a70d2c265a3f381783796f755e9', + '2': '962569ba07017fa2620f78198e1b6fb38f7b5b272ac9d4aee15c2ad598c015c3', + '3': 'd3a74f4d1fc55bc3df4e3fef2c3be04455001269086fc5f041019cebc3589208', + '4': '42762e145fe9084529efd08da04fdae630ab37a3232b795d37b8e6d29c5fbe13', + '5': 'd0a51a1e680ff68a19eef3396bd685b5feb61e93002893c80208e3b13a866602', + '6': 'e61de98562e589080abbb65d62983ef752e6cb0fe863d2a0b32f8f33916e50e1', + '7': '9c53afbf67e1eb029b3f44756c80cf9a03e5870976b1c6ed72510ff316e0d6f5', + '8': '325b6b8ea28c7adc9a032bbdffe69dd1df0a30d22cf46b49c503ae2206894a0e', + '9': '28f5c6f96b7305022635be5f252861470d9e7ca10dc5c37a0062ce9c09b509c3', } - def __init__(self, browser, page): - self.browser = browser + def __init__(self, page, codesep='|'): + self.codesep = codesep self.fingerprints = {} - col = 0 - - keys = page.doc.xpath('//ul[@class="password-input"]//button/@data-matrix-key') for button in page.doc.xpath('//ul[@class="password-input"]//button'): - txt = button.attrib['style'].replace('background-image:url(data:image/png;base64,', '').rstrip(');') - - img = Image.open(BytesIO(b64decode(txt.encode('ascii')))) - width, height = img.size - - img = img.crop((16, 6, width - 16, height - 23)) - width, height = img.size - - matrix = img.load() - s = "" - for y in range(height): - for x in range(width): - (r, g, b, a) = matrix[x, y] - # If the pixel is white and opaque enough - if a > 200 and r + g + b > 740: - s += "1" - else: - s += "0" - self.fingerprints[keys[col]] = s - col += 1 - - def get_symbol_code(self, char): - fingerprint = self.symbols[char] - for code, string in self.fingerprints.items(): - if fingerprint == string: - return code - # Image contains some noise, and the match is not always perfect - # (this is why we can't use md5 hashs) - # But if we can't find the perfect one, we can take the best one - best = 0 - result = None - for code, string in self.fingerprints.items(): - match = 0 - for j, bit in enumerate(string): - if bit == fingerprint[j]: - match += 1 - if match > best: - best = match - result = code - self.browser.logger.info(self.fingerprints[result] + "(" + result + ") match " + char) - return result + # src is like data:image/svg+xml;base64, [data] + # so we split to only keep the data + # hashed so that the symbols dict is smaller + img_data_hash = hashlib.sha256( + button.xpath('.//img')[0].attrib['src'].split()[1].encode('utf-8') + ).hexdigest() + self.fingerprints[img_data_hash] = button.attrib['data-matrix-key'] def get_string_code(self, string): - return '|'.join(self.get_symbol_code(c) for c in string) + return self.codesep.join( + self.fingerprints[self.symbols[digit]] for digit in string + ) -class LoginPage(LoginPage, HTMLPage): +class PasswordPage(LoginPage, HTMLPage): TO_DIGIT = {'2': ['a', 'b', 'c'], '3': ['d', 'e', 'f'], '4': ['g', 'h', 'i'], @@ -238,21 +202,21 @@ class LoginPage(LoginPage, HTMLPage): '9': ['w', 'x', 'y', 'z'] } - def login(self, login, password): + def enter_password(self, username, password): if not password.isdigit(): password = ''.join([c if c.isdigit() else [k for k, v in self.TO_DIGIT.items() if c in v][0] for c in password.lower()]) - form = self.get_form() + keyboard_page = self.browser.keyboard.open() - vk = BoursoramaVirtKeyboard(self.browser, keyboard_page) - code = vk.get_string_code(password) - form['form[login]'] = login - form['form[fakePassword]'] = len(password) * '•' - form['form[password]'] = code - form['form[matrixRandomChallenge]'] = re.search('val\("(.*)"', CleanText('//script')(keyboard_page.doc)).group(1) + vk = BoursoramaVirtKeyboard(keyboard_page) + + form = self.get_form() + form['form[clientNumber]'] = username + form['form[password]'] = vk.get_string_code(password) + form['form[matrixRandomChallenge]'] = Regexp(CleanText('//script'), r'val\("(.*)"')(keyboard_page.doc) form.submit() def get_error(self): - return CleanText('//li[contains(@id, "form") and contains(@id, "error")]')(self.doc) + return CleanText('//h2[contains(text(), "Erreur")]/following-sibling::div[contains(@class, "msg")]')(self.doc) class StatusPage(LoggedPage, PartialHTMLPage):