Thursday, June 2, 2011

A C++ Boost client for the Vyatta REST api

Here's a Vyatta REST client I modifed from a stock Boost https client example. In some tests a while back I found that using a remote command line client implemented in C++/c was significantly faster than using a scripted Perl implementation, which makes sense when latency isn't the overwhelming factor in the latency of the response. And as I alluded to, with claims of performance there are other important criteria that come into play, such as latency, jitter, response time on the server, etc. Needless to say I my queue (somewhere) a pending post to quantify this performance difference using different client implementations.

You can see from the help that the executable requires the target, url (i.e. command), and optionally password/username to run.

Usage: vyatta_boost   [options]
  -h  host name or ip
  -c  rest api command path
  -m  HTTP method (i.e. GET, DELETE, POST, PUT)
  -u  username
  -p  password

And running this command:



./test_boost  -t 10.3.0.159 -m GET -c "/rest/op/show/version/all"
####################
HTTP/1.0 200 OK
Content-Type: application/json
Vyatta-Specification-Version: 0.3
Cache-Control: no-cache
Content-Length: 95
Connection: close
Date: Mon, 30 May 2011 04:52:11 GMT
Server: lighttpd/1.4.28



{
  "action": "true",
  "help": " Show Vyatta version information plus all packages changes"
}

You can compile the source below with the following:

g++ test_boost.cc -lboost_system -lssl -otest_boost

With this post I want to drop and preserve a working piece of code.




The full source is below:

//
// to compile: g++ test_boost.cc -lboost_system -lssl -otest_boost
//

#include <cstdlib>
#include <iostream>
#include <string>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

using namespace std;

char*
Base64Encode( char *input, int length);

enum { max_length = 16384 };

class client
{

public:
  client(boost::asio::io_service& io_service, boost::asio::ssl::context& context,
  boost::asio::ip::tcp::resolver::iterator endpoint_iterator, std::string target,
  std::string command, std::string method, std::string username, std::string password)
    : socket_(io_service, context),target_(target),
      command_(command),method_(method),
      username_(username),password_(password)
  {
    boost::asio::ip::tcp::endpoint endpoint = *endpoint_iterator;
    socket_.lowest_layer().async_connect(endpoint,
      boost::bind(&client::handle_connect, this,
      boost::asio::placeholders::error, ++endpoint_iterator));
  }


  void handle_connect(const boost::system::error_code& error,
        boost::asio::ip::tcp::resolver::iterator endpoint_iterator)
  {
    if (!error) {
      socket_.async_handshake(boost::asio::ssl::stream_base::client,
        boost::bind(&client::handle_handshake, this,
        boost::asio::placeholders::error));
    }
    else if (endpoint_iterator != boost::asio::ip::tcp::resolver::iterator()) {
      socket_.lowest_layer().close();
      boost::asio::ip::tcp::endpoint endpoint = *endpoint_iterator;
      socket_.lowest_layer().async_connect(endpoint,
        boost::bind(&client::handle_connect, this,
        boost::asio::placeholders::error, ++endpoint_iterator));
    }
    else {
      cout << "Connect failed: " << error << "\n";
    }
  }
  

  void handle_handshake(const boost::system::error_code& error)
  {
    if (!error) {
      request_ = method_ + string(" ") + command_ + string(" HTTP/1.0\r\n");
      request_ += string("Host: ") + target_ + "\r\n";
      request_ += "Accept: */*\r\n"; 
      string foo = username_ + ":" + password_;
      request_ += "Authorization: Basic " + string(Base64Encode((char*)foo.c_str(),
        foo.length())) + "\r\n";
      request_ += "Content-Length:0\r\n\r\n";

     boost::asio::async_write(socket_,
     boost::asio::buffer(request_.c_str(), request_.length()),
     boost::bind(&client::handle_write, this,
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    }
    else {
      std::cout << "Handshake failed: " << error << "\n";
    }
  }

  void handle_write(const boost::system::error_code& error,
      size_t bytes_transferred)
  {

    if (!error) {
      boost::asio::async_read_until(socket_,
        reply_, "\r\n\r\n",
        boost::bind(&client::handle_read, this,
      boost::asio::placeholders::error,
      boost::asio::placeholders::bytes_transferred));
    }
    else {
      std::cout << "Write failed: " << error << "\n";
    }
  }

  void handle_read(const boost::system::error_code& error,
     size_t bytes_transferred)
  {
    if (!error) {
      std::ostringstream ss;
      ss<<&reply_;
      std::string s = ss.str();      
      char *loc = NULL;
      if ((loc = strstr(((char*)s.c_str()),"Content-Length: ")) != NULL) {
        unsigned long len = strtoul(loc+strlen("Content-Length: "),NULL,10);

        if (len > max_length) {
          len = max_length;
        }
 
        boost::asio::async_read(socket_,
        boost::asio::buffer(body_, len),
        boost::bind(&client::handle_read_body, this,
        boost::asio::placeholders::error,
        boost::asio::placeholders::bytes_transferred));
      }
    }
    else {
      std::cout << "Read failed: " << error << "\n";
    }
  }

  void handle_read_body(const boost::system::error_code& error,
     size_t bytes_transferred)
  {
    if (!error) {
      std::cout << endl << "Body: ";
      std::cout << body_ << endl;
      std::cout << "\n";
    }
    else {
      std::cout << "Read failed: " << error << "\n";
    }
  }

private:
  boost::asio::ssl::stream socket_;
  std::string request_; 
  std::string target_;
  std::string method_;
  std::string command_;
  std::string username_;
  std::string password_;
  boost::asio::streambuf reply_;
  char body_[max_length];
};


char*
Base64Encode( char *input, int length)
{
  BIO *bmem, *b64;
  BUF_MEM *bptr;

  b64 = BIO_new(BIO_f_base64());
  bmem = BIO_new(BIO_s_mem());
  b64 = BIO_push(b64, bmem);
  BIO_write(b64, input, length);
  BIO_flush(b64);
  BIO_get_mem_ptr(b64, &bptr);

  char *buff = (char *)malloc(bptr->length);
  memcpy(buff, bptr->data, bptr->length-1);
  buff[bptr->length-1] = 0;

  BIO_free_all(b64);

  return buff;
}

void
usage()
{
  cout << "Usage: vyatta_boost   [options]" << endl;
  cout << "  -h\t\thost name or ip" << endl;
  cout << "  -c\t\trest api command path" << endl;
  cout << "  -m\t\tHTTP method (i.e. GET, DELETE, POST, PUT)" << endl;
  cout << "  -u\t\tusername" << endl;
  cout << "  -p\t\tpassword" << endl;
  return;
}


int 
main(int argc, char* argv[])
{
  bool debug = false;
  char port[] = "https";
  string method,command,target;
  string username = "vyatta";
  string password = "vyatta";
  try {
    char ch;
    while ((ch = getopt(argc, argv, "ht:c:m:u:p:")) != -1) {
      switch (ch) {
      case 't':
        target = optarg;
        break;
      case 'c':
        command = optarg;
        break;
      case 'm':
        method = optarg;
        break;
      case 'u':
        username = optarg;
        break;
      case 'p':
        password = optarg;
        break;
      case 'h':
        usage();
        exit(0);
      }
    }  
    
    if (target.empty() || command.empty() || method.empty()) {
      usage();
      exit(0);
    }
    
    
    boost::asio::io_service io_service;
    boost::asio::ip::tcp::resolver resolver(io_service);
    boost::asio::ip::tcp::resolver::query query(target,port);
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve(query);
    boost::asio::ssl::context ctx(io_service, boost::asio::ssl::context::sslv23);
    ctx.set_verify_mode(boost::asio::ssl::context::verify_none);
    
    client c(io_service, ctx, iterator,target,command,method,username,password);
    io_service.run();
    io_service.reset();
  }
  catch (std::exception& e) {
    std::cerr << "Exception: " << e.what() << "\n";
  }
  
  return 0;
}

2 comments:

  1. Nice start. Are you planning on continue the effort? What was the reason to remove include files?

    ReplyDelete
  2. Thanks for the ping on the include files foo. What happened was the html converted the angle brackets into html instructions--had to do the html mangling to get them to show up. All better now.

    I wasn't planning on extending this--mainly because I didn't yet have a clear idea where I would take it next, but would love to hear any ideas. That might be just the motivation I need :)

    Thanks!

    ReplyDelete