libxml2 nanohttp, libcurl, Boost Asio http

ID: 32
creation date: 2016/06/24 20:29
modification date: 2016/07/02 15:41
owner: taiji
tags: HTTP, HTTP GET, nanohttp, libcurl, C/C++, Boost Asio

C/C++ でウェブページの取得をする際、libxml2 ライブラリに含まれている nanohttp は、HTTPS にも圧縮伸長にも対応していないので、結局、実用的ではありません。その上、ドキュメントが充実していないためか、ネット情報にて間違った使用方法が散見されましたので、以下に例示しておくことにしました。ちなみに、nanohttpLocation ヘッダによるリダイレクションに対応していますが、URL に対する相対 Location には対応していないことに注意です。

/*
  nanohtget.cc

  Copyright (C) 2016 Taiji Yamada <taiji@aihara.co.jp>
*/
#include <iostream>
#include <libxml/nanohttp.h>

int main(int argc, char *argv[])
{
  int rv = 0;

  if (argc < 2) {
    std::cerr << "usage:\n\t" << argv[0] << " url [filename.out]" << std::endl;
    return 1;
  }
  else if (2 < argc) {
    char *content_type = NULL;

    xmlNanoHTTPInit();
    rv = xmlNanoHTTPFetch(argv[1], argv[2], &content_type);
    std::cout << "Content-Type:\t" << (content_type ? content_type : "") << std::endl;
    std::cout << "Return-Value:\t" << rv << std::endl;
    if (content_type) free(content_type);
    xmlNanoHTTPCleanup();
  }
  else /*if (1 < argc) */{
    void *handler;
    char *content_type = NULL;

    xmlNanoHTTPInit();
    if (!(handler = xmlNanoHTTPOpen(argv[1], &content_type))) {
      std::cerr << "xmlNanoHTTPOpen: failure" << std::endl;
      if (content_type) free(content_type);
      return 1;
    }
    if (xmlNanoHTTPReturnCode(handler) != 200) {
      if (content_type) free(content_type);
      return 1;
    }

    std::cerr <<
      "Location:\t"             << (xmlNanoHTTPRedir(handler) ? xmlNanoHTTPRedir(handler) : argv[1]) << std::endl <<
      "Content-Type:\t"         << (content_type ? content_type : "") <<
      (xmlNanoHTTPEncoding(handler) ? std::string("; charset=") + xmlNanoHTTPEncoding(handler) : "") << std::endl <<
      "Mime-Type:\t"            << (xmlNanoHTTPMimeType(handler) ? xmlNanoHTTPMimeType(handler) : "") << std::endl <<
      "Content-Length:\t"       << xmlNanoHTTPContentLength(handler) << std::endl <<
      "WWW-Authenticate:\t"     << (xmlNanoHTTPAuthHeader(handler) ? xmlNanoHTTPAuthHeader(handler) : "") << std::endl;

    char buf[1024];
    int sz;
    while ((sz = xmlNanoHTTPRead(handler, buf, sizeof(buf) - 1)) > 0) {
      buf[sz] = '\0';
      std::cout << buf;
    }

    if (sz == 0)
      std::cerr << "xmlNanoHTTPRead: Connection closed" << std::endl;
    else if (sz == -1) {
      std::cerr << "xmlNanoHTTPRead: Parameter Error" << std::endl;
      rv = 1;
    }

    if (content_type) free(content_type);
    xmlNanoHTTPClose(handler);
    xmlNanoHTTPCleanup();
  }
  return rv;
}

例えば、以下のようにビルドすればよいでしょう。

$ make CPPFLAGS=-I/usr/include/libxml2 LDLIBS=-lxml2 nanohtget
g++ -I/usr/include/libxml2 nanohtget.cc -lxml2 -o nanohtget

間違えやすいのは xmlNanoHTTPRead にて常にナル終端されるわけではないので、読み込んだバッファを C の文字列として扱うには読み込んだサイズに応じてナル終端する必要があることです。ナル終端された行毎に読み込む xmlNanoHTTPReadLine もあるようですので、そちらを使った方がよいかも知れません。

さて、HTTPS に対応したものとして、libcurl が挙げられます。これを使うと流石に簡単に C/C++ でウェブページの取得が出来ます。やはりこれも、適切なネット情報がないようなので、以下に例示をすることにしました。

/*
  urlfetch.cc

  Copyright (C) 2016 Taiji Yamada <taiji@aihara.co.jp>
*/
#include <cstdio>
#include <iostream>
#include <curl.h>       /* must not be <curl/curl.h> */

int main(int argc, char *argv[])
{
  CURL *curl;
  CURLcode rc;
  FILE *fp = stdout;

  if (argc < 2) {
    std::cerr << "usage:\n\t" << argv[0] << " url [filename.out]" << std::endl;
    return 1;
  }
  else if (2 < argc) {
    if (!(fp = fopen(argv[2], "w"))) {
      std::cerr << "error: fopen() " << argv[2] << std::endl;
      return 1;
    }
  }

  if (!(curl = curl_easy_init())) {
    std::cerr << "error: curl_easy_init()" << std::endl;
    return 1;
  }
  curl_easy_setopt(curl, CURLOPT_URL, argv[1]);
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
  curl_easy_setopt(curl, CURLOPT_WRITEHEADER, stderr);

  rc = curl_easy_perform(curl);

  std::cerr << "curl_easy_perform: " << rc << std::endl;

  curl_easy_cleanup(curl);
  if (fp != stdout) fclose(fp);

  return rc == 0 ? 0 : 1;
}

例えば、以下のようにビルドすればよいでしょう。

$ make CPPFLAGS=-I/usr/include/curl LDLIBS=-lcurl urlfetch
g++ -I/usr/include/curl urlfetch.cc -lcurl -o urlfetch

libcurl のインストール状態にも依りますが、このまま HTTPS にも対応しているはずです。

また、リダイレクションは CURLOPT_FOLLOWLOCATION1 にセットしないと有効になりませんので、上記のようになっています。さらに、出力先として上記のようにファイルポインタを指定できます。

次に、Boost Asio を使ったウェブページを取得する方法ですが、まず、URL からホスト名とスキームその他にばらすものがあれば便利なので、まずそれを紹介します。以下の url.hpp です。

/*
  url.hpp

  Copyright (C) 2016 Taiji Yamada <taiji@aihara.co.jp>
*/
#ifndef _URL_HPP_
#define _URL_HPP_
#include <boost/xpressive/xpressive.hpp>

struct url {
  url() {}
  url(const char *arg)
  {
    const boost::xpressive::sregex re =
      !((boost::xpressive::s1 = +~(boost::xpressive::set='/', ':')) >> "://" >>
        !((boost::xpressive::s2 = +~(boost::xpressive::set=':', '@')) >>
          !(':' >> (boost::xpressive::s3 = +~(boost::xpressive::set='@'))) >> '@'
          ) >>
        (boost::xpressive::s4 = *~(boost::xpressive::set='/', ':')) >>
        !(':' >> (boost::xpressive::s5 = *~(boost::xpressive::set='.', '/', ':')))
        ) >>
      (boost::xpressive::s6 = *~boost::xpressive::_n);
    boost::xpressive::smatch what;
    if (boost::xpressive::regex_match(std::string(arg), what, re)) {
      if (what[1]) scheme       = what[1];
      if (what[2]) user         = what[2];
      if (what[3]) passwd       = what[3];
      if (what[4]) host         = what[4];
      if (what[5]) port         = what[5];
      if (what[6]) path         = what[6];
    }
  }
  std::string scheme, host, port, path;
  std::string user, passwd;
  bool internal() const
  {
    return host == "";
  }
  bool externalizable(const url &base_url)
  {
    return internal() && !base_url.internal();
  }
  const url external(const url &base_url)
  {
    url rv(*this);
    if (rv.scheme.empty())      rv.scheme       = base_url.scheme;
    if (rv.user.empty())        rv.user         = base_url.user;
    if (rv.passwd.empty())      rv.passwd       = base_url.passwd;
    if (true)                   rv.host         = base_url.host;
    if (rv.port.empty())        rv.port         = base_url.port;
    std::string base_path = base_url.path;
    size_t i;
    if ((i=base_path.rfind('/')) != std::string::npos)
      base_path = base_path.substr(0, i+1);
    if (rv.path.empty())
      rv.path = base_path;
    else if (rv.path[0] != '/')
      rv.path = base_path + rv.path;
    return rv;
  }
  bool connection_equals(const url &another) const
  {
    return scheme == another.scheme && host == another.host && port == another.port;
  }
  const std::string locator() const
  {
    return (scheme.empty() ? "" : scheme + "://") +
      (user.empty() ? "" : user + (passwd.empty() ? "" : ":" + passwd) + "@") +
      host + (port.empty() ? "" : ":" + port) + path;
  }
};

#endif

Boost Xpressive を使っています。便利です。

さて、ここでは同期通信に限ることにしますが、出来るだけ分かり易く書くと以下のようになりました。

/*
  htget.cc

  Written by Taiji Yamada <taiji@aihara.co.jp>

  Reference(s):
  [1] http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/example/http/client/sync_client.cpp
*/
#include <iostream>
#include <boost/asio.hpp>
#include "url.hpp"

int main(int argc, char *argv[])
{
  int rv = 0;

  if (argc < 2) {
    std::cerr << "usage:\n\t" << argv[0] << " url" << std::endl;
    return 1;
  }
  try {
    boost::asio::io_service io_service;
    boost::asio::ip::tcp::socket socket(io_service);

    url location(argv[1]);
    boost::asio::ip::tcp::resolver resolver(io_service);
    boost::asio::ip::tcp::resolver::query query((location.host != "") ? location.host : "localhost",
                                                (location.port != "") ? location.port : location.scheme);
    boost::asio::ip::tcp::endpoint endpoint(*resolver.resolve(query));

    socket.connect(endpoint);

    boost::asio::streambuf request;
    std::ostream request_ostream(&request);
    request_ostream << "GET " << location.path << " HTTP/1.0\r\n"
      "\r\n";
    boost::asio::write(socket, request);

    boost::asio::streambuf response;
    boost::system::error_code error_code;
    while (boost::asio::read(socket, response, boost::asio::transfer_at_least(1), error_code))
      std::cout << &response;

    socket.close();

    if (error_code != boost::asio::error::eof && error_code != boost::asio::error::shut_down)
      throw boost::system::system_error(error_code);
  }
  catch (std::exception &e) {
    std::cout << argv[0] << ": " << e.what() << std::endl;
    rv = 1;
  }
  return rv;
}

boost が /opt/local にインストールされているとして、ビルドは以下のようにします。

$ make CPPFLAGS=-I/opt/local/include LDFLAGS=-L/opt/local/lib LDLIBS='-lboost_system' htget
g++ -I/opt/local/include -L/opt/local/lib htget.cc -lboost_system -o htget

しかし、これでは HTTPS に対応していないので、実用的ではありません。HTTPS に対応したものを Unified diff 形式で表すと以下のようになります。

$ diff -u htget.cc s-htget.cc
--- htget.cc?2016-06-30 16:18:50.000000000 +0900
+++ s-htget.cc?2016-07-01 15:37:05.000000000 +0900
@@ -1,13 +1,15 @@
 /*
-  htget.cc
+  s-htget.cc
 
   Written by Taiji Yamada <taiji@aihara.co.jp>
 
   Reference(s):
   [1] http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/example/http/client/sync_client.cpp
+  [2] http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/example/ssl/client.cpp
 */
 #include <iostream>
 #include <boost/asio.hpp>
+#include <boost/asio/ssl.hpp>
 #include "url.hpp"
 
 int main(int argc, char *argv[])
@@ -20,7 +22,8 @@
   }
   try {
     boost::asio::io_service io_service;
-    boost::asio::ip::tcp::socket socket(io_service);
+    boost::asio::ssl::context context(io_service, boost::asio::ssl::context::sslv23_client);
+    boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(io_service, context);
 
     url location(argv[1]);
     boost::asio::ip::tcp::resolver resolver(io_service);
@@ -28,7 +31,10 @@
                                                (location.port != "") ? location.port : location.scheme);
     boost::asio::ip::tcp::endpoint endpoint(*resolver.resolve(query));
 
-    socket.connect(endpoint);
+    context.set_verify_mode(boost::asio::ssl::context::verify_peer);
+    context.load_verify_file("cacert.pem");
+    socket.lowest_layer().connect(endpoint);
+    socket.handshake(boost::asio::ssl::stream_base::client);
 
     boost::asio::streambuf request;
     std::ostream request_ostream(&request);
@@ -41,7 +47,7 @@
     while (boost::asio::read(socket, response, boost::asio::transfer_at_least(1), error_code))
       std::cout << &response;
 
-    socket.close();
+    socket.lowest_layer().close();
 
     if (error_code != boost::asio::error::eof && error_code != boost::asio::error::shut_down)
       throw boost::system::system_error(error_code);

boost が /opt/local にインストールされているとして、ビルドは以下のようにします。

$ make CPPFLAGS=-I/opt/local/include LDFLAGS=-L/opt/local/lib LDLIBS='-lssl -lcrypto -lboost_system' s-htget
g++ -I/opt/local/include -L/opt/local/lib s-htget.cc -lssl -lcrypto -lboost_system -o s-htget

しかし、これでは HTTP, HTTPS の双方には対応しておらず、リダイレクションにも未対応で、実用には程遠いと言わざるを得ません。教育的価値と言っても最低限、以下のような例示まであると、それなりに使えるものとなると思います。

/*
  shtget.cc

  Written by Taiji Yamada <taiji@aihara.co.jp>

  Reference(s):
  [1] http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/example/http/client/sync_client.cpp
  [2] http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/example/ssl/client.cpp
*/
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/xpressive/xpressive.hpp>
#include "url.hpp"

int main(int argc, char *argv[])
{
  int rv = 0;

  if (argc < 2) {
    std::cerr << "usage:\n\t" << argv[0] << " url" << std::endl;
    return 1;
  }
  try {
    boost::asio::io_service io_service;
    boost::asio::ip::tcp::socket socket(io_service);
    boost::asio::ssl::context context(io_service, boost::asio::ssl::context::sslv23_client);
    boost::asio::ssl::stream<boost::asio::ip::tcp::socket> secure_socket(io_service, context);

    url location(argv[1]);
    const int redirection_limit = 4;
    int redirection_count = 0;
    bool redirection;
    do {
      redirection = false;
      boost::asio::ip::tcp::resolver resolver(io_service);
      boost::asio::ip::tcp::resolver::query query((location.host != "") ? location.host : "localhost",
                                                  (location.port != "") ? location.port : location.scheme);
      boost::asio::ip::tcp::endpoint endpoint(*resolver.resolve(query));

      if (location.scheme != "https")
        socket.connect(endpoint);
      else {
        context.set_verify_mode(boost::asio::ssl::context::verify_peer);
        context.load_verify_file("cacert.pem");
        secure_socket.lowest_layer().connect(endpoint);
        secure_socket.handshake(boost::asio::ssl::stream_base::client);
      }

      boost::asio::streambuf request;
      std::ostream request_ostream(&request);
      request_ostream << "GET " << location.path << " HTTP/1.0\r\n"
        "\r\n";
      if (location.scheme != "https")
        boost::asio::write(socket, request);
      else
        boost::asio::write(secure_socket, request);

      boost::asio::streambuf response;
      boost::system::error_code error_code;
      std::string buffer;
      while ((location.scheme != "https") ?
             boost::asio::read(socket, response, boost::asio::transfer_at_least(1), error_code) :
             boost::asio::read(secure_socket, response, boost::asio::transfer_at_least(1), error_code))
        ;
      buffer = std::string(boost::asio::buffer_cast<const char *>(response.data())).substr(0, response.size());
      std::cout << buffer;

      if (location.scheme != "https")
        socket.close();
      else
        secure_socket.lowest_layer().close();

      if (error_code != boost::asio::error::eof && error_code != boost::asio::error::shut_down)
        throw boost::system::system_error(error_code);

      const boost::xpressive::sregex re =
        boost::xpressive::bos >> "HTTP/1.1" >> +boost::xpressive::_s >> (boost::xpressive::s1 = +boost::xpressive::_d) >> +boost::xpressive::_s >> *~boost::xpressive::_n >> +boost::xpressive::_n >>
        *boost::xpressive::_ >> boost::xpressive::after(boost::xpressive::_n) >> "Location:" >> +boost::xpressive::_s >> (boost::xpressive::s2 = *~boost::xpressive::_n) >> +boost::xpressive::_n >>
        *boost::xpressive::_ >>
        boost::xpressive::eos;
      boost::xpressive::smatch what;
      if (boost::xpressive::regex_match(buffer, what, re) &&
          (std::string(what[1]) == "301" || std::string(what[1]) == "302" || std::string(what[1]) == "303" ||
           std::string(what[1]) == "305" || std::string(what[1]) == "307")) {
        url redirect(std::string(what[2]).c_str());
        location = redirect.externalizable(location) ? redirect.external(location) : redirect;
        redirection = true;
      }
    } while (redirection && redirection_count++ < redirection_limit);
  }
  catch (std::exception &e) {
    std::cout << argv[0] << ": " << e.what() << std::endl;
    rv = 1;
  }
  return rv;
}

boost が /opt/local にインストールされているとして、ビルドは以下のようにします。

$ make CPPFLAGS=-I/opt/local/include LDFLAGS=-L/opt/local/lib LDLIBS='-lssl -lcrypto -lboost_system' shtget
g++ -I/opt/local/include -L/opt/local/lib shtget.cc -lssl -lcrypto -lboost_system -o shtget

気になる点としては、同ホストへのリダイレクションでも再接続する無駄があることです。例示としてはさらに複雑になってしまうので書きませんが、そういう時はソケットを close, connect せずにそのまま使うように改良することもできます。

0 コメント
ゲストコメント認証用なぞなぞ:
キーボードのLから左に全部打って下さい。それを二回やって下さい。 ...