aboutsummaryrefslogtreecommitdiff
path: root/external/glim/curl.hpp
blob: ff2ba9ac1ce0e59f36ab4438635a85aa408628a0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/** \file
 * Very simple header-only wrapper around libcurl.\n
 * See also: https://github.com/venam/Browser\n
 * See also: https://github.com/mologie/curl-asio\n
 * See also: http://thread.gmane.org/gmane.comp.web.curl.library/1322 (this one uses a temporary file). */

#ifndef _GLIM_CURL_INCLUDED
#define _GLIM_CURL_INCLUDED

#include "gstring.hpp"
#include "exception.hpp"
#include <curl/curl.h>
#include <algorithm>
#include <functional>
#include <string.h>
#include <stdint.h>

namespace glim {

inline size_t curlWriteToString (void *buffer, size_t size, size_t nmemb, void *userp) {
  ((std::string*) userp)->append ((const char*) buffer, size * nmemb);
  return size * nmemb;};

inline size_t curlReadFromString (void *ptr, size_t size, size_t nmemb, void *userdata);
inline size_t curlReadFromGString (void *ptr, size_t size, size_t nmemb, void *userdata);
inline size_t curlWriteHeader (void *ptr, size_t size, size_t nmemb, void *curlPtr);
inline int curlDebugCB (CURL* curl, curl_infotype type, char* bytes, size_t size, void* curlPtr);

/**
 Simple HTTP requests using cURL.
 Example: \code
   std::string w3 = glim::Curl() .http ("http://www.w3.org/") .go().str();
 \endcode
 */
class Curl {
 protected:
  Curl (const Curl&): _curl (NULL), _headers (NULL), _sent (0), _needs_cleanup (true) {} // No copying.
 public:
  struct PerformError: public glim::Exception {
    PerformError (const char* message, const char* file, int32_t line):
      glim::Exception (message, file, line) {}
  };
  struct GetinfoError: public glim::Exception {
    CURLcode _code; std::string _error;
    GetinfoError (CURLcode code, const std::string& error, const char* file, int32_t line):
      glim::Exception (error, file, line),
      _code (code), _error (error) {}
  };
 public:
  CURL* _curl;
  struct curl_slist *_headers;
  std::function<void (const char* header, int len)> _headerListener;
  std::function<void (curl_infotype type, char* bytes, size_t size)> _debugListener;
  std::string _sendStr; ///< We're using `std::string` instead of `gstring` in order to support payloads larger than 16 MiB.
  glim::gstring _sendGStr; ///< `gstring::view` and `gstring::ref` allow us to zero-copy.
  uint32_t _sent;
  std::string _got;
  bool _needs_cleanup:1; ///< ~Curl will do `curl_easy_cleanup` if `true`.
  char _errorBuf[CURL_ERROR_SIZE];

  Curl (Curl&&) = default;

  /// @param cleanup can be turned off if the cURL is freed elsewhere.
  Curl (bool cleanup = true): _curl (curl_easy_init()), _headers (NULL), _sent (0), _needs_cleanup (cleanup) {
    curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
    *_errorBuf = 0;}
  /// Wraps an existing handle (will invoke `curl_easy_cleanup` nevertheless).
  /// @param cleanup can be turned off if the cURL is freed elsewhere.
  Curl (CURL* curl, bool cleanup = true): _curl (curl), _headers (NULL), _sent (0), _needs_cleanup (cleanup) {
    curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
    *_errorBuf = 0;}
  ~Curl(){
    if (_headers) {curl_slist_free_all (_headers); _headers = NULL;}
    if (_curl) {if (_needs_cleanup) curl_easy_cleanup (_curl); _curl = NULL;}
  }

  /** Stores the content to be sent into an `std::string` inside `Curl`.
   * NB: In order to have an effect this method should be used *before* the `http()` and `smtp()` methods. */
  template<typename STR> Curl& send (STR&& text) {
    _sendStr = std::forward<STR> (text);
    _sendGStr.clear();
    _sent = 0;
    return *this;}

  /// Adds "Content-Type" header into `_headers`.
  Curl& contentType (const char* ct) {
    char ctb[64]; gstring cth (sizeof (ctb), ctb, false, 0);
    cth << "Content-Type: " << ct << "\r\n";
    _headers = curl_slist_append (_headers, cth.c_str());
    return *this;
  }

  /// @param fullHeader is a full HTTP header and a newline, e.g. "User-Agent: Me\r\n".
  Curl& header (const char* fullHeader) {
    _headers = curl_slist_append (_headers, fullHeader);
    return *this;
  }

  /**
   Sets the majority of options for the http request.
   NB: If `send` was used with a non-empty string then `http` will use `CURLOPT_UPLOAD`, setting http method to `PUT` (use the `method()` to override).
   \n
   Example: \code
     glim::Curl curl;
     curl.http (url.c_str()) .go();
     std::cout << curl.status() << std::endl << curl.str() << std::endl;
   \endcode
   */
  Curl& http (const char* url, int timeoutSec = 20) {
    curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
    curl_easy_setopt (_curl, CURLOPT_URL, url);
    curl_easy_setopt (_curl, CURLOPT_WRITEFUNCTION, curlWriteToString);
    curl_easy_setopt (_curl, CURLOPT_WRITEDATA, &_got);
    curl_easy_setopt (_curl, CURLOPT_TIMEOUT, timeoutSec);
    curl_easy_setopt (_curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
    curl_easy_setopt (_curl, CURLOPT_ERRORBUFFER, _errorBuf);
    if (_sendStr.size() || _sendGStr.size()) {
      curl_easy_setopt (_curl, CURLOPT_UPLOAD, 1L); // http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTUPLOAD
      if (_sendStr.size()) {
        curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendStr.size());
        curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromString);
      } else {
        curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendGStr.size());
        curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromGString);}
      curl_easy_setopt (_curl, CURLOPT_READDATA, this);}
    if (_headers)
      curl_easy_setopt (_curl, CURLOPT_HTTPHEADER, _headers); // http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTHTTPHEADER
    return *this;
  }

  /**
   Set options for smtp request.
   Example: \code
     long rc = glim::Curl().send ("Subject: subject\r\n\r\n" "text\r\n") .smtp ("from", "to") .go().status();
     if (rc != 250) std::cerr << "Error sending email: " << rc << std::endl;
   \endcode */
  Curl& smtp (const char* from, const char* to) {
    curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
    curl_easy_setopt (_curl, CURLOPT_URL, "smtp://127.0.0.1");
    if (from) curl_easy_setopt (_curl, CURLOPT_MAIL_FROM, from);
    bcc (to);
    if (_headers) curl_easy_setopt (_curl, CURLOPT_MAIL_RCPT, _headers);
    curl_easy_setopt (_curl, CURLOPT_WRITEFUNCTION, curlWriteToString);
    curl_easy_setopt (_curl, CURLOPT_WRITEDATA, &_got);
    if (_sendStr.size()) {
      curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendStr.size());
      curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromString);
      curl_easy_setopt (_curl, CURLOPT_READDATA, this);
    } else if (_sendGStr.size()) {
      curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendGStr.size());
      curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromGString);
      curl_easy_setopt (_curl, CURLOPT_READDATA, this);
    }
    curl_easy_setopt (_curl, CURLOPT_UPLOAD, 1L);  // cURL now needs this to actually send the email, cf. "http://curl.haxx.se/mail/lib-2013-12/0152.html".
    return *this;
  }

  /** Add SMTP recipient to the `_headers` (which are then set into `CURLOPT_MAIL_RCPT` by the `Curl::smtp`).
   * NB: Should be used *before* the `Curl::smtp`! */
  Curl& bcc (const char* to) {
    if (to) _headers = curl_slist_append (_headers, to);
    return *this;
  }

  /**
   Uses `CURLOPT_CUSTOMREQUEST` to set the http method.
   Can be used both before and after the `http` method.\n
   Example sending a POST request to ElasticSearch: \code
     glim::Curl curl;
     curl.send (C2GSTRING (R"({"query":{"match_all":{}},"facets":{"tags":{"terms":{"field":"tags","size":1000}}}})"));
     curl.method ("POST") .http ("http://127.0.0.1:9200/froples/frople/_search", 120);
     if (curl.verbose().go().status() != 200) GTHROW ("Error fetching tags: " + std::to_string (curl.status()) + ", " + curl.str());
     cout << curl.gstr() << endl;
   \endcode */
  Curl& method (const char* method) {
    curl_easy_setopt (_curl, CURLOPT_CUSTOMREQUEST, method);
    return *this;
  }

  /** Setup a handler to process the headers cURL gets from the response.
   * "The header callback will be called once for each header and only complete header lines are passed on to the callback".\n
   * See http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTHEADERFUNCTION */
  Curl& headerListener (std::function<void (const char* header, int len)> listener) {
    curl_easy_setopt (_curl, CURLOPT_HEADERFUNCTION, curlWriteHeader);
    curl_easy_setopt (_curl, CURLOPT_WRITEHEADER, this);
    _headerListener = listener;
    return *this;
  }

  /** Setup a handler to get the debug messages generated by cURL.
   * See http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION */
  Curl& debugListener (std::function<void (curl_infotype type, char* bytes, size_t size)> listener) {
    curl_easy_setopt (_curl, CURLOPT_DEBUGFUNCTION, curlDebugCB);
    curl_easy_setopt (_curl, CURLOPT_DEBUGDATA, this);
    _debugListener = listener;
    return verbose (true);
  }

  /**
   Setup a handler to get some of the debug messages generated by cURL.
   Listener gets a formatted text: outbound data is prepended with "> " and inbound with "< ".\n
   Usage example: \code
     auto curlDebug = std::make_shared<std::string>();
     curl->debugListenerF ([curlDebug](const char* bytes, size_t size) {curlDebug->append (bytes, size);});
     ...
     if (curl->status() != 200) std::cerr << "cURL status != 200; debug follows: " << *curlDebug << std::endl;
   \endcode
   See http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION
   @param listener The receiver of the debug information.
   @param data Whether to pass the data (`CURLINFO_DATA_IN`, `CURLINFO_DATA_OUT`) to the `listener`.
  */
  Curl& debugListenerF (std::function<void (const char* bytes, size_t size)> listener, bool data = false) {
    return debugListener ([listener/* = std::move (listener)*/,data] (curl_infotype type, char* bytes, size_t size) {
      GSTRING_ON_STACK (buf, 256);
      auto prepend = [&](const char* prefix) {
        buf << prefix; for (char *p = bytes, *end = bytes + size; p < end; ++p) {buf << *p; if (*p == '\n' && p + 2 < end) buf << prefix;}};
      if (type == CURLINFO_HEADER_IN || (type == CURLINFO_DATA_IN && data)) prepend ("< ");
      else if (type == CURLINFO_HEADER_OUT || (type == CURLINFO_DATA_OUT && data)) prepend ("> ");
      listener (buf.c_str(), buf.size());
    });
  }

  /// Whether to print debug information to `CURLOPT_STDERR`.
  /// Note that when `debugListener` is used, verbose output will go to the listener and not to `CURLOPT_STDERR`.
  Curl& verbose (bool on = true) {
    curl_easy_setopt (_curl, CURLOPT_VERBOSE, on ? 1L : 0L);
    return *this;
  }

  /// Reset the buffers and perform the cURL request.
  Curl& go() {
    _got.clear();
    *_errorBuf = 0;
    if (curl_easy_perform (_curl)) throw PerformError (_errorBuf, __FILE__, __LINE__);
    return *this;
  }

  /// The contents of the response.
  const std::string& str() const {return _got;}
  /// CString of `str`.
  const char* c_str() const {return _got.c_str();}
  /// Returns a gstring "view" into `str`.
  gstring gstr() const {return gstring (0, (void*) _got.data(), false, _got.size());}

  /// The status of the response (For HTTP it's 200 ok, 404 not found, 500 error, etc).
  long status() const {
    long status; CURLcode err = curl_easy_getinfo (_curl, CURLINFO_RESPONSE_CODE, &status);
    if (err) {
      GSTRING_ON_STACK (message, 128) << "CURL error " << (int) err << ": " << curl_easy_strerror (err);
      throw GetinfoError (err, message.str(), __FILE__, __LINE__);
    }
    return status;}
};

/** Moves the content to be sent into a `glim::gstring` inside `Curl`.
 * NB: In order to have an effect this method should be used *before* the `http()` and `smtp()` methods. */
template<> inline Curl& Curl::send<gstring> (gstring&& text) {
  _sendStr.clear();
  _sendGStr = std::move (text);
  _sent = 0;
  return *this;}

inline size_t curlReadFromString (void *ptr, size_t size, size_t nmemb, void *userdata) {
  Curl* curl = (Curl*) userdata;
  size_t len = std::min (curl->_sendStr.size() - curl->_sent, size * nmemb);
  if (len) memcpy (ptr, curl->_sendStr.data() + curl->_sent, len);
  curl->_sent += len;
  return len;}

inline size_t curlReadFromGString (void *ptr, size_t size, size_t nmemb, void *userdata) {
  Curl* curl = (Curl*) userdata;
  size_t len = std::min (curl->_sendGStr.size() - curl->_sent, size * nmemb);
  if (len) memcpy (ptr, curl->_sendGStr.data() + curl->_sent, len);
  curl->_sent += len;
  return len;}

// http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTHEADERFUNCTION
inline size_t curlWriteHeader (void *ptr, size_t size, size_t nmemb, void *curlPtr) {
  Curl* curl = (Curl*) curlPtr;
  std::function<void (const char* header, int len)>& listener = curl->_headerListener;
  int len = size * nmemb;
  if (listener) listener ((const char*) ptr, len);
  return (size_t) len;
}

// http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION
inline int curlDebugCB (CURL*, curl_infotype type, char* bytes, size_t size, void* curlPtr) {
  Curl* curl = (Curl*) curlPtr;
  auto& listener = curl->_debugListener;
  if (listener) listener (type, bytes, size);
  return 0;
}

/// Example: std::string w3 = glim::curl2str ("http://www.w3.org/");
inline std::string curl2str (const char* url, int timeoutSec = 20) {
  try {
    return glim::Curl().http (url, timeoutSec) .go().str();
  } catch (const std::exception&) {}
  return std::string();
}

}

#endif