// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "stdafx.h"
#include "resource.h"
#include "CommandLineArguments.h"
#include "IEServer.h"
#include <algorithm>
#include <iostream>
#include <map>
#include <iostream>
#include <string>
#include <vector>

// The prototypes for these functions must match those exported
// by the .dll produced by the IEDriver project in this solution.
// The definitions of these functions can be found in WebDriver.h
// in that project.
typedef void* (__cdecl *STARTSERVERPROC)(int, const std::wstring&, const std::wstring&, const std::wstring&, const std::wstring&, const std::wstring&);
typedef void (__cdecl *STOPSERVERPROC)(void);

#define ERR_DLL_EXTRACT_FAIL 1
#define ERR_DLL_LOAD_FAIL 2
#define ERR_FUNCTION_NOT_FOUND 3
#define ERR_SERVER_START 4

#define RESOURCE_TYPE L"BINARY"
#define TEMP_FILE_PREFIX L"IEDriver"
#define START_SERVER_EX_API_NAME "StartServer"
#define STOP_SERVER_API_NAME "StopServer"

#define PORT_COMMAND_LINE_ARG L"port"
#define HOST_COMMAND_LINE_ARG L"host"
#define LOGLEVEL_COMMAND_LINE_ARG L"log-level"
#define LOGFILE_COMMAND_LINE_ARG L"log-file"
#define SILENT_COMMAND_LINE_ARG L"silent"
#define EXTRACTPATH_COMMAND_LINE_ARG L"extract-path"
#define ACL_COMMAND_LINE_ARG L"whitelisted-ips"
#define BOOLEAN_COMMAND_LINE_ARG_MISSING_VALUE L"value-not-specified"

bool ExtractResource(unsigned short resource_id,
                     const std::wstring& output_file_name) {
  bool success = false; 
  try {
    // First find and load the required resource
    HRSRC resource_handle = ::FindResource(NULL, 
                                           MAKEINTRESOURCE(resource_id),
                                           RESOURCE_TYPE);
    HGLOBAL global_resouce_handle = ::LoadResource(NULL, resource_handle);

    // Now open and map this to a disk file
    LPVOID file_pointer = ::LockResource(global_resouce_handle);
    DWORD resource_size = ::SizeofResource(NULL, resource_handle);
 
    // Open the file and filemap
    HANDLE file_handle = ::CreateFile(output_file_name.c_str(),
                                      GENERIC_READ | GENERIC_WRITE,
                                      0,
                                      NULL,
                                      CREATE_ALWAYS, 
                                      FILE_ATTRIBUTE_NORMAL, 
                                      NULL);
    HANDLE file_mapping_handle = ::CreateFileMapping(file_handle,
                                                     NULL,
                                                     PAGE_READWRITE, 
                                                     0,
                                                     resource_size, 
                                                     NULL);
    LPVOID base_address_pointer = ::MapViewOfFile(file_mapping_handle,
                                                  FILE_MAP_WRITE,
                                                  0,
                                                  0,
                                                  0);

    // Write the file
    ::CopyMemory(base_address_pointer, file_pointer, resource_size);

    // Unmap the file and close the handles
    ::UnmapViewOfFile(base_address_pointer);
    ::CloseHandle(file_mapping_handle);
    ::CloseHandle(file_handle);
    success = true;
  } catch(...) {
    // Ignore all type of errors
  } 
  return success;
}

std::wstring GetProcessArchitectureDescription() {
  std::wstring arch_description = L"32-bit";
  SYSTEM_INFO system_info;
  ::GetNativeSystemInfo(&system_info);
  if (system_info.wProcessorArchitecture != 0) {
    BOOL is_emulated;
    HANDLE process_handle = ::GetCurrentProcess();
    ::IsWow64Process(process_handle, &is_emulated);
    if (!is_emulated) {
      arch_description = L"64-bit";
    }
    ::CloseHandle(process_handle);
  }

  return arch_description;
}

std::wstring GetExecutableVersion() {
  struct LANGANDCODEPAGE {
    WORD language;
    WORD code_page;
  } *lang_info;

  // get the filename of the executable containing the version resource
  std::vector<wchar_t> file_name_buffer(MAX_PATH + 1);
  ::GetModuleFileNameW(NULL, &file_name_buffer[0], MAX_PATH);

  DWORD dummy;
  DWORD length = ::GetFileVersionInfoSizeW(&file_name_buffer[0],
                                           &dummy);
  std::vector<BYTE> version_buffer(length);
  ::GetFileVersionInfoW(&file_name_buffer[0],
                       dummy,
                       length,
                       &version_buffer[0]);

  UINT page_count;
  BOOL query_result = ::VerQueryValueW(&version_buffer[0],
                                      L"\\VarFileInfo\\Translation",
                                      reinterpret_cast<void**>(&lang_info),
                                      &page_count);
    
  wchar_t sub_block[MAX_PATH];
  _snwprintf_s(sub_block,
               MAX_PATH,
               MAX_PATH,
               L"\\StringFileInfo\\%04x%04x\\FileVersion",
               lang_info->language,
               lang_info->code_page);
  LPVOID value = NULL;
  UINT size;
  query_result = ::VerQueryValueW(&version_buffer[0],
                                 sub_block,
                                 &value,
                                 &size);
  return static_cast<wchar_t*>(value);
}


void ShowUsage(void) {
  std::wcout << L"Launches the WebDriver server for the Internet Explorer driver" << std::endl
             << std::endl
             << L"IEDriverServer [/port=<port>] [/host=<host>] [/log-level=<level>]" << std::endl
             << L"               [/log-file=<file>] [/extract-path=<path>] [/silent]" << std::endl
             << L"               [/whitelisted-ips=<whitelisted-ips>] [/version]" << std::endl
             << std::endl
             << L"  /port=<port>  Specifies the port on which the server will listen for" << std::endl
             << L"                commands. Defaults to 5555 if not specified." << std::endl
             << L"  /host=<host>  Specifies the address of the host adapter on which the server" << std::endl
             << L"                will listen for commands." << std::endl
             << L"  /log-level=<level>" << std::endl
             << L"                Specifies the log level used by the server. Valid values are:" << std::endl
             << L"                TRACE, DEBUG, INFO, WARN, ERROR, and FATAL. Defaults to FATAL" << std::endl
             << L"                if not specified." << std::endl
             << L"  /log-file=<file>" << std::endl
             << L"                Specifies the full path and file name of the log file used by" << std::endl
             << L"                the server. Defaults logging to stdout if not specified. " << std::endl
             << L"  /extract-path=<path>" << std::endl
             << L"                Specifies the full path to the directory used to extract" << std::endl
             << L"                supporting files used by the server. Defaults to the TEMP" << std::endl
             << L"                directory if not specified." << std::endl
             << L"  /silent       Suppresses diagnostic output when the server is started." << std::endl
             << L"  /whitelisted-ips=<whitelisted-ips>" << std::endl
             << L"                Comma-separated whitelist of remote IPv4 addresses which" << std::endl
             << L"                are allowed to connect to the WebDriver server." << std::endl
             << L"  /version      Displays version information and exits. All other arguments" << std::endl
             << L"                are ignored." << std::endl;
}

int _tmain(int argc, _TCHAR* argv[]) {
  CommandLineArguments args(argc, argv);
  if (args.is_help_requested()) {
    ShowUsage();
    return 0;
  }
  vector<TCHAR> temp_file_name_buffer(MAX_PATH);
  vector<TCHAR> temp_path_buffer(MAX_PATH);

  //  Gets the temp path env string (no guarantee it's a valid path).
  unsigned long temp_path_length = ::GetTempPath(MAX_PATH,
                                                 &temp_path_buffer[0]);

  std::wstring extraction_path(&temp_path_buffer[0]);

  std::wstring extraction_path_arg = args.GetValue(EXTRACTPATH_COMMAND_LINE_ARG, L"");
  if (extraction_path_arg.size() != 0) {
    extraction_path = extraction_path_arg;
  }

  if (extraction_path.size() > 0 &&
      extraction_path[extraction_path.size() - 1] != L'\\') {
    extraction_path.append(L"\\");
  }

  std::wstring initial_file = extraction_path + TEMP_FILE_PREFIX + L".tmp";
  std::wstring temp_file_name = initial_file;
  WIN32_FIND_DATA find_file_data;
  HANDLE file_handle = ::FindFirstFile(initial_file.c_str(), &find_file_data);
  if (file_handle != INVALID_HANDLE_VALUE) {
    ::FindClose(file_handle);
    unsigned int error_code = ::GetTempFileName(extraction_path.c_str(),
                                                TEMP_FILE_PREFIX,
                                                0,
                                                &temp_file_name_buffer[0]);

    temp_file_name = &temp_file_name_buffer[0];
  }

  if (!ExtractResource(IDR_DRIVER_LIBRARY, temp_file_name)) {
    std::wcout << L"Failed to extract the library to temp directory: "
               << temp_file_name;
    return ERR_DLL_EXTRACT_FAIL;
  }

  HMODULE module_handle = ::LoadLibrary(temp_file_name.c_str());
  if (module_handle == NULL) {
    std::wcout << L"Failed to load the library from temp directory: "
               << temp_file_name;
    return ERR_DLL_LOAD_FAIL;
  }

  STARTSERVERPROC start_server_ex_proc = reinterpret_cast<STARTSERVERPROC>(
      ::GetProcAddress(module_handle, START_SERVER_EX_API_NAME));
  STOPSERVERPROC stop_server_proc = reinterpret_cast<STOPSERVERPROC>(
      ::GetProcAddress(module_handle, STOP_SERVER_API_NAME));
  if (start_server_ex_proc == NULL || stop_server_proc == NULL) {
    std::wcout << L"Could not find entry point in extracted library: "
               << temp_file_name;
    return ERR_FUNCTION_NOT_FOUND;
  }

  int port = _wtoi(args.GetValue(PORT_COMMAND_LINE_ARG, L"5555").c_str());
  std::wstring host_address = args.GetValue(HOST_COMMAND_LINE_ARG, L"");
  std::wstring log_level = args.GetValue(LOGLEVEL_COMMAND_LINE_ARG, L"");
  std::wstring log_file = args.GetValue(LOGFILE_COMMAND_LINE_ARG, L"");
  bool silent = args.GetValue(SILENT_COMMAND_LINE_ARG,
      BOOLEAN_COMMAND_LINE_ARG_MISSING_VALUE).size() == 0;
  std::wstring executable_version = GetExecutableVersion();
  std::wstring executable_architecture = GetProcessArchitectureDescription();
  std::wstring implementation = L"";
  std::wstring whitelist = args.GetValue(ACL_COMMAND_LINE_ARG, L"");

  // coerce log level and implementation to uppercase, making the values
  // case-insensitive, to match expected values.
  std::transform(log_level.begin(),
                 log_level.end(),
                 log_level.begin(),
                 toupper);
  std::transform(implementation.begin(),
                 implementation.end(),
                 implementation.begin(),
                 toupper);

  if (args.is_version_requested()) {
    std::wcout << L"IEDriverServer.exe"
               << L" " << executable_version
               << L" (" << executable_architecture << L")" << std::endl;
  } else {
    void* server_value = start_server_ex_proc(port,
                                              host_address,
                                              log_level,
                                              log_file,
                                              executable_version + L" (" + executable_architecture + L")",
                                              whitelist);
    if (server_value == NULL) {
      std::wcout << L"Failed to start the server with: "
                 << L"port = '" << port << L"', "
                 << L"host = '" << host_address << L"', "
                 << L"log level = '" << log_level << L"', "
                 << L"log file = '" << log_file << L"', "
                 << L"whitelisted ips = '" << whitelist << L"'.";
      return ERR_SERVER_START;
    }
    if (!silent) {
      std::wcout << L"Started InternetExplorerDriver server"
                 << L" (" << executable_architecture << L")"
                 << std::endl;
      std::wcout << executable_version
                 << std::endl;
      std::wcout << L"Listening on port " << port << std::endl;
      if (host_address.size() > 0) {
        std::wcout << L"Bound to network adapter with IP address " 
                   << host_address
                   << std::endl;
      }
      if (log_level.size() > 0) {
        std::wcout << L"Log level is set to "
                   << log_level
                   << std::endl;
      }
      if (log_file.size() > 0) {
        std::wcout << L"Log file is set to "
                   << log_file
                   << std::endl;
      }
      if (extraction_path_arg.size() > 0) {
        std::wcout << L"Library extracted to "
                   << extraction_path_arg
                   << std::endl;
      }
      if (whitelist.size() > 0) {
        std::wcout << L"IP addresses allowed to connect are "
                   << whitelist
                   << std::endl;
      } else {
        std::wcout << L"Only local connections are allowed"
                   << std::endl;
      }
    }

    // Create the shutdown event and wait for it to be signaled.
    DWORD process_id = ::GetCurrentProcessId();
    vector<wchar_t> process_id_buffer(10);
    _ltow_s(process_id, &process_id_buffer[0], process_id_buffer.size(), 10);
    std::wstring process_id_string(&process_id_buffer[0]);
    std::wstring event_name = IESERVER_SHUTDOWN_EVENT_NAME + process_id_string;
    HANDLE event_handle = ::CreateEvent(NULL,
                                        TRUE, 
                                        FALSE,
                                        event_name.c_str());
    ::WaitForSingleObject(event_handle, INFINITE);
    ::CloseHandle(event_handle);
    stop_server_proc();
  }

  ::FreeLibrary(module_handle);
  ::DeleteFile(temp_file_name.c_str());
  return 0;
}
