#include #include #define CURL_STATICLIB #include #include #include #include namespace rack { namespace network { static const std::vector methodNames = { "GET", "POST", "PUT", "DELETE", }; static CURL* createCurl() { CURL* curl = curl_easy_init(); assert(curl); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); std::string userAgent = APP_NAME + " " + APP_EDITION_NAME + "/" + APP_VERSION; curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); // 65 second timeout for connections, just a bit over the typical HTTP default. curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 65); // If curl can't resolve a DNS entry, it sends a signal to interrupt the process. // However, since we use curl on non-main thread, this crashes the application. // So tell curl not to signal. curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); std::string caPath = asset::system("cacert.pem"); curl_easy_setopt(curl, CURLOPT_CAINFO, caPath.c_str()); return curl; } static size_t writeStringCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { std::string* str = (std::string*) userdata; size_t len = size * nmemb; str->append(ptr, len); return len; } static std::string getCookieString(const CookieMap& cookies) { std::string s; for (const auto& pair : cookies) { s += encodeUrl(pair.first); s += "="; s += encodeUrl(pair.second); s += ";"; } return s; } void init() { // Because OpenSSL is compiled with no-pinshared, we need to initialize without defining atexit(), since we want to destroy it when libRack is unloaded. OPENSSL_init_crypto(OPENSSL_INIT_NO_ATEXIT, NULL); // curl_easy_init() calls this automatically, but it's good to make sure this is done on the main thread before other threads are spawned. // https://curl.haxx.se/libcurl/c/curl_easy_init.html curl_global_init(CURL_GLOBAL_ALL); } void destroy() { curl_global_cleanup(); // Don't destroy OpenSSL because it's not designed to be reinitialized. // OPENSSL_cleanup(); } json_t* requestJson(Method method, const std::string& url, json_t* dataJ, const CookieMap& cookies) { std::string urlS = url; CURL* curl = createCurl(); char* reqStr = NULL; // Process data if (dataJ) { if (method == METHOD_GET) { // Append ?key1=value1&key2=value2&... to url urlS += "?"; bool isFirst = true; const char* key; json_t* value; json_object_foreach(dataJ, key, value) { if (json_is_string(value)) { if (!isFirst) urlS += "&"; urlS += key; urlS += "="; const char* str = json_string_value(value); size_t len = json_string_length(value); char* escapedStr = curl_easy_escape(curl, str, len); urlS += escapedStr; curl_free(escapedStr); isFirst = false; } } } else { reqStr = json_dumps(dataJ, 0); } } curl_easy_setopt(curl, CURLOPT_URL, urlS.c_str()); // Set HTTP method if (method == METHOD_GET) { // This is CURL's default } else if (method == METHOD_POST) { curl_easy_setopt(curl, CURLOPT_POST, true); } else if (method == METHOD_PUT) { curl_easy_setopt(curl, CURLOPT_PUT, true); } else if (method == METHOD_DELETE) { curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); } // Set headers struct curl_slist* headers = NULL; headers = curl_slist_append(headers, "Accept: application/json"); headers = curl_slist_append(headers, "Content-Type: application/json"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // Cookies if (!cookies.empty()) { curl_easy_setopt(curl, CURLOPT_COOKIE, getCookieString(cookies).c_str()); } // Body callbacks if (reqStr) curl_easy_setopt(curl, CURLOPT_POSTFIELDS, reqStr); std::string resText; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resText); // Perform request INFO("Requesting JSON %s %s", methodNames[method].c_str(), urlS.c_str()); CURLcode res = curl_easy_perform(curl); // Cleanup if (reqStr) std::free(reqStr); curl_easy_cleanup(curl); curl_slist_free_all(headers); if (res != CURLE_OK) { WARN("Could not request %s: %s", urlS.c_str(), curl_easy_strerror(res)); return NULL; } // Parse JSON response json_error_t error; json_t* rootJ = json_loads(resText.c_str(), 0, &error); return rootJ; } static int xferInfoCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { float* progress = (float*) clientp; if (progress) { if (dltotal <= 0) *progress = 0.f; else *progress = (float)dlnow / dltotal; } return 0; } bool requestDownload(const std::string& url, const std::string& filename, float* progress, const CookieMap& cookies) { CURL* curl = createCurl(); FILE* file = std::fopen(filename.c_str(), "wb"); if (!file) return false; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); // Fail on 4xx and 5xx HTTP codes curl_easy_setopt(curl, CURLOPT_FAILONERROR, true); // Cookies if (!cookies.empty()) { curl_easy_setopt(curl, CURLOPT_COOKIE, getCookieString(cookies).c_str()); } INFO("Requesting download %s", url.c_str()); CURLcode res = curl_easy_perform(curl); curl_easy_cleanup(curl); std::fclose(file); if (res != CURLE_OK) { system::remove(filename); WARN("Could not download %s: %s", url.c_str(), curl_easy_strerror(res)); return false; } return true; } std::string encodeUrl(const std::string& s) { CURL* curl = createCurl(); DEFER({curl_easy_cleanup(curl);}); assert(curl); char* escaped = curl_easy_escape(curl, s.c_str(), s.size()); DEFER({curl_free(escaped);}); return std::string(escaped); } std::string urlPath(const std::string& url) { CURLU* curl = curl_url(); DEFER({curl_url_cleanup(curl);}); if (curl_url_set(curl, CURLUPART_URL, url.c_str(), 0)) return ""; char* buf; if (curl_url_get(curl, CURLUPART_PATH, &buf, 0)) return ""; std::string ret = buf; curl_free(buf); return ret; } } // namespace network } // namespace rack