304 lines
No EOL
9.1 KiB
C++
304 lines
No EOL
9.1 KiB
C++
/* 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;
|
|
} |