PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/code/Modules/HTTP/curl/curlURLLoader.cc

https://gitlab.com/villain/oryol
C++ | 245 lines | 177 code | 31 blank | 37 comment | 36 complexity | ceac8db326e9e496de204ae934922ca6 MD5 | raw file
  1. //------------------------------------------------------------------------------
  2. // curlURLLoader.cc
  3. //------------------------------------------------------------------------------
  4. #include "Pre.h"
  5. #include "curlURLLoader.h"
  6. #include "Core/String/StringConverter.h"
  7. #include "Core/Containers/Buffer.h"
  8. #include "curl/curl.h"
  9. #if LIBCURL_VERSION_NUM != 0x072400
  10. #error "Not using the right curl version, header search path fuckup?"
  11. #endif
  12. namespace Oryol {
  13. namespace _priv {
  14. bool curlURLLoader::curlInitCalled = false;
  15. std::mutex curlURLLoader::curlInitMutex;
  16. //------------------------------------------------------------------------------
  17. curlURLLoader::curlURLLoader() :
  18. contentTypeString("Content-Type"),
  19. curlSession(0),
  20. curlError(0) {
  21. // we need to do some one-time curl initialization here,
  22. // thread-protected because curl_global_init() is not thread-safe
  23. curlInitMutex.lock();
  24. if (!curlInitCalled) {
  25. CURLcode curlInitRes = curl_global_init(CURL_GLOBAL_ALL);
  26. o_assert(0 == curlInitRes);
  27. curlInitCalled = true;
  28. }
  29. curlInitMutex.unlock();
  30. // setup a new curl session
  31. this->setupCurlSession();
  32. }
  33. //------------------------------------------------------------------------------
  34. curlURLLoader::~curlURLLoader() {
  35. this->discardCurlSession();
  36. }
  37. //------------------------------------------------------------------------------
  38. void
  39. curlURLLoader::setupCurlSession() {
  40. o_assert(0 == this->curlError);
  41. o_assert(0 == this->curlSession);
  42. // setup the error buffer
  43. const int32 curlErrorBufferSize = CURL_ERROR_SIZE * 4;
  44. this->curlError = (char*) Memory::Alloc(curlErrorBufferSize);
  45. Memory::Clear(this->curlError, curlErrorBufferSize);
  46. // setup the curl session
  47. this->curlSession = curl_easy_init();
  48. o_assert(0 != this->curlSession);
  49. // set session options
  50. curl_easy_setopt(this->curlSession, CURLOPT_NOSIGNAL, 1L);
  51. curl_easy_setopt(this->curlSession, CURLOPT_NOPROGRESS, 1L);
  52. curl_easy_setopt(this->curlSession, CURLOPT_ERRORBUFFER, this->curlError);
  53. curl_easy_setopt(this->curlSession, CURLOPT_WRITEFUNCTION, curlWriteDataCallback);
  54. curl_easy_setopt(this->curlSession, CURLOPT_HEADERFUNCTION, curlHeaderCallback);
  55. curl_easy_setopt(this->curlSession, CURLOPT_WRITEHEADER, this);
  56. curl_easy_setopt(this->curlSession, CURLOPT_TCP_KEEPALIVE, 1L);
  57. curl_easy_setopt(this->curlSession, CURLOPT_TCP_KEEPIDLE, 10L);
  58. curl_easy_setopt(this->curlSession, CURLOPT_TCP_KEEPINTVL, 10L);
  59. curl_easy_setopt(this->curlSession, CURLOPT_TIMEOUT, 30);
  60. curl_easy_setopt(this->curlSession, CURLOPT_CONNECTTIMEOUT, 30);
  61. curl_easy_setopt(this->curlSession, CURLOPT_ACCEPT_ENCODING, ""); // all encodings supported by curl
  62. curl_easy_setopt(this->curlSession, CURLOPT_FOLLOWLOCATION, 1);
  63. }
  64. //------------------------------------------------------------------------------
  65. void
  66. curlURLLoader::discardCurlSession() {
  67. o_assert(0 != this->curlError);
  68. o_assert(0 != this->curlSession);
  69. curl_easy_cleanup(this->curlSession);
  70. this->curlSession = 0;
  71. Memory::Free(this->curlError);
  72. this->curlError = 0;
  73. }
  74. //------------------------------------------------------------------------------
  75. size_t
  76. curlURLLoader::curlWriteDataCallback(char* ptr, size_t size, size_t nmemb, void* userData) {
  77. // userData is expected to point to a Buffer object
  78. int32 bytesToWrite = (int32) (size * nmemb);
  79. if (bytesToWrite > 0) {
  80. Buffer* buf = (Buffer*) userData;
  81. buf->Add((const uint8*)ptr, bytesToWrite);
  82. return bytesToWrite;
  83. }
  84. else {
  85. return 0;
  86. }
  87. }
  88. //------------------------------------------------------------------------------
  89. size_t
  90. curlURLLoader::curlHeaderCallback(char* ptr, size_t size, size_t nmemb, void* userData) {
  91. // userData is expected to point to the curlURLLoader object
  92. curlURLLoader* self = (curlURLLoader*) userData;
  93. int32 receivedBytes = (int32) (size * nmemb);
  94. if (receivedBytes > 0) {
  95. self->stringBuilder.Set(ptr, 0, receivedBytes);
  96. int32 colonIndex = self->stringBuilder.FindFirstOf(0, receivedBytes, ":");
  97. if (InvalidIndex != colonIndex) {
  98. String key = self->stringBuilder.GetSubString(0, colonIndex);
  99. int32 endOfValueIndex = self->stringBuilder.FindFirstOf(colonIndex, EndOfString, "\r\n");
  100. String value = self->stringBuilder.GetSubString(colonIndex + 2, endOfValueIndex);
  101. self->responseHeaders.Add(key, value);
  102. }
  103. return receivedBytes;
  104. }
  105. else {
  106. return 0;
  107. }
  108. }
  109. //------------------------------------------------------------------------------
  110. bool
  111. curlURLLoader::doRequest(const Ptr<HTTPProtocol::HTTPRequest>& req) {
  112. if (baseURLLoader::doRequest(req)) {
  113. this->doRequestInternal(req);
  114. const Ptr<IOProtocol::Request>& ioReq = req->IoRequest;
  115. if (ioReq) {
  116. const Ptr<HTTPProtocol::HTTPResponse>& httpResponse = req->Response;
  117. ioReq->Status = httpResponse->Status;
  118. ioReq->Data = std::move(httpResponse->Body);
  119. ioReq->Type = httpResponse->Type;
  120. ioReq->ErrorDesc = httpResponse->ErrorDesc;
  121. ioReq->SetHandled();
  122. }
  123. req->SetHandled();
  124. return true;
  125. }
  126. else {
  127. // request was cancelled
  128. return false;
  129. }
  130. }
  131. //------------------------------------------------------------------------------
  132. void
  133. curlURLLoader::doRequestInternal(const Ptr<HTTPProtocol::HTTPRequest>& req) {
  134. o_assert(0 != this->curlSession);
  135. o_assert(0 != this->curlError);
  136. this->stringBuilder.Clear();
  137. this->responseHeaders.Clear();
  138. // set URL in curl
  139. const URL& url = req->Url;
  140. o_assert(url.Scheme() == "http");
  141. curl_easy_setopt(this->curlSession, CURLOPT_URL, url.AsCStr());
  142. if (url.HasPort()) {
  143. uint16 port = StringConverter::FromString<uint16>(url.Port());
  144. curl_easy_setopt(this->curlSession, CURLOPT_PORT, port);
  145. }
  146. // set the HTTP method
  147. /// @todo: only HTTP GET and POST supported for now
  148. switch (req->Method) {
  149. case HTTPMethod::Get: curl_easy_setopt(this->curlSession, CURLOPT_HTTPGET, 1); break;
  150. case HTTPMethod::Post: curl_easy_setopt(this->curlSession, CURLOPT_POST, 1); break;
  151. default: o_error("curlURLLoader: unsupported HTTP method '%s'\n", HTTPMethod::ToString(req->Method)); break;
  152. }
  153. // setup request header fields
  154. struct curl_slist* requestHeaders = 0;
  155. for (const auto& kvp : req->RequestHeaders) {
  156. this->stringBuilder.Set(kvp.Key());
  157. this->stringBuilder.Append(": ");
  158. this->stringBuilder.Append(kvp.Value());
  159. requestHeaders = curl_slist_append(requestHeaders, this->stringBuilder.AsCStr());
  160. }
  161. // if this is a POST, set the data to post
  162. if (req->Method == HTTPMethod::Post) {
  163. const int32 postDataSize = req->Body.Size(); // can be 0
  164. const uint8* postData = req->Body.Empty() ? nullptr : req->Body.Data();
  165. curl_easy_setopt(this->curlSession, CURLOPT_POSTFIELDS, postData);
  166. curl_easy_setopt(this->curlSession, CURLOPT_POSTFIELDSIZE, postDataSize);
  167. // add a Content-Type request header if the post-stream has a content-type set
  168. if (req->Type.IsValid()) {
  169. this->stringBuilder.Set("Content-Type: ");
  170. this->stringBuilder.Append(req->Type.AsCStr());
  171. requestHeaders = curl_slist_append(requestHeaders, this->stringBuilder.AsCStr());
  172. }
  173. }
  174. // set the http request headers
  175. if (0 != requestHeaders) {
  176. curl_easy_setopt(this->curlSession, CURLOPT_HTTPHEADER, requestHeaders);
  177. }
  178. // prepare the HTTPResponse and the response-body stream
  179. Ptr<HTTPProtocol::HTTPResponse> httpResponse = HTTPProtocol::HTTPResponse::Create();
  180. curl_easy_setopt(this->curlSession, CURLOPT_WRITEDATA, &(httpResponse->Body));
  181. // perform the request
  182. CURLcode performResult = curl_easy_perform(this->curlSession);
  183. // query the http code
  184. long curlHttpCode = 0;
  185. curl_easy_getinfo(this->curlSession, CURLINFO_RESPONSE_CODE, &curlHttpCode);
  186. httpResponse->Status = (IOStatus::Code) curlHttpCode;
  187. // check for error codes
  188. if (CURLE_PARTIAL_FILE == performResult) {
  189. // this seems to happen quite often even though all data has been received,
  190. // not sure what to do about this, but don't treat it as an error
  191. Log::Warn("curlURLLoader: CURLE_PARTIAL_FILE received for '%s', httpStatus='%ld'\n", req->Url.AsCStr(), curlHttpCode);
  192. httpResponse->ErrorDesc = this->curlError;
  193. }
  194. else if (0 != performResult) {
  195. // some other curl error
  196. Log::Warn("curlURLLoader: curl_easy_peform failed with '%s' for '%s', httpStatus='%ld'\n",
  197. req->Url.AsCStr(), this->curlError, curlHttpCode);
  198. httpResponse->ErrorDesc = this->curlError;
  199. }
  200. // check if the responseHeaders contained a Content-Type, if yes, set it on the responseBodyStream
  201. if (this->responseHeaders.Contains(this->contentTypeString)) {
  202. httpResponse->Type = this->responseHeaders[this->contentTypeString];
  203. }
  204. // close the responseBodyStream, and set the result
  205. httpResponse->ResponseHeaders = this->responseHeaders;
  206. req->Response = httpResponse;
  207. // free the previously allocated request headers
  208. if (0 != requestHeaders) {
  209. curl_slist_free_all(requestHeaders);
  210. requestHeaders = 0;
  211. }
  212. }
  213. } // namespace _priv
  214. } // namespace Oryol