diff --git a/hangups/auth.py b/hangups/auth.py index af66972e..28e16ece 100644 --- a/hangups/auth.py +++ b/hangups/auth.py @@ -16,6 +16,7 @@ import logging import platform import urllib.parse +import webbrowser import mechanicalsoup import requests @@ -47,6 +48,7 @@ FORM_SELECTOR = '#gaia_loginform' EMAIL_SELECTOR = '#Email' PASSWORD_SELECTOR = '#Passwd' +CAPTCHA_SELECTOR = '#logincaptcha' VERIFICATION_FORM_SELECTOR = '#challenge' TOTP_CHALLENGE_SELECTOR = '[action="/signin/challenge/totp/2"]' PHONE_CHALLENGE_SELECTOR = '[action="/signin/challenge/ipp/4"]' @@ -130,6 +132,27 @@ def get_authorization_code(): print(MANUAL_LOGIN_INSTRUCTIONS) return input('Authorization code: ') + @staticmethod + def get_captcha_text(url): + """Prompt for captcha text. + + Args: + url (str): The captcha image URL. + + Returns: + str: Captcha text. + + This method automatically opens the captcha image URL in a web browser + using the :mod:`webbrowser` module (exceptions are ignored) and then + prompts the user to enter the captcha text. + """ + try: + logger.info('Detected captcha, opening image in browser: %s', url) + webbrowser.open(url) + except Exception as e: + logger.warning('Failed to open captcha image in browser! (%s)', e) + return input('Captcha text: ') + class RefreshTokenCache(object): """File-based cache for refresh token. @@ -319,6 +342,19 @@ def _get_authorization_code(session, credentials_prompt): password = credentials_prompt.get_password() browser.submit_form(FORM_SELECTOR, {PASSWORD_SELECTOR: password}) + if browser.has_selector(CAPTCHA_SELECTOR): + for image in browser._page.soup.select('div.captcha-img img'): + captcha_text = credentials_prompt.get_captcha_text( + image.attrs['src'] + ) + browser.submit_form(FORM_SELECTOR, { + CAPTCHA_SELECTOR: captcha_text, + PASSWORD_SELECTOR: password, + }) + break + else: + logger.warning('Detected captcha but failed to extract image!') + if browser.has_selector(TOTP_CHALLENGE_SELECTOR): browser.submit_form(TOTP_CHALLENGE_SELECTOR, {}) elif browser.has_selector(PHONE_CHALLENGE_SELECTOR): @@ -331,9 +367,9 @@ def _get_authorization_code(session, credentials_prompt): input_selector = PHONE_CODE_SELECTOR else: raise GoogleAuthError('Unknown verification code input') - verfification_code = credentials_prompt.get_verification_code() + verification_code = credentials_prompt.get_verification_code() browser.submit_form( - VERIFICATION_FORM_SELECTOR, {input_selector: verfification_code} + VERIFICATION_FORM_SELECTOR, {input_selector: verification_code} ) try: