#include "XmlRpcClient.h"
#include "XmlRpcSocket.h"
#include "XmlRpc.h"
#include <stdio.h>
#include <stdlib.h>
using namespace XmlRpc;
// Static data
const char XmlRpcClient::REQUEST_BEGIN[] =
"<?xml version=\"1.0\"?>\r\n"
"<methodCall><methodName>";
const char XmlRpcClient::REQUEST_END_METHODNAME[] = "</methodName>\r\n";
const char XmlRpcClient::PARAMS_TAG[] = "<params>";
const char XmlRpcClient::PARAMS_ETAG[] = "</params>";
const char XmlRpcClient::PARAM_TAG[] = "<param>";
const char XmlRpcClient::PARAM_ETAG[] = "</param>";
const char XmlRpcClient::REQUEST_END[] = "</methodCall>\r\n";
const char XmlRpcClient::METHODRESPONSE_TAG[] = "<methodResponse>";
const char XmlRpcClient::FAULT_TAG[] = "<fault>";
XmlRpcClient::XmlRpcClient(const char* host, int port, const char* uri/*=0*/)
{
XmlRpcUtil::log(1, "XmlRpcClient new client: host %s, port %d.", host, port);
_host = host;
_port = port;
if (uri)
_uri = uri;
else
_uri = "/RPC2";
_connectionState = NO_CONNECTION;
_executing = false;
_eof = false;
// Default to keeping the connection open until an explicit close is done
setKeepOpen();
}
XmlRpcClient::~XmlRpcClient()
{
}
// Close the owned fd
void
XmlRpcClient::close()
{
XmlRpcUtil::log(4, "XmlRpcClient::close: fd %d.", getfd());
_connectionState = NO_CONNECTION;
_disp.exit();
_disp.removeSource(this);
XmlRpcSource::close();
}
// Clear the referenced flag even if exceptions or errors occur.
struct ClearFlagOnExit {
ClearFlagOnExit(bool& flag) : _flag(flag) {}
~ClearFlagOnExit() { _flag = false; }
bool& _flag;
};
// Execute the named procedure on the remote server.
// Params should be an array of the arguments for the method.
// Returns true if the request was sent and a result received (although the result
// might be a fault).
bool
XmlRpcClient::execute(const char* method, XmlRpcValue const& params, XmlRpcValue& result)
{
XmlRpcUtil::log(1, "XmlRpcClient::execute: method %s (_connectionState %d).", method, _connectionState);
// This is not a thread-safe operation, if you want to do multithreading, use separate
// clients for each thread. If you want to protect yourself from multiple threads
// accessing the same client, replace this code with a real mutex.
if (_executing)
return false;
_executing = true;
ClearFlagOnExit cf(_executing);
_sendAttempts = 0;
_isFault = false;
if ( ! setupConnection())
return false;
if ( ! generateRequest(method, params))
return false;
result.clear();
double msTime = -1.0; // Process until exit is called
_disp.work(msTime);
if (_connectionState != IDLE || ! parseResponse(result))
return false;
XmlRpcUtil::log(1, "XmlRpcClient::execute: method %s completed.", method);
_response = "";
return true;
}
// XmlRpcSource interface implementation
// Handle server responses. Called by the event dispatcher during execute.
unsigned
XmlRpcClient::handleEvent(unsigned eventType)
{
if (eventType == XmlRpcDispatch::Exception)
{
if (_connectionState == WRITE_REQUEST && _bytesWritten == 0)
XmlRpcUtil::error("Error in XmlRpcClient::handleEvent: could not connect to server (%s).",
XmlRpcSocket::getErrorMsg().c_str());
else
XmlRpcUtil::error("Error in XmlRpcClient::handleEvent (state %d): %s.",
_connectionState, XmlRpcSocket::getErrorMsg().c_str());
return 0;
}
if (_connectionState == WRITE_REQUEST)
if ( ! writeRequest()) return 0;
if (_connectionState == READ_HEADER)
if ( ! readHeader()) return 0;
if (_connectionState == READ_RESPONSE)
if ( ! readResponse()) return 0;
// This should probably always ask for Exception events too
return (_connectionState == WRITE_REQUEST)
? XmlRpcDispatch::WritableEvent : XmlRpcDispatch::ReadableEvent;
}
// Create the socket connection to the server if necessary
bool
XmlRpcClient::setupConnection()
{
// If an error occurred last time through, or if the server closed the connection, close our end
if ((_connectionState != NO_CONNECTION && _connectionState != IDLE) || _eof)
close();
_eof = false;
if (_connectionState == NO_CONNECTION)
if (! doConnect())
return false;
// Prepare to write the request
_connectionState = WRITE_REQUEST;
_bytesWritten = 0;
// Notify the dispatcher to listen on this source (calls handleEvent when the socket is writable)
_disp.removeSource(this); // Make sure nothing is left over
_disp.addSource(this, XmlRpcDispatch::WritableEvent | XmlRpcDispatch::Exception);
return true;
}
// Connect to the xmlrpc server
bool
XmlRpcClient::doConnect()
{
int fd = XmlRpcSocket::socket();
if (fd < 0)
{
XmlRpcUtil::error("Error in XmlRpcClient::doConnect: Could not create socket (%s).", XmlRpcSocket::getErrorMsg().c_str());
return false;
}
XmlRpcUtil::log(3, "XmlRpcClient::doConnect: fd %d.", fd);
this->setfd(fd);
// Don't block on connect/reads/writes
if ( ! XmlRpcSocket::setNonBlocking(fd))
{
this->close();
XmlRpcUtil::error("Error in XmlRpcClient::doConnect: Could not set socket to non-blocking IO mode (%s).", XmlRpcSocket::getErrorMsg().c_str());
return false;
}
if ( ! XmlRpcSocket::connect(fd, _host, _port))
{
this->close();
XmlRpcUtil::error("Error in XmlRpcClient::doConnect: Could not connect to server (%s).", XmlRpcSocket::getErrorMsg().c_str());
return false;
}
return true;
}
// Encode the request to call the specified method with the specified parameters into xml
bool
XmlRpcClient::generateRequest(const char* methodName, XmlRpcValue const& params)
{
std::string body = REQUEST_BEGIN;
body += methodName;
body += REQUEST_END_METHODNAME;
// If params is an array, each element is a separate parameter
if (params.valid()) {
body += PARAMS_TAG;
if (params.getType() == XmlRpcValue::TypeArray)
{
for (int i=0; i<params.size(); ++i) {
body += PARAM_TAG;
body += params[i].toXml();
body += PARAM_ETAG;
}
}
else
{
body += PARAM_TAG;
body += params.toXml();
body += PARAM_ETAG;
}
body += PARAMS_ETAG;
}
body += REQUEST_END;
std::string header = generateHeader(body);
XmlRpcUtil::log(4, "XmlRpcClient::generateRequest: header is %d bytes, content-length is %d.",
header.length(), body.length());
_request = header + body;
return true;
}
// Prepend http headers
std::string
XmlRpcClient::generateHeader(std::string const& body)
{
std::string header =
"POST " + _uri + " HTTP/1.1\r\n"
"User-Agent: ";
header += XMLRPC_VERSION;
header += "\r\nHost: ";
header += _host;
char buff[40];
sprintf(buff,":%d\r\n", _port);
header += buff;
header += "Content-Type: text/xml\r\nContent-length: ";
sprintf(buff,"%d\r\n\r\n", (int)body.size());
return header + buff;
}
bool
XmlRpcClient::writeRequest()
{
if (_bytesWritten == 0)
XmlRpcUtil::log(5, "XmlRpcClient::writeRequest (attempt %d):\n%s\n", _sendAttempts+1, _request.c_str());
// Try to write the request
if ( ! XmlRpcSocket::nbWrite(this->getfd(), _request, &_bytesWritten)) {
XmlRpcUtil::error("Error in XmlRpcClient::writeRequest: write error (%s).",XmlRpcSocket::getErrorMsg().c_str());
return false;
}
XmlRpcUtil::log(3, "XmlRpcClient::writeRequest: wrote %d of %d bytes.", _bytesWritten, _request.length());
// Wait for the result
if (_bytesWritten == int(_request.length())) {
_header = "";
_response = "";
_connectionState = READ_HEADER;
}
return true;
}
// Read the header from the response
bool
XmlRpcClient::readHeader()
{
// Read available data
if ( ! XmlRpcSocket::nbRead(this->getfd(), _header, &_eof) ||
(_eof && _header.length() == 0)) {
// If we haven't read any data yet and this is a keep-alive connection, the server may
// have timed out, so we try one more time.
if (getKeepOpen() && _header.length() == 0 && _sendAttempts++ == 0) {
XmlRpcUtil::log(4, "XmlRpcClient::readHeader: re-trying connection");
XmlRpcSource::close();
_connectionState = NO_CONNECTION;
_eof = false;
return setupConnection();
}
XmlRpcUtil::error("Error in XmlRpcClient::readHeader: error while reading header (%s) on fd %d.",
XmlRpcSocket::getErrorMsg().c_str(), getfd());
return false;
}
XmlRpcUtil::log(4, "XmlRpcClient::readHeader: client has read %d bytes", _header.length());
char *hp = (char*)_header.c_str(); // Start of header
char *ep = hp + _header.length(); // End of string
char *bp = 0; // Start of body
char *lp = 0; // Start of content-length value
for (char *cp = hp; (bp == 0) && (cp < ep); ++cp) {
if ((ep - cp > 16) && (strncasecmp(cp, "Content-length: ", 16) == 0))
lp = cp + 16;
else if ((ep - cp > 4) && (strncmp(cp, "\r\n\r\n", 4) == 0))
bp = cp + 4;
else if ((ep - cp > 2) && (strncmp(cp, "\n\n", 2) == 0))
bp = cp + 2;
}
// If we haven't gotten the entire header yet, return (keep reading)
if (bp == 0) {
if (_eof) // EOF in the middle of a response is an error
{
XmlRpcUtil::error("Error in XmlRpcClient::readHeader: EOF while reading header");
return false; // Close the connection
}
return true; // Keep reading
}
// Decode content length
if (lp == 0) {
XmlRpcUtil::error("Error XmlRpcClient::readHeader: No Content-length specified");
return false; // We could try to figure it out by parsing as we read, but for now...
}
_contentLength = atoi(lp);
if (_contentLength <= 0) {
XmlRpcUtil::error("Error in XmlRpcClient::readHeader: Invalid Content-length specified (%d).", _contentLength);
return false;
}
XmlRpcUtil::log(4, "client read content length: %d", _contentLength);
// Otherwise copy non-header data to response buffer and set state to read response.
_response = bp;
_header = ""; // should parse out any interesting bits from the header (connection, etc)...
_connectionState = READ_RESPONSE;
return true; // Continue monitoring this source
}
bool
XmlRpcClient::readResponse()
{
// If we dont have the entire response yet, read available data
if (int(_response.length()) < _contentLength) {
if ( ! XmlRpcSocket::nbRead(this->getfd(), _response, &_eof)) {
XmlRpcUtil::error("Error in XmlRpcClient::readResponse: read error (%s).",XmlRpcSocket::getErrorMsg().c_str());
return false;
}
// If we haven't gotten the entire _response yet, return (keep reading)
if (int(_response.length()) < _contentLength) {
if (_eof) {
XmlRpcUtil::error("Error in XmlRpcClient::readResponse: EOF while reading response");
return false;
}
return true;
}
}
// Otherwise, parse and return the result
XmlRpcUtil::log(3, "XmlRpcClient::readResponse (read %d bytes)", _response.length());
XmlRpcUtil::log(5, "response:\n%s", _response.c_str());
_connectionState = IDLE;
return false; // Stop monitoring this source (causes return from work)
}
// Convert the response xml into a result value
bool
XmlRpcClient::parseResponse(XmlRpcValue& result)
{
// Parse response xml into result
int offset = 0;
if ( ! XmlRpcUtil::findTag(METHODRESPONSE_TAG,_response,&offset)) {
XmlRpcUtil::error("Error in XmlRpcClient::parseResponse: Invalid response - no methodResponse. Response:\n%s", _response.c_str());
return false;
}
// Expect either <params><param>... or <fault>...
if ((XmlRpcUtil::nextTagIs(PARAMS_TAG,_response,&offset) &&
XmlRpcUtil::nextTagIs(PARAM_TAG,_response,&offset)) ||
(XmlRpcUtil::nextTagIs(FAULT_TAG,_response,&offset) && (_isFault = true)))
{
if ( ! result.fromXml(_response, &offset)) {
XmlRpcUtil::error("Error in XmlRpcClient::parseResponse: Invalid response value. Response:\n%s", _response.c_str());
_response = "";
return false;
}
} else {
XmlRpcUtil::error("Error in XmlRpcClient::parseResponse: Invalid response - no param or fault tag. Response:\n%s", _response.c_str());
_response = "";
return false;
}
_response = "";
return result.valid();
}