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)
+
+
+