phpcurlauthenticationgoogle-voice

Log In to Google Account through Curl


I am trying to get this script to work: https://github.com/aaronpk/Google-Voice-PHP-API

Google's recently changed their way to log in to your account. What would be the steps to login through cURL to Google?

It will throw this now when trying to post to the form on this page (https://accounts.google.com/signin/challenge/sl/password):

Type: Exception

Message: Could not parse for GALX or gxf token. Inputs from page: array ()

Solution

  • without checking that code specifically, here's the code i use to login to gmail, using hhb_curl (and note: initially i pretended to be an iPhone, because it was easier to login via the mobile login page than the desktop login page, but eventually i ported it to the desktop version, but there's still some mobile-version-residue in the code. also, sometimes gmail will randomly ask you to verify your recovery email when using this method, thus the $recoveryEmailChallengeAnswer variable. it doesn't happen everytime, but google notice that something is "off" with the login, and thus asks for recovery email verification randomly. my code handles that situation transparently.):

    function loginGmail(string $username, string $password, string $recoveryEmailChallengeAnswer, bool $requestHtmlVersion = true): \hhb_curl {
        $hc = new hhb_curl ( '', true );
        $hc->setopt_array ( array (
                CURLOPT_TIMEOUT => 20, // i just have a shitty connection :(
                CURLOPT_CONNECTTIMEOUT => 10 
        ) );
        if (0) {
            $hc->setopt_array ( array (
                    CURLOPT_USERAGENT => 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1' 
            ) );
        }
        $html = $hc->exec ( 'https://gmail.com' )->getStdOut ();
        $domd = @DOMDocument::loadHTML ( $html );
        $inputs = getDOMDocumentFormInputs ( $domd, true, false ) ['gaia_loginform'];
        // hhb_var_dump ( $hc->getStdErr (), $hc->getStdOut (), $inputs ) & die();
        $loginUrl = $domd->getElementById ( "gaia_loginform" )->getAttribute ( "action" );
        $inputs ['Email'] = $username;
        $html = $hc->setopt_array ( array (
                CURLOPT_POST => 1,
                CURLOPT_POSTFIELDS => http_build_query ( $inputs ),
                CURLOPT_URL => $loginUrl 
        ) )->exec ()->getStdOut ();
        $domd = @DOMDocument::loadHTML ( $html );
        $inputs = getDOMDocumentFormInputs ( $domd, true, false ) ['gaia_loginform'];
        // hhb_var_dump ( $hc->getStdErr (), $hc->getStdOut (), $inputs );
        $loginUrl = $domd->getElementById ( "gaia_loginform" )->getAttribute ( "action" );
        $inputs ['Passwd'] = $password;
        try {
            $starttime = microtime ( true );
            $html = $hc->setopt_array ( array (
                    CURLOPT_POST => 1,
                    CURLOPT_POSTFIELDS => http_build_query ( $inputs ),
                    CURLOPT_URL => $loginUrl 
            ) )->exec ()->getStdOut ();
        } finally{
            // hhb_var_dump ( $hc->getStdErr (), $hc->getStdOut (), $inputs, (microtime ( true ) - $starttime) ) & die ();
        }
        $domd = @DOMDocument::loadHTML ( $html );
        $xp = new DOMXPath ( $domd );
        $loginErrors = $xp->query ( '//span[contains(@class,"error-msg")]' );
        $loginErrorText = '';
        foreach ( $loginErrors as $tmp ) {
            $tmp = trim ( $tmp->textContent );
            if (strlen ( $tmp )) {
                $loginErrorText .= ' - ' . $tmp;
            }
        }
        $authChallenge = $domd->getElementById ( "challengePickerList" );
        if (NULL !== $authChallenge) {
            // gotdammit... got an auth challenge page, asking you to choose a challenge
            $loginErrorText .= ' - got an auth challenge page, asking you to choose a challenge: ' . trim ( $authChallenge->textContent );
            // TODO: automatically select "provide recovery email" here instead of throwing a login error exception.
        } else {
            if (false !== stripos ( $html, 'Enter recovery email' ) && NULL !== ($authChallenge = $domd->getElementById ( "challenge" ))) {
                // got an auth challenge for providing recovery email..
                // luckily, we can automatically recover from this.
                $inputs = getDOMDocumentFormInputs ( $domd, true, false ) ['challenge'];
                assert ( array_key_exists ( 'email', $inputs ) );
                $inputs ['email'] = $recoveryEmailChallengeAnswer;
                $url = $authChallenge->getAttribute ( "action" );
                if (! parse_url ( $url, PHP_URL_HOST )) {
                    $url = 'https://' . parse_url ( $hc->getinfo ( CURLINFO_EFFECTIVE_URL ), PHP_URL_HOST ) . $url;
                }
                $html = $hc->setopt_array ( array (
                        CURLOPT_POST => 1,
                        CURLOPT_POSTFIELDS => http_build_query ( $inputs ),
                        CURLOPT_URL => $url 
                ) )->exec ()->getStdOut ();
                $domd = @DOMDocument::loadHTML ( $html );
                $xp = new DOMXPath ( $domd );
                // TODO: detect incorrect recovery email supplied error here.
            }
        }
        unset ( $authChallenge );
    
        if (! empty ( $loginErrorText )) {
            throw new \RuntimeException ( 'errors loggin in: ' . $loginErrorText );
        } else {
            // logged in! :D
        }
        if ($requestHtmlVersion) {
            // now we need to enable HTML view, it's a <form> POST request, but we can't use getDOMDocumentFormInputs (bug?)
            $found = false;
            foreach ( $domd->getElementsByTagName ( "form" ) as $form ) {
                if (false === stripos ( $form->textContent, "Gmail's basic HTML view, which doesn't require JavaScript" ) && $xp->query ( "./input[@value='Load basic HTML']", $form )->length === 0) {
                    continue;
                }
                $found = true;
                $url = $form->getAttribute ( "action" );
                if (! parse_url ( $url, PHP_URL_HOST )) {
                    $url = $hc->getinfo ( CURLINFO_EFFECTIVE_URL ) . $url;
                }
                // hhb_var_dump ( $url ) & die ();
                $inputs = [ ];
                foreach ( $form->getElementsByTagName ( "input" ) as $input ) {
                    $name = $input->getAttribute ( "name" );
                    if (empty ( $name )) {
                        continue;
                    }
                    $inputs [$name] = $input->getAttribute ( "value" );
                }
                // hhb_var_dump ( $inputs ) & die ();
                break;
            }
            if (! $found) {
                $str = 'failed to find HTML version request form!';
                // hhb_var_dump ( $str, $hc->getStdErr (), $hc->getStdOut (), $inputs ); // & die ();
                throw new \RuntimeException ( $str );
            }
            $html = $hc->setopt_array ( array (
                    CURLOPT_POST => 1,
                    CURLOPT_POSTFIELDS => http_build_query ( $inputs ),
                    CURLOPT_URL => $url 
            ) )->exec ()->getStdOut ();
        }
        hhb_var_dump ( $hc->getStdErr (), $hc->getStdOut (), $inputs ); // & die ();
        return $hc;
    }
    function rightTrim($str, $needle, $caseSensitive = true) {
        $strPosFunction = $caseSensitive ? "strpos" : "stripos";
        if ($strPosFunction ( $str, $needle, strlen ( $str ) - strlen ( $needle ) ) !== false) {
            $str = substr ( $str, 0, - strlen ( $needle ) );
        }
        return $str;
    }
    function getDOMDocumentFormInputs(\DOMDocument $domd, bool $getOnlyFirstMatches = false, bool $getElements = true): array {
        // :DOMNodeList?
        if (! $getOnlyFirstMatches && ! $getElements) {
            throw new \InvalidArgumentException ( '!$getElements is currently only implemented for $getOnlyFirstMatches (cus im lazy and nobody has written the code yet)' );
        }
        $forms = $domd->getElementsByTagName ( 'form' );
        $parsedForms = array ();
        $isDescendantOf = function (\DOMNode $decendant, \DOMNode $ele): bool {
            $parent = $decendant;
            while ( NULL !== ($parent = $parent->parentNode) ) {
                if ($parent === $ele) {
                    return true;
                }
            }
            return false;
        };
        // i can't use array_merge on DOMNodeLists :(
        $merged = function () use (&$domd): array {
            $ret = array ();
            foreach ( $domd->getElementsByTagName ( "input" ) as $input ) {
                $ret [] = $input;
            }
            foreach ( $domd->getElementsByTagName ( "textarea" ) as $textarea ) {
                $ret [] = $textarea;
            }
            foreach ( $domd->getElementsByTagName ( "button" ) as $button ) {
                $ret [] = $button;
            }
            return $ret;
        };
        $merged = $merged ();
        foreach ( $forms as $form ) {
            $inputs = function () use (&$domd, &$form, &$isDescendantOf, &$merged): array {
                $ret = array ();
                foreach ( $merged as $input ) {
                    // hhb_var_dump ( $input->getAttribute ( "name" ), $input->getAttribute ( "id" ) );
                    if ($input->hasAttribute ( "disabled" )) {
                        // ignore disabled elements?
                        continue;
                    }
                    $name = $input->getAttribute ( "name" );
                    if ($name === '') {
                        // echo "inputs with no name are ignored when submitted by mainstream browsers (presumably because of specs)... follow suite?", PHP_EOL;
                        continue;
                    }
                    if (! $isDescendantOf ( $input, $form ) && $form->getAttribute ( "id" ) !== '' && $input->getAttribute ( "form" ) !== $form->getAttribute ( "id" )) {
                        // echo "this input does not belong to this form.", PHP_EOL;
                        continue;
                    }
                    if (! array_key_exists ( $name, $ret )) {
                        $ret [$name] = array (
                                $input 
                        );
                    } else {
                        $ret [$name] [] = $input;
                    }
                }
                return $ret;
            };
            $inputs = $inputs (); // sorry about that, Eclipse gets unstable on IIFE syntax.
            $hasName = true;
            $name = $form->getAttribute ( "id" );
            if ($name === '') {
                $name = $form->getAttribute ( "name" );
                if ($name === '') {
                    $hasName = false;
                }
            }
            if (! $hasName) {
                $parsedForms [] = array (
                        $inputs 
                );
            } else {
                if (! array_key_exists ( $name, $parsedForms )) {
                    $parsedForms [$name] = array (
                            $inputs 
                    );
                } else {
                    $parsedForms [$name] [] = $tmp;
                }
            }
        }
        unset ( $form, $tmp, $hasName, $name, $i, $input );
        if ($getOnlyFirstMatches) {
            foreach ( $parsedForms as $key => $val ) {
                $parsedForms [$key] = $val [0];
            }
            unset ( $key, $val );
            foreach ( $parsedForms as $key1 => $val1 ) {
                foreach ( $val1 as $key2 => $val2 ) {
                    $parsedForms [$key1] [$key2] = $val2 [0];
                }
            }
        }
        if ($getElements) {
            return $parsedForms;
        }
        $ret = array ();
        foreach ( $parsedForms as $formName => $arr ) {
            $ret [$formName] = array ();
            foreach ( $arr as $ele ) {
                $ret [$formName] [$ele->getAttribute ( "name" )] = $ele->getAttribute ( "value" );
            }
        }
        return $ret;
    }