summaryrefslogtreecommitdiffstats
path: root/other-licenses/nsis/Contrib/HttpPostFile
diff options
context:
space:
mode:
Diffstat (limited to 'other-licenses/nsis/Contrib/HttpPostFile')
-rw-r--r--other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.cpp304
-rw-r--r--other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.sln22
-rw-r--r--other-licenses/nsis/Contrib/HttpPostFile/HttpPostFile.vcxproj63
-rw-r--r--other-licenses/nsis/Contrib/HttpPostFile/test/postdriver.nsi63
-rw-r--r--other-licenses/nsis/Contrib/HttpPostFile/test/unittest.py247
5 files changed, 699 insertions, 0 deletions
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 <Windows.h>
+#include <Wininet.h>
+
+#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 <File> <Content-Type header with \r\n> <URL>
+//
+// 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <VCProjectVersion>16.0</VCProjectVersion>
+ <Keyword>Win32Proj</Keyword>
+ <ProjectGuid>{a8bf99fd-8603-4137-862a-1d14268d7812}</ProjectGuid>
+ <RootNamespace>HttpPostFile</RootNamespace>
+ <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>false</UseDebugLibraries>
+ <PlatformToolset>v142</PlatformToolset>
+ <WholeProgramOptimization>true</WholeProgramOptimization>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+ <ImportGroup Label="ExtensionSettings">
+ </ImportGroup>
+ <ImportGroup Label="Shared">
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <LinkIncremental>false</LinkIncremental>
+ </PropertyGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <ClCompile>
+ <WarningLevel>Level3</WarningLevel>
+ <FunctionLevelLinking>true</FunctionLevelLinking>
+ <IntrinsicFunctions>true</IntrinsicFunctions>
+ <PreprocessorDefinitions>WINVER=0x601;_WIN32_WINNT=0x601;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <OmitDefaultLibName>true</OmitDefaultLibName>
+ <ExceptionHandling>false</ExceptionHandling>
+ <SDLCheck>false</SDLCheck>
+ <BufferSecurityCheck>false</BufferSecurityCheck>
+ </ClCompile>
+ <Link>
+ <SubSystem>Console</SubSystem>
+ <EnableCOMDATFolding>true</EnableCOMDATFolding>
+ <OptimizeReferences>true</OptimizeReferences>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <AdditionalDependencies>wininet.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ <EntryPointSymbol>DllMain</EntryPointSymbol>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemGroup>
+ <ClCompile Include="HttpPostFile.cpp" />
+ </ItemGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+ <ImportGroup Label="ExtensionTargets">
+ </ImportGroup>
+</Project> \ 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")