Commit 8e82bfc4 authored by Sylvie Ye's avatar Sylvie Ye Committed by Romain Bignon

[ing] handle API website

create separate browser and page for API website
API website is not working for now (can only retrieve checking and card accounts information),
Redirect to old browser after new website login
Also fix multispace redirect
parent 9cdd4beb
# -*- coding: utf-8 -*-
# Copyright(C) 2019 Sylvie Ye
#
# 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 <http://www.gnu.org/licenses/>.
from .login import LoginPage
__all__ = ['LoginPage', ]
# -*- coding: utf-8 -*-
# Copyright(C) 2019 Sylvie Ye
#
# 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 <http://www.gnu.org/licenses/>.
from io import BytesIO
from PIL import Image, ImageFilter
import random
from weboob.tools.captcha.virtkeyboard import SimpleVirtualKeyboard
from weboob.browser.pages import JsonPage
from weboob.browser.filters.json import Dict
class INGVirtKeyboard(SimpleVirtualKeyboard):
tile_margin = 3
margin = (0, 4, 0, 0)
convert = 'RGB'
symbols = {
'0': ('117b18365105224c7207d3ec0ce7516f',),
'1': ('112a72c31ebdf0cdafb84e67c6e1f8f2',),
'2': ('df8534cb28a19e600976d39af2c4f6fe',),
'3': ('911dbe595604da336fbdd360f89bada1',),
'4': ('8a22058801980e4afb25c414e388bfa8',),
'5': ('c7d430083b55fbe2834c912c7cded124',),
'6': ('64f8b9f3a93bc534443646f0b54e26ad',),
'7': ('6c14303e9bffdcd1880ce415b6f0efb2',),
'8': ('a62e9e25b047160090de1634c8d3b0f6',),
'9': ('2b9bc97ce4ccc67d4ae0c3ca54957b33', 'afc9d2840290b7da08bf1d0b27b6c302'),
}
# Clean image
def alter_image(self):
# original image size is (484, 190), save the original image
self.original_image = self.image
# create miniature of image to get more reliable hash
self.image = self.image.resize((100, 40), resample=Image.BILINEAR)
# See ImageFilter.UnsharpMask from Pillow
self.image = self.image.filter(ImageFilter.UnsharpMask(radius=2, percent=135, threshold=3))
self.image = Image.eval(self.image, lambda px: 0 if px <= 160 else 255)
def password_tiles_coord(self, password):
# get image original size to get password coord
image_width, image_height = self.original_image.size
tile_width, tile_height = image_width // self.cols, image_height // self.rows
password_tiles = []
for digit in password:
for tile in self.tiles:
if tile.md5 in self.symbols[digit]:
password_tiles.append(tile)
break
else:
# Dump file only when the symbol is not found
self.dump_tiles(self.path)
raise Exception("Symbol '%s' not found; all symbol hashes are available in %s"
% (digit, self.path))
formatted_password = []
safe_margin = 10
for tile in password_tiles:
# default matching_symbol is str(range(cols*rows))
x0 = (int(tile.matching_symbol) % self.cols) * tile_width
y0 = (int(tile.matching_symbol) // self.cols) * tile_height
tile_original_coords = (
x0 + safe_margin, y0 + safe_margin,
x0 + tile_width - safe_margin, y0 + tile_height - safe_margin,
)
formatted_password.append([
random.uniform(tile_original_coords[0], tile_original_coords[2]),
random.uniform(tile_original_coords[1], tile_original_coords[3]),
])
return formatted_password
class LoginPage(JsonPage):
@property
def is_logged(self):
return 'firstName' in self.doc
def get_password_coord(self, img, password):
assert 'pinPositions' in self.doc, 'Virtualkeyboard position has failed'
assert 'keyPadUrl' in self.doc, 'Virtualkeyboard image url is missing'
pin_position = Dict('pinPositions')(self.doc)
image = BytesIO(img)
vk = INGVirtKeyboard(image, 5, 2)
password_radom_coords = vk.password_tiles_coord(password)
# pin positions (website side) start at 1, our positions start at 0
return [password_radom_coords[index-1] for index in pin_position]
def get_error(self):
if 'error' in self.doc:
return (Dict('error/code')(self.doc), Dict('error/message')(self.doc))
# -*- coding: utf-8 -*-
# Copyright(C) 2019 Sylvie Ye
#
# 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 <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from collections import OrderedDict
from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser.exceptions import ClientError
from .api import LoginPage
from .web import StopPage, ActionNeededPage
from .browser import IngBrowser
class IngAPIBrowser(LoginBrowser):
BASEURL = 'https://m.ing.fr'
# Login
context = URL(r'/secure/api-v1/session/context')
login = URL(r'/secure/api-v1/login/cif', LoginPage)
keypad = URL(r'/secure/api-v1/login/keypad', LoginPage)
pin_page = URL(r'/secure/api-v1/login/pin', LoginPage)
# Error on old website
errorpage = URL(r'https://secure.ing.fr/.*displayCoordonneesCommand.*', StopPage)
actioneeded = URL(r'https://secure.ing.fr/general\?command=displayTRAlertMessage',
r'https://secure.ing.fr/protected/pages/common/eco1/moveMoneyForbidden.jsf', ActionNeededPage)
def __init__(self, *args, **kwargs):
self.birthday = kwargs.pop('birthday')
super(IngAPIBrowser, self).__init__(*args, **kwargs)
self.old_browser = IngBrowser(*args, **kwargs)
def do_login(self):
assert self.password.isdigit()
assert self.birthday.isdigit()
# login on new website
# update cookies
self.context.go()
data = OrderedDict([
('birthDate', self.birthday),
('cif', self.username),
])
self.login.go(json=data)
data = '{"keyPadSize":{"width":3800,"height":1520},"mode":""}'
self.keypad.go(data=data, headers={'Content-Type': 'application/json'})
img = self.open('/secure/api-v1/keypad/newkeypad.png').content
data = {
'clickPositions': self.page.get_password_coord(img, self.password)
}
try:
self.pin_page.go(json=data, headers={'Referer': 'https://m.ing.fr/secure/login/pin'})
except ClientError:
# handle error later
pass
error = self.page.get_error()
if not self.page.is_logged:
assert error
if error[0] == 'AUTHENTICATION.INVALID_PIN_CODE':
raise BrowserIncorrectPassword(error[1])
assert error[0] != 'INPUT_INVALID', '%s' % error[1]
raise BrowserUnavailable(error[1])
self.auth_token = self.page.response.headers['Ingdf-Auth-Token']
self.session.headers['Ingdf-Auth-Token'] = self.auth_token
self.session.cookies['ingdfAuthToken'] = self.auth_token
# Go on old website because new website is not stable
self.redirect_to_old_browser()
def redirect_to_old_browser(self):
token = self.location(
'https://m.ing.fr/secure/api-v1/sso/exit?context={"originatingApplication":"SECUREUI"}&targetSystem=INTERNET',
method='POST'
).content
data = {
'token': token,
'next': 'protected/pages/index.jsf',
'redirectUrl': 'protected/pages/index.jsf',
'targetApplication': 'INTERNET',
'accountNumber': 'undefined'
}
self.session.cookies['produitsoffres'] = 'comptes'
self.location('https://secure.ing.fr', data=data, headers={'Referer': 'https://secure.ing.fr'})
self.old_browser.session.cookies.update(self.session.cookies)
def deinit(self):
super(IngAPIBrowser, self).deinit()
self.old_browser.deinit()
@need_login
def get_accounts_list(self):
return self.old_browser.get_accounts_list()
@need_login
def get_account(self, _id):
raise BrowserUnavailable()
@need_login
def get_coming(self, account):
raise BrowserUnavailable()
@need_login
def get_history(self, account):
raise BrowserUnavailable()
@need_login
def iter_recipients(self, account):
raise BrowserUnavailable()
@need_login
def init_transfer(self, account, recipient, transfer):
raise BrowserUnavailable()
@need_login
def execute_transfer(self, transfer):
raise BrowserUnavailable()
@need_login
def get_investments(self, account):
raise BrowserUnavailable()
############# CapDocument #############
@need_login
def get_subscriptions(self):
raise BrowserUnavailable()
@need_login
def get_documents(self, subscription):
raise BrowserUnavailable()
def download_document(self, bill):
raise BrowserUnavailable()
############# CapProfile #############
@need_login
def get_profile(self):
raise BrowserUnavailable()
......@@ -26,14 +26,14 @@ import json
from requests.exceptions import SSLError
from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.exceptions import BrowserUnavailable
from weboob.browser.exceptions import ServerError
from weboob.capabilities.bank import Account, AccountNotFound
from weboob.capabilities.base import find_object, NotAvailable
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from .pages import (
AccountsList, LoginPage, NetissimaPage, TitrePage,
from .web import (
AccountsList, NetissimaPage, TitrePage,
TitreHistory, TransferPage, BillsPage, StopPage, TitreDetails,
TitreValuePage, ASVHistory, ASVInvest, DetailFondsPage, IbanPage,
ActionNeededPage, ReturnPage, ProfilePage, LoanTokenPage, LoanDetailPage,
......@@ -75,7 +75,6 @@ class IngBrowser(LoginBrowser):
lifeback = URL(r'https://ingdirectvie.ing.fr/b2b2c/entreesite/EntAccExit', ReturnPage)
# Login and error
loginpage = URL(r'/public/displayLogin.jsf.*', LoginPage)
errorpage = URL(r'.*displayCoordonneesCommand.*', StopPage)
actioneeded = URL(r'/general\?command=displayTRAlertMessage',
r'/protected/pages/common/eco1/moveMoneyForbidden.jsf', ActionNeededPage)
......@@ -108,7 +107,6 @@ class IngBrowser(LoginBrowser):
__states__ = ['where']
def __init__(self, *args, **kwargs):
self.birthday = kwargs.pop('birthday')
self.where = None
LoginBrowser.__init__(self, *args, **kwargs)
self.cache = {}
......@@ -126,24 +124,14 @@ class IngBrowser(LoginBrowser):
self.current_subscription = None
def do_login(self):
assert self.password.isdigit()
assert self.birthday.isdigit()
self.do_logout()
self.loginpage.go()
self.page.prelogin(self.username, self.birthday)
self.page.login(self.password)
if self.page.error():
raise BrowserIncorrectPassword()
if self.errorpage.is_here():
raise BrowserIncorrectPassword('Please login on website to fill the form and retry')
self.page.check_for_action_needed()
pass
@need_login
def set_multispace(self):
self.where = 'start'
self.page.load_space_page()
if not self.page.is_multispace_page():
self.page.load_space_page()
self.multispace = self.page.get_multispace()
......@@ -156,6 +144,7 @@ class IngBrowser(LoginBrowser):
@need_login
def change_space(self, space):
if self.multispace and not self.is_same_space(space, self.current_space):
self.logger.info('Change spaces')
self.accountspage.go()
self.where = 'start'
self.page.load_space_page()
......
......@@ -33,7 +33,7 @@ from weboob.capabilities.base import find_object
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword, ValueDate
from .browser import IngBrowser
from .api_browser import IngAPIBrowser
__all__ = ['INGModule']
......@@ -55,7 +55,7 @@ class INGModule(Module, CapBankWealth, CapBankTransfer, CapDocument, CapProfile)
label='Date de naissance',
formats=('%d%m%Y', '%d/%m/%Y', '%d-%m-%Y'))
)
BROWSER = IngBrowser
BROWSER = IngAPIBrowser
accepted_document_types = (DocumentTypes.STATEMENT,)
......
# -*- coding: utf-8 -*-
# Copyright(C) 2009-2014 Florent Fourcot, Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from io import BytesIO
from weboob.exceptions import BrowserIncorrectPassword, ActionNeeded
from weboob.tools.captcha.virtkeyboard import VirtKeyboard
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.browser.filters.html import Attr
from weboob.browser.filters.standard import CleanText
class INGVirtKeyboard(VirtKeyboard):
symbols = {'0': '327208d491507341908cf6920f26b586',
'1': '615ff37b15645da106cebc4605b399de',
'2': 'fb04e648c93620f8b187981f9742b57e',
'3': 'b786d471a70de83657d57bdedb6a2f38',
'4': '41b5501219e8d8f6d3b0baef3352ce88',
'5': 'c72b372fb035160f2ff8dae59cd7e174',
'6': '392fa79e9a1749f5c8c0170f6a8ec68b',
'7': 'fb495b5cf7f46201af0b4977899b56d4',
'8': 'e8fea1e1aa86f8fca7f771db9a1dca4d',
'9': '82e63914f2e52ec04c11cfc6fecf7e08'
}
color = 64
coords = {"11": (5, 5, 33, 33),
"21": (45, 5, 73, 33),
"31": (85, 5, 113, 33),
"41": (125, 5, 153, 33),
"51": (165, 5, 193, 33),
"12": (5, 45, 33, 73),
"22": (45, 45, 73, 73),
"32": (85, 45, 113, 73),
"42": (125, 45, 153, 73),
"52": (165, 45, 193, 73)
}
def __init__(self, page):
self.page = page
img = page.doc.xpath("//div[has-class('clavier')]/img")
if len(img) == 0:
raise BrowserIncorrectPassword()
url = Attr('.', "src")(img[1])
VirtKeyboard.__init__(self, BytesIO(self.page.browser.open(url).content),
self.coords, self.color)
self.check_symbols(self.symbols, self.page.browser.responses_dirname)
def get_string_code(self, string):
code = ''
first = True
for c in string:
if not first:
code += ","
else:
first = False
codesymbol = self.get_symbol_code(self.symbols[c])
x = (self.coords[codesymbol][0] + self.coords[codesymbol][2]) / 2
y = (self.coords[codesymbol][1] + self.coords[codesymbol][3]) / 2
code += "%d,%d" % (x, y)
return code
def get_coordinates(self, password):
temppasswd = ""
elems = self.page.doc.xpath('//div[@class="digitpad"]/span/font')
for i, font in enumerate(elems):
if Attr('.', 'class')(font) == "vide":
temppasswd += password[i]
coordinates = self.get_string_code(temppasswd)
self.page.browser.logger.debug("Coordonates: " + coordinates)
return coordinates
class LoginPage(HTMLPage):
def prelogin(self, login, birthday):
# First step : login and birthday
form = self.get_form(name='zone1Form')
form['zone1Form:numClient'] = login
form['zone1Form:dateDay'] = birthday[0:2]
form['zone1Form:dateMonth'] = birthday[2:4]
form['zone1Form:dateYear'] = birthday[4:9]
form['zone1Form:idRememberMyCifCheck'] = False
form.submit()
def error(self):
err = self.doc.find('//span[@class="error"]')
return err is not None
def login(self, password):
# 2) And now, the virtual Keyboard
vk = INGVirtKeyboard(self)
form = self.get_form(name='mrc')
form['mrc:mrg'] = 'mrc:mrg'
form['AJAXREQUEST'] = '_viewRoot'
form['mrc:mrldisplayLogin'] = vk.get_coordinates(password)
form.submit()
def check_for_action_needed(self):
link = Attr('//meta[@content="/general?command=displayTRAlertMessage"]', 'content', default=None)(self.doc)
if link:
self.browser.location(link)
class ActionNeededPage(HTMLPage):
def on_load(self):
if self.doc.xpath(u'//form//h1[1][contains(text(), "Accusé de reception du chéquier")]'):
form = self.get_form(name='Alert')
form['command'] = 'validateAlertMessage'
form['radioValide_1_2_40003039944'] = 'Non'
form.submit()
elif self.doc.xpath(u'//p[@class="cddErrorMessage"]'):
error_message = CleanText(u'//p[@class="cddErrorMessage"]')(self.doc)
# TODO python2 handles unicode exceptions badly, fix when passing to python3
raise ActionNeeded(error_message.encode('ascii', 'replace'))
else:
raise ActionNeeded(CleanText(u'//form//h1[1]')(self.doc))
class StopPage(HTMLPage):
pass
class ReturnPage(LoggedPage, HTMLPage):
def on_load(self):
self.get_form(name='retoursso').submit()
......@@ -22,7 +22,7 @@ from .accounts_list import (
AccountsList, TitreDetails, ASVInvest, DetailFondsPage, IbanPage,
ProfilePage, LoanTokenPage, LoanDetailPage,
)
from .login import LoginPage, StopPage, ActionNeededPage, ReturnPage
from .login import StopPage, ActionNeededPage, ReturnPage
from .transfer import TransferPage
from .bills import BillsPage
from .titre import NetissimaPage, TitrePage, TitreHistory, TitreValuePage, ASVHistory
......@@ -32,7 +32,7 @@ class AccountPrelevement(AccountsList):
pass
__all__ = ['AccountsList', 'LoginPage', 'NetissimaPage','TitreDetails',
__all__ = ['AccountsList', 'NetissimaPage','TitreDetails',
'AccountPrelevement', 'TransferPage',
'BillsPage', 'StopPage', 'TitrePage', 'TitreHistory', 'IbanPage',
'TitreValuePage', 'ASVHistory', 'ASVInvest','DetailFondsPage',
......
......@@ -318,11 +318,14 @@ class AccountsList(LoggedPage, HTMLPage):
def load_space_page(self):
# The accounts page exists in two forms: with the spaces list and without
# When having the spaceless page, a form must be submit to access the space page
form = self.get_form(id='user-menu')
on_click = self.doc.xpath('//a[contains(@class, "comptes")]/@onclick')[1]
form = self.get_form(id='header-menu')
on_click = Attr('//a[@class="home"]', 'onclick')(self.doc)
self.fillup_form(form, r"\),\{(.*)\},'", on_click)
form.submit()
def is_multispace_page(self):
return self.doc.xpath('//a[contains(@name, "mainMenu")]')
class IbanPage(LoggedPage, HTMLPage):
def get_iban(self):
......
# -*- coding: utf-8 -*-
# Copyright(C) 2009-2014 Florent Fourcot, Romain Bignon
#
# 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 <http://www.gnu.org/licenses/>.
from weboob.exceptions import ActionNeeded
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.browser.filters.standard import CleanText
class ActionNeededPage(HTMLPage):
def on_load(self):
if self.doc.xpath(u'//form//h1[1][contains(text(), "Accusé de reception du chéquier")]'):
form = self.get_form(name='Alert')
form['command'] = 'validateAlertMessage'
form['radioValide_1_2_40003039944'] = 'Non'
form.submit()
elif self.doc.xpath(u'//p[@class="cddErrorMessage"]'):
error_message = CleanText(u'//p[@class="cddErrorMessage"]')(self.doc)
# TODO python2 handles unicode exceptions badly, fix when passing to python3
raise ActionNeeded(error_message.encode('ascii', 'replace'))
else:
raise ActionNeeded(CleanText(u'//form//h1[1]')(self.doc))
class StopPage(HTMLPage):
pass
class ReturnPage(LoggedPage, HTMLPage):
def on_load(self):
self.get_form(name='retoursso').submit()
......@@ -29,7 +29,7 @@ from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.capabilities.bank.iban import is_iban_valid
from weboob.tools.date import parse_french_date
from .login import INGVirtKeyboard
from ..api.login import INGVirtKeyboard
class MyRecipient(ItemElement):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment