/* 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;
}