256 lines
11 KiB
C++
256 lines
11 KiB
C++
// 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 <event2/event.h>
|
|
#include <event2/dns.h>
|
|
#include <evhttp.h> // http://stackoverflow.com/a/5237994; http://archives.seul.org/libevent/users/Sep-2010/msg00050.html
|
|
|
|
#include <memory>
|
|
#include <functional>
|
|
#include <stdexcept>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#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<void(hgot&)> handler;
|
|
hgetContext (struct evhttp_connection* conn, std::function<void(hgot&)> 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<struct event_base> _evbase;
|
|
std::shared_ptr<struct evdns_base> _dnsbase;
|
|
std::function<void(struct evhttp_request*)> _requestBuilder;
|
|
enum evhttp_cmd_type _method;
|
|
public:
|
|
typedef std::shared_ptr<struct evhttp_uri> uri_t;
|
|
/// The third parameter is the request number, starting from 1.
|
|
typedef std::function<float(hgot&,uri_t,int32_t)> until_handler_t;
|
|
public:
|
|
hget (std::shared_ptr<struct event_base> evbase, std::shared_ptr<struct evdns_base> dnsbase):
|
|
_evbase (evbase), _dnsbase (dnsbase), _method (EVHTTP_REQ_GET) {}
|
|
|
|
/// Modifies the request before its execution.
|
|
hget& setRequestBuilder (std::function<void(struct evhttp_request*)> 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<typename STR> 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<void(hgot&)> 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<void(hgot&)> handler) {
|
|
return go (std::shared_ptr<struct evhttp_uri> (evhttp_uri_parse (url), evhttp_uri_free), timeoutSec, handler);
|
|
}
|
|
|
|
void goUntil (std::vector<uri_t> 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<typename URLS> void goUntilS (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) {
|
|
std::vector<uri_t> 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<const char*, 2> 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<typename URLS> void goUntilC (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) {
|
|
std::vector<uri_t> 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<hget::uri_t> _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<hget::uri_t> 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<HgetUntilHandler> 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<uri_t> urls, until_handler_t handler, int32_t timeoutSec) {
|
|
HgetUntilHandler (*this, handler, urls, timeoutSec) .start();
|
|
}
|
|
|
|
}
|
|
|
|
#endif // _GLIM_HGET_INCLUDED
|