// Copyright 2019 Google LLC
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google LLC nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <assert.h>
#include <stdio.h>
#include <Windows.h>
#include <WinInet.h>

#include <vector>

#include "tools/windows/converter_exe/http_download.h"
#include "tools/windows/converter_exe/winhttp_client.h"
#include "tools/windows/converter_exe/wininet_client.h"

namespace crash {
static const std::vector<char>::size_type kVectorChunkSize = 4096;  // 4 KB

using std::vector;

// Class that atuo closes the contained HttpHandle when the object
// goes out of scope.
class AutoHttpHandle {
 public:
  AutoHttpHandle() : handle_(NULL) {}
  explicit AutoHttpHandle(HttpHandle handle) : handle_(handle) {}
  ~AutoHttpHandle() {
    if (handle_) {
      InternetCloseHandle(handle_);
    }
  }

  HttpHandle get() { return handle_; }
  HttpHandle* get_handle_addr () { return &handle_; }

 private:
  HttpHandle handle_;
};

// Template class for auto releasing the contained pointer when
// the object goes out of scope.
template<typename T>
class AutoPtr {
 public:
  explicit AutoPtr(T* ptr) : ptr_(ptr) {}
  ~AutoPtr() {
    if (ptr_) {
      delete ptr_;
    }
  }

  T* get() { return ptr_; }
  T* operator -> () { return ptr_; }

 private:
  T* ptr_;
};

// CheckParameters ensures that the parameters in |parameters| are safe for
// use in an HTTP URL.  Returns true if they are, false if unsafe characters
// are present.
static bool CheckParameters(const map<wstring, wstring>* parameters) {
  for (map<wstring, wstring>::const_iterator iterator = parameters->begin();
       iterator != parameters->end();
       ++iterator) {
    const wstring& key = iterator->first;
    if (key.empty()) {
      // Disallow empty parameter names.
      return false;
    }
    for (unsigned int i = 0; i < key.size(); ++i) {
      wchar_t c = key[i];
      if (c < 32 || c == '"' || c == '?' || c == '&' || c > 127) {
        return false;
      }
    }

    const wstring& value = iterator->second;
    for (unsigned int i = 0; i < value.size(); ++i) {
      wchar_t c = value[i];
      if (c < 32 || c == '"' || c == '?' || c == '&' || c > 127) {
        return false;
      }
    }
  }

  return true;
}

HttpClient* HTTPDownload::CreateHttpClient(const wchar_t* url) {
  const TCHAR* kHttpApiPolicyEnvironmentVariable = TEXT("USE_WINHTTP");
  TCHAR buffer[2] = {0};
  HttpClient* http_client = NULL;

  if (::GetEnvironmentVariable(kHttpApiPolicyEnvironmentVariable,
                               buffer,
                               sizeof(buffer)/sizeof(buffer[0])) > 0) {
    fprintf(stdout,
            "Environment variable [%ws] is set, use WinHttp\n",
            kHttpApiPolicyEnvironmentVariable);
    http_client = CreateWinHttpClient(url);
    if (http_client == NULL) {
      fprintf(stderr, "WinHttpClient not created, Is the protocol HTTPS? "
                      "Fall back to WinInet API.\n");
    }
  } else {
    fprintf(stderr,
            "Environment variable [%ws] is NOT set, use WinInet API\n",
            kHttpApiPolicyEnvironmentVariable);
  }

  if (http_client == NULL) {
    return CreateWinInetClient(url);
  }

  return http_client;
}

// static
bool HTTPDownload::Download(const wstring& url,
                            const map<wstring, wstring>* parameters,
                            string *content, int *status_code) {
  assert(content);
  AutoPtr<HttpClient> http_client(CreateHttpClient(url.c_str()));

  if (!http_client.get()) {
    fprintf(stderr, "Failed to create any http client.\n");
    return false;
  }

  if (status_code) {
    *status_code = 0;
  }

  wchar_t scheme[16] = {0};
  wchar_t host[256] = {0};
  wchar_t path[256] = {0};
  int port = 0;
  if (!http_client->CrackUrl(url.c_str(),
                             0,
                             scheme,
                             sizeof(scheme)/sizeof(scheme[0]),
                             host,
                             sizeof(host)/sizeof(host[0]),
                             path,
                             sizeof(path)/sizeof(path[0]),
                             &port)) {
    fprintf(stderr,
            "HTTPDownload::Download: InternetCrackUrl: error %lu for %ws\n",
            GetLastError(), url.c_str());
    return false;
  }

  bool secure = false;
  if (_wcsicmp(scheme, L"https") == 0) {
    secure = true;
  } else if (wcscmp(scheme, L"http") != 0) {
    fprintf(stderr,
            "HTTPDownload::Download: scheme must be http or https for %ws\n",
            url.c_str());
    return false;
  }

  AutoHttpHandle internet;
  if (!http_client->Open(NULL,  // user agent
                         HttpClient::ACCESS_TYPE_PRECONFIG,
                         NULL,  // proxy name
                         NULL,  // proxy bypass
                         internet.get_handle_addr())) {
    fprintf(stderr,
            "HTTPDownload::Download: Open: error %lu for %ws\n",
            GetLastError(), url.c_str());
    return false;
  }

  AutoHttpHandle connection;
  if (!http_client->Connect(internet.get(),
                            host,
                            port,
                            connection.get_handle_addr())) {
    fprintf(stderr,
            "HTTPDownload::Download: InternetConnect: error %lu for %ws\n",
            GetLastError(), url.c_str());
    return false;
  }

  wstring request_string = path;
  if (parameters) {
    // TODO(mmentovai): escape bad characters in parameters instead of
    // forbidding them.
    if (!CheckParameters(parameters)) {
      fprintf(stderr,
              "HTTPDownload::Download: invalid characters in parameters\n");
      return false;
    }

    bool added_parameter = false;
    for (map<wstring, wstring>::const_iterator iterator = parameters->begin();
         iterator != parameters->end();
         ++iterator) {
      request_string.append(added_parameter ? L"&" : L"?");
      request_string.append(iterator->first);
      request_string.append(L"=");
      request_string.append(iterator->second);
      added_parameter = true;
    }
  }

  AutoHttpHandle request;
  if (!http_client->OpenRequest(connection.get(),
                                L"GET",
                                request_string.c_str(),
                                NULL,    // version
                                NULL,    // referer
                                secure,
                                request.get_handle_addr())) {
    fprintf(stderr,
            "HttpClient::OpenRequest: error %lu for %ws, request: %ws\n",
            GetLastError(), url.c_str(), request_string.c_str());
    return false;
  }

  if (!http_client->SendRequest(request.get(), NULL, 0)) {
    fprintf(stderr,
            "HttpClient::SendRequest: error %lu for %ws\n",
            GetLastError(), url.c_str());
    return false;
  }

  if (!http_client->ReceiveResponse(request.get())) {
    fprintf(stderr,
            "HttpClient::ReceiveResponse: error %lu for %ws\n",
            GetLastError(), url.c_str());
    return false;
  }

  int http_status = 0;
  if (!http_client->GetHttpStatusCode(request.get(), &http_status)) {
    fprintf(stderr,
            "HttpClient::GetHttpStatusCode: error %lu for %ws\n",
            GetLastError(), url.c_str());
    return false;
  }
  if (http_status != 200) {
    fprintf(stderr,
            "HTTPDownload::Download: HTTP status code %d for %ws\n",
            http_status, url.c_str());
    return false;
  }

  DWORD content_length = 0;
  vector<char>::size_type buffer_size = 0;
  http_client->GetContentLength(request.get(), &content_length);
  if (content_length == HttpClient::kUnknownContentLength) {
    buffer_size = kVectorChunkSize;
  } else {
    buffer_size = content_length;
  }

  if (content_length != 0) {
    vector<char> response_buffer = vector<char>(buffer_size+1);
    DWORD size_read;
    DWORD total_read = 0;
    bool read_result;
    do {
      if (content_length == HttpClient::kUnknownContentLength
          && buffer_size == total_read) {
        // The content length wasn't specified in the response header, so we
        // have to keep growing the buffer until we're done reading.
        buffer_size += kVectorChunkSize;
        response_buffer.resize(buffer_size);
      }
      read_result = !!http_client->ReadData(
          request.get(),
          &response_buffer[total_read],
          static_cast<DWORD>(buffer_size) - total_read,
          &size_read);
      total_read += size_read;
    } while (read_result && (size_read != 0));

    if (!read_result) {
      fprintf(stderr,
              "HttpClient::ReadData: error %lu for %ws\n",
              GetLastError(),
              url.c_str());
      return false;
    } else if (size_read != 0) {
      fprintf(stderr,
              "HttpClient::ReadData: error %lu/%lu for %ws\n",
              total_read,
              content_length,
              url.c_str());
      return false;
    }
    content->assign(&response_buffer[0], total_read);
  } else {
    content->clear();
  }
  return true;
}

}  // namespace crash
