/* Copyright 2022 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "build.h" #include "http_client.h" #include "stdclass.h" namespace http { int get(const std::string& url, std::vector& content, std::string& contentType) { Headers headers; int code = get(url, content, nullptr, &headers); for (const auto& [ key, value ] : headers) { if (key == "content-type") { contentType = value; break; } } return code; } } #ifdef _WIN32 #ifndef TARGET_UWP #include "stdclass.h" #include #include namespace http { static HINTERNET hInet; void init() { if (hInet == NULL) { hInet = InternetOpen(getUserAgent().c_str(), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); DWORD timeout = 30 * 1000; // 30 s InternetSetOption(hInet, INTERNET_OPTION_CONNECT_TIMEOUT, &timeout, sizeof(timeout)); } } struct CrackedUrl { char scheme[16]; char host[256]; char path[2048]; URL_COMPONENTS components; }; static bool crackUrl(const std::string& url, CrackedUrl& cracked) { URL_COMPONENTS& components = cracked.components; components = {}; components.dwStructSize = sizeof(components); components.lpszScheme = cracked.scheme; components.dwSchemeLength = sizeof(cracked.scheme) / sizeof(cracked.scheme[0]); components.lpszHostName = cracked.host; components.dwHostNameLength = sizeof(cracked.host) / sizeof(cracked.host[0]); components.lpszUrlPath = cracked.path; components.dwUrlPathLength = sizeof(cracked.path) / sizeof(cracked.path[0]); return InternetCrackUrlA(url.c_str(), url.length(), 0, &components); } static HINTERNET connect(const URL_COMPONENTS& comp) { return InternetConnect(hInet, comp.lpszHostName, comp.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); } int get(const std::string& url, std::vector& content, const Headers *reqHeaders, Headers *respHeaders) { CrackedUrl cracked; if (!crackUrl(url, cracked)) return 500; const URL_COMPONENTS& components = cracked.components; bool https = !strcmp(components.lpszScheme, "https"); int rc = 500; HINTERNET ic = connect(components); if (ic == NULL) return rc; HINTERNET hreq = HttpOpenRequest(ic, "GET", components.lpszUrlPath, NULL, NULL, NULL, https ? INTERNET_FLAG_SECURE : 0, 0); if (hreq == NULL) { InternetCloseHandle(ic); WARN_LOG(NETWORK, "Open URL failed: %lx", GetLastError()); return 500; } std::string headers; if (reqHeaders != nullptr) { for (const auto& [ name, value ] : *reqHeaders) headers += name + ": " + value + "\r\n"; HttpAddRequestHeaders(hreq, headers.c_str(), headers.length(), HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE); } if (!HttpSendRequest(hreq, nullptr, -1, nullptr, 0)) { WARN_LOG(NETWORK, "HttpSendRequest Error %d", GetLastError()); } else { DWORD status; DWORD size = sizeof(status); DWORD index = 0; if (HttpQueryInfo(hreq, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status, &size, &index)) rc = status; else WARN_LOG(NETWORK, "HttpQueryInfo Error %d", GetLastError()); if (respHeaders != nullptr) { std::string headers(4096, '\0'); DWORD size = headers.size(); DWORD index = 0; if (!HttpQueryInfo(hreq, HTTP_QUERY_RAW_HEADERS, headers.data(), &size, &index)) { WARN_LOG(NETWORK, "HttpQueryInfo Error %d", GetLastError()); } else { headers.resize(size); size_t pos = 0; while (pos < headers.size()) { size_t nulpos = headers.find('\0', pos); if (nulpos == headers.npos) break; std::string line = headers.substr(pos, nulpos - pos); if (line.empty()) break; pos = nulpos + 1; size_t colon = line.find(':'); if (colon == line.npos) continue; std::string name = trim_ws(line.substr(0, colon)); string_tolower(name); std::string value = trim_ws(line.substr(colon + 1)); respHeaders->emplace_back(name, value); } } } u8 buffer[4096]; DWORD bytesRead = sizeof(buffer); content.clear(); while (true) { if (!InternetReadFile(hreq, buffer, sizeof(buffer), &bytesRead)) { WARN_LOG(NETWORK, "InternetReadFile failed: %lx", GetLastError()); InternetCloseHandle(hreq); InternetCloseHandle(ic); return 500; } if (bytesRead == 0) break; content.insert(content.end(), buffer, buffer + bytesRead); } } InternetCloseHandle(hreq); InternetCloseHandle(ic); return rc; } static int post(const std::string& url, const char *headers, const u8 *payload, u32 payloadSize, std::vector& reply) { CrackedUrl cracked; if (!crackUrl(url, cracked)) return 500; const URL_COMPONENTS& components = cracked.components; bool https = !strcmp(components.lpszScheme, "https"); int rc = 500; HINTERNET ic = connect(components); if (ic == NULL) return rc; HINTERNET hreq = HttpOpenRequest(ic, "POST", components.lpszUrlPath, NULL, NULL, NULL, https ? INTERNET_FLAG_SECURE : 0, 0); if (hreq == NULL) { InternetCloseHandle(ic); return rc; } if (payloadSize > 0) { char clen[128]; snprintf(clen, sizeof(clen), "Content-Length: %d\r\n", payloadSize); HttpAddRequestHeaders(hreq, clen, -1L, HTTP_ADDREQ_FLAG_ADD_IF_NEW); } if (!HttpSendRequest(hreq, headers, -1, (void *)payload, payloadSize)) WARN_LOG(NETWORK, "HttpSendRequest Error %d", GetLastError()); else { DWORD status; DWORD size = sizeof(status); DWORD index = 0; if (!HttpQueryInfo(hreq, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status, &size, &index)) WARN_LOG(NETWORK, "HttpQueryInfo Error %d", GetLastError()); else { rc = status; reply.clear(); u8 buffer[4096]; DWORD bytesRead = sizeof(buffer); while (true) { if (!InternetReadFile(hreq, buffer, sizeof(buffer), &bytesRead)) { WARN_LOG(NETWORK, "InternetReadFile failed: %lx", GetLastError()); InternetCloseHandle(hreq); rc = 500; break; } if (bytesRead == 0) break; reply.insert(reply.end(), buffer, buffer + bytesRead); } } } InternetCloseHandle(hreq); InternetCloseHandle(ic); return rc; } int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { char buf[512]; if (contentType != nullptr) { snprintf(buf, sizeof(buf), "Content-Type: %s", contentType); contentType = buf; } return post(url, contentType, (const u8 *)payload, strlen(payload), reply); } int post(const std::string& url, const std::vector& fields) { static const std::string boundary("----flycast-boundary-8304529454"); std::string content; for (const PostField& field : fields) { content += "--" + boundary + "\r\n"; content += "Content-Disposition: form-data; name=\"" + field.name + '"'; if (!field.contentType.empty()) { size_t pos = get_last_slash_pos(field.value); std::string filename; if (pos == std::string::npos) filename = field.value; else filename = field.value.substr(pos + 1); content += "; filename=\"" + filename + '"'; } content += "\r\n"; if (!field.contentType.empty()) content += "Content-Type: " + field.contentType + "\r\n"; content += "\r\n"; if (field.contentType.empty()) { content += field.value; } else { FILE *f = nowide::fopen(field.value.c_str(), "rb"); if (f == nullptr) { WARN_LOG(NETWORK, "Can't open mime file %s", field.value.c_str()); return 500; } fseek(f, 0, SEEK_END); size_t size = ftell(f); fseek(f, 0, SEEK_SET); std::vector data; data.resize(size); size_t read = fread(data.data(), 1, size, f); if (read != size) { fclose(f); WARN_LOG(NETWORK, "Truncated read on mime file %s: %d -> %d", field.value.c_str(), (int)size, (int)read); return 500; } fclose(f); content += std::string(data.data(), size); } content += "\r\n"; } content += "--" + boundary + "--\r\n"; std::vector reply; std::string header("Content-Type: multipart/form-data; boundary=" + boundary); return post(url, header.c_str(), (const u8 *)&content[0], content.length(), reply); } void term() { if (hInet != NULL) { InternetCloseHandle(hInet); hInet = NULL; } } } #endif // !TARGET_UWP #elif !defined(__ANDROID__) && !defined(__APPLE__) #include namespace http { void init() { curl_global_init(CURL_GLOBAL_ALL); } static size_t receiveData(void *buffer, size_t size, size_t nmemb, std::vector *recvBuffer) { recvBuffer->insert(recvBuffer->end(), (u8 *)buffer, (u8 *)buffer + size * nmemb); return nmemb * size; } static size_t receiveHeader(const char *buffer, size_t size, size_t nitems, Headers *headers) { const char *sep = strchr(buffer, ':'); if (sep != nullptr) { std::string name = trim_ws(std::string(buffer, sep), " \t\r\n"); string_tolower(name); std::string value = trim_ws(std::string(sep + 1, buffer + nitems), " \t\r\n"); headers->emplace_back(name, value); } return nitems; } static CURL *makeCurlEasy(const std::string& url) { CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_USERAGENT, getUserAgent().c_str()); curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30); // default is 300 s so 5 min curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); return curl; } int get(const std::string& url, std::vector& content, const Headers *reqHeaders, Headers *respHeaders) { CURL *curl = makeCurlEasy(url); curl_slist *headers = nullptr; if (reqHeaders != nullptr) { for (const auto& [ key, value ] : *reqHeaders) headers = curl_slist_append(headers, (key + ": " + value).c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); } std::vector recvBuffer; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &recvBuffer); if (respHeaders != nullptr) { curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, receiveHeader); curl_easy_setopt(curl, CURLOPT_HEADERDATA, respHeaders); } CURLcode res = curl_easy_perform(curl); long httpCode = 500; if (res == CURLE_OK) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); content = recvBuffer; } curl_slist_free_all(headers); curl_easy_cleanup(curl); return (int)httpCode; } int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { CURL *curl = makeCurlEasy(url); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curl, CURLOPT_POST, 1); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); curl_slist *headers = nullptr; if (contentType != nullptr) { headers = curl_slist_append(headers, ("Content-Type: " + std::string(contentType)).c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); } std::vector recvBuffer; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &recvBuffer); CURLcode res = curl_easy_perform(curl); long httpCode = 500; if (res == CURLE_OK) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); reply = recvBuffer; } curl_slist_free_all(headers); curl_easy_cleanup(curl); return (int)httpCode; } static size_t nullCallback(char *ptr, size_t size, size_t nmemb, void *userdata) { return size * nmemb; } int post(const std::string& url, const std::vector& fields) { CURL *curl = makeCurlEasy(url); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_mime *mime = curl_mime_init(curl); for (const auto& field : fields) { curl_mimepart *part = curl_mime_addpart(mime); curl_mime_name(part, field.name.c_str()); if (field.contentType.empty()) { curl_mime_data(part, field.value.c_str(), CURL_ZERO_TERMINATED); } else { curl_mime_filedata(part, field.value.c_str()); curl_mime_type(part, field.contentType.c_str()); } } curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nullCallback); CURLcode res = curl_easy_perform(curl); long httpCode = 500; if (res == CURLE_OK) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_mime_free(mime); curl_easy_cleanup(curl); return (int)httpCode; } void term() { curl_global_cleanup(); } } #endif // !_WIN32 && !__ANDROID__ && !__APPLE__