diff --git a/dll/dll/steam_http.h b/dll/dll/steam_http.h index 0ead950d..0b85edda 100644 --- a/dll/dll/steam_http.h +++ b/dll/dll/steam_http.h @@ -23,11 +23,17 @@ struct Steam_Http_Request { + enum class Protocol_t { + Web, + File, + }; + HTTPRequestHandle handle{}; EHTTPMethod request_method{}; std::string url{}; uint64 timeout_sec = 60; bool requires_valid_ssl = false; + Protocol_t protocol = Protocol_t::Web; constexpr const static char STEAM_DEFAULT_USER_AGENT[] = "Valve/Steam HTTP Client 1.0"; // check Steam_HTTP::SetHTTPRequestHeaderValue() and make sure to bypass the ones that should be reserved @@ -69,6 +75,10 @@ public ISteamHTTP Steam_Http_Request *get_request(HTTPRequestHandle hRequest); void online_http_request(Steam_Http_Request *request, SteamAPICall_t call_res_id); + void create_http_request_web( struct Steam_Http_Request &request, unsigned url_index ); + void create_http_request_file( struct Steam_Http_Request &request, unsigned file_index ); + + public: Steam_HTTP(class Settings *settings, class Networking *network, class SteamCallResults *callback_results, class SteamCallBacks *callbacks); diff --git a/dll/steam_http.cpp b/dll/steam_http.cpp index f5a7778b..983a4010 100644 --- a/dll/steam_http.cpp +++ b/dll/steam_http.cpp @@ -27,11 +27,107 @@ Steam_HTTP::Steam_HTTP(class Settings *settings, class Networking *network, clas Steam_Http_Request *Steam_HTTP::get_request(HTTPRequestHandle hRequest) { - auto conn = std::find_if(requests.begin(), requests.end(), [&hRequest](struct Steam_Http_Request const& conn) { return conn.handle == hRequest;}); - if (conn == requests.end()) return NULL; + auto conn = std::find_if(requests.begin(), requests.end(), [&hRequest](struct Steam_Http_Request const& conn) { + return conn.handle == hRequest; + }); + if (conn == requests.end()) return nullptr; + return &(*conn); } +void Steam_HTTP::create_http_request_web( struct Steam_Http_Request &request, unsigned url_index ) +{ + request.protocol = Steam_Http_Request::Protocol_t::Web; + + std::string_view url_path(request.url.c_str() + url_index, request.url.size() - url_index); + // strip last occurrences pf '/' + auto last_non_slash = url_path.find_last_not_of('/'); + if (std::string_view::npos != last_non_slash) { + url_path = url_path.substr(0, last_non_slash + 1); + } + + PRINT_DEBUG("parsed url path '%.*s'", url_path.size(), url_path.data()); + + if (!url_path.empty()) { + request.target_filepath = + Local_Storage::get_game_settings_path() + + "http" + PATH_SEPARATOR + + Local_Storage::sanitize_string(std::string(url_path)); + PRINT_DEBUG("parsed filepath (absolute) '%s'", request.target_filepath.c_str()); + + auto file_size = file_size_(request.target_filepath); + if (file_size > 0) { + request.response.resize(file_size); + auto read = Local_Storage::get_file_data(request.target_filepath, (char *)&request.response[0], file_size, 0); + if (read < 0) read = 0; + if (read != file_size) request.response.resize(static_cast(read)); + PRINT_DEBUG("read file data size=%i/%u", read, file_size); + } + } +} + +void Steam_HTTP::create_http_request_file( struct Steam_Http_Request &request, unsigned file_index ) +{ + request.protocol = Steam_Http_Request::Protocol_t::File; + + std::string_view url_path(request.url.c_str() + file_index, request.url.size() - file_index); + // strip last occurrences pf '/' + auto last_non_slash = url_path.find_last_not_of('/'); + if (std::string_view::npos != last_non_slash) { + url_path = url_path.substr(0, last_non_slash + 1); + } + + // https://datatracker.ietf.org/doc/html/rfc8089 + // 7.2. Informative References + // Appendix B. + enum class FilepathType { + MinimalAbsolute, // "file:/path/to/file" <<>> "file:/D:/path/to/file" + NonLocalWithAuthority, // "file://host.example.com/path/to/file" <<>> "file://host.example.com/D:/path/to/file" + NoAuthorityAbsolute, // "file:///path/to/file" <<>> "file:///D:/path/to/file" + }; + + // detect type + FilepathType file_type = FilepathType::MinimalAbsolute; + if (!url_path.empty() && url_path.front() == '/') { + file_type = FilepathType::NonLocalWithAuthority; + url_path = url_path.substr(1); + } + + if (FilepathType::NonLocalWithAuthority == file_type && !url_path.empty() && url_path.front() == '/') { + file_type = FilepathType::NoAuthorityAbsolute; + url_path = url_path.substr(1); + } + + // fix non-local part with authority, by removing the authority part + if (FilepathType::NonLocalWithAuthority == file_type) { + auto authority_end = url_path.find('/'); + if (std::string_view::npos != authority_end) { + url_path = url_path.substr(authority_end + 1); + } + } + + PRINT_DEBUG("parsed url path '%.*s'", url_path.size(), url_path.data()); + + if (!url_path.empty()) { + request.target_filepath = common_helpers::to_absolute( + url_path, + get_full_program_path() + ); + PRINT_DEBUG("parsed filepath (absolute) '%s'", request.target_filepath.c_str()); + + unsigned int file_size = file_size_(request.target_filepath); + if (file_size > 0) { + request.response.resize(file_size); + auto read = Local_Storage::get_file_data(request.target_filepath, (char *)&request.response[0], file_size, 0); + if (read < 0) read = 0; + if (read != file_size) request.response.resize(static_cast(read)); + PRINT_DEBUG("read file data size=%i/%u", read, file_size); + } + } +} + + + // Initializes a new HTTP request, returning a handle to use in further operations on it. Requires // the method (GET or POST) and the absolute URL for the request. Both http and https are supported, // so this string must start with http:// or https:// and should look like http://store.steampowered.com/app/250/ @@ -43,39 +139,40 @@ HTTPRequestHandle Steam_HTTP::CreateHTTPRequest( EHTTPMethod eHTTPRequestMethod, if (!pchAbsoluteURL) return INVALID_HTTPREQUEST_HANDLE; - std::string url = pchAbsoluteURL; + std::string_view url = pchAbsoluteURL; + unsigned url_index = 0; + unsigned file_index = 0; if (url.rfind("https://", 0) == 0) { url_index = sizeof("https://") - 1; } else if (url.rfind("http://", 0) == 0) { url_index = sizeof("http://") - 1; + } else if (url.rfind("file:/", 0) == 0) { + file_index = sizeof("file:/") - 1; } + static HTTPRequestHandle http_handle = 0; + ++http_handle; + if (!http_handle) ++http_handle; + struct Steam_Http_Request request{}; request.request_method = eHTTPRequestMethod; request.url = url; - if (url_index) { - if (url.back() == '/') url += "index.html"; - std::string file_path = Local_Storage::get_game_settings_path() + "http" + PATH_SEPARATOR + Local_Storage::sanitize_string(url.substr(url_index)); - request.target_filepath = file_path; - unsigned int file_size = file_size_(file_path); - if (file_size) { - request.response.resize(file_size); - long long read = Local_Storage::get_file_data(file_path, (char *)&request.response[0], file_size, 0); - if (read < 0) read = 0; - if (read != file_size) request.response.resize(static_cast(read)); - } + request.context_value = 0; + request.protocol = Steam_Http_Request::Protocol_t::Web; // force web request anyway to keep compatibility + request.handle = http_handle; + + if (url_index > 0) { + PRINT_DEBUG("URL is a web link"); + create_http_request_web(request, url_index); + } else if (file_index > 0) { + PRINT_DEBUG("URL is a filepath"); + create_http_request_file(request, file_index); + } - static HTTPRequestHandle h = 0; - ++h; - if (!h) ++h; - - request.handle = h; - request.context_value = 0; - - requests.push_back(request); - return request.handle; + requests.emplace_back(std::move(request)); // request object reference is invalidated after this move, be careful! + return http_handle; } @@ -222,11 +319,24 @@ void Steam_HTTP::online_http_request(Steam_Http_Request *request, SteamAPICall_t directory_path = "."; file_name = request->target_filepath; } - PRINT_DEBUG("directory: '%s', filename '%s', target_filepath '%s'", directory_path.c_str(), file_name.c_str(), request->target_filepath.c_str()); - Local_Storage::store_file_data(directory_path, file_name, (char *)"", sizeof("")); - PRINT_DEBUG("Temp file created with empty data"); //TODO remove this + PRINT_DEBUG("directory: '%s', filename '%s'", directory_path.c_str(), file_name.c_str()); + Local_Storage::store_file_data(directory_path, file_name, (char *)"", sizeof("")); // create empty file + + FILE *hfile = nullptr; + + { + const auto fsp = std::filesystem::u8path(request->target_filepath); +#if defined(__WINDOWS__) + // TODO use "\\?\" to solve max path problem + // https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + // note that this bypasses the NT object manager, note from above link: + // "File I/O functions in the Windows API convert "/" to "\" as part of converting the name to an NT-style name, except when using the "\\?\" prefix" + hfile = _wfopen(fsp.c_str(), L"wb"); +#else + hfile = std::fopen(fsp.c_str(), "wb"); +#endif + } - FILE *hfile = std::fopen(request->target_filepath.c_str(), "wb"); if (!hfile) { PRINT_DEBUG("failed to open file for writing"); send_callresult(); @@ -374,18 +484,46 @@ bool Steam_HTTP::SendHTTPRequest( HTTPRequestHandle hRequest, SteamAPICall_t *pC return false; } - if (request->response.empty() && request->target_filepath.size() && - !settings->disable_networking && settings->download_steamhttp_requests) { - auto call_res_id = callback_results->reserveCallResult(); - if (pCallHandle) *pCallHandle = call_res_id; + switch (request->protocol) + { + case Steam_Http_Request::Protocol_t::Web: { + bool bad_local_file = request->response.empty() && !request->target_filepath.empty(); // we need the filepath since we want to save the response to that file + bool internet_allowed = !settings->disable_networking && settings->download_steamhttp_requests; + if (bad_local_file && internet_allowed) { + auto call_res_id = callback_results->reserveCallResult(); + if (pCallHandle) *pCallHandle = call_res_id; - std::thread(&Steam_HTTP::online_http_request, this, request, call_res_id).detach(); - } else { + std::thread(&Steam_HTTP::online_http_request, this, request, call_res_id).detach(); + } else { + struct HTTPRequestCompleted_t data{}; + data.m_hRequest = request->handle; + data.m_ulContextValue = request->context_value; + data.m_unBodySize = static_cast(request->response.size()); + if (request->response.empty() && !settings->force_steamhttp_success) { + data.m_bRequestSuccessful = false; + data.m_eStatusCode = k_EHTTPStatusCode404NotFound; + } else { + data.m_bRequestSuccessful = true; + data.m_eStatusCode = k_EHTTPStatusCode200OK; + } + + auto callres = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.1); + if (pCallHandle) *pCallHandle = callres; + + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.1); + } + } + break; + + case Steam_Http_Request::Protocol_t::File: { struct HTTPRequestCompleted_t data{}; data.m_hRequest = request->handle; data.m_ulContextValue = request->context_value; data.m_unBodySize = static_cast(request->response.size()); - if (request->response.empty() && !settings->force_steamhttp_success) { + if (request->target_filepath.empty()) { // malformed filepath + data.m_bRequestSuccessful = false; + data.m_eStatusCode = k_EHTTPStatusCode403Forbidden; + } else if (request->response.empty() && !settings->force_steamhttp_success) { data.m_bRequestSuccessful = false; data.m_eStatusCode = k_EHTTPStatusCode404NotFound; } else { @@ -393,10 +531,28 @@ bool Steam_HTTP::SendHTTPRequest( HTTPRequestHandle hRequest, SteamAPICall_t *pC data.m_eStatusCode = k_EHTTPStatusCode200OK; } - auto callres = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.1); + auto callres = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.03); if (pCallHandle) *pCallHandle = callres; - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.1); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.03); + } + break; + + default: { + PRINT_DEBUG("[X] unhandled protocol type <%i>", (int)request->protocol); + struct HTTPRequestCompleted_t data{}; + data.m_hRequest = request->handle; + data.m_ulContextValue = request->context_value; + data.m_unBodySize = 0; + data.m_bRequestSuccessful = false; + data.m_eStatusCode = k_EHTTPStatusCode501NotImplemented; + + auto callres = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.05); + if (pCallHandle) *pCallHandle = callres; + + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.05); + } + break; } return true; @@ -460,6 +616,8 @@ bool Steam_HTTP::GetHTTPResponseHeaderSize( HTTPRequestHandle hRequest, const ch PRINT_DEBUG("'%s'", pchHeaderName); std::lock_guard lock(global_mutex); + if (unResponseHeaderSize) *unResponseHeaderSize = 0; + if (!pchHeaderName) return false; Steam_Http_Request *request = get_request(hRequest); @@ -479,7 +637,7 @@ bool Steam_HTTP::GetHTTPResponseHeaderSize( HTTPRequestHandle hRequest, const ch // BGetHTTPResponseHeaderSize to check for the presence of the header and to find out the size buffer needed. bool Steam_HTTP::GetHTTPResponseHeaderValue( HTTPRequestHandle hRequest, const char *pchHeaderName, uint8 *pHeaderValueBuffer, uint32 unBufferSize ) { - PRINT_DEBUG("'%s'", pchHeaderName); + PRINT_DEBUG("'%s' [%u]", pchHeaderName, unBufferSize); std::lock_guard lock(global_mutex); if (!pchHeaderName) return false; @@ -490,6 +648,8 @@ bool Steam_HTTP::GetHTTPResponseHeaderValue( HTTPRequestHandle hRequest, const c } const auto hdr = request->headers.find(pchHeaderName); if (request->headers.end() == hdr) return false; + PRINT_DEBUG(" required header buffer size = %zu", hdr->second.size()); + if (unBufferSize < hdr->second.size()) return false; if (pHeaderValueBuffer) { memset(pHeaderValueBuffer, 0, unBufferSize); @@ -506,6 +666,8 @@ bool Steam_HTTP::GetHTTPResponseBodySize( HTTPRequestHandle hRequest, uint32 *un PRINT_DEBUG("%u", hRequest); std::lock_guard lock(global_mutex); + if (unBodySize) *unBodySize = 0; + Steam_Http_Request *request = get_request(hRequest); if (!request) { return false; @@ -521,7 +683,7 @@ bool Steam_HTTP::GetHTTPResponseBodySize( HTTPRequestHandle hRequest, uint32 *un // the correct buffer size to use. bool Steam_HTTP::GetHTTPResponseBodyData( HTTPRequestHandle hRequest, uint8 *pBodyDataBuffer, uint32 unBufferSize ) { - PRINT_DEBUG("%p %u", pBodyDataBuffer, unBufferSize); + PRINT_DEBUG("%p [%u]", pBodyDataBuffer, unBufferSize); std::lock_guard lock(global_mutex); Steam_Http_Request *request = get_request(hRequest); @@ -589,6 +751,8 @@ bool Steam_HTTP::GetHTTPDownloadProgressPct( HTTPRequestHandle hRequest, float * PRINT_DEBUG("%u", hRequest); std::lock_guard lock(global_mutex); + if (pflPercentOut) *pflPercentOut = 0; + Steam_Http_Request *request = get_request(hRequest); if (!request) { return false;