diff --git a/modules/boursorama/backend.py b/modules/boursorama/backend.py index fbc72b6f..08e98eef 100644 --- a/modules/boursorama/backend.py +++ b/modules/boursorama/backend.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright(C) 2012 Gabriel Serme # Copyright(C) 2011 Gabriel Kerneis # Copyright(C) 2010-2011 Jocelyn Jaubert # @@ -24,7 +25,7 @@ from __future__ import with_statement from weboob.capabilities.bank import ICapBank, AccountNotFound from weboob.tools.backend import BaseBackend, BackendConfig -from weboob.tools.value import ValueBackendPassword +from weboob.tools.value import ValueBackendPassword, ValueBool, Value from .browser import Boursorama @@ -40,12 +41,17 @@ class BoursoramaBackend(BaseBackend, ICapBank): LICENSE = 'AGPLv3+' DESCRIPTION = u'Boursorama French bank website' CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), - ValueBackendPassword('password', label='Password')) + ValueBackendPassword('password', label='Password'), + ValueBool('enable_twofactors', label='Send validation sms', default=False), + Value('device', label='Device name', regexp='\w*'),) BROWSER = Boursorama def create_default_browser(self): - return self.create_browser(self.config['login'].get(), - self.config['password'].get()) + return self.create_browser( + self.config["device"].get() + , self.config["enable_twofactors"].get() + , self.config['login'].get() + , self.config['password'].get()) def iter_accounts(self): for account in self.browser.get_accounts_list(): diff --git a/modules/boursorama/browser.py b/modules/boursorama/browser.py index f357cf1f..56dd2660 100644 --- a/modules/boursorama/browser.py +++ b/modules/boursorama/browser.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright(C) 2012 Gabriel Serme # Copyright(C) 2011 Gabriel Kerneis # Copyright(C) 2010-2011 Jocelyn Jaubert # @@ -20,25 +21,33 @@ from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword -from .pages import LoginPage, AccountsList, AccountHistory, UpdateInfoPage +from .pages import LoginPage, AccountsList, AccountHistory, UpdateInfoPage, AuthenticationPage __all__ = ['Boursorama'] +class BrowserIncorrectAuthenticationCode(BrowserIncorrectPassword): + pass + + class Boursorama(BaseBrowser): DOMAIN = 'www.boursorama.com' PROTOCOL = 'https' CERTHASH = '74429081f489cb723a82171a94350913d42727053fc86cf5bf5c3d65d39ec449' ENCODING = None # refer to the HTML encoding PAGES = { - '.*connexion.phtml.*': LoginPage, - '.*/comptes/synthese.phtml': AccountsList, - '.*/mouvements.phtml.*': AccountHistory, + '.*/connexion/securisation/index.phtml': AuthenticationPage, + '.*connexion.phtml.*': LoginPage, + '.*/comptes/synthese.phtml': AccountsList, + '.*/comptes/banque/detail/mouvements.phtml.*': AccountHistory, '.*/date_anniversaire.phtml.*': UpdateInfoPage, } - def __init__(self, *args, **kwargs): + def __init__(self, device="weboob", enable_twofactors=False + , *args, **kwargs): + self.device = device + self.enable_twofactors = enable_twofactors BaseBrowser.__init__(self, *args, **kwargs) def home(self): @@ -47,11 +56,25 @@ class Boursorama(BaseBrowser): def is_logged(self): return not self.is_on_page(LoginPage) + def handle_authentication(self): + if self.is_on_page(AuthenticationPage): + if self.enable_twofactors: + self.page.authenticate(self.device) + else: + print \ + """Boursorama - activate the two factor authentication in boursorama config."""\ + """ You will receive SMS code but are limited in request per day (around 15)""" + def login(self): assert isinstance(self.username, basestring) assert isinstance(self.password, basestring) + assert isinstance(self.device, basestring) + assert isinstance(self.enable_twofactors, bool) assert self.password.isdigit() + #for debug, save requested pages to tmp dir + #self.SAVE_RESPONSES = True + if not self.is_on_page(LoginPage): self.location('https://' + self.DOMAIN + '/connexion.phtml') @@ -60,6 +83,22 @@ class Boursorama(BaseBrowser): if self.is_on_page(LoginPage): raise BrowserIncorrectPassword() + #after login, we might be redirected to the two factor + #authentication page + #print "handle authentication" + self.handle_authentication() + + self.location('/comptes/synthese.phtml', no_login=True) + + #if the login was correct but authentication code failed, + #we need to verify if bourso redirect us to login page or authentication page + if self.is_on_page(LoginPage): + #print "not correct after handling authentication" + raise BrowserIncorrectAuthenticationCode() + + #print "login over" + + def get_accounts_list(self): if not self.is_on_page(AccountsList): self.location('/comptes/synthese.phtml') diff --git a/modules/boursorama/pages/__init__.py b/modules/boursorama/pages/__init__.py index 1b670040..57be314e 100644 --- a/modules/boursorama/pages/__init__.py +++ b/modules/boursorama/pages/__init__.py @@ -23,6 +23,7 @@ from .account_history import AccountHistory from .accounts_list import AccountsList from .login import LoginPage, UpdateInfoPage +from .two_authentication import AuthenticationPage class AccountPrelevement(AccountsList): pass @@ -31,4 +32,5 @@ __all__ = ['LoginPage', 'AccountsList', 'AccountHistory', 'UpdateInfoPage', + 'AuthenticationPage', ] diff --git a/modules/boursorama/pages/two_authentication.py b/modules/boursorama/pages/two_authentication.py new file mode 100644 index 00000000..2e182252 --- /dev/null +++ b/modules/boursorama/pages/two_authentication.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gabriel Serme +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.browser import BasePage, BrowserIncorrectPassword +import urllib2 +import re + +__all__ = ['AuthenticationPage'] + + +class BrowserAuthenticationCodeMaxLimit(BrowserIncorrectPassword): + pass + +def write_debug(string, fi): + f = open(fi, "w") + f.write(string) + + +class AuthenticationPage(BasePage): + + + MAX_LIMIT = "vous avez atteint le nombre maximum "\ + "d'utilisation de l'authentification forte." + + def on_loaded(self): + pass + + def authenticate(self, device): + """This function simulates the registration of a device on + boursorama two factor authentification web page. + I + @param device device name to register + @exception BrowserAuthenticationCodeMaxLimit when daily limit is consumed + @exception BrowserIncorrectAuthenticationCode when code is not correct + """ + DOMAIN = self.browser.DOMAIN + SECURE_PAGE = "https://www.boursorama.com/comptes/connexion/securisation/index.phtml" + REFERER = SECURE_PAGE + + #print "Need to authenticate for device", device + #print "Domain information", DOMAIN + + url = "https://%s/ajax/banque/otp.phtml?org=%s&alertType=10100" % (DOMAIN, REFERER) + #print url + headers = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows " + "NT 5.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8" + " GTB7.1 (.NET CLR 3.5.30729)", + "Referer": REFERER, + } + + headers_ajax = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows " + "NT 5.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8" + " GTB7.1 (.NET CLR 3.5.30729)", + "Accept": "application/json", + "X-Requested-With": "XMLHttpRequest", + "X-Request": "JSON", + "X-Brs-Xhr-Request": "true", + "X-Brs-Xhr-Schema": "DATA+OUT", + "Referer": REFERER, + } + + req = urllib2.Request(url, headers=headers_ajax) + response = self.browser.open(req) + #extrat authentication token from response (in form) + info = response.read() + + #write_debug(info, "step1.html") + + regex = re.compile(r"vous avez atteint le nombre maximum d'utilisation de l'authentification forte.") + r = regex.search(info) + if r: + print "Boursorama - Vous avez atteint le nombre maximum d'utilisation de l'authentification forte" + raise BrowserAuthenticationCodeMaxLimit() + + #print "Response from initial request,", len(info), response.info() + regex = re.compile(r"name=\\\"authentificationforteToken\\\" " + r"value=\\\"(?P\w*?)\\\"") + r = regex.search(info) + token = r.group('value') + #print "Extracted token", token + #self.print_cookies() + + #step2 + url = "https://" + DOMAIN + "/ajax/banque/otp.phtml" + data = "authentificationforteToken=%s&authentificationforteStep=start&alertType=10100&org=%s&validate=" % (token, REFERER) + req = urllib2.Request(url, data, headers_ajax) + response = self.browser.open(req) + #info = response.read() + #print "after asking to send token authentification" \ + # ,len(info), response.info() + #write_debug(info, "step2.html") + + #self.print_cookies() + + pin = raw_input('Enter the "Boursorama Banque" access code:') + #print "Pin access code: ''%s''" % (pin) + url = "https://" + DOMAIN + "/ajax/banque/otp.phtml" + data = "authentificationforteToken=%s&authentificationforteStep=otp&alertType=10100&org=%s&otp=%s&validate=" % (token, REFERER, pin) + req = urllib2.Request(url, data, headers_ajax) + response = self.browser.open(req) + #info = response.read() + #print "after pin authentification", len(info), response.info() + #write_debug(info, "step3.html") + + #self.print_cookies() + + url = "%s?" % (SECURE_PAGE) + data = "org=/&device=%s" % (device) + req = urllib2.Request(url, data, headers=headers) + response = self.browser.open(req) + + #result = response.read() + #print response, "\n", response.info() + #write_debug(result, "step4.html") + + #self.print_cookies() + + def print_cookies(self): + for c in self.browser._ua_handlers["_cookies"].cookiejar: + print "%s : %s" % (c.name, c.value) + + +