/*************************************************************************** msnsockethttp.cpp - description ------------------- begin : Thu Apr 4 2008 copyright : (C) 2008 by Valerio Pilo email : valerio@kmess.org ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "msnsockethttp.h" #include <config-kmess.h> #include "../kmessdebug.h" #include <QNetworkReply> #include <QHostAddress> #include <QSslError> #include <KLocale> #include <KMessageBox> #ifdef KMESSDEBUG_CONNECTION // Area-specific debug statements #define KMESSDEBUG_CONNECTION_SOCKET_HTTP #define KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS #endif /** * Default Gateway address/port * * This define holds the default address which KMess uses to send HTTP * messages. The port is usually 80. */ #define DEFAULT_GATEWAY_ADDRESS "gateway.messenger.hotmail.com" #define DEFAULT_GATEWAY_PORT 80 /** * @brief The constructor * * Initializes the HTTP interface, prepares the default HTTP header we'll use * throughout the connection, and sets up the request/ping timer. */ 00054 MsnSocketHttp::MsnSocketHttp( ServerType serverType ) : MsnSocketBase( serverType ) , canSendRequests_( true ) , sendPings_( false ) { setObjectName( "MsnSocketHttp" ); // Create the http interface http_ = new QNetworkAccessManager( this ); // Connect the socket's signals for parsing connect( http_, SIGNAL( finished(QNetworkReply*) ), this, SLOT ( slotReplyReceived(QNetworkReply*) ) ); connect( http_, SIGNAL( proxyAuthenticationRequired(const QNetworkProxy&,QAuthenticator*) ), this, SLOT ( proxyAuthenticate(const QNetworkProxy&,QAuthenticator*) ) ); // Connect the server request timer connect( &pollingTimer_, SIGNAL( timeout() ), this, SLOT ( slotQueryServer() ) ); // We need the timer to only fire once to trigger a single update; // the next one will be determined dynamically pollingTimer_.setSingleShot( true ); // Set the default request values baseServerPath_ = QString ( "/gateway/gateway.dll?" ); requestHeader_.setHeader ( QNetworkRequest::ContentTypeHeader, "text/xml; charset=utf-8" ); requestHeader_.setRawHeader( "Accept", "*" "/" "*" ); requestHeader_.setRawHeader( "User-Agent", "KMess/" KMESS_VERSION ); requestHeader_.setRawHeader( "Connection", "Keep-Alive" ); requestHeader_.setRawHeader( "Cache-Control", "no-cache" ); #ifdef KMESSTEST KMESS_ASSERT( http_ != 0 ); #endif } /** * @brief The destructor * * Closes the connection, cleans up request queues, and deletes the interface. */ 00097 MsnSocketHttp::~MsnSocketHttp() { // Disconnect from the server on exit if( connected_ ) { disconnectFromServer(); } // Stop the polling timer pollingTimer_.stop(); // Clean up and delete the http interface cleanUp(); delete http_; #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "DESTROYED"; #endif } /** * @brief Clean up the connection * * This internal method empties all queues and removes any pending connection - * including any in-progress ones. */ 00125 void MsnSocketHttp::cleanUp() { // Clear the request queues queuedRequests_ .clear(); } /** * @brief Connect to the given server * * The initial connection should always be made through the main Gateway. * The first server response usually carries the new gateway IP * which will be used for the rest of the connection. * * @param server The server hostname or IP address. * @param port The port to connect to. Not used - all of our * traffic goes through the default port. */ 00144 void MsnSocketHttp::connectToServer( const QString& server, const quint16 port ) { // We use our default HTTP port only Q_UNUSED( port ); // When no server is specified, use the default msn one QString desiredAddress; if( server.isEmpty() ) { desiredAddress = DEFAULT_GATEWAY_ADDRESS; } else { desiredAddress = server; } #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Connecting to server at " << desiredAddress << ":" << DEFAULT_GATEWAY_PORT << "."; #endif // Set up the gateway where to send our messages setGateway( desiredAddress ); // Translate the server type to a 2-letter string to send in our requests QString serverType; switch( serverType_ ) { case SERVER_NOTIFICATION: serverType = "NS"; break; case SERVER_SWITCHBOARD: serverType = "SB"; break; default: kWarning() << "Unknown type of server" << serverType_ << "!"; return; } // Set the parameters for the next request nextRequestParameters_ = "Action=open&Server=" + serverType + "&IP=" + desiredAddress; // Flush any active or pending request if the interface is closing a previous connection cleanUp(); // Set our internal state variables connected_ = true; canSendRequests_ = true; // Signal we're ready emit connected(); emit statusMessage( i18n( "Connected" ), false ); } /** * @brief Disconnect from the server * * This method closes the connection to the server and does some cleaning up. * It empties all buffers, and resets the internal state. * To avoid losing data, all pending requests are merged into a single huge * request and sent altogether, before closing the connection. We won't * receive responses for them; but at least we lose no outgoing data. * * @param isTransfer When set, a disconnected() signal won't be fired. * This is used to handle transfers to another server * without noticing a disconnect. */ 00208 void MsnSocketHttp::disconnectFromServer( bool isTransfer ) { // Stop pinging setSendPings( false ); pollingTimer_.stop(); // Just clean up if we are disconnected already if( ! connected_ ) { return; } connected_ = false; // Make sure all remaining requests get sent, so that we don't lose messages. // Even if we're disconnecting because of an error, send them anyway: we'll ignore the // responses anyway. if( ! queuedRequests_.isEmpty() ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Sending" << queuedRequests_.count() << "remaining requests before closing."; #endif QNetworkRequest request( requestHeader_ ); QByteArray data; // Get the oldest message RequestInfo info = queuedRequests_.takeFirst(); data = info.data; #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP int mergedRequests = 0; #endif // Merge together all the queued data to save bandwidth while( ! queuedRequests_.isEmpty() ) { const RequestInfo &queuedRequest = queuedRequests_.first(); #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP mergedRequests++; #endif // Merge the data data.append( queuedRequest.data ); // Remove the request from the queue, since it is going to be sent now queuedRequests_.takeFirst(); } #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Merged" << mergedRequests << "requests out from the queue."; #endif // Set up the header request.setHeader( QNetworkRequest::ContentLengthHeader, data.size() ); request.setUrl( QUrl( gatewayAddress_ + baseServerPath_ + "SessionID=" + nextRequestId_ ) ); // There should be no content-type header if there is no data if( data.size() == 0 ) { request.setHeader( QNetworkRequest::ContentTypeHeader, QVariant() ); } // Send it http_->post( request, data ); /* // Wait until this request has been sent, using a local loop. QEventLoop loop( this ); // Block the timer signals before starting, we're just using it to timeout the request pollingTimer_.blockSignals( true ); pollingTimer_.start( 1000 ); while( http_->currentId() != 0 && pollingTimer_.isActive() ) { loop.processEvents(); } // Enable back its signals now pollingTimer_.blockSignals( false ); #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP if( http_->currentId() != 0 && ! pollingTimer_.isActive() ) { kDebug() << "Could not send the last request within timeout."; #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS kDebug() << "Full dump:" << endl << header.toString() << QString::fromUtf8( data ); #endif } #endif */ } // Clear the queues, so pending messages will be ignored cleanUp(); // Do not signal a disconnection when we are transferring to another gateway if( isTransfer ) { return; } #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Emitting disconnection signal."; #endif emit disconnected(); } // Dump the content of requests and replies void MsnSocketHttp::dump( void *obj, QByteArray data, bool isRequest ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS if( isRequest ) { QNetworkRequest *request = (QNetworkRequest *)obj; kDebug() << "-- Request dump below --" << endl << "URL:" << request->url() << endl << "Contents:" << QString::fromUtf8( data ); int i = 1; kDebug() << request->rawHeaderList().size() << "header items present:"; foreach( const QByteArray &headerName, request->rawHeaderList() ) { kDebug() << "(" << i++ << ")" << headerName << ": " << request->rawHeader( headerName ); } kDebug() << "-- Request dump above --"; } else { QNetworkReply *reply = (QNetworkReply *)obj; kDebug() << "-- Reply dump below --" << endl << "URL:" << reply->url() << endl << "Contents:" << QString::fromUtf8( data ); int i = 1; kDebug() << reply->rawHeaderList().size() << "header items present:"; foreach( const QByteArray &headerName, reply->rawHeaderList() ) { kDebug() << "(" << i++ << ")" << headerName << ": " << reply->rawHeader( headerName ); } kDebug() << "-- Reply dump above --"; } #else Q_UNUSED( obj ); Q_UNUSED( data ); Q_UNUSED( isRequest ); #endif } /** * @brief Return the local IP address * * Using HTTP, we don't really care about our address. Not just that, * we're probably behind a firewall, so finding out our real IP wouldn't * help either. * This method just returns "127.0.0.1". * * @returns The IP address the socket uses at the system. */ 00373 QString MsnSocketHttp::getLocalIp() const { // We don't care about our own IP, return a generic one return QHostAddress( QHostAddress::LocalHost ).toString(); } /** * @brief Sends the next queued request * * Data to send is stored in a queue of requests. This method just takes the * oldest message and sends it. */ 00387 void MsnSocketHttp::sendNextRequest() { // Verify if we can send another message, if there is any if( ! connected_ || ! canSendRequests_ ) { return; } QTime now( QTime::currentTime() ); // Do not send messages too fast, or the server will ignore them int elapsedMsecs = lastRequestTime_.msecsTo( now ); if( elapsedMsecs <= 1000 ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Not sending another message in" << elapsedMsecs << "more milliseconds."; #endif // The polling method will call this method back pollingTimer_.start( 1000 - elapsedMsecs ); return; } // Lock the sending process: only one message at most can be 'on the wire' in any given moment canSendRequests_ = false; bool sendingPollingRequest = false; QNetworkRequest request( requestHeader_ ); QByteArray data; QString url( gatewayAddress_ + baseServerPath_ ); // When the queue is empty, send polling requests if( queuedRequests_.isEmpty() ) { sendingPollingRequest = true; url += "Action=poll&SessionID=" + nextRequestId_; request.setHeader( QNetworkRequest::ContentLengthHeader, 0 ); request.setHeader( QNetworkRequest::ContentTypeHeader, QVariant() ); // There should be no content-type header. } else { // Get the oldest message RequestInfo info = queuedRequests_.takeFirst(); data = info.data; #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP int mergedRequests = 0; #endif // Merge together all the queued data to save bandwidth while( ! queuedRequests_.isEmpty() ) { const RequestInfo &queuedRequest = queuedRequests_.first(); // Stop merging requests if we get a special one if( ! queuedRequest.requestQueryString.isEmpty() ) { break; } #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP mergedRequests++; #endif // Merge the data data.append( queuedRequest.data ); // Remove the request from the queue, since it is going to be sent now queuedRequests_.takeFirst(); } #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Merged" << mergedRequests << "requests out from the queue."; #endif // Set up the header request.setHeader( QNetworkRequest::ContentLengthHeader, data.size() ); if( info.requestQueryString.isEmpty() ) { url += "SessionID=" + nextRequestId_; } else { url += info.requestQueryString; } // There should be no content-type header if there is no data if( data.size() == 0 ) { request.setHeader( QNetworkRequest::ContentTypeHeader, QVariant() ); } } // Send it request.setUrl( QUrl( url ) ); http_->post( request, data ); #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Sent a request. Queue size now:" << queuedRequests_.size(); #endif #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS dump( &request, data, true ); #endif // Set the last request type lastRequestWasPolling_ = sendingPollingRequest; // Stop polling, the response to this message will carry new messages along pollingTimer_.stop(); lastRequestTime_ = now; } /** * @brief Change the server connection gateway * * Set the address of the gateway, from this point on, all queued data will * be sent to the new gateway. * * @param host The new gateway hostname or IP address. */ 00510 void MsnSocketHttp::setGateway( QString host ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Using server at" << host << ":" << DEFAULT_GATEWAY_PORT << "as gateway."; #endif // Save the server address to use in our requests requestHeader_.setRawHeader( QByteArray( "Host" ), host.toUtf8() ); gatewayAddress_ = "http://" + host; } /** * @brief Set whether to send pings or not * * When sending pings, the application receives a periodic signal * which is used to perform timed updates (like receiving display pics). * * @param sendPings True to send pings. */ 00532 void MsnSocketHttp::setSendPings( bool sendPings ) { sendPings_ = sendPings; } /** * @brief Send a polling message to the server, to retrieve pending commands * * This method gets called every two seconds: each time it checks if there still are * messages queued to be sent, and if not, it polls the gateway for new messages. */ 00545 void MsnSocketHttp::slotQueryServer() { QDateTime now( QDateTime::currentDateTime() ); // Emit a "ping" signal every about 30 seconds, to let the app perform scheduled tasks if( sendPings_ && ( ! lastPing_.isValid() || lastPing_.secsTo( now ) > 30 ) ) { lastPing_ = now; emit pingSent(); } // Don't send polling messages when: 1) we are disconnected; 2) a message is still being // responded at (we'll receive new messages along with the response); 3) there still are // messages to send (same reason as point 2). if( ! connected_ || ! canSendRequests_ ) { return; } // If the queue isn't empty send the next message if( ! queuedRequests_.isEmpty() ) { sendNextRequest(); return; } // The last response had data; maybe there's more so keep checking // if( ! lastResponseWasEmpty_ ) // { sendNextRequest(); // } // else if( lastResponseWasEmpty_ ) { // Send a poll five seconds after the last command and ten after the last polling request pollingTimer_.start( lastRequestWasPolling_ ? 10000 : 5000 ); } } /** * @brief Read received data and pass along the contained commands * * This method parses the incoming raw data for the syntax used by the MSN protocol. * Data arrives here only when it's been received completely. This may make it appear * slower than usual TCP. * Incoming messages contain all new messages from the server, so we need to bounce * them back individually to MsnConnection. * * Example response message: * @code HTTP/1.0 200 OK Content-Length: 0 Content-Type: application/x-msn-messenger X-MSN-Messenger: SessionID=1848331483.1171767614; GW-IP=207.46.108.49 X-MSN-Host: BY1MSG4176117.gateway.edge.messenger.live.com X-MSNSERVER: BY1MSG4176117 Date: Thu, 14 Aug 2008 10:12:14 GMT X-Cache: MISS from fw.akademy.lan X-Cache-Lookup: MISS from fw.akademy.lan:3128 Via: 1.0 fw.akademy.lan:3128 (squid/2.6.STABLE18) Connection: keep-alive @endcode * * @param reply The response contents */ 00613 void MsnSocketHttp::slotReplyReceived( QNetworkReply *reply ) { if( ! connected_ ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Disconnected, ignoring reply."; #endif #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS dump( reply, reply->readAll(), false ); #endif // We're supposed to clean up delete reply; return; } // Get the message header and contents QNetworkRequest request = reply->request(); QByteArray responseData = reply->readAll(); bool hasMsnHeader = reply->hasRawHeader( "X-MSN-Messenger" ); // Catch errors. Also, catch responses without the "X-MSN-Messenger" header: // responses without it aren't coming from an MSN server, so maybe there is // some HTTP redirection happening, like an authentication web page (e.g. web-based login proxy) if( reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() != 200 || ! hasMsnHeader ) { kWarning() << "Received a type" << reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() << "response."; #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS dump( reply, responseData, false ); #endif // Report the right type of error QString errorMsg( reply->errorString() ); ErrorType errorType; switch( reply->error() ) { case QNetworkReply::NoError: // Invalid MSN responses (web authentication redirects often replace the // original response contents). Treat like a connection error. case QNetworkReply::HostNotFoundError: case QNetworkReply::ConnectionRefusedError: case QNetworkReply::TimeoutError: errorType = ERROR_CONNECTING; break; case QNetworkReply::RemoteHostClosedError: case QNetworkReply::UnknownNetworkError: errorType = ERROR_DROP; break; case QNetworkReply::UnknownContentError: errorType = ERROR_DATA; break; case QNetworkReply::ContentAccessDenied: case QNetworkReply::ContentNotFoundError: case QNetworkReply::ContentOperationNotPermittedError: case QNetworkReply::ProtocolUnknownError: case QNetworkReply::ProtocolInvalidOperationError: case QNetworkReply::ProtocolFailure: errorType = ERROR_INTERNAL; break; default: errorType = ERROR_UNKNOWN; errorMsg = i18nc( "Error message shown with HTTP connection", "%1 (Internal error code: %2)", errorMsg, reply->error() ); break; } // We may be on a network where you need to auth via a webpage if( errorType == ERROR_CONNECTING && ( ! hasMsnHeader || reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) != QVariant().toUrl() ) ) { errorType = ERROR_CONNECTING_GATEWAY; errorMsg = i18nc( "Error message shown with HTTP connection", "%1 (Internal error code: %2)<br/>" "<i>Response:</i> %3 %4<br/>" "<i>Redirection target:</i> %5", errorMsg, reply->error(), reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toString(), reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString(), reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toString() ); } // Ignore errors when disconnected: avoids duplicate error dialogs when two // following requests are answered with an error. if( connected_ ) { emit error( errorMsg, errorType ); } // We're supposed to clean up delete reply; // Allow the connection to continue working: if the error is fatal, the // connection will be disconnected so no more messages will be sent canSendRequests_ = true; sendNextRequest(); return; } #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP_DUMPS dump( reply, responseData, false ); #endif // Extract the MSN informative header from the response QList<QByteArray> msnInfo = reply->rawHeader( "X-MSN-Messenger" ).split( ';' ); // Extract the MSN session information from the headers foreach( QString item, msnInfo ) { item = item.trimmed(); int keyEnd = item.indexOf( '=' ); QString key ( item.left( keyEnd ) ); QString value( item.mid ( keyEnd + 1 ) ); if( key == "SessionID" ) { // Every message has a different session ID, which needs to be sent along // with the next message. nextRequestId_ = value; } else if( key == "GW-IP" ) { // Change the gateway, if we didn't do it already if( ! gatewayAddress_.contains( value ) ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Switching gateways:" << gatewayAddress_ << "->" << value; #endif setGateway( value ); } } else if( key == "Session" ) { // Session close request. We've been kicked or the connection was closed by us first. // Do nothing if we are disconnected already. // TODO Behavior when receiving this command needs to be tested. if( value == "close" && connected_ ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Received disconnection command"; #endif cleanUp(); disconnectFromServer(); } } else { #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Unknown attribute received in the MSN Header:" << item; #endif } } lastResponseWasEmpty_ = true; // Extract all new messages from the response data if( ! responseData.isEmpty() ) { int startingPos = 0; QStringList command; bool hasPayload = false; QString commandLine; int commandLineSize; QByteArray payloadData; // Loop until the entire response has been parsed while( true ) { if( startingPos >= responseData.size() ) { break; } // Retrieve the first available command from the response body commandLineSize = responseData.indexOf( "\r\n", startingPos ) - startingPos + 2; // Add the newline characters to the command size commandLine = QString::fromUtf8( responseData.mid( startingPos, commandLineSize ) ); command = commandLine.trimmed().split(" "); // Stop if there's nothing more to parse (unlikely, most times the first // 'if' statement above ends the loop) if( command[0].isEmpty() ) { break; } // See if it's a payload command, and extract the payload from the response data // See if it's a payload command or an error if( isErrorCommand( command[0] ) ) { // If the command contains a payload length parameter, also parse it hasPayload = ( command.size() > 2 ); } else { hasPayload = isPayloadCommand( command[0] ); } if( hasPayload ) { int payloadSize = command[ command.count() - 1 ].toInt(); // Last arg is payload length. payloadData = responseData.mid( startingPos + commandLineSize, payloadSize ); startingPos += commandLineSize + payloadSize; } else // Not a payload message, just go on from right after the command { payloadData.clear(); startingPos += commandLineSize; } // We've received a message, hand it over to MsnConnection for interpretation emit dataReceived( command, payloadData ); lastResponseWasEmpty_ = false; } } // Allow sending new messages, now that this has been done canSendRequests_ = true; // We're supposed to clean up delete reply; // Keep querying for more slotQueryServer(); } /** * @brief Write data to the gateway without any conversion * * This method creates a new request and queues it to be sent when possible. * See sendNextRequest() for more info on sending. * * @param data Contents of the message which will be sent * @return -1 on error, or else always the exact size of the sent data. */ 00860 qint64 MsnSocketHttp::writeBinaryData( const QByteArray &data ) { if( ! connected_ ) { kWarning() << "Attempted to write data to a disconnected interface, aborting."; return -1; } // Create the new request with the data RequestInfo newRequest; newRequest.data = data; // Special query strings override the default one if( ! nextRequestParameters_.isEmpty() ) { newRequest.requestQueryString = nextRequestParameters_; nextRequestParameters_ = QString(); } else { newRequest.requestQueryString = QString(); } // Queue the new request queuedRequests_.append( newRequest ); #ifdef KMESSDEBUG_CONNECTION_SOCKET_HTTP kDebug() << "Queued a new request"; #endif // Try to immediately send the new message, if possible sendNextRequest(); return data.size(); } /** * @brief Write a string to the gateway * * This is a convenience method for writeBinaryData, which can be used to directly send a QString. * * @param data The message which will be sent * @return -1 on error, or else always the exact size of the sent data. */ 00906 qint64 MsnSocketHttp::writeData( const QString &data ) { return writeBinaryData( data.toUtf8() ); } #include "msnsockethttp.moc"