Logo Search packages:      
Sourcecode: kmess version File versions

httpsoapconnection.cpp

/***************************************************************************
                          httpsoapconnection.cpp -  description
                             -------------------
    begin                : Sun Sep 25 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : vdboor --at-- codingdomain.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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "httpsoapconnection.h"

#include "soapmessage.h"
#include "xmlfunctions.h"

#include "../mimemessage.h"
#include "../../kmessdebug.h"

#include <kapplication.h>
#include <kaboutdata.h>

#ifdef KMESSDEBUG_HTTPSOAPCONNECTION
#define KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
#endif



/**
 * @brief The constructor
 *
 * Initializes the class.
 * The actual connection is established when a SOAP call is made.
 *
 * @param  endpoint  The endpoint of the webservice, should be a http:// or https:// URL.
 * @param  parent    The Qt parent object. The object is deleted when the parent is deleted.
 * @param  name      The Qt object name, for debugging signals.
 */
00045 HttpSoapConnection::HttpSoapConnection( const QString &endpoint, QObject *parent, const char *name )
  : TcpConnectionBase(parent, name)
  , keepAlive_(false)
  , parseError_(false)
  , requestActive_(false)
  , responseCode_(0)
  , responseLength_(0)
{
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "CREATED HttpSoapConnection(endpoint=" << endpoint << ")" << endl;
#endif
#ifdef KMESSTEST
  ASSERT( endpoint.startsWith("http://") || endpoint.startsWith("https://") );
#endif

  endpoint_ = endpoint;
}



/**
 * @brief The destructor.
 *
 * Closes the sockets if these are still open.
 */
00070 HttpSoapConnection::~HttpSoapConnection()
{
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "DESTROYED HttpSoapConnection" << endl;
#endif

  closeConnection();
}



/**
 * @brief Close the connection, or the connection got closed by the server.
 */
00084 void HttpSoapConnection::closeConnection()
{
  // Reset statew vars
  requestActive_ = false;
  keepAlive_     = false;

  // Close the connection
  TcpConnectionBase::closeConnection();
}



/**
 * @brief Escape a string for inclusion in XML.
 * @param  value  The value to escape for inclusion in an XML message.
 * @return  The escaped value, with the &lt;, &gt;, &amp; and " tokens escaped.
 */
00101 QString HttpSoapConnection::escapeString( QString value ) const
{
  // Only escape basic properties, nothing else!
  return value.replace("<", "&lt;")
              .replace(">", "&gt;")
              .replace("\"", "&quot;")
              .replace("&", "&amp;");
}



/**
 * @brief Connect to the endpoint.
 * @return  Whether the connection request is pending, or it failed.
 */
00116 bool HttpSoapConnection::connectToEndpoint()
{
  // Determine the port
  int port;
  if( endpoint_.port() != 0 )
  {
    port = endpoint_.port();
  }
  else if( endpoint_.protocol() == "http" )
  {
    port = 80;
  }
  else if( endpoint_.protocol() == "https" )
  {
    port = 443;
  }
  else
  {
    kdWarning() << "HttpSoapConnection::sendRequest: Invalid protocol for endpoint set "
                << "(endpoint=" << endpoint_ << ")." << endl;
    emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                              .arg("Unable to create a socket") );
    return false;
  }

  // Start connecting
  if( ! openConnection( endpoint_.host(), port, (endpoint_.protocol() == "https") ) )
  {
    kdWarning() << "HttpSoapConnection: unable to create a socket to endpoint: " << endpoint_ << "!" << endl;
    emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                              .arg("Unable to create a socket") );
    return false;
  }

  return true;
}



/**
 * @brief Return the endpoint
 * @return  Returns the endpoint given with the constructor.
 */
00159 QString HttpSoapConnection::getEndpoint() const
{
  return endpoint_.url();
}



/**
 * @brief Return the received HTTP status code
 * @return  The HTTP status code, like <code>200</code> or <code>500</code>.
 */
00170 int HttpSoapConnection::getHttpStatusCode() const
{
  return responseCode_;
}



/**
 * @brief Return the last request ID
 *
 * This parameter is not used internally, but allows other classes to
 * distinguish between requests, or pass user-data for callbacks.
 *
 * @return  The last request ID.
 */
00185 const QString & HttpSoapConnection::getRequestId() const
{
  return requestId_;
}



/**
 * @brief Return the last SOAP action
 * @return  The last SOAP action sent.
 */
00196 const QString & HttpSoapConnection::getSoapAction() const
{
  return soapAction_;
}



/**
 * @brief Return whether the connection is idle.
 * @return  Returns true when the connection is idle, false when a SOAP request is pending / being processed.
 */
00207 bool HttpSoapConnection::isIdle()
{
  return ( ! requestActive_ );
}



/**
 * @brief Parse the HTTP header
 *
 * This method can be overwritten to implement custom parsing,
 * e.g. support different content-types and status codes.
 * This default method only checks whether the the status code
 * is <code>200 OK</code> and the Content-Type is <code>text/xml</code>.
 * Use getHttpStatusCode() to read the status code.
 *
 * @param  preamble  The HTTP status-line/preamble
 * @param  header    The HTTP header fields.
 * @return  Whether the parsing should continue. When false, the class aborts with an error.
 */
00227 bool HttpSoapConnection::parseHttpHeader( const QString &/*preamble*/, const MimeMessage &header )
{
  // Check the response code.
  if( responseCode_ != 200 )
  {
    kdWarning() << "HttpSoapConnection: Received HTTP status line: " << responseCode_ << endl;
    return false;
  }

  // Parse the Content-Type
  QString contentType = header.getValue("Content-Type");
  if( contentType.contains(";") )
  {
    //QString charset = contentType.section(';', 1, 1);
    contentType = contentType.section(';', 0, 0);
  }

  // Content-Type should be text/xml
  if( contentType != "text/xml" )
  {
    kdWarning() << "HttpSoapConnection: Invalid Content-Type received: " << contentType << ", "
                << "expected text/xml (endpoint=" << endpoint_ << ")!" << endl;
    return false;
  }

  return true;
}



/**
 * @brief Parse the received server response.
 *
 * This body extracts the XML message from the body.
 * It detects SOAP Faults automatically.
 * Other responses are returned to requestFinished() and slotRequestFinished().
 *
 * @param   responseBody  The full message body received from the HTTP server.
 * @return  Whether there is a parsing error that should be emitted.
 */
00267 bool HttpSoapConnection::parseHttpBody( const QByteArray &responseBody )
{
  // Attempt to convert to XML
  QString xmlError;
  QDomDocument xml;
  if( ! xml.setContent( responseBody, true, &xmlError ) )
  {
    kdWarning() << "HttpSoapConnection: Unable to parse XML "
                << "(error='" << xmlError << "' endpoint=" << endpoint_ << ")" << endl;
    return false;
  }

  // Get the main body node
  QDomElement resultNode = XmlFunctions::getNode(xml, "/Envelope/Body").firstChild().toElement();
  if( resultNode.isNull() )
  {
    kdWarning() << "HttpSoapConnection: Unable to parse XML: result node is null "
                << "(root=" << xml.nodeName()  << " endpoint=" << endpoint_ << ")." << endl;
    return false;
  }

  // See if it's a soap error message
  if( resultNode.nodeName() == "Fault" )
  {
    QString faultCode   = XmlFunctions::getNodeValue( resultNode, "/faultcode" );
    QString faultString = XmlFunctions::getNodeValue( resultNode, "/faultstring" );
    emit requestFailed( this, faultCode, faultString, resultNode );
    return true;   // no parsing error, it's a valid response.
  }

  // WS-I compliant webservices wrap the response in another object so the response has one root element.
  // So you get a <MethodNameResponse><MethodNameResult>...</MethodNameResult></MethodNameResponse>
  // This object wrapper is stripped transparently in .Net, also do this here:
  QString resultNodeName = resultNode.nodeName();
  if( resultNodeName.endsWith("Response")
  &&  resultNode.childNodes().length() == 1
  &&  resultNode.firstChild().nodeName() == resultNodeName.replace("Response", "Result") )
  {
    resultNode = resultNode.firstChild().toElement();
  }

  // Request completed, indicate derived class and listeners.
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "HttpSoapConnection: Request finished, signalling to start parsing "
            << "(main node=" << resultNodeName << ")" << endl;
#endif
  requestActive_ = false;

  // Notify listeners
  emit requestFinished( this, resultNode );
  slotRequestFinished( resultNode );
  return true;
}



/**
 * @brief  Send the a raw POST request to the server.
 *
 * This method is used internally by sendRequest().
 *
 * @param  content     The POST data to send.
 * @param  soapAction  The SOAPAction header to send, may be QString::null to omit the header.
 * @param  requestId   The ID of the request, allowing callbacks to distinguish between requests.
 */
00332 void HttpSoapConnection::sendRawPostRequest( const QString &content, const QString &soapAction, const QString &requestId )
{
  // Set once, used a lot
  bool connected = isConnected();

  // Protect against parallel usage.
  // It's easy to create a pendingRequests_ list, the problem however is:
  // - should the next request be sent if the previous one failed badly? (e.g. http parse error)
  // - would you like to see dozens of "Could not parse" error messages at the same time?
  if( connected )
  {
    if( requestActive_ )
    {
      kdWarning() << "HttpSoapConnection: unable to send another SOAP request "
                  << "(soapAction=" << soapAction << ")." << endl;
      return;
    }
    else
    {
#ifdef KMESSTEST
      ASSERT( keepAlive_ );
#endif
      // No request active, connection was kept alive and idle
      requestActive_ = true;
    }
  }

  // Create the HTTP request
  QString soapActionLine;
  if( ! soapAction.isNull() )
  {
    soapActionLine = "SoapAction: \"" + soapAction + "\"\r\n";
  }

  // Create HTTP header
  QCString rawPostData = content.utf8();
  QString httpHeader = "POST " + endpoint_.path() + " HTTP/1.1\r\n"
                       "Accept: */*\r\n"
                       + soapActionLine +
                       "Content-Type: text/xml; charset=utf-8\r\n"
                       "Content-Length: " + QString::number(rawPostData.length()) + "\r\n"
                       "User-Agent: KMess " + kapp->aboutData()->version() + "\r\n"
                       "Host: " + endpoint_.host() + "\r\n"
                       "Connection: Keep-Alive\r\n"
                       "Cache-Control: no-cache\r\n"
                       "\r\n";

  // Store HTTP header to send when the connection is established
  request_    = httpHeader.utf8() + rawPostData;
  parseError_ = false;

  // When not connected, connect first
  if( ! connected && ! connectToEndpoint() )
  {
    // Connection attempt failed.
    return;
  }
  // else Keep waiting until slotConnectionEstablished() or slotConnectionFailed() is called.


  // Initialize state vars
  soapAction_    = soapAction_;
  requestActive_ = true;
  requestId_     = requestId;

#ifdef KMESS_NETWORK_WINDOW
  KURL soapActionUrl = soapAction;
  if( soapAction.isEmpty() || ! soapActionUrl.isValid() )
  {
    KMESS_NET_INIT(this, "SOAP " + endpoint_.path() );
  }
  else
  {
    KMESS_NET_INIT(this, "SOAP " + soapActionUrl.filename() );
  }
#endif

  // When the connection was already active (kept alive), send request now.
  if( connected )
  {
    slotConnectionEstablished();
  }
}



/**
 * @brief Send a SOAP request to the webservice.
 *
 * This is a convenience function for sendRawPostRequest().
 * The message is generated by SoapMessage::getMessage().
 *
 * @param  soapAction  The SOAPAction header to send, may be QString::null to omit the header.
 * @param  message     The SOAP message to send.
 * @param  requestId   The ID of the request, allowing callbacks to distinguish between requests.
 */
00428 void HttpSoapConnection::sendRequest(const QString &soapAction, const SoapMessage &message, const QString &requestId )
{
  sendRawPostRequest( message.getMessage(), soapAction, requestId );
}



/**
 * @brief Send a SOAP request to the webservice.
 *
 * This method constructs the <code>&lt;soap:Envelope&gt;</code>,
 * <code>&lt;soap:Header&gt;</code> and <code>&lt;soap:Body&gt;</code> fields
 * to embedding the message. The generated message is sent with sendRawPostRequest().
 *
 * When an example defines additional XML namespaces in the <code>&lt;soap:Envelope&gt;</code> header,
 * include those namespaces in the sub elements instead. Make sure the XML namespace is always
 * declared in the parent element which uses the XML namespace. 
 *
 * @param  soapAction     The SOAPAction header to send, may be QString::null to omit the header.
 * @param  messageBody    The raw XML body to send. For a readable formatting in the network window, use 4 spaces indent and no trailing newline.
 * @param  messageHeader  The raw XML header to send.
 * @param  requestId   The ID of the request, allowing callbacks to distinguish between requests.
 */
00451 void HttpSoapConnection::sendRequest(const QString &soapAction, const QString &messageBody, const QString &messageHeader, const QString &requestId)
{
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "HttpSoapConnection: Connecting to endpoint for SOAP action '" << soapAction << "'." << endl;
#endif

  // Build the SOAP request
  QString postData = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                     "<soap:Envelope"
                     " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                     " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\""
                     " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n";

  // Append header
  if( ! messageHeader.isEmpty() )
  {
    postData += "  <soap:Header>\n" + messageHeader + "\n  </soap:Header>\n";
  }

  // Append body
  postData += "  <soap:Body>\n" + messageBody + "\n  </soap:Body>\n</soap:Envelope>\n";

  sendRawPostRequest( postData, soapAction, requestId );
}



/**
 * @brief Called when the connection is established
 *
 * This method sends the SOAP request.
 */
00483 void HttpSoapConnection::slotConnectionEstablished()
{
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "HttpSoapConnection: Socket connected, sending SOAP request." << endl;
#endif

  // Reset all state variables
  parseError_     = false;
  responseCode_   = 0;
  responseLength_ = 0;

  // Send the entire request
  // Explictly use length because a \0 from QCString->QByteArray conversion should not be included
  bool writeSuccess = writeBlock( request_.data(), request_.length() );

  // If the block could not be written, it's possible the remote server
  // has closed the connection unexpectedly. Reconnect to the server.
  if( ! writeSuccess && keepAlive_ )
  {
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
    kdDebug() << "HttpSoapConnection: Write failed, connection is likely closed but Keep-Alive was set, reconnecting." << endl;
#endif

    closeSockets();          // reset sockets, but don't reset state
    keepAlive_     = false;  // avoid a second attempt
    requestActive_ = true;
    connectToEndpoint();
  }
}



/**
 * @brief Called when the connection could not be made.
 *
 * This method emits a requestFailed() signal.
 */
00520 void HttpSoapConnection::slotConnectionFailed()
{
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "HttpSoapConnection: Connection failed, socket error: " << getSocketError() << "." << endl;
#endif

  requestActive_ = false;

  // Inform listeners the request failed.
  emit requestFailed( this, i18n("Unable to make a connection: %1").arg(getSocketError()) );
}



/**
 * @brief Called when the connection received data.
 *
 * This method parses the HTTP preamble, and detects whether the full header and body are received.
 * The methods parseHttpHeader() and parseHttpBody() are called to process the header and body.
 * The <code>Connection</code> and <code>Content-Length</code> fields are handled internally as well.
 */
00541 void HttpSoapConnection::slotDataReceived()
{
  // Check the number of bytes available.
  // It returns -2 when the socket is closed
  // (this event was still in the event queue)
  int bytesAvailable = getBytesAvailable();
  if( bytesAvailable < 0 )
  {
    kdWarning() << "HttpSoapConnection::slotDataReceived: could not read data "
                << "(code=" << bytesAvailable << " endpoint=" << endpoint_ << ")" << endl;
    return;
  }

  // If the length of the response
  if( responseLength_ == 0 )
  {
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
    kdDebug() << "HttpSoapConnection: Connection received first data block, parsing HTTP header." << endl;
#endif

    // Reset states until proven otherwise
    keepAlive_ = false;


    QByteArray buffer( QMIN(4000, bytesAvailable) );
    // Attempt to parse the next part of the header.
    int bytesRead = peekBlock( &buffer );
    if( bytesRead <= 0 )
    {
      kdWarning() << "HttpSoapConnection::slotDataReceived: No data read from HTTP header "
                  << "(endpoint=" << endpoint_ << " syserror=" << getSocketError() << ")!" << endl;
      parseError_ = true;
      closeConnection();  // close first so crashes are avoided if the listeners destroy this object.
      emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                                .arg("Could not read HTTP header") );
      return;
    }


    // The data is peeked. To avoid repeated output in the network window,
    // it will only be displayed when the connection is either closed directly,
    // or the full header is received.

    QString header = QString::fromLatin1( buffer.data(), bytesRead );
    int endPos = header.find("\r\n\r\n");
    if( endPos == -1 )
    {
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
      kdDebug() << "HttpSoapConnection: End of header not found in the first " << bytesRead << " bytes" << endl;
#endif
      // Rely on base class to keep the first part in the buffer
      // Wait for another slotDataReceived() call.
      return;
    }

    // Consume HTTP header, truncate to exact end.
    bytesRead = readBlock( &buffer, endPos + 4 );
    header = QString::fromLatin1( buffer.data(), bytesRead );

#ifdef KMESS_NETWORK_WINDOW
    // display header in network window.
    QByteArray wrapper;
    wrapper.setRawData( buffer.data(), bytesRead );
    KMESS_NET_RECEIVED(this, wrapper);
    wrapper.resetRawData( buffer.data(), bytesRead );
#endif

    // Find the HTTP preamble.
    int preambleEnd = header.find("\r\n");
    if( preambleEnd == -1 )
    {
#ifdef KMESS_NETWORK_WINDOW
      KMESS_NET_RECEIVED(this, buffer);
#endif

      kdWarning() << "httpSoapConnection: End of HTTP preamble not found "
                  << "(endpoint=" << endpoint_ << " syserror=" << getSocketError() << ")!" << endl;
      closeConnection();  // close first so crashes are avoided if the listeners destroy this object.
      emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                                .arg("Could not parse HTTP response header") );
      return;
    }

    // Parse the HTTP preamble.
    QString preamble = header.left( preambleEnd );
    float protocol = preamble.section( ' ', 0, 0 ).section('/', 1, 1).toFloat();
    responseCode_ = preamble.section( ' ', 1, 1 ).toUInt();  // HTTP/1.0 200 OK

    // Parse the Content-Length
    MimeMessage httpHeaders( header.mid( preambleEnd + 2, endPos - preambleEnd - 2) );
    responseLength_ = httpHeaders.getValue("Content-Length").toUInt();
    bool headerOk = parseHttpHeader( preamble, httpHeaders );

    if( ! headerOk )
    {
#ifdef KMESSTEST
      // Deal with aborting later when full body is displayed in the network window.
      parseError_ = true;
#else
      // Close when it's no longer important.
      closeConnection();  // close first so crashes are avoided if the listeners destroy this object.
      emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                                .arg("Could not parse SOAP response") );
      return;
#endif
    }

    // Test whether the server requested to keep the connection open.
    // For HTTP 1.0: only keep alive is explicitly stated.
    // For HTTP 1.1: always keep alive unless closed explictly
    if( protocol <= 1.0 )
    {
      // See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1.2
      keepAlive_ = httpHeaders.hasField("Connection") &&
                   httpHeaders.getValue("Connection") == "Keep-Alive";
    }
    else
    {
      // See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.6.2
      keepAlive_ = ! httpHeaders.hasField("Connection") ||
                     httpHeaders.getValue("Connection") != "close";
    }

#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
    kdDebug() << "HttpSoapConnection: Header parsed, Keep-Alive: " << keepAlive_
              << " (protocol version=" << protocol << ")" << endl;
#endif

    // When header is parsed, continue to parse the next
    // received part of the body.
  }


  // Header successfully processed
#ifdef KMESSDEBUG_HTTPSOAPCONNECTION_GENERAL
  kdDebug() << "HttpSoapConnection: Received " << getBytesAvailable()
            << " of " << responseLength_ << " bytes" << endl;
#endif

  // Http header parsed, check whether the full response has arrived
  if( responseLength_ > (uint) getBytesAvailable() )
  {
    return;
  }


  // Read all data from the socket.
  QByteArray xmlData( getBytesAvailable() );
  readBlock( &xmlData );
  //QByteArray xmlData = readAll();
  //QString allBody = QString::fromUtf8( xmlData.data(), xmlData.size() );

#ifdef KMESS_NETWORK_WINDOW
  KMESS_NET_RECEIVED(this, xmlData);
#endif
#ifdef KMESSTEST
  // When the response was not a 200 OK, it will be aborted immediately.
  // For debugging the connection is left open so the full response
  // can be displayed in the network window (particulary useful for SSL).
  // Then close 
  if( parseError_ )
  {
    kdDebug() << "HttpSoapConnection: Closing connection due parsing errors "
              << "(delayed so full body is displayed in network window)." << endl;
    closeConnection();  // close first so crashes are avoided if the listeners destroy this object.
    emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                              .arg("Could not parse SOAP response") );
    return;
  }
#endif

  // Close connection unless server allowed it to remain open.
  // Avoids problems when a listener starts a new request from the slot.
  if( ! keepAlive_ )
  {
    closeConnection();
  }

  // Parse the received HTTP body
  bool bodyOk = parseHttpBody( xmlData );

  if( ! bodyOk )
  {
    closeConnection();
    emit requestFailed( this, i18n("There was an internal error in KMess: %1")
                              .arg("Unable to parse SOAP response") );
  }

#ifdef KMESSTEST
  ASSERT( keepAlive_ == isConnected() );
#endif
}



/**
 * @brief Called when the SOAP request failed with an SOAP fault.
 *
 * This method can be overwritten to process the response and emit signals afterwards.
 *
 * @param  faultCode    The SOAP fault code.
 * @param  faultString  The SOAP fault string.
 * @param  faultNode    The complete fault node received from the server.
 */
00745 void HttpSoapConnection::slotRequestFailed( const QString &/*faultCode*/,
                                            const QString &/*faultString*/,
                                            QDomElement &/*faultNode*/ )
{
  // For subclasses to extent
  // It's implemented here so the class is not abstract
  // The error handling can also be done with slots.
}


/**
 * @brief Called when the SOAP request completed.
 *
 * This method can be overwritten to implement custom event handling.
 *
 * @param  resultRoot  The result body element of the SOAP response.
 */
00762 void HttpSoapConnection::slotRequestFinished( QDomElement &/*resultRoot*/ )
{
  // For subclasses to extent
  // It's implemented here so the class is not abstract
  // The response handling can also be done with slots.
}


#include "httpsoapconnection.moc"

Generated by  Doxygen 1.6.0   Back to index