1 #include "coeurl/request.hpp"
2 
3 #include "coeurl/client.hpp"
4 
5 // for TCP_MAXRT
6 #if __has_include(<winsock2.h>)
7 #include <winsock2.h>
8 #endif
9 
10 // for TCP_USER_TIMEOUT
11 #if __has_include(<netinet/tcp.h>)
12 #include <netinet/tcp.h>
13 #endif
14 
15 namespace coeurl {
16 /* CURLOPT_WRITEFUNCTION */
write_cb(void * ptr,size_t size,size_t nmemb,void * data)17 size_t Request::write_cb(void *ptr, size_t size, size_t nmemb, void *data) {
18     Request *request = (Request *)data;
19     Client::log->trace("Write: {} ({})", request->url_, nmemb);
20     request->response_.insert(request->response_.end(), (uint8_t *)ptr, (uint8_t *)ptr + nmemb);
21 
22     return size * nmemb;
23 }
24 
25 /* CURLOPT_WRITEFUNCTION */
read_cb(char * buffer,size_t size,size_t nitems,void * data)26 size_t Request::read_cb(char *buffer, size_t size, size_t nitems, void *data) {
27     Request *request = (Request *)data;
28 
29     size_t data_left = request->request_.size() - request->readoffset;
30 
31     auto data_to_copy = std::min(data_left, nitems * size);
32 
33     Client::log->trace("Read: {} ({})", request->url_, data_to_copy);
34 
35     if (data_to_copy) {
36         auto read_ptr = request->request_.data() + request->readoffset;
37         Client::log->trace("Copying: {}", std::string_view(read_ptr, data_to_copy));
38         std::copy(read_ptr, read_ptr + data_to_copy, buffer);
39         Client::log->trace("Copied: {}", std::string_view(buffer, data_to_copy));
40 
41         request->readoffset += data_to_copy;
42     }
43 
44     return data_to_copy;
45 }
46 
trim(std::string_view val)47 static std::string_view trim(std::string_view val) {
48     while (val.size() && isspace(val.front()))
49         val.remove_prefix(1);
50     while (val.size() && isspace(val.back()))
51         val.remove_suffix(1);
52 
53     return val;
54 }
55 
ascii_lower(char c)56 static char ascii_lower(char c) {
57     if (c >= 'A' && c <= 'Z')
58         c |= 0b00100000;
59     return c;
60 }
61 
operator ()(const std::string & a,const std::string & b) const62 bool header_less::operator()(const std::string &a, const std::string &b) const {
63     if (a.size() != b.size())
64         return a.size() < b.size();
65 
66     for (size_t i = 0; i < a.size(); i++) {
67         auto a_c = ascii_lower(a[i]);
68         auto b_c = ascii_lower(b[i]);
69 
70         if (a_c != b_c)
71             return a_c < b_c;
72     }
73 
74     return false;
75 }
76 
77 /* CURLOPT_HEADERFUNCTION */
header_cb(char * buffer,size_t,size_t nitems,void * data)78 size_t Request::header_cb(char *buffer, size_t, size_t nitems, void *data) {
79     Request *request = (Request *)data;
80     std::string_view header(buffer, nitems);
81 
82     if (auto pos = header.find(':'); pos != std::string_view::npos) {
83         auto key = trim(header.substr(0, pos));
84         auto val = trim(header.substr(pos + 1));
85 
86         Client::log->debug("Header: {} ({}: {})", request->url_, key, val);
87 
88         request->response_headers_.insert({std::string(key), std::string(val)});
89     }
90 
91     return nitems;
92 }
93 
94 /* CURLOPT_PROGRESSFUNCTION */
prog_cb(void * p,curl_off_t dltotal,curl_off_t dlnow,curl_off_t ult,curl_off_t uln)95 int Request::prog_cb(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ult, curl_off_t uln) {
96     Request *r = (Request *)p;
97     (void)ult;
98     (void)uln;
99 
100     if (r->on_upload_progress_)
101         r->on_upload_progress_(uln, ult);
102     if (r->on_download_progess_)
103         r->on_download_progess_(dlnow, dltotal);
104 
105     Client::log->trace("Progress: {} ({}/{}):({}/{})", r->url_, uln, ult, dlnow, dltotal);
106     return 0;
107 }
108 
Request(Client * client,Method m,std::string url__)109 Request::Request(Client *client, Method m, std::string url__) : url_(std::move(url__)), global(client), method(m) {
110     this->easy = curl_easy_init();
111     if (!this->easy) {
112         Client::log->critical("curl_easy_init() failed, exiting!");
113         throw std::bad_alloc();
114     }
115 
116     curl_easy_setopt(this->easy, CURLOPT_URL, this->url_.c_str());
117     curl_easy_setopt(this->easy, CURLOPT_WRITEFUNCTION, write_cb);
118     curl_easy_setopt(this->easy, CURLOPT_WRITEDATA, this);
119     curl_easy_setopt(this->easy, CURLOPT_HEADERFUNCTION, header_cb);
120     curl_easy_setopt(this->easy, CURLOPT_HEADERDATA, this);
121     // curl_easy_setopt(this->easy, CURLOPT_VERBOSE, 1L);
122     curl_easy_setopt(this->easy, CURLOPT_ERRORBUFFER, this->error);
123     curl_easy_setopt(this->easy, CURLOPT_PRIVATE, this);
124     curl_easy_setopt(this->easy, CURLOPT_NOPROGRESS, 1L);
125     curl_easy_setopt(this->easy, CURLOPT_XFERINFOFUNCTION, prog_cb);
126     curl_easy_setopt(this->easy, CURLOPT_XFERINFODATA, this);
127     curl_easy_setopt(this->easy, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
128     // default to all supported encodings
129     curl_easy_setopt(this->easy, CURLOPT_ACCEPT_ENCODING, "");
130 
131     switch (m) {
132     case Method::Delete:
133         curl_easy_setopt(this->easy, CURLOPT_HTTPGET, 0L);
134         curl_easy_setopt(this->easy, CURLOPT_CUSTOMREQUEST, "DELETE");
135         break;
136     case Method::Get:
137         curl_easy_setopt(this->easy, CURLOPT_HTTPGET, 1L);
138         break;
139     case Method::Head:
140         curl_easy_setopt(this->easy, CURLOPT_NOBODY, 1L);
141         break;
142     case Method::Options:
143         curl_easy_setopt(this->easy, CURLOPT_CUSTOMREQUEST, "OPTIONS");
144         break;
145     case Method::Patch:
146         curl_easy_setopt(this->easy, CURLOPT_CUSTOMREQUEST, "PATCH");
147         break;
148     case Method::Post:
149         curl_easy_setopt(this->easy, CURLOPT_POST, 1L);
150         request("");
151         break;
152     case Method::Put:
153         curl_easy_setopt(this->easy, CURLOPT_CUSTOMREQUEST, "PUT");
154         request("");
155         break;
156     }
157 
158     verify_peer(this->global->does_verify_peer());
159 }
160 
~Request()161 Request::~Request() {
162     curl_easy_cleanup(this->easy);
163     curl_slist_free_all(request_headers_);
164 }
165 
max_redirects(long amount)166 Request &Request::max_redirects(long amount) {
167     curl_easy_setopt(this->easy, CURLOPT_FOLLOWLOCATION, 1L);
168     curl_easy_setopt(this->easy, CURLOPT_MAXREDIRS, amount);
169     return *this;
170 }
171 
verify_peer(bool verify)172 Request &Request::verify_peer(bool verify) {
173     curl_easy_setopt(this->easy, CURLOPT_SSL_VERIFYPEER, verify ? 1L : 0L);
174     return *this;
175 }
176 
request(std::string r,std::string contenttype)177 Request &Request::request(std::string r, std::string contenttype) {
178     this->request_ = std::move(r);
179     this->request_contenttype_ = std::move(contenttype);
180 
181     curl_easy_setopt(this->easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(request_.size()));
182     curl_easy_setopt(this->easy, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(request_.size()));
183     curl_easy_setopt(this->easy, CURLOPT_READDATA, this);
184     curl_easy_setopt(this->easy, CURLOPT_READFUNCTION, read_cb);
185     curl_easy_setopt(this->easy, CURLOPT_POSTFIELDS, nullptr);
186     return *this;
187 }
188 
request_headers(const Headers & h)189 Request &Request::request_headers(const Headers &h) {
190     if (request_headers_)
191         curl_slist_free_all(request_headers_);
192 
193     for (const auto &[k, v] : h) {
194         request_headers_ = curl_slist_append(request_headers_, (k + ": " + v).c_str());
195     }
196 
197     if (!request_contenttype_.empty())
198         request_headers_ = curl_slist_append(request_headers_, ("content-type: " + request_contenttype_).c_str());
199 
200     curl_easy_setopt(this->easy, CURLOPT_HTTPHEADER, request_headers_);
201     return *this;
202 }
203 
connection_timeout(long t)204 Request &Request::connection_timeout(long t) {
205     // only allow values that are > 0, if they are divided by 3
206     if (t <= 2)
207         return *this;
208 
209     // enable keepalive
210     curl_easy_setopt(this->easy, CURLOPT_TCP_KEEPALIVE, 1L);
211 
212     // send keepalives every third of the timeout interval. This allows for
213     // retransmission of 2 keepalive while still giving the server a third of the
214     // time to reply
215     curl_easy_setopt(this->easy, CURLOPT_TCP_KEEPIDLE, t / 3);
216     curl_easy_setopt(this->easy, CURLOPT_TCP_KEEPINTVL, t / 3);
217 
218     this->connection_timeout_ = t;
219 #ifdef TCP_USER_TIMEOUT
220     // The + is needed to convert this to a function pointer!
221     curl_easy_setopt(
222         this->easy, CURLOPT_SOCKOPTFUNCTION, +[](void *clientp, curl_socket_t curlfd, curlsocktype) -> int {
223             unsigned int val = static_cast<Request *>(clientp)->connection_timeout_ * 1000 /*ms*/;
224             setsockopt(curlfd, SOL_TCP, TCP_USER_TIMEOUT, (const char *)&val, sizeof(val));
225             return CURLE_OK;
226         });
227 #elif defined(TCP_MAXRT)
228     // The + is needed to convert this to a function pointer!
229     curl_easy_setopt(
230         this->easy, CURLOPT_SOCKOPTFUNCTION, +[](void *clientp, curl_socket_t sock, curlsocktype) -> int {
231             unsigned int maxrt = static_cast<Request *>(clientp)->connection_timeout_ /*s*/;
232             setsockopt(sock, IPPROTO_TCP, TCP_MAXRT, (const char *)&maxrt, sizeof(maxrt));
233             return CURLE_OK;
234         });
235 #endif
236     curl_easy_setopt(this->easy, CURLOPT_SOCKOPTDATA, this);
237 
238     return *this;
239 }
240 
on_complete(std::function<void (const Request &)> handler)241 Request &Request::on_complete(std::function<void(const Request &)> handler) {
242     on_complete_ = handler;
243     return *this;
244 }
245 
on_upload_progress(std::function<void (size_t progress,size_t total)> handler)246 Request &Request::on_upload_progress(std::function<void(size_t progress, size_t total)> handler) {
247     on_upload_progress_ = handler;
248     curl_easy_setopt(this->easy, CURLOPT_NOPROGRESS, 0L);
249     return *this;
250 }
on_download_progess(std::function<void (size_t progress,size_t total)> handler)251 Request &Request::on_download_progess(std::function<void(size_t progress, size_t total)> handler) {
252     on_download_progess_ = handler;
253     curl_easy_setopt(this->easy, CURLOPT_NOPROGRESS, 0L);
254     return *this;
255 }
256 
response_code() const257 int Request::response_code() const {
258     long http_code;
259     curl_easy_getinfo(const_cast<CURL *>(this->easy), CURLINFO_RESPONSE_CODE, &http_code);
260     return static_cast<int>(http_code);
261 }
262 } // namespace coeurl
263