Blob Blame History Raw
/* libcmis
 * Version: MPL 1.1 / GPLv2+ / LGPLv2+
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License or as specified alternatively below. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Major Contributor(s):
 * Copyright (C) 2013 Cao Cuong Ngo <cao.cuong.ngo@gmail.com>
 *
 *
 * All Rights Reserved.
 *
 * For minor contributions see the git repository.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPLv2+"), or
 * the GNU Lesser General Public License Version 2 or later (the "LGPLv2+"),
 * in which case the provisions of the GPLv2+ or the LGPLv2+ are applicable
 * instead of those above.
 */

#include <cassert>

#include <libxml/HTMLparser.h>
#include <libxml/xmlreader.h>

#include "oauth2-providers.hxx"
#include "http-session.hxx"

using namespace std;

namespace {

// See <https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer>:
void addXWwwFormUrlencoded(std::string * buffer, std::string const & data) {
    assert(buffer);
    for (string::const_iterator i = data.begin(); i != data.end(); ++i) {
        unsigned char c = static_cast<unsigned char>(*i);
        if (c == ' ' || c == '*' || c == '-' || c == '.' || (c >= '0' && c <= '9')
            || (c >= 'A' && c <= 'Z') || c == '_' || (c >= 'a' && c <= 'z'))
        {
            *buffer += static_cast<char>(c);
        } else {
            static const char hex[16] = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
            *buffer += '%';
            *buffer += hex[c >> 4];
            *buffer += hex[c & 0xF];
        }
    }
}

}

string OAuth2Providers::OAuth2Gdrive( HttpSession* session, const string& authUrl,
                                      const string& username, const string& password )
{
    /* This member function implements 'Google OAuth 2.0'
     *
     * The interaction is carried out by libcmis, with no web browser involved.
     *
     * Normal sequence (without 2FA) is:
     * 1) a get to activate login page
     *    receive first login page, html format
     * 2) subsequent post to sent email
     *    receive html page for password input
     * 3) subsequent post to send password
     *    receive html page for application consent
     * 4) subsequent post to send a consent for the application
     *    receive a single-use authorization code
     *    this code is returned as a string
     */

    static const string CONTENT_TYPE( "application/x-www-form-urlencoded" );
    // STEP 1: Log in
    string res;
    try
    {
        // send the first get, receive the html login page
        res = session->httpGetRequest( authUrl )->getStream( )->str( );
    }
    catch ( const CurlException& e )
    {
        return string( );
    }

    string loginEmailPost, loginEmailLink;
    if ( !parseResponse( res.c_str( ), loginEmailPost, loginEmailLink ) )
        return string( );

    loginEmailPost += "Email=";
    addXWwwFormUrlencoded(&loginEmailPost, username);

    istringstream loginEmailIs( loginEmailPost );
    string loginEmailRes;
    try
    {
        // send a post with user email, receive the html page for password input
        loginEmailRes = session->httpPostRequest ( loginEmailLink, loginEmailIs, CONTENT_TYPE )
                        ->getStream( )->str( );
    }
    catch ( const CurlException& e )
    {
        return string( );
    }

    string loginPasswdPost, loginPasswdLink;
    if ( !parseResponse( loginEmailRes.c_str( ), loginPasswdPost, loginPasswdLink ) )
        return string( );

    loginPasswdPost += "Passwd=";
    addXWwwFormUrlencoded(&loginPasswdPost, password);

    istringstream loginPasswdIs( loginPasswdPost );
    string loginPasswdRes;
    try
    {
        // send a post with user password, receive the application consent page
        loginPasswdRes = session->httpPostRequest ( loginPasswdLink, loginPasswdIs, CONTENT_TYPE )
                        ->getStream( )->str( );
    }
    catch ( const CurlException& e )
    {
        return string( );
    }

    // STEP 2: allow libcmis to access google drive
    string approvalPost, approvalLink;
    if ( !parseResponse( loginPasswdRes. c_str( ), approvalPost, approvalLink) )
        return string( );
    approvalPost += "submit_access=true";

    istringstream approvalIs( approvalPost );
    string approvalRes;
    try
    {
        // send a post with application consent
        approvalRes = session->httpPostRequest ( approvalLink, approvalIs,
                            CONTENT_TYPE) ->getStream( )->str( );
    }
    catch ( const CurlException& e )
    {
        throw e.getCmisException( );
    }

    // STEP 3: Take the authentication code from the text bar
    string code = parseCode( approvalRes.c_str( ) );

    return code;
}

string OAuth2Providers::OAuth2Onedrive( HttpSession* /*session*/, const string& /*authUrl*/,
                                      const string& /*username*/, const string& /*password*/ )
{
    return string( );
}

string OAuth2Providers::OAuth2Alfresco( HttpSession* session, const string& authUrl,
                                        const string& username, const string& password )
{
    static const string CONTENT_TYPE( "application/x-www-form-urlencoded" );
   
    // Log in
    string res;
    try
    {
        res = session->httpGetRequest( authUrl )->getStream( )->str( );
    }
    catch ( const CurlException& e )
    {
        return string( );
    }

    string loginPost, loginLink;

    if ( !parseResponse( res.c_str( ), loginPost, loginLink ) ) 
        return string( );
    
    loginPost += "username=";  
    loginPost += string( username );
    loginPost += "&password=";
    loginPost += string( password );
    loginPost += "&action=Grant";

    istringstream loginIs( loginPost );
   
    libcmis::HttpResponsePtr resp;

    // Get the code
    try 
    {
        // Alfresco code is in the redirect link
        resp = session->httpPostRequest( loginLink, loginIs, CONTENT_TYPE, false ); 
    }
    catch ( const CurlException& e )
    {
        return string( );
    }

    string header = resp->getHeaders()["Location"];
    string code;
    int start = header.find("code=");
    int notFound = ( int ) ( string::npos );
    if ( start != notFound )
    {   
        start += 5;
        int end = header.find("&");
        if ( end != notFound )
            code = header.substr( start, end - start );
        else code = header.substr( start );
    }

    return code;
}

OAuth2Parser OAuth2Providers::getOAuth2Parser( const std::string& url )
{
    if ( url.find( "https://api.alfresco.com/" ) == 0 )
        // For Alfresco in the cloud, only match the hostname as there can be several
        // binding URLs created with it.
        return OAuth2Alfresco;
    else if ( url.find( "https://www.googleapis.com/drive/v2" ) == 0 )
        return OAuth2Gdrive;
    else if ( url.find( "https://apis.live.net/v5.0" ) == 0 )
        return OAuth2Onedrive;

    return OAuth2Gdrive;
}

int OAuth2Providers::parseResponse ( const char* response, string& post, string& link )
{
    xmlDoc *doc = htmlReadDoc ( BAD_CAST( response ), NULL, 0,
            HTML_PARSE_NOWARNING | HTML_PARSE_RECOVER | HTML_PARSE_NOERROR );
    if ( doc == NULL ) return 0;
    xmlTextReaderPtr reader =   xmlReaderWalker( doc );
    if ( reader == NULL ) return 0;
    while ( true )
    {
        // Go to the next node, quit if not found
        if ( xmlTextReaderRead ( reader ) != 1) break;
        xmlChar* nodeName = xmlTextReaderName ( reader );
        if ( nodeName == NULL ) continue;
        // Find the redirect link
        if ( xmlStrEqual( nodeName, BAD_CAST( "form" ) ) )
        {
            xmlChar* action = xmlTextReaderGetAttribute( reader, 
                                                         BAD_CAST( "action" ));
            if ( action != NULL )
            {
                if ( xmlStrlen(action) > 0)
                    link = string ( (char*) action);
                xmlFree (action);
            }
        }
        // Find input values
        if ( !xmlStrcmp( nodeName, BAD_CAST( "input" ) ) )
        {
            xmlChar* name = xmlTextReaderGetAttribute( reader, 
                                                       BAD_CAST( "name" ));
            xmlChar* value = xmlTextReaderGetAttribute( reader, 
                                                        BAD_CAST( "value" ));
            if ( ( name != NULL ) && ( value!= NULL ) )
            {
                if ( ( xmlStrlen( name ) > 0) && ( xmlStrlen( value ) > 0) )
                {
                    post += libcmis::escape( ( char * ) name ); 
                    post += string ( "=" ); 
                    post += libcmis::escape( ( char * ) value ); 
                    post += string ( "&" );
                }
            }
            xmlFree( name );
            xmlFree( value );
        }
        xmlFree( nodeName );
    }
    xmlFreeTextReader( reader );              
    xmlFreeDoc( doc );
    if ( link.empty( ) || post.empty () ) 
        return 0;
    return 1;
}

string OAuth2Providers::parseCode( const char* response )
{
    string authCode;
    xmlDoc *doc = htmlReadDoc ( BAD_CAST( response ), NULL, 0,
            HTML_PARSE_NOWARNING | HTML_PARSE_RECOVER | HTML_PARSE_NOERROR );
    if ( doc == NULL ) return authCode;
    xmlTextReaderPtr reader = xmlReaderWalker( doc );
    if ( reader == NULL ) return authCode;

    while ( true )
    {
        // Go to the next node, quit if not found
        if ( xmlTextReaderRead ( reader ) != 1) break;
        xmlChar* nodeName = xmlTextReaderName ( reader );
        if ( nodeName == NULL ) continue;
        // Find the code 
        if ( xmlStrEqual( nodeName, BAD_CAST ( "input" ) ) )
        { 
            xmlChar* id = xmlTextReaderGetAttribute( reader, BAD_CAST( "id" ));
            if ( id != NULL )
            {
                if ( xmlStrEqual( id, BAD_CAST ( "code" ) ) )
                {
                    xmlChar* code = xmlTextReaderGetAttribute( 
                        reader, BAD_CAST("value") );
                    if ( code!= NULL )
                    {
                        authCode = string ( (char*) code );
                        xmlFree( code );
                    }
                }
                xmlFree ( id );
            }
        }
        xmlFree( nodeName );
    }
    xmlFreeTextReader( reader );              
    xmlFreeDoc( doc );

    return authCode;
}