/* * This file is part of OpenTTD. * OpenTTD 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, version 2. * OpenTTD 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 OpenTTD. If not, see . */ /** * @file http_winhttp.cpp WinHTTP-based implementation for HTTP requests. */ #include "../../stdafx.h" #include "../../debug.h" #include "../../rev.h" #include "../network_internal.h" #include "http.h" #include #include "../../safeguards.h" static HINTERNET _winhttp_session = nullptr; /** Single HTTP request. */ class NetworkHTTPRequest { private: const std::wstring uri; ///< URI to connect to. HTTPCallback *callback; ///< Callback to send data back on. const std::string data; ///< Data to send, if any. HINTERNET connection = nullptr; ///< Current connection object. HINTERNET request = nullptr; ///< Current request object. std::atomic finished = false; ///< Whether we are finished with the request. int depth = 0; ///< Current redirect depth we are in. public: NetworkHTTPRequest(const std::wstring &uri, HTTPCallback *callback, const std::string &data); ~NetworkHTTPRequest(); void Connect(); bool Receive(); void WinHttpCallback(DWORD code, void *info, DWORD length); }; static std::vector _http_requests; static std::vector _new_http_requests; /** * Create a new HTTP request. * * @param uri the URI to connect to (https://.../..). * @param callback the callback to send data back on. * @param data the data we want to send. When non-empty, this will be a POST request, otherwise a GET request. */ NetworkHTTPRequest::NetworkHTTPRequest(const std::wstring &uri, HTTPCallback *callback, const std::string &data) : uri(uri), callback(callback), data(data) { } static std::string GetLastErrorAsString() { char buffer[512]; DWORD error_code = GetLastError(); if (FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS, GetModuleHandleA("winhttp.dll"), error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buffer, sizeof(buffer), NULL) == 0) { return fmt::format("unknown error {}", error_code); } return buffer; } /** * Callback from the WinHTTP library, called when-ever something changes about the HTTP request status. * * The callback needs to call some WinHttp functions for certain states, so WinHttp continues * to read the request. This also allows us to abort when things go wrong, by simply not calling * those functions. * Comments with "Next step:" mark where WinHttp needs a call to continue. * * @param code The code of the event. * @param info The information about the event. * @param length The length of the information. */ void NetworkHTTPRequest::WinHttpCallback(DWORD code, void *info, DWORD length) { if (this->finished) return; switch (code) { case WINHTTP_CALLBACK_STATUS_RESOLVING_NAME: case WINHTTP_CALLBACK_STATUS_NAME_RESOLVED: case WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER: case WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER: case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST: case WINHTTP_CALLBACK_STATUS_REQUEST_SENT: case WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE: case WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED: case WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION: case WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED: case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED: case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING: /* We don't care about these events, and explicitly ignore them. */ break; case WINHTTP_CALLBACK_STATUS_REDIRECT: /* Make sure we are not in a redirect loop. */ if (this->depth++ > 5) { Debug(net, 0, "HTTP request failed: too many redirects"); this->finished = true; this->callback->OnFailure(); return; } break; case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: /* Next step: read response. */ WinHttpReceiveResponse(this->request, nullptr); break; case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: { /* Retrieve the status code. */ DWORD status_code = 0; DWORD status_code_size = sizeof(status_code); WinHttpQueryHeaders(this->request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX); Debug(net, 3, "HTTP request status code: {}", status_code); /* If there is any error, we simply abort the request. */ if (status_code >= 400) { Debug(net, 0, "HTTP request failed: status-code {}", status_code); this->finished = true; this->callback->OnFailure(); return; } /* Next step: query for any data. */ WinHttpQueryDataAvailable(this->request, nullptr); } break; case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: { /* Retrieve the amount of data available to process. */ DWORD size = *(DWORD *)info; /* Next step: read the data in a temporary allocated buffer. * The buffer will be free'd in the next step. */ char *buffer = size == 0 ? nullptr : MallocT(size); WinHttpReadData(this->request, buffer, size, 0); } break; case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: Debug(net, 4, "HTTP callback: {} bytes", length); this->callback->OnReceiveData(static_cast(info), length); /* Free the temporary buffer that was allocated in the previous step. */ free(info); if (length == 0) { /* Next step: no more data available: request is finished. */ this->finished = true; Debug(net, 1, "HTTP request succeeded"); } else { /* Next step: query for more data. */ WinHttpQueryDataAvailable(this->request, nullptr); } break; case WINHTTP_CALLBACK_STATUS_SECURE_FAILURE: case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: Debug(net, 0, "HTTP request failed: {}", GetLastErrorAsString()); this->finished = true; this->callback->OnFailure(); break; default: Debug(net, 0, "HTTP request failed: unexepected callback code 0x{:x}", code); this->finished = true; this->callback->OnFailure(); return; } } static void CALLBACK StaticWinHttpCallback(HINTERNET handle, DWORD_PTR context, DWORD code, void *info, DWORD length) { if (context == 0) return; NetworkHTTPRequest *request = (NetworkHTTPRequest *)context; request->WinHttpCallback(code, info, length); } /** * Start the HTTP request handling. * * This is done in an async manner, so we can do other things while waiting for * the HTTP request to finish. The actual receiving of the data is done in * Receive(). */ void NetworkHTTPRequest::Connect() { Debug(net, 1, "HTTP request to {}", std::string(uri.begin(), uri.end())); URL_COMPONENTS url_components = {}; wchar_t scheme[32]; wchar_t hostname[128]; wchar_t url_path[4096]; /* Convert the URL to its components. */ url_components.dwStructSize = sizeof(url_components); url_components.lpszScheme = scheme; url_components.dwSchemeLength = lengthof(scheme); url_components.lpszHostName = hostname; url_components.dwHostNameLength = lengthof(hostname); url_components.lpszUrlPath = url_path; url_components.dwUrlPathLength = lengthof(url_path); WinHttpCrackUrl(this->uri.c_str(), 0, 0, &url_components); /* Create the HTTP connection. */ this->connection = WinHttpConnect(_winhttp_session, url_components.lpszHostName, url_components.nPort, 0); if (this->connection == nullptr) { Debug(net, 0, "HTTP request failed: {}", GetLastErrorAsString()); this->finished = true; this->callback->OnFailure(); return; } this->request = WinHttpOpenRequest(connection, data.empty() ? L"GET" : L"POST", url_components.lpszUrlPath, nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, url_components.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); if (this->request == nullptr) { WinHttpCloseHandle(this->connection); Debug(net, 0, "HTTP request failed: {}", GetLastErrorAsString()); this->finished = true; this->callback->OnFailure(); return; } /* Send the request (possibly with a payload). */ if (data.empty()) { WinHttpSendRequest(this->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, reinterpret_cast(this)); } else { WinHttpSendRequest(this->request, L"Content-Type: application/x-www-form-urlencoded\r\n", -1, const_cast(data.c_str()), static_cast(data.size()), static_cast(data.size()), reinterpret_cast(this)); } } /** * Poll and process the HTTP request/response. * * @return True iff the request is done; no call to Receive() should be done after it returns true. */ bool NetworkHTTPRequest::Receive() { if (this->callback->IsCancelled()) { Debug(net, 1, "HTTP request failed: cancelled by user"); this->finished = true; this->callback->OnFailure(); return true; } return this->finished; } /** * Destructor of the HTTP request. * * Makes sure all handlers are closed, and all memory is free'd. */ NetworkHTTPRequest::~NetworkHTTPRequest() { if (this->request) { WinHttpCloseHandle(this->request); WinHttpCloseHandle(this->connection); } } /* static */ void NetworkHTTPSocketHandler::Connect(const std::string &uri, HTTPCallback *callback, const std::string data) { auto request = new NetworkHTTPRequest(std::wstring(uri.begin(), uri.end()), callback, data); request->Connect(); _new_http_requests.push_back(request); } /* static */ void NetworkHTTPSocketHandler::HTTPReceive() { if (!_new_http_requests.empty()) { /* We delay adding new requests, as Receive() below can cause a callback which adds a new requests. */ _http_requests.insert(_http_requests.end(), _new_http_requests.begin(), _new_http_requests.end()); _new_http_requests.clear(); } if (_http_requests.empty()) return; for (auto it = _http_requests.begin(); it != _http_requests.end(); /* nothing */) { NetworkHTTPRequest *cur = *it; if (cur->Receive()) { it = _http_requests.erase(it); delete cur; continue; } ++it; } } void NetworkHTTPInitialize() { /* We create a single session, from which we build up every other request. */ std::string user_agent = fmt::format("OpenTTD/{}", GetNetworkRevisionString()); _winhttp_session = WinHttpOpen(std::wstring(user_agent.begin(), user_agent.end()).c_str(), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); /* Set the callback function for all requests. The "context" maps it back into the actual request instance. */ WinHttpSetStatusCallback(_winhttp_session, StaticWinHttpCallback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0); /* 10 seconds timeout for requests. */ WinHttpSetTimeouts(_winhttp_session, 10000, 10000, 10000, 10000); } void NetworkHTTPUninitialize() { WinHttpCloseHandle(_winhttp_session); }