#include #include #include #include "upload.h" #include "protocol.h" #include "multipart.h" #include #include #include #include #include namespace dingosdk::backtrace { namespace { struct Internet { HINTERNET value{}; ~Internet() { if (value) WinHttpCloseHandle(value); } }; std::string utf8(std::wstring_view value) { const auto size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); if (!size) throw std::runtime_error("Invalid text"); std::string result(static_cast(size), '\0'); WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), result.data(), size, nullptr, nullptr); return result; } void remove_report(const std::filesystem::path& dump) { std::error_code error; std::filesystem::remove(dump, error); if (!error) { std::filesystem::remove(dump.wstring() + L".json", error); std::filesystem::remove(log_attachment_path(dump), error); } } void status_file(const std::filesystem::path& directory, const std::string& text) { // Overwrite a bounded status file. Never include the endpoint, token, response body, or local paths. std::ofstream(directory / L"status.txt", std::ios::trunc) << text << '\n'; } } bool parse_endpoint(const std::wstring& url, Endpoint& endpoint) { if (url.empty() || url.size() >= 4096 || url.find_first_of(L"\r\n\t #") != std::wstring::npos) return false; URL_COMPONENTS parts{}; parts.dwStructSize = sizeof(parts); parts.dwHostNameLength = parts.dwUrlPathLength = parts.dwExtraInfoLength = parts.dwUserNameLength = parts.dwPasswordLength = static_cast(-1); if (!WinHttpCrackUrl(url.c_str(), static_cast(url.size()), 0, &parts) || parts.nScheme != INTERNET_SCHEME_HTTPS || !parts.dwHostNameLength || parts.dwUserNameLength || parts.dwPasswordLength || !parts.dwUrlPathLength) return false; endpoint.host.assign(parts.lpszHostName, parts.dwHostNameLength); endpoint.resource.assign(parts.lpszUrlPath, parts.dwUrlPathLength); if (parts.dwExtraInfoLength) endpoint.resource.append(parts.lpszExtraInfo, parts.dwExtraInfoLength); endpoint.port = parts.nPort; return true; } bool accepted_response(DWORD status, std::string_view response, std::string& report_id) { report_id.clear(); if (status < 200 || status >= 300) return false; try { const auto json = Json::parse(response, {65536, 8, 256}); if (json.value("response", "") != "ok") return false; const auto id = json.value("_rxid", ""); if (id.empty() || id.size() > 128 || id.find_first_not_of("0123456789abcdefABCDEF-") != std::string::npos) return false; report_id = id; return true; } catch (...) { return false; } } std::filesystem::path queue_directory(const std::filesystem::path& root, const std::wstring& url) { // Hash the destination so changing projects cannot send an old queue to a new account. const auto encoded = utf8(url); std::array hash{}; if (BCryptHash(BCRYPT_SHA256_ALG_HANDLE, nullptr, 0, reinterpret_cast(const_cast(encoded.data())), static_cast(encoded.size()), hash.data(), static_cast(hash.size())) < 0) throw std::runtime_error("Cannot identify report queue"); std::wstring name; for (const auto byte : hash) name += std::format(L"{:02x}", byte); return root / name; } UploadResult upload_report(const std::wstring& url, const std::filesystem::path& dump, const Json& attributes) { UploadResult result; Endpoint endpoint; if (!parse_endpoint(url, endpoint) || !attributes.is_object()) { result.error = ERROR_INVALID_PARAMETER; return result; } const auto boundary = std::format("ReSkate-{:x}-{:x}", GetCurrentProcessId(), GetTickCount64()); MultipartReport body(dump, attributes, boundary); if (body.error()) { result.error = body.error(); return result; } Internet session{WinHttpOpen(L"ReSkate-CrashReporter/1", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, 0)}; if (!session.value) { result.error = GetLastError(); return result; } if (!WinHttpSetTimeouts(session.value, 5000, 5000, 10000, 10000)) { result.error = GetLastError(); return result; } Internet connection{WinHttpConnect(session.value, endpoint.host.c_str(), endpoint.port, 0)}; if (!connection.value) { result.error = GetLastError(); return result; } Internet request{WinHttpOpenRequest(connection.value, L"POST", endpoint.resource.c_str(), nullptr, nullptr, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE)}; if (!request.value) { result.error = GetLastError(); return result; } DWORD redirect = WINHTTP_OPTION_REDIRECT_POLICY_NEVER, logon = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH; if (!WinHttpSetOption(request.value, WINHTTP_OPTION_REDIRECT_POLICY, &redirect, sizeof(redirect)) || !WinHttpSetOption(request.value, WINHTTP_OPTION_AUTOLOGON_POLICY, &logon, sizeof(logon))) { result.error = GetLastError(); return result; } const std::wstring wide_boundary(boundary.begin(), boundary.end()); const auto headers = L"Content-Type: multipart/form-data; boundary=" + wide_boundary + L"\r\n"; if (!WinHttpSendRequest(request.value, headers.c_str(), static_cast(headers.size()), nullptr, 0, body.size(), 0)) { result.error = GetLastError(); return result; } const auto started = GetTickCount64(); const auto write = [&](const void* data, DWORD bytes) { DWORD written{}; return GetTickCount64() - started < 60000 && WinHttpWriteData(request.value, data, bytes, &written) && written == bytes; }; if (!body.write(write)) { result.error = ERROR_WRITE_FAULT; return result; } if (!WinHttpReceiveResponse(request.value, nullptr)) { result.error = GetLastError(); return result; } DWORD status_size = sizeof(result.http_status); if (!WinHttpQueryHeaders(request.value, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, nullptr, &result.http_status, &status_size, nullptr)) { result.error = GetLastError(); return result; } std::string response; std::array buffer{}; for (;;) { DWORD read{}; if (GetTickCount64() - started >= 60000 || !WinHttpReadData(request.value, buffer.data(), static_cast(buffer.size()), &read)) { result.error = ERROR_TIMEOUT; return result; } if (!read) break; if (response.size() + read > 65536) { result.error = ERROR_INVALID_DATA; return result; } response.append(buffer.data(), read); } result.accepted = accepted_response(result.http_status, response, result.report_id); return result; } std::size_t upload_pending(const std::wstring& url, const std::filesystem::path& directory, const Sender& sender) noexcept { try { Handle lock(CreateFileW((directory / L"queue.lock").c_str(), GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); if (lock.value == INVALID_HANDLE_VALUE) return 0; std::vector pending; const auto cutoff = std::filesystem::file_time_type::clock::now() - std::chrono::hours(24 * 14); for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (!entry.is_regular_file() || entry.path().extension() != L".dmp") continue; if (entry.last_write_time() < cutoff || entry.file_size() > max_dump_bytes || !entry.file_size()) remove_report(entry.path()); else pending.push_back(entry); } std::sort(pending.begin(), pending.end(), [](const auto& a, const auto& b) { return a.last_write_time() < b.last_write_time(); }); while (pending.size() > max_pending_reports) { remove_report(pending.front().path()); pending.erase(pending.begin()); } std::size_t uploaded{}; for (const auto& entry : pending) { const auto metadata = entry.path().wstring() + L".json"; std::error_code error; const auto size = std::filesystem::file_size(metadata, error); if (error || size > 65536) continue; std::ifstream input(metadata, std::ios::binary); std::string contents(static_cast(size), '\0'); if (!input.read(contents.data(), static_cast(contents.size()))) continue; input.close(); // Windows cannot remove an open metadata file after confirmation. Json attributes; try { attributes = Json::parse(contents, {65536, 8, 256}); } catch (...) { continue; } const auto result = sender(url, entry.path(), attributes); if (!result.accepted) { status_file(directory, std::format("Upload pending; HTTP {}; Windows error {}. Retrying on next launch.", result.http_status, result.error)); break; } status_file(directory, "Accepted by Backtrace: " + result.report_id); remove_report(entry.path()); if (++uploaded == 4) break; // Bound work for a single launch. } return uploaded; } catch (...) { return 0; } } }