def maybe_generate_error(self, rate): """ Generate an error according to login and random rate. If login is a specific login matching an error type (``invalidpassword``, ``actionneeded``, ``expiredpassword``), systematically trigger the matching error. If login is ``noerror``, never throw any error. In all other cases, throw a random error according to given rate. :param rate: Rate at which the errors should be generated. """ login = self.config['login'].get() if login == 'invalidpassword': raise BrowserIncorrectPassword if login == 'actionneeded': raise ActionNeeded if login == 'expiredpassword': raise BrowserPasswordExpired if login == 'authmethodnotimplemented': raise AuthMethodNotImplemented if login == 'appvalidation' and self.config.get('resume', Value()).get() is None: raise AppValidation("Please confirm login!") if login == '2fa' and self.config.get('code', Value()).get() is None: raise BrowserQuestion( Value('code', label="Please enter some fake 2fa code!")) if login not in ['noerror', 'session', 'appvalidation', '2fa']: self.random_errors(rate)
def do_login(self): if self.config['pin_code'].get(): self.validate_security_form() if not self.page.is_logged(): raise BrowserIncorrectPassword( "Login / Password or authentication pin_code incorrect") else: return self.login.go() if self.page.is_logged(): return self.page.login(self.username, self.password) self.page.check_user_double_auth() if self.page.check_website_double_auth(): self.otp_form = self.page.get_security_form() self.otp_url = self.url raise BrowserQuestion( Value('pin_code', label=self.page.get_otp_message()[0] or 'Please type the OTP you received')) if not self.page.is_logged(): raise BrowserIncorrectPassword
def on_load(self): receive_code_btn = bool( self.doc.xpath( '//div[has-class("authentification-bloc-content-btn-bloc")][count(input)=1]' )) submit_input = self.doc.xpath('//input[@type="submit"]') if receive_code_btn and len(submit_input) == 1: form = self.get_form( xpath= '//form[.//div[has-class("authentification-bloc-content-btn-bloc")][count(input)=1]]', submit= '//div[has-class("authentification-bloc-content-btn-bloc")]//input[@type="submit"]' ) # sending mail with code form.submit() raise BrowserQuestion( Value('otp', label=u'Veuillez saisir votre code de sécurité')) send_code_form = bool( self.doc.xpath( '//form[.//div[has-class("authentification-bloc-content-btn-bloc")]]' )) # TODO move this code in browser otp = self.browser.config['otp'].get( ) if 'otp' in self.browser.config else None if send_code_form and otp: self.check_error() self.send_otp(otp)
def sms_second_step(self): form = self.get_form() self.browser.auth_token = form['flow_secureForm_instance'] form['otp_prepare[receiveCode]'] = '' form.submit() raise BrowserQuestion(Value('pin_code', label='Enter the PIN Code'))
def send_sms(self): """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 """ url = "https://%s/ajax/banque/otp.phtml?org=%s&alertType=10100" % ( self.browser.DOMAIN, self.REFERER) req = urllib2.Request(url, headers=self.headers_ajax) response = self.browser.open(req) #extrat authentication token from response (in form) info = response.read() regex = re.compile(self.MAX_LIMIT) r = regex.search(info) if r: raise BrowserAuthenticationCodeMaxLimit( "Vous avez atteint le nombre maximum d'utilisation de l'authentification forte" ) regex = re.compile(r"name=\\\"authentificationforteToken\\\" " r"value=\\\"(?P<value>\w*?)\\\"") r = regex.search(info) self.browser.auth_token = r.group('value') #step2 url = "https://" + self.browser.DOMAIN + "/ajax/banque/otp.phtml" data = "authentificationforteToken=%s&authentificationforteStep=start&alertType=10100&org=%s&validate=" % ( self.browser.auth_token, self.REFERER) req = urllib2.Request(url, data, self.headers_ajax) response = self.browser.open(req) raise BrowserQuestion(Value('pin_code', label='Enter the PIN Code'))
def do_login(self): self.code = self.config['code'].get() if self.resume: return self.handle_polling() elif self.code: return self.handle_sms() self.login_without_2fa() self.auth_page.go() if self.auth_page.is_here(): # Handle 2FA # 2FA seem to be handled by LBP. Indeed logins after 2FA will redirect to the LBP main page # Consequently no state for future connexion needs to be kept if self.request_information is None: raise NeedInteractiveFor2FA() auth_method = self.page.get_auth_method() if auth_method == 'cer+': # We force here the first device present self.decoupled_page.go(params={'deviceSelected': '0'}) raise AppValidation(self.page.get_decoupled_message()) elif auth_method == 'cer': self.location('/voscomptes/canalXHTML/securite/gestionAuthentificationForte/authenticateCerticode-gestionAuthentificationForte.ea') self.page.check_if_is_blocked() self.sms_form = self.page.get_sms_form() raise BrowserQuestion(Value('code', label='Entrez le code reçu par SMS')) elif auth_method == 'no2fa': self.location(self.page.get_skip_url())
def handle_security(self): if self.page.doc.xpath('//span[@class="a-button-text"]'): self.page.send_code() self.otp_form = self.page.get_response_form() self.otp_url = self.url raise BrowserQuestion( Value('pin_code', label=self.page.get_otp_message() if self.page.get_otp_message() else 'Please type the OTP you received'))
def sms_second_step(self): # <div class="form-errors"><ul><li>Vous avez atteint le nombre maximal de demandes pour aujourd'hui</li></ul></div> error = CleanText('//div[has-class("form-errors")]')(self.doc) if len(error) > 0: raise BrowserIncorrectPassword(error) form = self.get_form() self.browser.auth_token = form['flow_secureForm_instance'] form['otp_prepare[receiveCode]'] = '' form.submit() raise BrowserQuestion(Value('pin_code', label='Enter the PIN Code'))
def check_auth_methods(self): if self.mobile_confirmation.is_here(): self.page.check_bypass() if self.mobile_confirmation.is_here(): self.polling_data = self.page.get_polling_data() assert self.polling_data, "Can't proceed to polling if no polling_data" raise AppValidation(self.page.get_validation_msg()) if self.otp_validation_page.is_here(): self.otp_data = self.page.get_otp_data() assert self.otp_data, "Can't proceed to SMS handling if no otp_data" raise BrowserQuestion(Value('code', label=self.page.get_message())) self.check_otp_blocked()
def do_otp(self, mfaToken): data = { 'challengeType': 'otp', 'mfaToken': mfaToken } try: result = self.request('/api/mfa/challenge', json=data) except ClientError as e: json_response = e.response.json() # if we send more than 5 otp without success, the server will warn the user to # wait 12h before retrying, but in fact it seems that we can resend otp 5 mins later if e.response.status_code == 429: raise BrowserUnavailable(json_response['detail']) raise BrowserQuestion(Value('otp', label='Veuillez entrer le code reçu par sms au ' + result['obfuscatedPhoneNumber']))
def handle_sms(self): self.otp_data['final_url_params']['otp_password'] = self.code self.finalize_twofa(self.otp_data) ## cases where 2FA is not finalized # Too much wrong OTPs, locked down after total 3 wrong inputs self.check_otp_blocked() # OTP is expired after 15', we end up on login page if self.login.is_here(): raise BrowserIncorrectPassword("Le code de confirmation envoyé par SMS n'est plus utilisable") # Wrong OTP leads to same form with error message, re-raise BrowserQuestion elif self.otp_validation_page.is_here(): error_msg = self.page.get_error_message() if 'erroné' not in error_msg: raise BrowserUnavailable(error_msg) else: label = '%s %s' % (error_msg, self.page.get_message()) raise BrowserQuestion(Value('code', label=label)) self.otp_data = {}
def handle_security(self): otp_type = self.page.get_otp_type() if otp_type == '/ap/signin': # this otp will be always present until user deactivate it raise ActionNeeded( 'You have enabled otp in your options, please deactivate it before synchronize' ) if self.page.doc.xpath('//span[@class="a-button-text"]'): self.page.send_code() form = self.page.get_response_form() self.otp_form = form['form'] self.otp_url = self.url self.otp_style = form['style'] self.otp_headers = dict(self.session.headers) raise BrowserQuestion( Value('pin_code', label=self.page.get_otp_message() if self.page.get_otp_message() else 'Please type the OTP you received'))
def do_login(self): # ********** admire how login works on edf par website ********** # login part on edf particulier website is very tricky # FIRST time we connect we have an otp, BUT not password, we can't know if it is wrong at this moment # SECOND time we use password, and not otp auth_params = {'realm': '/INTERNET'} if self.config['otp'].get(): self.otp_data['callbacks'][0]['input'][0]['value'] = self.config[ 'otp'].get() headers = { 'X-Requested-With': 'XMLHttpRequest', } self.authenticate.go(json=self.otp_data, params=auth_params, headers=headers) self.id_token1 = self.page.get_data( )['callbacks'][1]['output'][0]['value'] # id_token1 is VERY important, we keep it indefinitely, without it edf will ask again otp else: self.location('/bin/edf_rc/servlets/sasServlet', params={'processus': 'TDB'}) if self.connected.is_here(): # we are already logged # sometimes even if password is wrong, you can be logged if you retry self.logger.info('already logged') return self.authenticate.go(method='POST', params=auth_params) data = self.page.get_data() data['callbacks'][0]['input'][0]['value'] = self.username self.authenticate.go(json=data, params=auth_params) data = self.page.get_data( ) # yes, we have to get response and send it again, beautiful isn't it ? if data['stage'] == 'UsernameAuth2': # username is wrong raise BrowserIncorrectPassword( data['callbacks'][1]['output'][0]['value']) if self.id_token1: data['callbacks'][0]['input'][0]['value'] = self.id_token1 else: # the FIRST time we connect, we don't have id_token1, we have no choice, we'll receive an otp data['callbacks'][0]['input'][0]['value'] = ' ' self.authenticate.go(json=data, params=auth_params) data = self.page.get_data() assert data['stage'] in ( 'HOTPcust3', 'PasswordAuth2'), 'stage is %s' % data['stage'] if data['stage'] == 'HOTPcust3': # OTP part if self.id_token1: # this shouldn't happen except if id_token1 expire one day, who knows... self.logger.warning( 'id_token1 is not null but edf ask again for otp') # a legend say this url is the answer to life the universe and everything, because it is use EVERYWHERE in login self.authenticate.go(json=self.page.get_data(), params=auth_params) self.otp_data = self.page.get_data() label = self.otp_data['callbacks'][0]['output'][0]['value'] raise BrowserQuestion(Value('otp', label=label)) if data['stage'] == 'PasswordAuth2': # password part data['callbacks'][0]['input'][0]['value'] = self.password self.authenticate.go(json=self.page.get_data(), params=auth_params) # should be SetPasAuth2 if password is ok if self.page.get_data()['stage'] == 'PasswordAuth2': attempt_number = self.page.get_data( )['callbacks'][1]['output'][0]['value'] # attempt_number is the number of wrong password msg = self.wrong_password.go().get_wrongpass_message( attempt_number) raise BrowserIncorrectPassword(msg) data = self.page.get_data() # yes, send previous data again, i know i know self.authenticate.go(json=data, params=auth_params) self.session.cookies['ivoiream'] = self.page.get_token() self.user_status.go() """ call check_authenticate url before get subscription in profil, or we'll get an error 'invalid session' we do nothing with this response (which contains false btw) but edf website expect we call it before or will reject us """ self.check_authenticate.go()
def get_parcel_tracking(self, _id): """ Get information about a parcel. :param _id: _id of the parcel :type _id: :class:`str` :rtype: :class:`Parcel` :raises: :class:`ParcelNotFound` """ # Tracking number format: # - 2 chars: optional merchant identifier (eg, AM for Amazon, 85 for cdiscount, ...) # - 10 digits: shipment tracking number # - 2 digits: optional suffix, seems to always be "01" when present but is never sent to the API # # Many merchants seem to give only the 10 digits tracking number so the user needs to # manually select the merchant from a list in that case. merchant = None code = None _id = _id.strip().upper() if len(_id) == 10: code = _id elif len(_id) in (12, 14): merchant = _id[:2] code = _id[2:12] else: raise ParcelNotFound( "Tracking number must be 10, 12 or 14 characters long.") merchant = merchant or self.config['merchant'].get() if not merchant: # No merchant info in the tracking number # we have to ask the user to select it merchants = self.browser.get_merchants() raise BrowserQuestion( Value( 'merchant', label='Merchant prefix (prepend to tracking number): ', tiny=False, choices=merchants, )) self.config['merchant'].set(None) name = self.config['last_name'].get()[:4].ljust(4).upper() events = list(self.browser.iter_events(merchant, code, name)) parcel = Parcel(merchant + code) parcel.arrival = NotAvailable # This is what the legacy tracking website used to show # when there are no events yet parcel.info = "Votre commande est en cours d'acheminement dans notre réseau." parcel.history = events parcel.status = Parcel.STATUS_IN_TRANSIT if not events: parcel.status = Parcel.STATUS_PLANNED return parcel parcel.info = events[0].activity arrived_event = next( (event for event in events if "Votre colis est disponible" in event.activity), None) if arrived_event: parcel.status = Parcel.STATUS_ARRIVED parcel.arrival = arrived_event.date return parcel
def check_auth_method(self): auth_method = self.page.get_auth_method() if not auth_method: self.logger.warning('No auth method available !') raise ActionNeeded( 'Veuillez ajouter un numéro de téléphone sur votre banque et/ou activer votre Pass Sécurité' ) if auth_method['unavailability_reason'] == "ts_non_enrole": raise ActionNeeded( 'Veuillez ajouter un numéro de téléphone sur votre banque') elif auth_method['unavailability_reason']: assert False, 'Unknown unavailability reason "%s" found' % auth_method[ 'unavailability_reason'] if auth_method['type_proc'].lower() == 'auth_oob': self.location( '/sec/oob_sendooba.json', method='POST', headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) donnees = self.page.doc['donnees'] self.polling_transaction = donnees['id-transaction'] if donnees.get('expiration_date_hh') and donnees.get( 'expiration_date_mm'): now = datetime.now() expiration_date = now.replace( hour=int(donnees['expiration_date_hh']), minute=int(donnees['expiration_date_mm'])) self.polling_duration = int( (expiration_date - now).total_seconds()) message = "Veuillez valider l'opération dans votre application" terminal_name = auth_method['terminal'][0]['nom'] if terminal_name: message += " sur " + terminal_name raise AppValidation(message) elif auth_method['type_proc'].lower() == 'auth_csa': if auth_method['mode'] == "SMS": self.location('/sec/csa/send.json', data={ 'csa_op': "auth", }) raise BrowserQuestion( Value( 'code', label= 'Entrez le Code Sécurité reçu par SMS sur le numéro ' + auth_method['ts'])) self.logger.warning('Unknown CSA method "%s" found', auth_method['mod']) else: self.logger.warning('Unknown sign method "%s" found', auth_method['type_proc']) assert False, 'Unknown auth method "%s: %s" found' % ( auth_method['type_proc'], auth_method.get('mod'))