2017-03-28 11:31:38 +03:00
/*
* Copyright ( C ) by Olivier Goffart < ogoffart @ woboq . com >
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
*
* This program 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 General Public License
* for more details .
*/
# include <QDesktopServices>
# include <QNetworkReply>
# include <QTimer>
# include "account.h"
# include "creds/oauth.h"
# include <QJsonObject>
# include <QJsonDocument>
# include "theme.h"
2017-09-08 17:43:59 +03:00
# include "networkjobs.h"
2017-03-28 11:31:38 +03:00
namespace OCC {
2017-06-13 11:24:26 +03:00
Q_LOGGING_CATEGORY ( lcOauth , " sync.credentials.oauth " , QtInfoMsg )
2017-03-28 11:31:38 +03:00
OAuth : : ~ OAuth ( )
{
}
2017-07-03 15:51:20 +03:00
static void httpReplyAndClose ( QTcpSocket * socket , const char * code , const char * html ,
const char * moreHeaders = nullptr )
2017-03-28 11:31:38 +03:00
{
socket - > write ( " HTTP/1.1 " ) ;
socket - > write ( code ) ;
socket - > write ( " \r \n Content-Type: text/html \r \n Connection: close \r \n Content-Length: " ) ;
socket - > write ( QByteArray : : number ( qstrlen ( html ) ) ) ;
2017-07-03 15:51:20 +03:00
if ( moreHeaders ) {
socket - > write ( " \r \n " ) ;
socket - > write ( moreHeaders ) ;
}
2017-03-28 11:31:38 +03:00
socket - > write ( " \r \n \r \n " ) ;
socket - > write ( html ) ;
socket - > disconnectFromHost ( ) ;
2017-07-03 15:51:20 +03:00
// We don't want that deleting the server too early prevent queued data to be sent on this socket.
// The socket will be deleted after disconnection because disconnected is connected to deleteLater
socket - > setParent ( nullptr ) ;
2017-03-28 11:31:38 +03:00
}
void OAuth : : start ( )
{
// Listen on the socket to get a port which will be used in the redirect_uri
if ( ! _server . listen ( QHostAddress : : LocalHost ) ) {
emit result ( NotSupported , QString ( ) ) ;
return ;
}
if ( ! openBrowser ( ) )
return ;
QObject : : connect ( & _server , & QTcpServer : : newConnection , this , [ this ] {
while ( QTcpSocket * socket = _server . nextPendingConnection ( ) ) {
QObject : : connect ( socket , & QTcpSocket : : disconnected , socket , & QTcpSocket : : deleteLater ) ;
QObject : : connect ( socket , & QIODevice : : readyRead , this , [ this , socket ] {
QByteArray peek = socket - > peek ( qMin ( socket - > bytesAvailable ( ) , 4000LL ) ) ; //The code should always be within the first 4K
if ( peek . indexOf ( ' \n ' ) < 0 )
return ; // wait until we find a \n
QRegExp rx ( " ^GET / \\ ?code=([a-zA-Z0-9]+) [ & ] " ) ; // Match a /?code=... URL
if ( rx . indexIn ( peek ) ! = 0 ) {
httpReplyAndClose ( socket , " 404 Not Found " , " <html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html> " ) ;
return ;
}
QString code = rx . cap ( 1 ) ; // The 'code' is the first capture of the regexp
QUrl requestToken ( _account - > url ( ) . toString ( )
+ QLatin1String ( " /index.php/apps/oauth2/api/v1/token?grant_type=authorization_code&code= " )
+ code
+ QLatin1String ( " &redirect_uri=http://localhost: " ) + QString : : number ( _server . serverPort ( ) ) ) ;
QNetworkRequest req ;
req . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/x-www-form-urlencoded " ) ;
2017-09-11 13:23:52 +03:00
QString basicAuth = QString ( " %1:%2 " ) . arg (
Theme : : instance ( ) - > oauthClientId ( ) , Theme : : instance ( ) - > oauthClientSecret ( ) ) ;
req . setRawHeader ( " Authorization " , " Basic " + basicAuth . toUtf8 ( ) . toBase64 ( ) ) ;
2017-09-08 17:43:59 +03:00
auto job = _account - > sendRequest ( " POST " , requestToken , req ) ;
QObject : : connect ( job , & SimpleNetworkJob : : finishedSignal , this , [ this , socket ] ( QNetworkReply * reply ) {
2017-03-28 11:31:38 +03:00
auto jsonData = reply - > readAll ( ) ;
QJsonParseError jsonParseError ;
QJsonObject json = QJsonDocument : : fromJson ( jsonData , & jsonParseError ) . object ( ) ;
QString accessToken = json [ " access_token " ] . toString ( ) ;
QString refreshToken = json [ " refresh_token " ] . toString ( ) ;
QString user = json [ " user_id " ] . toString ( ) ;
2017-07-03 15:51:20 +03:00
QUrl messageUrl = json [ " message_url " ] . toString ( ) ;
2017-03-28 11:31:38 +03:00
if ( reply - > error ( ) ! = QNetworkReply : : NoError | | jsonParseError . error ! = QJsonParseError : : NoError
| | json . isEmpty ( ) | | refreshToken . isEmpty ( ) | | accessToken . isEmpty ( )
| | json [ " token_type " ] . toString ( ) ! = QLatin1String ( " Bearer " ) ) {
2017-07-03 15:51:20 +03:00
QString errorReason ;
QString errorFromJson = json [ " error " ] . toString ( ) ;
if ( ! errorFromJson . isEmpty ( ) ) {
errorReason = tr ( " Error returned from the server: <em>%1</em> " )
. arg ( errorFromJson . toHtmlEscaped ( ) ) ;
} else if ( reply - > error ( ) ! = QNetworkReply : : NoError ) {
errorReason = tr ( " There was an error accessing the 'token' endpoint: <br><em>%1</em> " )
. arg ( reply - > errorString ( ) . toHtmlEscaped ( ) ) ;
} else if ( jsonParseError . error ! = QJsonParseError : : NoError ) {
errorReason = tr ( " Could not parse the JSON returned from the server: <br><em>%1</em> " )
. arg ( jsonParseError . errorString ( ) ) ;
} else {
errorReason = tr ( " The reply from the server did not contain all expected fields " ) ;
}
qCWarning ( lcOauth ) < < " Error when getting the accessToken " < < json < < errorReason ;
httpReplyAndClose ( socket , " 500 Internal Server Error " ,
tr ( " <h1>Login Error</h1><p>%1</p> " ) . arg ( errorReason ) . toUtf8 ( ) . constData ( ) ) ;
2017-03-28 11:31:38 +03:00
emit result ( Error ) ;
return ;
}
2017-07-13 16:58:07 +03:00
if ( ! _expectedUser . isNull ( ) & & user ! = _expectedUser ) {
// Connected with the wrong user
QString message = tr ( " <h1>Wrong user</h1> "
" <p>You logged-in with user <em>%1</em>, but must login with user <em>%2</em>.<br> "
" Please log out of %3 in another tab, then <a href='%4'>click here</a> "
" and log in as user %2</p> " )
. arg ( user , _expectedUser , Theme : : instance ( ) - > appNameGUI ( ) ,
authorisationLink ( ) . toString ( QUrl : : FullyEncoded ) ) ;
httpReplyAndClose ( socket , " 200 OK " , message . toUtf8 ( ) . constData ( ) ) ;
// We are still listening on the socket so we will get the new connection
return ;
}
2017-07-03 15:51:20 +03:00
const char * loginSuccessfullHtml = " <h1>Login Successful</h1><p>You can close this window.</p> " ;
if ( messageUrl . isValid ( ) ) {
httpReplyAndClose ( socket , " 303 See Other " , loginSuccessfullHtml ,
QByteArray ( " Location: " + messageUrl . toEncoded ( ) ) . constData ( ) ) ;
} else {
httpReplyAndClose ( socket , " 200 OK " , loginSuccessfullHtml ) ;
}
2017-03-28 11:31:38 +03:00
emit result ( LoggedIn , user , accessToken , refreshToken ) ;
} ) ;
} ) ;
}
} ) ;
QTimer : : singleShot ( 5 * 60 * 1000 , this , [ this ] { result ( Error ) ; } ) ;
}
2017-07-13 12:27:02 +03:00
QUrl OAuth : : authorisationLink ( ) const
2017-03-28 11:31:38 +03:00
{
Q_ASSERT ( _server . isListening ( ) ) ;
2017-07-14 16:56:32 +03:00
QUrl url = QUrl ( _account - > url ( ) . toString ( )
2017-03-28 11:31:38 +03:00
+ QLatin1String ( " /index.php/apps/oauth2/authorize?response_type=code&client_id= " )
+ Theme : : instance ( ) - > oauthClientId ( )
+ QLatin1String ( " &redirect_uri=http://localhost: " ) + QString : : number ( _server . serverPort ( ) ) ) ;
2017-07-14 16:56:32 +03:00
if ( ! _expectedUser . isNull ( ) )
url . addQueryItem ( " user " , _expectedUser ) ;
return url ;
2017-07-13 12:27:02 +03:00
}
2017-03-28 11:31:38 +03:00
2017-07-13 12:27:02 +03:00
bool OAuth : : openBrowser ( )
{
if ( ! QDesktopServices : : openUrl ( authorisationLink ( ) ) ) {
2017-03-28 11:31:38 +03:00
// We cannot open the browser, then we claim we don't support OAuth.
emit result ( NotSupported , QString ( ) ) ;
return false ;
}
return true ;
}
} // namespace OCC