1 // ----------------------------------------------------------------------------
2 //
3 // flxmlrpc Copyright (c) 2015 by W1HKJ, Dave Freese <iam_w1hkj@w1hkj.com>
4 //
5 // XmlRpc++ Copyright (c) 2002-2008 by Chris Morley
6 //
7 // This file is part of fldigi
8 //
9 // flxmlrpc is free software; you can redistribute it and/or modify
10 // it under the terms of the GNU Lesser General Public License as published by
11 // the Free Software Foundation; either version 3 of the License, or
12 // (at your option) any later version.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 // ----------------------------------------------------------------------------
17 
18 #include <config.h>
19 
20 #include <sys/types.h>
21 #include <unistd.h>
22 
23 #include "XmlRpcClient.h"
24 
25 #include "XmlRpcSocket.h"
26 #include "XmlRpc.h"
27 
28 #include "XmlRpcBase64.h"   // For HTTP authentication encoding
29 
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <strings.h>
34 #include <string>
35 
36 using namespace XmlRpc;
37 using namespace std;
38 
39 // Static data
40 const char REQUEST_BEGIN[] =
41   "<?xml version=\"1.0\"?>\r\n";
42 const char REQUEST_BEGIN_METHODNAME[] =
43   "<methodCall><methodName>";
44 const char REQUEST_END_METHODNAME[] = "</methodName>\r\n";
45 
46 const char PARAMS_TAG[] = "<params>";
47 const char PARAMS_ETAG[] = "</params>";
48 const char PARAM_TAG[] = "<param>";
49 const char PARAM_ETAG[] =  "</param>";
50 const char REQUEST_END[] = "</methodCall>\r\n";
51 
XmlRpcClient(const char * host,int port,const char * uri)52 XmlRpcClient::XmlRpcClient(const char* host, int port, const char* uri/*=0*/)
53 {
54   XmlRpcUtil::log(1, "XmlRpcClient new client: host %s, port %d.", host, port);
55 
56   _host = host;
57   _port = port;
58   if (uri && *uri)
59     _uri = uri;
60   else
61     _uri = "/RPC2";
62   _connectionState = NO_CONNECTION;
63   _executing = false;
64   _eof = false;
65 
66   // Default to keeping the connection open until an explicit close is done
67   setKeepOpen();
68 }
69 
70 
XmlRpcClient(const char * host,int port,const char * login,const char * password,const char * uri)71 XmlRpcClient::XmlRpcClient(const char* host, int port,
72                            const char* login, const char* password, const char* uri/*=0*/)
73 {
74   XmlRpcUtil::log(1, "XmlRpcClient new client: host %s, port %d, login %s.", host, port, login);
75 
76   _host = host;
77   _port = port;
78 
79   _login = login ? login : "";
80   _password = password ? password : "";
81 
82   _uri = uri ? uri : "/RPC2";
83 
84   _connectionState = NO_CONNECTION;
85   _executing = false;
86   _eof = false;
87 
88   // Default to keeping the connection open until an explicit close is done
89   setKeepOpen();
90 }
91 
92 
93 
~XmlRpcClient()94 XmlRpcClient::~XmlRpcClient()
95 {
96   XmlRpcUtil::log(1, "XmlRpcClient dtor client: host %s, port %d.", _host.c_str(), _port);
97   if (_connectionState != NO_CONNECTION) close();
98 }
99 
100 
101 // Close the owned fd
102 void
close()103 XmlRpcClient::close()
104 {
105   XmlRpcUtil::log(4, "XmlRpcClient::close: fd %d.", getfd());
106   _connectionState = NO_CONNECTION;
107   _disp.exit();
108   _disp.removeSource(this);
109 
110   XmlRpcSource::close();
111 }
112 
113 
114 // Clear the referenced flag even if exceptions or errors occur.
115 struct ClearFlagOnExit {
ClearFlagOnExitClearFlagOnExit116   ClearFlagOnExit(bool& flag) : _flag(flag) {}
~ClearFlagOnExitClearFlagOnExit117   ~ClearFlagOnExit() { _flag = false; }
118   bool& _flag;
119 };
120 
121 // Execute the named procedure on the remote server.
122 // Params should be an array of the arguments for the method.
123 // Returns true if the request was sent and a result received (although the result
124 // might be a fault).
125 bool
execute(const char * method,XmlRpcValue const & params,XmlRpcValue & result,double timeoutSeconds)126 XmlRpcClient::execute(const char* method, XmlRpcValue const& params, XmlRpcValue& result, double timeoutSeconds)
127 {
128   XmlRpcUtil::log(1, "XmlRpcClient::execute: method %s (_connectionState %d).", method, _connectionState);
129 
130   // This is not a thread-safe operation, if you want to do multithreading, use separate
131   // clients for each thread. If you want to protect yourself from multiple threads
132   // accessing the same client, replace this code with a real mutex.
133   if (_executing)
134     return false;
135 
136   _executing = true;
137   ClearFlagOnExit cf(_executing);
138 
139   _sendAttempts = 0;
140   _isFault = false;
141 
142   if ( ! setupConnection())
143     return false;
144 
145   if ( ! generateRequest(method, params))
146     return false;
147 
148   result.clear();
149 
150   // Process until either a response is received or the timeout period passes
151   _disp.work(timeoutSeconds);
152 
153   if (_connectionState != IDLE || ! parseResponse(result))
154     return false;
155 
156   XmlRpcUtil::log(1, "XmlRpcClient::execute: method %s completed.", method);
157   _response.clear();
158   return true;
159 }
160 
161 // XmlRpcSource interface implementation
162 // Handle server responses. Called by the event dispatcher during execute.
163 unsigned
handleEvent(unsigned eventType)164 XmlRpcClient::handleEvent(unsigned eventType)
165 {
166   if (eventType == XmlRpcDispatch::Exception)
167   {
168     //if (XmlRpcSocket::nonFatalError())
169     //  return (_connectionState == WRITE_REQUEST)
170     //        ? XmlRpcDispatch::WritableEvent : XmlRpcDispatch::ReadableEvent;
171 
172     if (_connectionState == WRITE_REQUEST && _bytesWritten == 0)
173       XmlRpcUtil::error("Error in XmlRpcClient::handleEvent: could not connect to server (%s).",
174                        XmlRpcSocket::getErrorMsg().c_str());
175     else
176       XmlRpcUtil::error("Error in XmlRpcClient::handleEvent (state %d): %s.",
177                         _connectionState, XmlRpcSocket::getErrorMsg().c_str());
178     return 0;
179   }
180 
181   if (_connectionState == WRITE_REQUEST)
182     if ( ! writeRequest()) return 0;
183 
184   if (_connectionState == READ_HEADER)
185     if ( ! readHeader()) return 0;
186 
187   if (_connectionState == READ_RESPONSE)
188     if ( ! readResponse()) return 0;
189 
190   // This should probably always ask for Exception events too
191   return (_connectionState == WRITE_REQUEST)
192         ? XmlRpcDispatch::WritableEvent : XmlRpcDispatch::ReadableEvent;
193 }
194 
195 
196 // Create the socket connection to the server if necessary
197 bool
setupConnection()198 XmlRpcClient::setupConnection()
199 {
200   // If an error occurred last time through, or if the server closed the connection, close our end
201   if ((_connectionState != NO_CONNECTION && _connectionState != IDLE) || _eof)
202     close();
203 
204   _eof = false;
205   if (_connectionState == NO_CONNECTION)
206     if (! doConnect())
207       return false;
208 
209   // Prepare to write the request
210   _connectionState = WRITE_REQUEST;
211   _bytesWritten = 0;
212 
213   // Notify the dispatcher to listen on this source (calls handleEvent when the socket is writable)
214   _disp.removeSource(this);       // Make sure nothing is left over
215   _disp.addSource(this, XmlRpcDispatch::WritableEvent | XmlRpcDispatch::Exception);
216 
217   return true;
218 }
219 
220 
221 // Connect to the xmlrpc server
222 bool
doConnect()223 XmlRpcClient::doConnect()
224 {
225   XmlRpcSocket::Socket fd = XmlRpcSocket::socket();
226   if (fd == XmlRpcSocket::Invalid)
227   {
228     XmlRpcUtil::error("Error in XmlRpcClient::doConnect: Could not create socket (%s).", XmlRpcSocket::getErrorMsg().c_str());
229     return false;
230   }
231 
232   XmlRpcUtil::log(3, "XmlRpcClient::doConnect: fd %d.", fd);
233   this->setfd(fd);
234 
235   // Don't block on connect/reads/writes
236   if ( ! XmlRpcSocket::setNonBlocking(fd))
237   {
238     this->close();
239     XmlRpcUtil::error("Error in XmlRpcClient::doConnect: Could not set socket to non-blocking IO mode (%s).", XmlRpcSocket::getErrorMsg().c_str());
240     return false;
241   }
242 
243   if ( ! XmlRpcSocket::connect(fd, _host, _port))
244   {
245     this->close();
246     XmlRpcUtil::error("Error in XmlRpcClient::doConnect: Could not connect to server (%s).", XmlRpcSocket::getErrorMsg().c_str());
247     return false;
248   }
249 
250   return XmlRpcSource::doConnect();
251 }
252 
253 // Encode the request to call the specified method with the specified parameters into xml
254 
255 std::string XmlRpc::pname = "N/A";
256 
257 void
set_pname(std::string pn)258 XmlRpc::set_pname(std::string pn)
259 {
260   XmlRpc::pname = pn;
261 };
262 
263 bool
generateRequest(const char * methodName,XmlRpcValue const & params)264 XmlRpcClient::generateRequest(const char* methodName, XmlRpcValue const& params)
265 {
266   std::string body = REQUEST_BEGIN;
267   if (XmlRpc::pname != "N/A") {
268     char pid[100];
269     snprintf(pid, sizeof(pid), "%s %d", XmlRpc::pname.c_str(), getpid());
270     body.append("<?clientid=\"").append(pid).append("\"?>\r\n");
271   }
272   body.append(REQUEST_BEGIN_METHODNAME);
273   body.append(methodName);
274   body.append(REQUEST_END_METHODNAME);
275 
276   // If params is an array, each element is a separate parameter
277   if (params.valid()) {
278     body += PARAMS_TAG;
279     if (params.getType() == XmlRpcValue::TypeArray)
280     {
281       for (int i=0; i<params.size(); ++i) {
282         body += PARAM_TAG;
283         body += params[i].toXml();
284         body += PARAM_ETAG;
285       }
286     }
287     else
288     {
289       body += PARAM_TAG;
290       body += params.toXml();
291       body += PARAM_ETAG;
292     }
293 
294     body += PARAMS_ETAG;
295   }
296   body += REQUEST_END;
297 
298   std::string header = generateHeader(body);
299   XmlRpcUtil::log(4, "XmlRpcClient::generateRequest: header is %d bytes, content-length is %d.",
300                   header.length(), body.length());
301 
302   _request = header + body;
303   return true;
304 }
305 
306 // Prepend http headers
307 std::string
generateHeader(std::string const & body)308 XmlRpcClient::generateHeader(std::string const& body)
309 {
310   std::string header =
311     "POST " + _uri + " HTTP/1.1\r\n"
312     "User-Agent: ";
313   header += XMLRPC_VERSION;
314   header += "\r\nHost: ";
315   header += _host;
316 
317   char buff[40];
318   sprintf(buff,":%d\r\n", _port);
319 
320   header += buff;
321 
322   if (_login.length() != 0)
323   {
324     // convert to base64
325     std::vector<char> base64data;
326     int iostatus = 0;
327     xmlrpc_base64<char> encoder;
328     std::back_insert_iterator<std::vector<char> > ins =
329       std::back_inserter(base64data);
330 
331     std::string authBuf = _login + ":" + _password;
332 
333     encoder.put(authBuf.begin(), authBuf.end(), ins, iostatus,
334                 xmlrpc_base64<>::crlf());
335 
336     header += "Authorization: Basic ";
337     std::string authEnc(base64data.begin(), base64data.end());
338     // handle pesky linefeed characters
339     string::size_type lf;
340     while ( (lf = authEnc.find("\r")) != string::npos ) {
341       authEnc.erase(lf, 1);
342     }
343     while ( (lf = authEnc.find("\n")) != string::npos ) {
344       authEnc.erase(lf, 1);
345     }
346     header += authEnc;
347     header += "\r\n";
348   }
349 
350   header += "Content-Type: text/xml\r\nContent-length: ";
351 
352   sprintf(buff,"%d\r\n\r\n", static_cast<int>(body.size()));
353 
354   return header + buff;
355 }
356 
357 bool
writeRequest()358 XmlRpcClient::writeRequest()
359 {
360   if (_bytesWritten == 0)
361     XmlRpcUtil::log(5, "XmlRpcClient::writeRequest (attempt %d):\n%s\n", _sendAttempts+1, _request.c_str());
362 
363   // Try to write the request
364   if ( ! nbWrite(_request, &_bytesWritten))
365   {
366     XmlRpcUtil::error("Error in XmlRpcClient::writeRequest: write error (%s).",XmlRpcSocket::getErrorMsg().c_str());
367     return false;
368   }
369 
370   XmlRpcUtil::log(3, "XmlRpcClient::writeRequest: wrote %d of %d bytes.", _bytesWritten, _request.length());
371 
372   // Wait for the result
373   if (_bytesWritten == int(_request.length()))
374   {
375     _header = "";
376     _response = "";
377     _connectionState = READ_HEADER;
378   }
379   return true;
380 }
381 
382 
383 // Read the header from the response
384 bool
readHeader()385 XmlRpcClient::readHeader()
386 {
387   // Read available data
388   if ( ! nbRead(_header, &_eof) || (_eof && _header.length() == 0))
389   {
390     // If we haven't read any data yet and this is a keep-alive connection, the server may
391     // have timed out, so we try one more time.
392     if (getKeepOpen() && _header.length() == 0 && _sendAttempts++ == 0)
393     {
394       XmlRpcUtil::log(4, "XmlRpcClient::readHeader: re-trying connection");
395       XmlRpcSource::close();
396 
397       _connectionState = NO_CONNECTION;
398       _eof = false;
399       return setupConnection();
400     }
401 
402     XmlRpcUtil::error("Error in XmlRpcClient::readHeader: error while reading header (%s) on fd %d.",
403                       XmlRpcSocket::getErrorMsg().c_str(), getfd());
404     return false;
405   }
406 
407   XmlRpcUtil::log(4, "XmlRpcClient::readHeader: client has read %d bytes", _header.length());
408 
409   return parseHeader();
410 }
411 
412 bool
parseHeader()413 XmlRpcClient::parseHeader()
414 {
415   char const *hp = _header.c_str();         // Start of header
416   char const *ep = hp + _header.length();   // End of string
417   char const *bp = 0;                       // Start of body
418   char const *lp = 0;                       // Start of content-length value
419 
420   std::string const CONTINUE100("100 Continue");
421   int nc100 = int(CONTINUE100.length());
422   for (char const *cp = hp; (bp == 0) && (cp < ep); ++cp)
423   {
424     if ((ep - cp > 16) && (strncasecmp(cp, "Content-length: ", 16) == 0))
425     {
426       lp = cp + 16;
427     }
428     else if ((ep - cp > 4) && (strncmp(cp, "\r\n\r\n", 4) == 0))
429     {
430       if (cp - hp > nc100 && strncmp(cp-CONTINUE100.length(), CONTINUE100.c_str(), CONTINUE100.length()) == 0)
431         cp += 3;
432       else
433         bp = cp + 4;
434     }
435     else if ((ep - cp > 2) && (strncmp(cp, "\n\n", 2) == 0))
436     {
437       if (cp - hp > nc100 && strncmp(cp-CONTINUE100.length(), CONTINUE100.c_str(), CONTINUE100.length()) == 0)
438         ++ cp;
439       else
440         bp = cp + 2;
441     }
442   }
443 
444   // If we haven't gotten the entire header yet, return (keep reading)
445   if (bp == 0)
446   {
447     if (_eof)          // EOF in the middle of a response is an error
448     {
449       XmlRpcUtil::error("Error in XmlRpcClient::readHeader: EOF while reading header");
450       return false;   // Close the connection
451     }
452 
453     return true;  // Keep reading
454   }
455 
456   // Decode content length
457   if (lp == 0)
458   {
459     XmlRpcUtil::error("Error XmlRpcClient::readHeader: No Content-length specified");
460     return false;   // We could try to figure it out by parsing as we read, but for now...
461   }
462 
463   _contentLength = atoi(lp);
464   if (_contentLength <= 0)
465   {
466     XmlRpcUtil::error("Error in XmlRpcClient::readHeader: Invalid Content-length specified (%d).", _contentLength);
467     return false;
468   }
469 
470   XmlRpcUtil::log(4, "client read content length: %d", _contentLength);
471 
472   // Otherwise copy non-header data to response buffer and set state to read response.
473   _response = bp;
474   _header = "";   // should parse out any interesting bits from the header (connection, etc)...
475   _connectionState = READ_RESPONSE;
476   return true;    // Continue monitoring this source
477 }
478 
479 
480 bool
readResponse()481 XmlRpcClient::readResponse()
482 {
483   // If we dont have the entire response yet, read available data
484   if (int(_response.length()) < _contentLength)
485   {
486     if ( ! nbRead(_response, &_eof))
487     {
488       XmlRpcUtil::error("Error in XmlRpcClient::readResponse: read error (%s).",XmlRpcSocket::getErrorMsg().c_str());
489       return false;
490     }
491 
492     // If we haven't gotten the entire _response yet, return (keep reading)
493     if (int(_response.length()) < _contentLength)
494     {
495       if (_eof)
496       {
497         XmlRpcUtil::error("Error in XmlRpcClient::readResponse: EOF while reading response");
498         return false;
499       }
500       return true;
501     }
502   }
503 
504   // Otherwise, parse and return the result
505   XmlRpcUtil::log(3, "XmlRpcClient::readResponse (read %d bytes)", _response.length());
506   XmlRpcUtil::log(5, "response:\n%s", _response.c_str());
507 
508   _connectionState = IDLE;
509 
510   return false;    // Stop monitoring this source (causes return from work)
511 }
512 
513 
514 // Convert the response xml into a result value
515 bool
parseResponse(XmlRpcValue & result)516 XmlRpcClient::parseResponse(XmlRpcValue& result)
517 {
518   std::string r;
519   _response.swap(r);
520 
521   // Parse response xml into result
522   bool emptyParam;
523   int offset = 0;
524   if ( ! XmlRpcUtil::findTag("methodResponse",r,&offset,&emptyParam) || emptyParam)
525   {
526     XmlRpcUtil::error("Error in XmlRpcClient::parseResponse: Invalid response - no methodResponse. Response:\n%s", r.c_str());
527     return false;
528   }
529 
530   // Expect either <params><param>... or <fault>...
531   if (XmlRpcUtil::nextTagIs("params",r,&offset,&emptyParam) &&
532       XmlRpcUtil::nextTagIs("param",r,&offset,&emptyParam))
533   {
534     if (emptyParam)
535     {
536       result = 0; // No result?
537     }
538     else if (  ! result.fromXml(r, &offset))
539     {
540       XmlRpcUtil::error("Error in XmlRpcClient::parseResponse: Invalid response value. Response:\n%s", r.c_str());
541       return false;
542     }
543   }
544   else if (XmlRpcUtil::nextTagIs("fault",r,&offset,&emptyParam))
545   {
546     _isFault = true;
547 
548     if (emptyParam || ! result.fromXml(r, &offset))
549     {
550       result = 0; // No result?
551       return false;
552     }
553   }
554   else
555   {
556     XmlRpcUtil::error("Error in XmlRpcClient::parseResponse: Invalid response - no param or fault tag. Response:\n%s", r.c_str());
557     return false;
558   }
559 
560   return result.valid();
561 }
562 
563