HttpPostFile.cpp (9332B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // To explain some of the oddities: 6 // This plugin avoids linking against a runtime that might not be present, thus 7 // it avoids standard library functions. 8 // NSIS requires GlobalAlloc/GlobalFree for its interfaces, and I use them for 9 // other allocations (vs e.g. HeapAlloc) for the sake of consistency. 10 11 #include <Windows.h> 12 #include <Wininet.h> 13 14 #define AGENT_NAME L"HttpPostFile plugin" 15 16 PBYTE LoadFileData(LPWSTR fileName, DWORD& cbData); 17 bool HttpPost(LPURL_COMPONENTS pUrl, LPWSTR contentTypeHeader, PBYTE data, 18 DWORD cbData); 19 20 // NSIS API 21 typedef struct _stack_t { 22 struct _stack_t* next; 23 WCHAR text[1]; 24 } stack_t; 25 26 // Unlink and return the topmost element of the stack, if any. 27 static stack_t* popstack(stack_t** stacktop) { 28 if (!stacktop || !*stacktop) return nullptr; 29 stack_t* element = *stacktop; 30 *stacktop = element->next; 31 element->next = nullptr; 32 return element; 33 } 34 35 // Allocate a new stack element (with space for `stringsize`), copy the string, 36 // add to the top of the stack. 37 static void pushstring(LPCWSTR str, stack_t** stacktop, 38 unsigned int stringsize) { 39 stack_t* element; 40 if (!stacktop) return; 41 42 // The allocation here has space for stringsize+1 WCHARs, because stack_t.text 43 // is 1 element long. This is consistent with the NSIS ExDLL example, though 44 // inconsistent with the comment that says the array "should be the length of 45 // g_stringsize when allocating". I'm sticking to consistency with 46 // the code, and erring towards having a larger buffer than necessary. 47 48 element = (stack_t*)GlobalAlloc( 49 GPTR, (sizeof(stack_t) + stringsize * sizeof(*str))); 50 lstrcpynW(element->text, str, stringsize); 51 element->next = *stacktop; 52 *stacktop = element; 53 } 54 55 BOOL APIENTRY DllMain(HINSTANCE instance, DWORD reason, LPVOID) { 56 // No initialization or cleanup is needed. 57 return TRUE; 58 } 59 60 extern "C" { 61 62 // HttpPostFile::Post <File> <Content-Type header with \r\n> <URL> 63 // 64 // e.g. HttpPostFile "C:\blah.json" "Content-Type: application/json$\r$\n" 65 // "https://example.com" 66 // 67 // Leaves a result string on the stack, "success" if the POST was successful, an 68 // error message otherwise. 69 // The status code from the server is not checked, as long as we got some 70 // response the result will be "success". The response is read, but discarded. 71 void __declspec(dllexport) 72 Post(HWND hwndParent, int string_size, char* /* variables */, 73 stack_t** stacktop, void* /* extra_parameters */) { 74 static const URL_COMPONENTS kZeroComponents = {0}; 75 const WCHAR* errorMsg = L"error"; 76 77 DWORD cbData = INVALID_FILE_SIZE; 78 PBYTE data = nullptr; 79 80 // Copy a constant, because initializing an automatic variable with {0} ends 81 // up linking to memset, which isn't available. 82 URL_COMPONENTS components = kZeroComponents; 83 84 // Get args, taking ownership of the strings from the stack, to avoid 85 // allocating and copying strings. 86 stack_t* postFileName = popstack(stacktop); 87 stack_t* contentTypeHeader = popstack(stacktop); 88 stack_t* url = popstack(stacktop); 89 90 if (!postFileName || !contentTypeHeader || !url) { 91 errorMsg = L"error getting arguments"; 92 goto finish; 93 } 94 95 data = LoadFileData(postFileName->text, cbData); 96 if (!data || cbData == INVALID_FILE_SIZE) { 97 errorMsg = L"error reading file"; 98 goto finish; 99 } 100 101 { 102 // This length is used to allocate for the host name and path components, 103 // which should be no longer than the source URL. 104 int urlBufLen = lstrlenW(url->text) + 1; 105 106 components.dwStructSize = sizeof(components); 107 components.dwHostNameLength = urlBufLen; 108 components.dwUrlPathLength = urlBufLen; 109 components.lpszHostName = 110 (LPWSTR)GlobalAlloc(GPTR, urlBufLen * sizeof(WCHAR)); 111 components.lpszUrlPath = 112 (LPWSTR)GlobalAlloc(GPTR, urlBufLen * sizeof(WCHAR)); 113 } 114 115 errorMsg = L"error parsing URL"; 116 if (components.lpszHostName && components.lpszUrlPath && 117 InternetCrackUrl(url->text, 0, 0, &components) && 118 (components.nScheme == INTERNET_SCHEME_HTTP || 119 components.nScheme == INTERNET_SCHEME_HTTPS)) { 120 errorMsg = L"error sending HTTP request"; 121 if (HttpPost(&components, contentTypeHeader->text, data, cbData)) { 122 // success! 123 errorMsg = nullptr; 124 } 125 } 126 127 finish: 128 if (components.lpszUrlPath) { 129 GlobalFree(components.lpszUrlPath); 130 } 131 if (components.lpszHostName) { 132 GlobalFree(components.lpszHostName); 133 } 134 if (data) { 135 GlobalFree(data); 136 } 137 138 // Free args taken from the NSIS stack 139 if (url) { 140 GlobalFree(url); 141 } 142 if (contentTypeHeader) { 143 GlobalFree(contentTypeHeader); 144 } 145 if (postFileName) { 146 GlobalFree(postFileName); 147 } 148 149 if (errorMsg) { 150 pushstring(errorMsg, stacktop, string_size); 151 } else { 152 pushstring(L"success", stacktop, string_size); 153 } 154 } 155 } 156 157 // Returns buffer with file contents on success, placing the size in cbData. 158 // Returns nullptr on failure. 159 // Caller must use GlobalFree() on the returned buffer if non-null. 160 PBYTE LoadFileData(LPWSTR fileName, DWORD& cbData) { 161 bool success = false; 162 163 HANDLE hPostFile = INVALID_HANDLE_VALUE; 164 165 PBYTE data = nullptr; 166 167 DWORD bytesRead; 168 DWORD bytesReadTotal; 169 170 hPostFile = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, nullptr, 171 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); 172 if (hPostFile == INVALID_HANDLE_VALUE) { 173 goto finish; 174 } 175 176 cbData = GetFileSize(hPostFile, NULL); 177 if (cbData == INVALID_FILE_SIZE) { 178 goto finish; 179 } 180 181 data = (PBYTE)GlobalAlloc(GPTR, cbData); 182 if (!data) { 183 goto finish; 184 } 185 186 bytesReadTotal = 0; 187 do { 188 if (!ReadFile(hPostFile, data + bytesReadTotal, cbData - bytesReadTotal, 189 &bytesRead, nullptr /* overlapped */)) { 190 goto finish; 191 } 192 bytesReadTotal += bytesRead; 193 } while (bytesReadTotal < cbData && bytesRead > 0); 194 195 if (bytesReadTotal == cbData) { 196 success = true; 197 } 198 199 finish: 200 if (!success) { 201 if (data) { 202 GlobalFree(data); 203 data = nullptr; 204 } 205 cbData = INVALID_FILE_SIZE; 206 } 207 if (hPostFile != INVALID_HANDLE_VALUE) { 208 CloseHandle(hPostFile); 209 hPostFile = INVALID_HANDLE_VALUE; 210 } 211 212 return data; 213 } 214 215 // Returns true on success 216 bool HttpPost(LPURL_COMPONENTS pUrl, LPWSTR contentTypeHeader, PBYTE data, 217 DWORD cbData) { 218 bool success = false; 219 220 HINTERNET hInternet = nullptr; 221 HINTERNET hConnect = nullptr; 222 HINTERNET hRequest = nullptr; 223 224 hInternet = InternetOpen(AGENT_NAME, INTERNET_OPEN_TYPE_PRECONFIG, 225 nullptr, // proxy 226 nullptr, // proxy bypass 227 0 // flags 228 ); 229 if (!hInternet) { 230 goto finish; 231 } 232 233 hConnect = InternetConnect(hInternet, pUrl->lpszHostName, pUrl->nPort, 234 nullptr, // userName, 235 nullptr, // password 236 INTERNET_SERVICE_HTTP, 237 0, // flags 238 0 // context 239 ); 240 if (!hConnect) { 241 goto finish; 242 } 243 244 { 245 // NOTE: Some of these settings are perhaps unnecessary for a POST. 246 DWORD httpFlags = INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_COOKIES | 247 INTERNET_FLAG_NO_UI | INTERNET_FLAG_RELOAD; 248 if (pUrl->nScheme == INTERNET_SCHEME_HTTPS) { 249 // NOTE: nsJSON sets flags to allow redirecting HTTPS to HTTP, or HTTP to 250 // HTTPS I left those out because it seemed undesirable for our use case. 251 httpFlags |= INTERNET_FLAG_SECURE; 252 } 253 hRequest = HttpOpenRequest(hConnect, L"POST", pUrl->lpszUrlPath, 254 nullptr, // version, 255 nullptr, // referrer 256 nullptr, // accept types 257 httpFlags, 258 0 // context 259 ); 260 if (!hRequest) { 261 goto finish; 262 } 263 } 264 265 if (contentTypeHeader) { 266 if (!HttpAddRequestHeaders(hRequest, contentTypeHeader, 267 -1L, // headers length (count string length) 268 HTTP_ADDREQ_FLAG_ADD)) { 269 goto finish; 270 } 271 } 272 273 if (!HttpSendRequestW(hRequest, 274 nullptr, // additional headers 275 0, // headers length 276 data, cbData)) { 277 goto finish; 278 } 279 280 BYTE readBuffer[1024]; 281 DWORD bytesRead; 282 do { 283 if (!InternetReadFile(hRequest, readBuffer, sizeof(readBuffer), 284 &bytesRead)) { 285 goto finish; 286 } 287 // read data is thrown away 288 } while (bytesRead > 0); 289 290 success = true; 291 292 finish: 293 if (hRequest) { 294 InternetCloseHandle(hRequest); 295 } 296 if (hConnect) { 297 InternetCloseHandle(hConnect); 298 } 299 if (hInternet) { 300 InternetCloseHandle(hInternet); 301 } 302 303 return success; 304 }