From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../nsis/Contrib/HttpPostFile/HttpPostFile.cpp | 304 +++++++++++++++++++++ .../nsis/Contrib/HttpPostFile/HttpPostFile.sln | 22 ++ .../nsis/Contrib/HttpPostFile/HttpPostFile.vcxproj | 63 +++++ .../nsis/Contrib/HttpPostFile/test/postdriver.nsi | 63 +++++ .../nsis/Contrib/HttpPostFile/test/unittest.py | 247 +++++++++++++++++ 5 files changed, 699 insertions(+) create mode 100644 other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.cpp create mode 100644 other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.sln create mode 100644 other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.vcxproj create mode 100644 other-licenses/nsis/Contrib/HttpPostFile/test/postdriver.nsi create mode 100644 other-licenses/nsis/Contrib/HttpPostFile/test/unittest.py (limited to 'other-licenses/nsis/Contrib/HttpPostFile') diff --git a/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.cpp b/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.cpp new file mode 100644 index 0000000000..7b55493829 --- /dev/null +++ b/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.cpp @@ -0,0 +1,304 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// To explain some of the oddities: +// This plugin avoids linking against a runtime that might not be present, thus +// it avoids standard library functions. +// NSIS requires GlobalAlloc/GlobalFree for its interfaces, and I use them for +// other allocations (vs e.g. HeapAlloc) for the sake of consistency. + +#include +#include + +#define AGENT_NAME L"HttpPostFile plugin" + +PBYTE LoadFileData(LPWSTR fileName, DWORD& cbData); +bool HttpPost(LPURL_COMPONENTS pUrl, LPWSTR contentTypeHeader, PBYTE data, + DWORD cbData); + +// NSIS API +typedef struct _stack_t { + struct _stack_t* next; + WCHAR text[1]; +} stack_t; + +// Unlink and return the topmost element of the stack, if any. +static stack_t* popstack(stack_t** stacktop) { + if (!stacktop || !*stacktop) return nullptr; + stack_t* element = *stacktop; + *stacktop = element->next; + element->next = nullptr; + return element; +} + +// Allocate a new stack element (with space for `stringsize`), copy the string, +// add to the top of the stack. +static void pushstring(LPCWSTR str, stack_t** stacktop, + unsigned int stringsize) { + stack_t* element; + if (!stacktop) return; + + // The allocation here has space for stringsize+1 WCHARs, because stack_t.text + // is 1 element long. This is consistent with the NSIS ExDLL example, though + // inconsistent with the comment that says the array "should be the length of + // g_stringsize when allocating". I'm sticking to consistency with + // the code, and erring towards having a larger buffer than necessary. + + element = (stack_t*)GlobalAlloc( + GPTR, (sizeof(stack_t) + stringsize * sizeof(*str))); + lstrcpynW(element->text, str, stringsize); + element->next = *stacktop; + *stacktop = element; +} + +BOOL APIENTRY DllMain(HINSTANCE instance, DWORD reason, LPVOID) { + // No initialization or cleanup is needed. + return TRUE; +} + +extern "C" { + +// HttpPostFile::Post +// +// e.g. HttpPostFile "C:\blah.json" "Content-Type: application/json$\r$\n" +// "https://example.com" +// +// Leaves a result string on the stack, "success" if the POST was successful, an +// error message otherwise. +// The status code from the server is not checked, as long as we got some +// response the result will be "success". The response is read, but discarded. +void __declspec(dllexport) + Post(HWND hwndParent, int string_size, char* /* variables */, + stack_t** stacktop, void* /* extra_parameters */) { + static const URL_COMPONENTS kZeroComponents = {0}; + const WCHAR* errorMsg = L"error"; + + DWORD cbData = INVALID_FILE_SIZE; + PBYTE data = nullptr; + + // Copy a constant, because initializing an automatic variable with {0} ends + // up linking to memset, which isn't available. + URL_COMPONENTS components = kZeroComponents; + + // Get args, taking ownership of the strings from the stack, to avoid + // allocating and copying strings. + stack_t* postFileName = popstack(stacktop); + stack_t* contentTypeHeader = popstack(stacktop); + stack_t* url = popstack(stacktop); + + if (!postFileName || !contentTypeHeader || !url) { + errorMsg = L"error getting arguments"; + goto finish; + } + + data = LoadFileData(postFileName->text, cbData); + if (!data || cbData == INVALID_FILE_SIZE) { + errorMsg = L"error reading file"; + goto finish; + } + + { + // This length is used to allocate for the host name and path components, + // which should be no longer than the source URL. + int urlBufLen = lstrlenW(url->text) + 1; + + components.dwStructSize = sizeof(components); + components.dwHostNameLength = urlBufLen; + components.dwUrlPathLength = urlBufLen; + components.lpszHostName = + (LPWSTR)GlobalAlloc(GPTR, urlBufLen * sizeof(WCHAR)); + components.lpszUrlPath = + (LPWSTR)GlobalAlloc(GPTR, urlBufLen * sizeof(WCHAR)); + } + + errorMsg = L"error parsing URL"; + if (components.lpszHostName && components.lpszUrlPath && + InternetCrackUrl(url->text, 0, 0, &components) && + (components.nScheme == INTERNET_SCHEME_HTTP || + components.nScheme == INTERNET_SCHEME_HTTPS)) { + errorMsg = L"error sending HTTP request"; + if (HttpPost(&components, contentTypeHeader->text, data, cbData)) { + // success! + errorMsg = nullptr; + } + } + +finish: + if (components.lpszUrlPath) { + GlobalFree(components.lpszUrlPath); + } + if (components.lpszHostName) { + GlobalFree(components.lpszHostName); + } + if (data) { + GlobalFree(data); + } + + // Free args taken from the NSIS stack + if (url) { + GlobalFree(url); + } + if (contentTypeHeader) { + GlobalFree(contentTypeHeader); + } + if (postFileName) { + GlobalFree(postFileName); + } + + if (errorMsg) { + pushstring(errorMsg, stacktop, string_size); + } else { + pushstring(L"success", stacktop, string_size); + } +} +} + +// Returns buffer with file contents on success, placing the size in cbData. +// Returns nullptr on failure. +// Caller must use GlobalFree() on the returned buffer if non-null. +PBYTE LoadFileData(LPWSTR fileName, DWORD& cbData) { + bool success = false; + + HANDLE hPostFile = INVALID_HANDLE_VALUE; + + PBYTE data = nullptr; + + DWORD bytesRead; + DWORD bytesReadTotal; + + hPostFile = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hPostFile == INVALID_HANDLE_VALUE) { + goto finish; + } + + cbData = GetFileSize(hPostFile, NULL); + if (cbData == INVALID_FILE_SIZE) { + goto finish; + } + + data = (PBYTE)GlobalAlloc(GPTR, cbData); + if (!data) { + goto finish; + } + + bytesReadTotal = 0; + do { + if (!ReadFile(hPostFile, data + bytesReadTotal, cbData - bytesReadTotal, + &bytesRead, nullptr /* overlapped */)) { + goto finish; + } + bytesReadTotal += bytesRead; + } while (bytesReadTotal < cbData && bytesRead > 0); + + if (bytesReadTotal == cbData) { + success = true; + } + +finish: + if (!success) { + if (data) { + GlobalFree(data); + data = nullptr; + } + cbData = INVALID_FILE_SIZE; + } + if (hPostFile != INVALID_HANDLE_VALUE) { + CloseHandle(hPostFile); + hPostFile = INVALID_HANDLE_VALUE; + } + + return data; +} + +// Returns true on success +bool HttpPost(LPURL_COMPONENTS pUrl, LPWSTR contentTypeHeader, PBYTE data, + DWORD cbData) { + bool success = false; + + HINTERNET hInternet = nullptr; + HINTERNET hConnect = nullptr; + HINTERNET hRequest = nullptr; + + hInternet = InternetOpen(AGENT_NAME, INTERNET_OPEN_TYPE_PRECONFIG, + nullptr, // proxy + nullptr, // proxy bypass + 0 // flags + ); + if (!hInternet) { + goto finish; + } + + hConnect = InternetConnect(hInternet, pUrl->lpszHostName, pUrl->nPort, + nullptr, // userName, + nullptr, // password + INTERNET_SERVICE_HTTP, + 0, // flags + 0 // context + ); + if (!hConnect) { + goto finish; + } + + { + // NOTE: Some of these settings are perhaps unnecessary for a POST. + DWORD httpFlags = INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_COOKIES | + INTERNET_FLAG_NO_UI | INTERNET_FLAG_RELOAD; + if (pUrl->nScheme == INTERNET_SCHEME_HTTPS) { + // NOTE: nsJSON sets flags to allow redirecting HTTPS to HTTP, or HTTP to + // HTTPS I left those out because it seemed undesirable for our use case. + httpFlags |= INTERNET_FLAG_SECURE; + } + hRequest = HttpOpenRequest(hConnect, L"POST", pUrl->lpszUrlPath, + nullptr, // version, + nullptr, // referrer + nullptr, // accept types + httpFlags, + 0 // context + ); + if (!hRequest) { + goto finish; + } + } + + if (contentTypeHeader) { + if (!HttpAddRequestHeaders(hRequest, contentTypeHeader, + -1L, // headers length (count string length) + HTTP_ADDREQ_FLAG_ADD)) { + goto finish; + } + } + + if (!HttpSendRequestW(hRequest, + nullptr, // additional headers + 0, // headers length + data, cbData)) { + goto finish; + } + + BYTE readBuffer[1024]; + DWORD bytesRead; + do { + if (!InternetReadFile(hRequest, readBuffer, sizeof(readBuffer), + &bytesRead)) { + goto finish; + } + // read data is thrown away + } while (bytesRead > 0); + + success = true; + +finish: + if (hRequest) { + InternetCloseHandle(hRequest); + } + if (hConnect) { + InternetCloseHandle(hConnect); + } + if (hInternet) { + InternetCloseHandle(hInternet); + } + + return success; +} \ No newline at end of file diff --git a/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.sln b/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.sln new file mode 100644 index 0000000000..c6fd9ca5e5 --- /dev/null +++ b/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30128.74 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HttpPostFile", "HttpPostFile.vcxproj", "{A8BF99FD-8603-4137-862A-1D14268D7812}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8BF99FD-8603-4137-862A-1D14268D7812}.Release|x86.ActiveCfg = Release|Win32 + {A8BF99FD-8603-4137-862A-1D14268D7812}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5EF33D14-5BB9-4E44-A347-9FF33E86D9DC} + EndGlobalSection +EndGlobal diff --git a/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.vcxproj b/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.vcxproj new file mode 100644 index 0000000000..32ca342ea2 --- /dev/null +++ b/other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.vcxproj @@ -0,0 +1,63 @@ + + + + + Release + Win32 + + + + 16.0 + Win32Proj + {a8bf99fd-8603-4137-862a-1d14268d7812} + HttpPostFile + 10.0 + + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + false + + + + Level3 + true + true + WINVER=0x601;_WIN32_WINNT=0x601;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + false + false + false + + + Console + true + true + true + wininet.lib;%(AdditionalDependencies) + DllMain + + + + + + + + + \ No newline at end of file diff --git a/other-licenses/nsis/Contrib/HttpPostFile/test/postdriver.nsi b/other-licenses/nsis/Contrib/HttpPostFile/test/postdriver.nsi new file mode 100644 index 0000000000..6a54bf3ca3 --- /dev/null +++ b/other-licenses/nsis/Contrib/HttpPostFile/test/postdriver.nsi @@ -0,0 +1,63 @@ +; Any copyright is dedicated to the Public Domain. +; http://creativecommons.org/publicdomain/zero/1.0/ + +; Simple driver for HttpPostFile, passes command line args to HttpPostFile::Post and +; writes the result string to a file for automated checking. +; Always specifies Content-Type: application/json +; +; Usage: posttest /postfile=postfile.json /url=http://example.com /resultfile=result.txt + +!include LogicLib.nsh +!include FileFunc.nsh + +OutFile "postdriver.exe" +RequestExecutionLevel user +ShowInstDetails show +Unicode true + +!addplugindir ..\..\..\Plugins + +Var PostFileArg +Var UrlArg +Var ResultFileArg +Var ResultString + +Section + +StrCpy $ResultString "error getting command line arguments" + +ClearErrors +${GetParameters} $0 +IfErrors done + +ClearErrors +${GetOptions} " $0" " /postfile=" $PostFileArg +IfErrors done + +${GetOptions} " $0" " /url=" $UrlArg +IfErrors done + +${GetOptions} " $0" " /resultfile=" $ResultFileArg +IfErrors done + +DetailPrint "POST File = $PostFileArg" +DetailPrint "URL = $UrlArg" +DetailPrint "Result File = $ResultFileArg" + +StrCpy $ResultString "error running plugin" +HttpPostFile::Post $PostFileArg "Content-Type: application/json$\r$\n" $UrlArg +Pop $ResultString + +done: +${If} $ResultString != "success" +DetailPrint $ResultString +${EndIf} + +ClearErrors +FileOpen $0 $ResultFileArg "w" +${Unless} ${Errors} +FileWrite $0 $ResultString +FileClose $0 +${EndUnless} + +SectionEnd diff --git a/other-licenses/nsis/Contrib/HttpPostFile/test/unittest.py b/other-licenses/nsis/Contrib/HttpPostFile/test/unittest.py new file mode 100644 index 0000000000..2d5cd94d26 --- /dev/null +++ b/other-licenses/nsis/Contrib/HttpPostFile/test/unittest.py @@ -0,0 +1,247 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +# Unit test for the HttpPostFile plugin, using a server on localhost. +# +# This test has not been set up to run in continuous integration. It is +# intended to be run manually, and only on Windows. +# +# Requires postdriver.exe, which can be built from postdriver.nsi with makensis +# from MozillaBuild: +# +# makensis-3.01.exe postdriver.nsi +# +# It can then be run from this directory as: +# +# python3 test.py + +import os +import subprocess +import http.server +import socketserver +import threading + +DRIVER_EXE_FILE_NAME = "postdriver.exe" +JSON_FILE_NAME = "test1.json" +RESULT_FILE_NAME = "result.txt" +BIND_HOST = "127.0.0.1" +BIND_PORT = 8080 +COMMON_URL = f"http://{BIND_HOST}:{BIND_PORT}/submit" +COMMON_JSON_BYTES = '{"yes": "indeed",\n"and": "ij"}'.encode('utf-8') + +DRIVER_TIMEOUT_SECS = 60 +SERVER_TIMEOUT_SECS = 120 + + +class PostHandler(http.server.BaseHTTPRequestHandler): + """BaseHTTPRequestHandler, basically just here to have a configurable do_POST handler""" + + +last_submission = None +last_content_type = None +server_response = 'Hello, plugin'.encode('utf-8') + + +def server_accept_submit(handler): + """Plugs into PostHandler.do_POST, accepts a POST on /submit and saves it into + the globals""" + + global last_submission + global last_content_type + global server_response + + last_submission = None + last_content_type = None + + if handler.path == "/submit": + handler.send_response(200, 'Ok') + content_length = int(handler.headers['Content-Length']) + last_submission = handler.rfile.read(content_length) + last_content_type = handler.headers['Content-Type'] + else: + handler.send_response(404, 'Not found') + handler.end_headers() + + handler.wfile.write(server_response) + handler.wfile.flush() + + handler.log_message("sent response") + + +server_hang_event = None + + +def server_hang(handler): + """Plugs into PostHandler.do_POST, waits on server_hang_event or until timeout""" + server_hang_event.wait(SERVER_TIMEOUT_SECS) + + +def run_and_assert_result(handle_request, post_file, url, expected_result): + """Sets up the server on another thread, runs the NSIS driver, and checks the result""" + global last_submission + global server_hang_event + + try: + os.remove(RESULT_FILE_NAME) + except FileNotFoundError: + pass + + PostHandler.do_POST = handle_request + last_submission = None + + def handler_thread(): + with socketserver.TCPServer((BIND_HOST, BIND_PORT), PostHandler) as httpd: + httpd.timeout = SERVER_TIMEOUT_SECS + httpd.handle_request() + + if handle_request: + server_thread = threading.Thread(target=handler_thread) + server_thread.start() + + try: + subprocess.call([DRIVER_EXE_FILE_NAME, f'/postfile={post_file}', f'/url={url}', + f'/resultfile={RESULT_FILE_NAME}', '/S'], timeout=DRIVER_TIMEOUT_SECS) + + with open(RESULT_FILE_NAME, "r") as result_file: + result = result_file.read() + + if result != expected_result: + raise AssertionError(f'{result} != {expected_result}') + + finally: + if server_hang_event: + server_hang_event.set() + + if handle_request: + server_thread.join() + os.remove(RESULT_FILE_NAME) + + +def create_json_file(json_bytes=COMMON_JSON_BYTES): + with open(JSON_FILE_NAME, "wb") as outfile: + outfile.write(json_bytes) + + +def check_submission(json_bytes=COMMON_JSON_BYTES): + if last_submission != json_bytes: + raise AssertionError(f'{last_submission.hex()} != {COMMON_JSON_BYTES}') + + +def cleanup_json_file(): + os.remove(JSON_FILE_NAME) + + +# Tests begin here + +try: + cleanup_json_file() +except FileNotFoundError: + pass + +# Basic test + +create_json_file() +run_and_assert_result(server_accept_submit, JSON_FILE_NAME, COMMON_URL, "success") +check_submission() +assert last_content_type == 'application/json' +cleanup_json_file() + +print("Basic test OK\n") + +# Test with missing file + +try: + cleanup_json_file() +except FileNotFoundError: + pass + +run_and_assert_result(None, JSON_FILE_NAME, COMMON_URL, "error reading file") + +print("Missing file test OK\n") + +# Test with empty file + +create_json_file(bytes()) +run_and_assert_result(server_accept_submit, JSON_FILE_NAME, COMMON_URL, "success") +check_submission(bytes()) +cleanup_json_file() + +print("Empty file test OK\n") + +# Test with large file + +# NOTE: Not actually JSON, but nothing here should care +four_mbytes = bytes([x & 255 for x in range(4*1024*1024)]) +create_json_file(four_mbytes) +run_and_assert_result(server_accept_submit, JSON_FILE_NAME, COMMON_URL, "success") +if last_submission != four_mbytes: + raise AssertionError("large file mismatch") +cleanup_json_file() + +print("Large file test OK\n") + +# Test with long file name + +# Test with bad URL + +bogus_url = "notAUrl" +create_json_file() +run_and_assert_result(None, JSON_FILE_NAME, bogus_url, "error parsing URL") +cleanup_json_file() + +print("Bad URL test OK\n") + +# Test with empty response + +server_response = bytes() +create_json_file() +run_and_assert_result(server_accept_submit, JSON_FILE_NAME, COMMON_URL, "success") +check_submission() +cleanup_json_file() + +print("Empty response test OK\n") + +# Test with large response + +server_response = four_mbytes +create_json_file() +run_and_assert_result(server_accept_submit, JSON_FILE_NAME, COMMON_URL, "success") +check_submission() +cleanup_json_file() + +print("Large response test OK\n") + +# Test with 404 +# NOTE: This succeeds since the client doesn't check the status code + +create_json_file() +nonexistent_url = f"http://{BIND_HOST}:{BIND_PORT}/bad" +run_and_assert_result(server_accept_submit, JSON_FILE_NAME, nonexistent_url, "success") +cleanup_json_file() + +print("404 response test OK\n") + +# Test with no server on the port +# NOTE: I'm assuming nothing else has been able to bind to the port + +print("Running no server test, this will take a few seconds...") + +create_json_file() +run_and_assert_result(None, JSON_FILE_NAME, COMMON_URL, "error sending HTTP request") +cleanup_json_file() + +print("No server test OK\n") + +# Test with server that hangs on response +# NOTE: HttpPostFile doesn't currently set the timeouts. Defaults seem to be around 30 seconds, +# but if they end up being longer than the 60 second driver timeout then this will fail. + +print("Running server hang test, this will take up to a minute...") + +server_hang_event = threading.Event() +create_json_file() +run_and_assert_result(server_hang, JSON_FILE_NAME, COMMON_URL, "error sending HTTP request") +cleanup_json_file() +server_hang_event = None + +print("Server hang test OK\n") -- cgit v1.2.3