http_downloader_curl.cpp (5932B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "http_downloader_curl.h" 5 6 #include "common/assert.h" 7 #include "common/log.h" 8 #include "common/string_util.h" 9 #include "common/timer.h" 10 11 #include <algorithm> 12 #include <functional> 13 #include <pthread.h> 14 #include <signal.h> 15 16 Log_SetChannel(HTTPDownloader); 17 18 HTTPDownloaderCurl::HTTPDownloaderCurl() : HTTPDownloader() 19 { 20 } 21 22 HTTPDownloaderCurl::~HTTPDownloaderCurl() 23 { 24 if (m_multi_handle) 25 curl_multi_cleanup(m_multi_handle); 26 } 27 28 std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(std::string user_agent) 29 { 30 std::unique_ptr<HTTPDownloaderCurl> instance(std::make_unique<HTTPDownloaderCurl>()); 31 if (!instance->Initialize(std::move(user_agent))) 32 return {}; 33 34 return instance; 35 } 36 37 static bool s_curl_initialized = false; 38 static std::once_flag s_curl_initialized_once_flag; 39 40 bool HTTPDownloaderCurl::Initialize(std::string user_agent) 41 { 42 if (!s_curl_initialized) 43 { 44 std::call_once(s_curl_initialized_once_flag, []() { 45 s_curl_initialized = curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK; 46 if (s_curl_initialized) 47 { 48 std::atexit([]() { 49 curl_global_cleanup(); 50 s_curl_initialized = false; 51 }); 52 } 53 }); 54 if (!s_curl_initialized) 55 { 56 ERROR_LOG("curl_global_init() failed"); 57 return false; 58 } 59 } 60 61 m_multi_handle = curl_multi_init(); 62 if (!m_multi_handle) 63 { 64 ERROR_LOG("curl_multi_init() failed"); 65 return false; 66 } 67 68 m_user_agent = std::move(user_agent); 69 return true; 70 } 71 72 size_t HTTPDownloaderCurl::WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) 73 { 74 Request* req = static_cast<Request*>(userdata); 75 const size_t current_size = req->data.size(); 76 const size_t transfer_size = size * nmemb; 77 const size_t new_size = current_size + transfer_size; 78 req->data.resize(new_size); 79 req->start_time = Common::Timer::GetCurrentValue(); 80 std::memcpy(&req->data[current_size], ptr, transfer_size); 81 82 if (req->content_length == 0) 83 { 84 curl_off_t length; 85 if (curl_easy_getinfo(req->handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &length) == CURLE_OK) 86 req->content_length = static_cast<u32>(length); 87 } 88 89 return nmemb; 90 } 91 92 HTTPDownloader::Request* HTTPDownloaderCurl::InternalCreateRequest() 93 { 94 Request* req = new Request(); 95 req->handle = curl_easy_init(); 96 if (!req->handle) 97 { 98 delete req; 99 return nullptr; 100 } 101 102 return req; 103 } 104 105 void HTTPDownloaderCurl::InternalPollRequests() 106 { 107 // Apparently OpenSSL can fire SIGPIPE... 108 sigset_t old_block_mask = {}; 109 sigset_t new_block_mask = {}; 110 sigemptyset(&old_block_mask); 111 sigemptyset(&new_block_mask); 112 sigaddset(&new_block_mask, SIGPIPE); 113 if (pthread_sigmask(SIG_BLOCK, &new_block_mask, &old_block_mask) != 0) 114 WARNING_LOG("Failed to block SIGPIPE"); 115 116 int running_handles; 117 const CURLMcode err = curl_multi_perform(m_multi_handle, &running_handles); 118 if (err != CURLM_OK) 119 ERROR_LOG("curl_multi_perform() returned {}", static_cast<int>(err)); 120 121 for (;;) 122 { 123 int msgq; 124 struct CURLMsg* msg = curl_multi_info_read(m_multi_handle, &msgq); 125 if (!msg) 126 break; 127 128 if (msg->msg != CURLMSG_DONE) 129 { 130 WARNING_LOG("Unexpected multi message {}", static_cast<int>(msg->msg)); 131 continue; 132 } 133 134 Request* req; 135 if (curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &req) != CURLE_OK) 136 { 137 ERROR_LOG("curl_easy_getinfo() failed"); 138 continue; 139 } 140 141 if (msg->data.result == CURLE_OK) 142 { 143 long response_code = 0; 144 curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response_code); 145 req->status_code = static_cast<s32>(response_code); 146 147 char* content_type = nullptr; 148 if (curl_easy_getinfo(req->handle, CURLINFO_CONTENT_TYPE, &content_type) == CURLE_OK && content_type) 149 req->content_type = content_type; 150 151 DEV_LOG("Request for '{}' returned status code {} and {} bytes", req->url, req->status_code, req->data.size()); 152 } 153 else 154 { 155 ERROR_LOG("Request for '{}' returned error {}", req->url, static_cast<int>(msg->data.result)); 156 } 157 158 req->state.store(Request::State::Complete, std::memory_order_release); 159 } 160 161 if (pthread_sigmask(SIG_UNBLOCK, &new_block_mask, &old_block_mask) != 0) 162 WARNING_LOG("Failed to unblock SIGPIPE"); 163 } 164 165 bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request) 166 { 167 Request* req = static_cast<Request*>(request); 168 curl_easy_setopt(req->handle, CURLOPT_URL, request->url.c_str()); 169 curl_easy_setopt(req->handle, CURLOPT_USERAGENT, m_user_agent.c_str()); 170 curl_easy_setopt(req->handle, CURLOPT_WRITEFUNCTION, &HTTPDownloaderCurl::WriteCallback); 171 curl_easy_setopt(req->handle, CURLOPT_WRITEDATA, req); 172 curl_easy_setopt(req->handle, CURLOPT_NOSIGNAL, 1L); 173 curl_easy_setopt(req->handle, CURLOPT_PRIVATE, req); 174 curl_easy_setopt(req->handle, CURLOPT_FOLLOWLOCATION, 1L); 175 176 if (request->type == Request::Type::Post) 177 { 178 curl_easy_setopt(req->handle, CURLOPT_POST, 1L); 179 curl_easy_setopt(req->handle, CURLOPT_POSTFIELDS, request->post_data.c_str()); 180 } 181 182 DEV_LOG("Started HTTP request for '{}'", req->url); 183 req->state.store(Request::State::Started, std::memory_order_release); 184 req->start_time = Common::Timer::GetCurrentValue(); 185 186 const CURLMcode err = curl_multi_add_handle(m_multi_handle, req->handle); 187 if (err != CURLM_OK) 188 { 189 ERROR_LOG("curl_multi_add_handle() returned {}", static_cast<int>(err)); 190 req->callback(HTTP_STATUS_ERROR, std::string(), req->data); 191 curl_easy_cleanup(req->handle); 192 delete req; 193 return false; 194 } 195 196 return true; 197 } 198 199 void HTTPDownloaderCurl::CloseRequest(HTTPDownloader::Request* request) 200 { 201 Request* req = static_cast<Request*>(request); 202 DebugAssert(req->handle); 203 curl_multi_remove_handle(m_multi_handle, req->handle); 204 curl_easy_cleanup(req->handle); 205 delete req; 206 }