OpenClonk
C4HTTP.cpp
Go to the documentation of this file.
1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2017, The OpenClonk Team and contributors
5  *
6  * Distributed under the terms of the ISC license; see accompanying file
7  * "COPYING" for details.
8  *
9  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
10  * See accompanying file "TRADEMARK" for details.
11  *
12  * To redistribute this file separately, substitute the full license texts
13  * for the above references.
14  */
15 #include "C4Include.h"
16 #include "network/C4HTTP.h"
18 #include "C4Version.h"
19 
20 #include <regex>
21 
22 // Enable to have curl print out all HTTP requests and responses to the console.
23 //#define C4HTTP_DEBUG
24 
25 // *** C4HTTPClient
26 
28 {
29 }
30 
32 {
33  Cancel(nullptr);
34  if (MultiHandle)
35  curl_multi_cleanup(MultiHandle);
36 #ifdef STDSCHEDULER_USE_EVENTS
37  if (Event != nullptr)
38  WSACloseEvent(Event);
39 #endif
40 }
41 
43 {
44  MultiHandle = curl_multi_init();
45  if (!MultiHandle) return false;
46 #ifdef STDSCHEDULER_USE_EVENTS
47  if ((Event = WSACreateEvent()) == WSA_INVALID_EVENT)
48  {
49  SetError("could not create socket event");
50  curl_multi_cleanup(MultiHandle);
51  return false;
52  }
53 #endif
54  curl_multi_setopt(MultiHandle, CURLMOPT_SOCKETFUNCTION, &C4HTTPClient::SSocketCallback);
55  curl_multi_setopt(MultiHandle, CURLMOPT_SOCKETDATA, this);
56  return true;
57 }
58 
59 bool C4HTTPClient::Execute(int iMaxTime, pollfd *readyfd)
60 {
61  int running;
62 #ifdef STDSCHEDULER_USE_EVENTS
63  // On Windows, StdScheduler doesn't inform us about which fd triggered the
64  // event, so we have to check manually.
65  if (WaitForSingleObject(Event, 0) == WAIT_OBJECT_0)
66  {
67  // sockets might be modified during the call to curl_multi_socket_action() below.
68  auto sockets_tmp = sockets;
69  for (const auto& kv : sockets_tmp)
70  {
71  auto socket = kv.first;
72  WSANETWORKEVENTS NetworkEvents;
73  if (WSAEnumNetworkEvents(socket, Event, &NetworkEvents) != SOCKET_ERROR)
74  {
75  int ev_bitmask = 0;
76  if (NetworkEvents.lNetworkEvents & (FD_READ|FD_ACCEPT|FD_CLOSE)) ev_bitmask |= CURL_CSELECT_IN;
77  if (NetworkEvents.lNetworkEvents & (FD_WRITE|FD_CONNECT)) ev_bitmask |= CURL_CSELECT_OUT;
78  curl_multi_socket_action(MultiHandle, socket, ev_bitmask, &running);
79  }
80  }
81  }
82 #else
83  if (readyfd)
84  {
85  int ev_bitmask = 0;
86  if (readyfd->revents & POLLIN) ev_bitmask |= CURL_CSELECT_IN;
87  if (readyfd->revents & POLLOUT) ev_bitmask |= CURL_CSELECT_OUT;
88  if (readyfd->revents & POLLERR) ev_bitmask |= CURL_CSELECT_ERR;
89  curl_multi_socket_action(MultiHandle, readyfd->fd, ev_bitmask, &running);
90  }
91 #endif
92  else
93  {
94  curl_multi_socket_action(MultiHandle, CURL_SOCKET_TIMEOUT, 0, &running);
95  }
96 
97  CURLMsg *m;
98  do {
99  int msgq = 0;
100  m = curl_multi_info_read(MultiHandle, &msgq);
101  if(m && (m->msg == CURLMSG_DONE)) {
102  CURL *e = m->easy_handle;
103  assert(e == CurlHandle);
104  CurlHandle = nullptr;
105  curl_multi_remove_handle(MultiHandle, e);
106 
107  // Check for errors and notify listeners. Note that curl fills
108  // the Error buffer automatically.
109  fSuccess = m->data.result == CURLE_OK;
110  if (!fSuccess && !*Error.getData())
111  Error.Copy(curl_easy_strerror(m->data.result));
112  char *ip;
113  curl_easy_getinfo(e, CURLINFO_PRIMARY_IP, &ip);
114  ServerAddr.SetHost(StdStrBuf(ip));
115  if (pNotify)
116  pNotify->PushEvent(Ev_HTTP_Response, this);
117 
118  curl_easy_cleanup(e);
119  }
120  } while(m);
121 
122  return true;
123 }
124 
126 {
127  long timeout;
128  curl_multi_timeout(MultiHandle, &timeout);
129  if (timeout < 0)
130  timeout = 1000;
131  return tNow + timeout;
132 }
133 
134 #ifndef STDSCHEDULER_USE_EVENTS
135 void C4HTTPClient::GetFDs(std::vector<pollfd> &pollfds)
136 {
137  for (const auto& kv : sockets)
138  {
139  pollfd pfd;
140  pfd.fd = kv.first;
141  pfd.revents = 0;
142  switch (kv.second)
143  {
144  case CURL_POLL_IN:
145  pfd.events = POLLIN; break;
146  case CURL_POLL_OUT:
147  pfd.events = POLLOUT; break;
148  case CURL_POLL_INOUT:
149  pfd.events = POLLIN | POLLOUT; break;
150  default:
151  pfd.events = 0;
152  }
153  pollfds.push_back(pfd);
154  }
155 }
156 #endif
157 
158 bool C4HTTPClient::Query(const StdBuf &Data, bool fBinary)
159 {
160  if (URL.isNull()) return false;
161  // Cancel previous request
162  if (CurlHandle)
163  Cancel("Cancelled");
164  // No result known yet
166  // store mode
167  this->fBinary = fBinary;
168  // Create request
169  CURL *curl = curl_easy_init();
170  if (!curl) return false;
171 #ifdef C4HTTP_DEBUG
172  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
173 #endif
174  curl_easy_setopt(curl, CURLOPT_URL, URL.getData());
175  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
176  curl_easy_setopt(curl, CURLOPT_USERAGENT, C4ENGINENAME "/" C4VERSION );
177  curl_easy_setopt(curl, CURLOPT_TIMEOUT, C4HTTPQueryTimeout);
178  curl_slist *headers = nullptr;
179  headers = curl_slist_append(headers, "Accept-Charset: utf-8");
180  headers = curl_slist_append(headers, FormatString("Accept-Language: %s", Config.General.LanguageEx).getData());
181  if (!headerAcceptedResponseType.empty())
182  headers = curl_slist_append(headers, headerAcceptedResponseType.c_str());
183 
184  if (Data.getSize())
185  {
186  RequestData = Data;
187  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, RequestData.getData());
188  curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, RequestData.getSize());
189  // Disable the Expect: 100-Continue header which curl automatically
190  // adds for POST requests.
191  headers = curl_slist_append(headers, "Expect:");
192  headers = curl_slist_append(headers, "Content-Type: text/plain; charset=utf-8");
193  }
194 
195  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
196  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &C4HTTPClient::SWriteCallback);
197  curl_easy_setopt(curl, CURLOPT_WRITEDATA, this);
198  curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &C4HTTPClient::SProgressCallback);
199  curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
200  Error.Clear();
201  Error.SetLength(CURL_ERROR_SIZE);
202  *Error.getMData() = '\0';
203  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, Error.getMData());
204 
205  curl_multi_add_handle(MultiHandle, curl);
206  CurlHandle = curl;
207  iDownloadedSize = iTotalSize = 0;
208 
209  int running;
210  curl_multi_socket_action(MultiHandle, CURL_SOCKET_TIMEOUT, 0, &running);
211 
212  return true;
213 }
214 
215 size_t C4HTTPClient::SWriteCallback(char *ptr, size_t size, size_t nmemb, void *userdata)
216 {
217  C4HTTPClient *client = reinterpret_cast<C4HTTPClient*>(userdata);
218  return client->WriteCallback(ptr, size * nmemb);
219 }
220 
221 size_t C4HTTPClient::WriteCallback(char *ptr, size_t realsize)
222 {
223  if (fBinary)
224  ResultBin.Append(ptr, realsize);
225  else
226  ResultString.Append(ptr, realsize);
227  return realsize;
228 }
229 
230 int C4HTTPClient::SProgressCallback(void *clientp, int64_t dltotal, int64_t dlnow, int64_t ultotal, int64_t ulnow)
231 {
232  C4HTTPClient *client = reinterpret_cast<C4HTTPClient*>(clientp);
233  return client->ProgressCallback(dltotal, dlnow, ultotal, ulnow);
234 }
235 
236 int C4HTTPClient::ProgressCallback(int64_t dltotal, int64_t dlnow, int64_t ultotal, int64_t ulnow)
237 {
238  iDownloadedSize = dlnow;
239  iTotalSize = dltotal;
240  return 0;
241 }
242 
243 int C4HTTPClient::SSocketCallback(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp)
244 {
245  C4HTTPClient *client = reinterpret_cast<C4HTTPClient*>(userp);
246  return client->SocketCallback(easy, s, what, socketp);
247 }
248 
249 int C4HTTPClient::SocketCallback(CURL *easy, curl_socket_t s, int what, void *socketp)
250 {
251 #ifdef STDSCHEDULER_USE_EVENTS
252  long NetworkEvents;
253  switch (what)
254  {
255  case CURL_POLL_IN:
256  NetworkEvents = FD_READ | FD_ACCEPT | FD_CLOSE; break;
257  case CURL_POLL_OUT:
258  NetworkEvents = FD_WRITE | FD_CONNECT; break;
259  case CURL_POLL_INOUT:
260  NetworkEvents = FD_READ | FD_ACCEPT | FD_CLOSE | FD_WRITE | FD_CONNECT; break;
261  default:
262  NetworkEvents = 0;
263  }
264  if (WSAEventSelect(s, Event, NetworkEvents) == SOCKET_ERROR)
265  {
266  SetError("could not set event");
267  return 1;
268  }
269 #endif
270  if (what == CURL_POLL_REMOVE)
271  sockets.erase(s);
272  else
273  sockets[s] = what;
274  return 0;
275 }
276 
277 void C4HTTPClient::Cancel(const char *szReason)
278 {
279  if (CurlHandle)
280  {
281  curl_multi_remove_handle(MultiHandle, CurlHandle);
282  curl_easy_cleanup(CurlHandle);
283  CurlHandle = nullptr;
284  }
285  fBinary = false;
286  iDownloadedSize = iTotalSize = 0;
287  Error = szReason;
288 }
289 
291 {
292  Cancel(nullptr);
293  ServerAddr.Clear();
294  ResultBin.Clear();
296 }
297 
298 bool C4HTTPClient::SetServer(const char *szServerAddress)
299 {
300  static std::regex HostnameRegex(R"(^(:?[a-z]+:\/\/)?([^/:]+).*)", std::regex::icase);
301  std::cmatch match;
302  if (std::regex_match(szServerAddress, match, HostnameRegex))
303  {
304  // CURL validates URLs only on connect.
305  URL.Copy(szServerAddress);
306  ServerName.Copy(match[2].str().c_str());
307  return true;
308  }
309  // The HostnameRegex above is pretty stupid, so we will reject only very
310  // malformed URLs immediately.
311  SetError("Malformed URL");
312  return false;
313 }
314 
316 {
317  switch (type)
318  {
319  case C4HTTPClient::ResponseType::XML:
320  headerAcceptedResponseType = "Accept: application/xml";
321  break;
322  case C4HTTPClient::ResponseType::NoPreference: // fallthrough
323  default:
324  headerAcceptedResponseType = "";
325  break;
326  };
327 }
328 
#define s
C4Config Config
Definition: C4Config.cpp:930
const int C4HTTPQueryTimeout
Definition: C4HTTP.h:30
@ Ev_HTTP_Response
#define SOCKET_ERROR
Definition: C4NetIO.cpp:47
StdStrBuf FormatString(const char *szFmt,...)
Definition: StdBuf.cpp:270
char LanguageEx[CFG_MaxString+1]
Definition: C4Config.h:38
C4ConfigGeneral General
Definition: C4Config.h:255
StdCopyStrBuf Error
Definition: C4HTTP.h:79
void SetError(const char *strnError)
Definition: C4HTTP.h:80
void SetExpectedResponseType(ResponseType type)
Definition: C4HTTP.cpp:315
bool Execute(int iMaxTime=-1, pollfd *readyfds=nullptr) override
Definition: C4HTTP.cpp:59
bool Init()
Definition: C4HTTP.cpp:42
bool SetServer(const char *szServerAddress)
Definition: C4HTTP.cpp:298
bool Query(const StdBuf &Data, bool fBinary)
Definition: C4HTTP.cpp:158
void GetFDs(std::vector< struct pollfd > &) override
Definition: C4HTTP.cpp:135
~C4HTTPClient() override
Definition: C4HTTP.cpp:31
void Clear()
Definition: C4HTTP.cpp:290
C4TimeMilliseconds GetNextTick(C4TimeMilliseconds tNow) override
Definition: C4HTTP.cpp:125
StdCopyStrBuf ResultString
Definition: C4HTTP.h:78
StdCopyBuf ResultBin
Definition: C4HTTP.h:77
void Cancel(const char *szReason)
Definition: C4HTTP.cpp:277
C4HTTPClient()
Definition: C4HTTP.cpp:27
bool PushEvent(C4InteractiveEventType eEventType, void *pData=nullptr)
Definition: StdBuf.h:30
size_t getSize() const
Definition: StdBuf.h:101
const void * getData() const
Definition: StdBuf.h:99
void Clear()
Definition: StdBuf.h:190
void Append(const void *pnData, size_t inSize)
Definition: StdBuf.h:254
void SetLength(size_t iLength)
Definition: StdBuf.h:509
const char * getData() const
Definition: StdBuf.h:442
char * getMData()
Definition: StdBuf.h:443
bool isNull() const
Definition: StdBuf.h:441
void Copy()
Definition: StdBuf.h:467
void Append(const char *pnData, size_t iChars)
Definition: StdBuf.h:519
void Clear()
Definition: StdBuf.h:466
void SetHost(const sockaddr *addr)
Definition: C4NetIO.cpp:346