Files
flycast/core/oslib/http_client.cpp
vkedwardli 89b72a8e3e windows: fix HTTP URL parsing race and GET status handling (#2509)
Use per-request URL buffers for GET and POST to prevent concurrent requests from overwriting each other's host and path.

Return the actual HTTP status for GET requests, retaining an error status when sending the request or querying its status fails
[ci skip]
2026-10-01 11:30:16 +02:00

480 lines
13 KiB
C++

/*
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 <https://www.gnu.org/licenses/>.
*/
#include "build.h"
#include "http_client.h"
#include "stdclass.h"
namespace http
{
int get(const std::string& url, std::vector<u8>& 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;
}
}
#if !defined(__ANDROID__) && !defined(__APPLE__)
#ifdef _WIN32
#ifndef TARGET_UWP
#include "stdclass.h"
#include <windows.h>
#include <wininet.h>
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<u8>& 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<u8>& 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<u8>& 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<PostField>& 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<char> 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<u8> 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
#else
#include <curl/curl.h>
namespace http {
void init()
{
curl_global_init(CURL_GLOBAL_ALL);
}
static size_t receiveData(void *buffer, size_t size, size_t nmemb, std::vector<u8> *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<u8>& 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<u8> 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<u8>& 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<u8> 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<PostField>& 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
#endif // !defined(__ANDROID__) && !defined(__APPLE__)