// Simple header-only wrapper around libevent's evhttp client. // See also: https://github.com/cpp-netlib/cpp-netlib/issues/160 #ifndef _GLIM_HGET_INCLUDED #define _GLIM_HGET_INCLUDED #include #include #include // http://stackoverflow.com/a/5237994; http://archives.seul.org/libevent/users/Sep-2010/msg00050.html #include #include #include #include #include #include #include #include #include "exception.hpp" #include "gstring.hpp" namespace glim { /// HTTP results struct hgot { int32_t status = 0; /// Uses errno codes. int32_t error = 0; struct evbuffer* body = 0; struct evhttp_request* req = 0; size_t bodyLength() const {return body ? evbuffer_get_length (body) : 0;} /// Warning: the string is NOT zero-terminated. const char* bodyData() {return body ? (const char*) evbuffer_pullup (body, -1) : "";} /// Returns a zero-terminated string. Warning: modifies the `body` every time in order to add the terminator. const char* cbody() {if (!body) return ""; evbuffer_add (body, "", 1); return (const char*) evbuffer_pullup (body, -1);} /// A gstring *view* into the `body`. glim::gstring gbody() { if (!body) return glim::gstring(); return glim::gstring (glim::gstring::ReferenceConstructor(), (const char*) evbuffer_pullup (body, -1), evbuffer_get_length (body));} }; /// Used internally to pass both connection and handler into callback. struct hgetContext { struct evhttp_connection* conn; std::function handler; hgetContext (struct evhttp_connection* conn, std::function handler): conn (conn), handler (handler) {} }; /// Invoked when evhttp finishes a request. inline void hgetCB (struct evhttp_request* req, void* ctx_){ hgetContext* ctx = (hgetContext*) ctx_; hgot gt; if (req == NULL) gt.error = ETIMEDOUT; else if (req->response_code == 0) gt.error = ECONNREFUSED; else { gt.status = req->response_code; gt.body = req->input_buffer; gt.req = req; } try { ctx->handler (gt); } catch (const std::runtime_error& ex) { // Shouldn't normally happen: std::cerr << "glim::hget, handler exception: " << ex.what() << std::endl; } evhttp_connection_free ((struct evhttp_connection*) ctx->conn); //freed by libevent//if (req != NULL) evhttp_request_free (req); delete ctx; } /** C++ wrapper around libevent's http client. Example: \code hget (evbase, dnsbase) .setRequestBuilder ([](struct evhttp_request* req){ evbuffer_add (req->output_buffer, "foo", 3); evhttp_add_header (req->output_headers, "Content-Length", "3"); }) .go ("http://127.0.0.1:8080/test", [](hgot& got){ if (got.error) log_warn ("127.0.0.1:8080 " << strerror (got.error)); else if (got.status != 200) log_warn ("127.0.0.1:8080 != 200"); else log_info ("got " << evbuffer_get_length (got.body) << " bytes from /test: " << evbuffer_pullup (got.body, -1)); }); \endcode */ class hget { public: std::shared_ptr _evbase; std::shared_ptr _dnsbase; std::function _requestBuilder; enum evhttp_cmd_type _method; public: typedef std::shared_ptr uri_t; /// The third parameter is the request number, starting from 1. typedef std::function until_handler_t; public: hget (std::shared_ptr evbase, std::shared_ptr dnsbase): _evbase (evbase), _dnsbase (dnsbase), _method (EVHTTP_REQ_GET) {} /// Modifies the request before its execution. hget& setRequestBuilder (std::function rb) { _requestBuilder = rb; return *this; } /** Uses a simple request builder to send the `str`. * `str` is a `char` string class with methods `data` and `size`. */ template hget& payload (STR str, const char* contentType = nullptr, enum evhttp_cmd_type method = EVHTTP_REQ_POST) { _method = method; return setRequestBuilder ([str,contentType](struct evhttp_request* req) { if (contentType) evhttp_add_header (req->output_headers, "Content-Type", contentType); char buf[64]; *glim::itoa (buf, (int) str.size()) = 0; evhttp_add_header (req->output_headers, "Content-Length", buf); evbuffer_add (req->output_buffer, (const void*) str.data(), (size_t) str.size()); }); } struct evhttp_request* go (uri_t uri, int32_t timeoutSec, std::function handler) { int port = evhttp_uri_get_port (uri.get()); if (port == -1) port = 80; struct evhttp_connection* conn = evhttp_connection_base_new (_evbase.get(), _dnsbase.get(), evhttp_uri_get_host (uri.get()), port); evhttp_connection_set_timeout (conn, timeoutSec); struct evhttp_request *req = evhttp_request_new (hgetCB, new hgetContext(conn, handler)); int ret = evhttp_add_header (req->output_headers, "Host", evhttp_uri_get_host (uri.get())); if (ret) throw std::runtime_error ("hget: evhttp_add_header(Host) != 0"); if (_requestBuilder) _requestBuilder (req); const char* get = evhttp_uri_get_path (uri.get()); const char* qs = evhttp_uri_get_query (uri.get()); if (qs == NULL) { ret = evhttp_make_request (conn, req, _method, get); } else { size_t getLen = strlen (get); size_t qsLen = strlen (qs); char buf[getLen + 1 + qsLen + 1]; char* caret = stpcpy (buf, get); *caret++ = '?'; caret = stpcpy (caret, qs); assert (caret - buf < sizeof (buf)); ret = evhttp_make_request (conn, req, _method, buf); } if (ret) throw std::runtime_error ("hget: evhttp_make_request != 0"); return req; } struct evhttp_request* go (const char* url, int32_t timeoutSec, std::function handler) { return go (std::shared_ptr (evhttp_uri_parse (url), evhttp_uri_free), timeoutSec, handler); } void goUntil (std::vector urls, until_handler_t handler, int32_t timeoutSec = 20); /** Parse urls and call `goUntil`. Example (trying ten times to reach the servers): \code std::string path ("/path"); hget.goUntilS (boost::assign::list_of ("http://server1" + path) ("http://server2" + path), [](hgot& got, hget::uri_t uri, int32_t num)->float { std::cout << "server: " << evhttp_uri_get_host (uri.get()) << "; request number: " << num << std::endl; if (got.status != 200 && num < 10) return 1.f; // Retry in a second. return -1.f; // No need to retry the request. }); \endcode @param urls is a for-compatible container of strings (where string has methods `data` and `size`). */ template void goUntilS (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) { std::vector parsedUrls; for (auto&& url: urls) { // Copying to stack might be cheaper than malloc in c_str. int len = url.size(); char buf[len + 1]; memcpy (buf, url.data(), len); buf[len] = 0; struct evhttp_uri* uri = evhttp_uri_parse (buf); if (!uri) GTHROW (std::string ("!evhttp_uri_parse: ") + buf); parsedUrls.push_back (uri_t (uri, evhttp_uri_free)); } goUntil (parsedUrls, handler, timeoutSec); } /** Parse urls and call `goUntil`. Example (trying ten times to reach the servers): \code hget.goUntilC (boost::assign::list_of ("http://server1/") ("http://server2/"), [](hgot& got, hget::uri_t uri, int32_t num)->float { std::cout << "server: " << evhttp_uri_get_host (uri.get()) << "; request number: " << num << std::endl; if (got.status != 200 && num < 10) return 1.f; // Retry in a second. return -1.f; // No need to retry the request. }); \endcode Or with `std::array` instead of `boost::assign::list_of`: \code std::array urls {{"http://server1/", "http://server2/"}}; hget.goUntilC (urls, [](hgot& got, hget::uri_t uri, int32_t num)->float { return got.status != 200 && num < 10 ? 0.f : -1.f;}); \endcode @param urls is a for-compatible container of C strings (const char*). */ template void goUntilC (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) { std::vector parsedUrls; for (auto url: urls) { struct evhttp_uri* uri = evhttp_uri_parse (url); if (!uri) GTHROW (std::string ("Can't parse url: ") + url); parsedUrls.push_back (uri_t (uri, evhttp_uri_free)); } goUntil (parsedUrls, handler, timeoutSec); } }; inline void hgetUntilRetryCB (evutil_socket_t, short, void* utilHandlerPtr); // event_callback_fn /** `hget::goUntil` implementation. * This function object is passed to `hget::go` as a handler and calls `hget::go` again if necessary. */ struct HgetUntilHandler { hget _hget; hget::until_handler_t _handler; std::vector _urls; int32_t _timeoutSec; int32_t _requestNum; uint8_t _nextUrl; ///< A round-robin pointer to the next url in `_urls`. HgetUntilHandler (hget& hg, hget::until_handler_t handler, std::vector urls, int32_t timeoutSec): _hget (hg), _handler (handler), _urls (urls), _timeoutSec (timeoutSec), _requestNum (0), _nextUrl (0) {} void operator() (hgot& got) { uint8_t urlNum = _nextUrl ? _nextUrl - 1 : _urls.size() - 1; float retryAfterSec = _handler (got, _urls[urlNum], _requestNum); if (retryAfterSec == 0.f) retry(); else if (retryAfterSec > 0.f) { struct timeval wait; wait.tv_sec = (int) retryAfterSec; retryAfterSec -= wait.tv_sec; wait.tv_usec = (int) (retryAfterSec * 1000000.f); int rc = event_base_once (_hget._evbase.get(), -1, EV_TIMEOUT, hgetUntilRetryCB, new HgetUntilHandler (*this), &wait); if (rc) throw std::runtime_error ("HgetUntilHandler: event_base_once != 0"); } } void start() {retry();} void retry() { uint8_t nextUrl = _nextUrl++; if (_nextUrl >= _urls.size()) _nextUrl = 0; ++_requestNum; _hget.go (_urls[nextUrl], _timeoutSec, *this); } }; /// Used in `hget::goUntil` to wait in `evtimer_new` before repeating the request. inline void hgetUntilRetryCB (evutil_socket_t, short, void* utilHandlerPtr) { // event_callback_fn std::unique_ptr untilHandler ((HgetUntilHandler*) utilHandlerPtr); untilHandler->retry(); } /** * Allows to retry the request using multiple URLs in a round-robin fashion. * The `handler` returns the number of seconds to wait before retrying the request or -1 if no retry is necessary. */ inline void hget::goUntil (std::vector urls, until_handler_t handler, int32_t timeoutSec) { HgetUntilHandler (*this, handler, urls, timeoutSec) .start(); } } #endif // _GLIM_HGET_INCLUDED