// Copyright 2010-2021, Google Inc.
// All rights reserved.
//
// 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 Inc. 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.

#ifdef _WIN32

#include "win32/cache_service/cache_service_manager.h"

// clang-format off
#include <windows.h>
#include <wincrypt.h>
// clang-format on

#include <cassert>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "absl/log/log.h"
#include "absl/types/span.h"
#include "base/const.h"
#include "base/file_util.h"
#include "base/protobuf/message.h"
#include "base/system_util.h"
#include "base/win32/wide_char.h"
#include "win32/cache_service/win32_service_state.pb.h"

namespace mozc {
namespace {

using win32::Utf8ToWide;

constexpr uint64_t kMinimumRequiredMemorySizeForInstall = 384 * 1024 * 1024;

class ScopedSCHandle {
 public:
  ScopedSCHandle() : handle_(nullptr) {}
  explicit ScopedSCHandle(SC_HANDLE handle) : handle_(handle) {}

  ScopedSCHandle(const ScopedSCHandle &) = delete;
  ScopedSCHandle &operator=(const ScopedSCHandle &) = delete;

  ScopedSCHandle(ScopedSCHandle &&other) noexcept : handle_(other.handle_) {
    other.handle_ = nullptr;
  }
  ScopedSCHandle &operator=(ScopedSCHandle &&other) noexcept {
    using std::swap;
    swap(handle_, other.handle_);
    return *this;
  }

  ~ScopedSCHandle() {
    if (nullptr != handle_) {
      ::CloseServiceHandle(handle_);
    }
  }

  SC_HANDLE get() const { return handle_; }
  explicit operator bool() const { return handle_ != nullptr; }

 private:
  SC_HANDLE handle_;
};

// This function serializes a given message (any type of protobuf message)
// as a std::wstring encoded by base64.
// Returns true if succeeds.
bool SerializeToBase64WString(const mozc::protobuf::Message &message,
                              std::wstring *dest) {
  if (dest == nullptr) {
    return false;
  }

  const int serialized_len = message.ByteSize();
  std::unique_ptr<BYTE[]> serialized(new BYTE[serialized_len]);
  if (!message.SerializeToArray(serialized.get(), serialized_len)) {
    LOG(ERROR) << "SerializeAsArray failed";
    return false;
  }

  DWORD base64_string_len = 0;
  BOOL result =
      ::CryptBinaryToString(serialized.get(), serialized_len,
                            CRYPT_STRING_BASE64, nullptr, &base64_string_len);
  if (result == FALSE) {
    return false;
  }
  std::unique_ptr<wchar_t[]> base64_string(new wchar_t[base64_string_len]);
  result = ::CryptBinaryToString(serialized.get(), serialized_len,
                                 CRYPT_STRING_BASE64, base64_string.get(),
                                 &base64_string_len);
  if (result == FALSE) {
    return false;
  }
  dest->assign(base64_string.get(), base64_string_len);
  return true;
}

// This function deserializes a message (any type of protobuf message)
// from the given std::wstring.  This wstring should be generated by
// SerializeToBase64WString.
// Returns true if succeeds
bool DeserializeFromBase64WString(const std::wstring &src,
                                  mozc::protobuf::Message *message) {
  if (message == nullptr) {
    return false;
  }

  DWORD buffer_len = 0;
  BOOL result =
      ::CryptStringToBinary(src.c_str(), src.size(), CRYPT_STRING_BASE64,
                            nullptr, &buffer_len, nullptr, nullptr);
  if (result == FALSE) {
    return false;
  }
  std::unique_ptr<BYTE[]> buffer(new BYTE[buffer_len]);
  result = ::CryptStringToBinary(src.c_str(), src.size(), CRYPT_STRING_BASE64,
                                 buffer.get(), &buffer_len, nullptr, nullptr);
  if (result == FALSE) {
    return false;
  }
  if (!message->ParseFromArray(buffer.get(), buffer_len)) {
    LOG(ERROR) << "ParseFromArray failed";
    return false;
  }
  return true;
}

// A helper function to retrieve a service handle of the cache service
// with specified access rights.
// Returns true if one of the following conditions is satisfied.
//  - A valid service handle is retrieved.  In this case, |handle| owns the
//    retrieved handle.
//  - It turns out w/o any error that the cache service is not installed.
//    In this case, |handle| owns a nullptr handle.
// Otherwise, returns false.
bool GetCacheService(DWORD service_controller_rights, DWORD service_rights,
                     ScopedSCHandle *handle) {
  if (handle == nullptr) {
    return false;
  }

  ScopedSCHandle sc_handle(
      ::OpenSCManager(nullptr, nullptr, service_controller_rights));
  if (!sc_handle) {
    LOG(ERROR) << "OpenSCManager failed: " << ::GetLastError();
    return false;
  }

  ScopedSCHandle service_handle(::OpenService(
      sc_handle.get(), CacheServiceManager::GetServiceName(), service_rights));
  const int error = ::GetLastError();

  if (!service_handle && ERROR_SERVICE_DOES_NOT_EXIST != error) {
    LOG(ERROR) << "OpenService failed: " << error;
    return false;
  }

  // |service_handle| is nullptr if and only if the cache service is not
  // installed.
  *handle = std::move(service_handle);
  return true;
}

bool IsServiceRunning(const ScopedSCHandle &service_handle) {
  if (!service_handle) {
    return false;
  }

  SERVICE_STATUS service_status = {};
  if (!::QueryServiceStatus(service_handle.get(), &service_status)) {
    LOG(ERROR) << "QueryServiceStatus failed: " << ::GetLastError();
    return false;
  }

  return service_status.dwCurrentState == SERVICE_RUNNING;
}

bool StartServiceInternal(const ScopedSCHandle &service_handle,
                          absl::Span<const std::wstring> arguments) {
  if (arguments.empty()) {
    if (!::StartService(service_handle.get(), 0, nullptr)) {
      LOG(ERROR) << "StartService failed: " << ::GetLastError();
      return false;
    }
    return true;
  }

  std::unique_ptr<const wchar_t *[]> args(
      new const wchar_t *[arguments.size()]);
  for (size_t i = 0; i < arguments.size(); ++i) {
    args[i] = arguments[i].c_str();
  }
  if (!::StartService(service_handle.get(), arguments.size(), args.get())) {
    LOG(ERROR) << "StartService failed: " << ::GetLastError();
    return false;
  }

  return true;
}

bool StopService(const ScopedSCHandle &service_handle) {
  SERVICE_STATUS status;
  if (!::ControlService(service_handle.get(), SERVICE_CONTROL_STOP, &status)) {
    LOG(ERROR) << "ControlService failed: " << ::GetLastError();
    return false;
  }
  return true;
}

// Set *nice-to-have* features for the cache service.
// We do not care about any failure because it is not critical for the
// functionality of the cache service itself.
void SetAdvancedConfig(const ScopedSCHandle &service_handle) {
  // Windows Vista or later has some nice features as described in the
  // following documents.
  // http://msdn.microsoft.com/en-us/magazine/cc164252.aspx

  // Enable "Delayed Auto-start".
  {
    SERVICE_DELAYED_AUTO_START_INFO info = {};
    info.fDelayedAutostart = TRUE;

    if (!::ChangeServiceConfig2(service_handle.get(),
                                SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
                                &info)) {
      LOG(ERROR) << "ChangeServiceConfig2 failed: " << ::GetLastError();
    }
  }

  // http://blogs.technet.com/richard_macdonald/archive/2007/06/27/1375523.aspx
  // See also http://b/2470180

  // Only SE_INC_BASE_PRIORITY_NAME and SE_CHANGE_NOTIFY are needed.
  // According to MSDN Library, we need not explicitly specify the latter.
  // See http://msdn.microsoft.com/en-us/library/ms685976.aspx for details.
  {
    SERVICE_REQUIRED_PRIVILEGES_INFO privileges_info = {};
    // |SERVICE_REQUIRED_PRIVILEGES_INFO::pmszRequiredPrivileges| needs to be
    // terminated with two L'\0's.
    std::wstring required_privileges(SE_INC_BASE_PRIORITY_NAME);
    required_privileges.push_back(L'\0');
    required_privileges.push_back(L'\0');
    // |SERVICE_REQUIRED_PRIVILEGES_INFO::pmszRequiredPrivileges| needs to be
    // writable for some reasons.
    privileges_info.pmszRequiredPrivileges = required_privileges.data();
    if (!::ChangeServiceConfig2(service_handle.get(),
                                SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO,
                                &privileges_info)) {
      LOG(ERROR) << "ChangeServiceConfig2 failed: " << ::GetLastError();
    }
  }

  // Remove write privileges from the cache service.
  // See http://msdn.microsoft.com/en-us/library/ms685987.aspx for details.
  // This may restrict glog functions such as LOG(ERROR).
  // TODO(yukawa): Output logging messages as debug strings, or output them
  // to the Win32 event log.
  {
    SERVICE_SID_INFO sid_info = {};
    sid_info.dwServiceSidType = SERVICE_SID_TYPE_RESTRICTED;
    if (!::ChangeServiceConfig2(service_handle.get(),
                                SERVICE_CONFIG_SERVICE_SID_INFO, &sid_info)) {
      LOG(ERROR) << "ChangeServiceConfig2 failed: " << ::GetLastError();
    }
  }
}

// This function updates the following settings of the cache service as
// specified.
//  - Display name
//  - Desctiption
//  - Load type (regardless of the amount of the system memory)
// This function also starts or stops the cache service.
// Win32ServiceState::IsInstalled() will be ignored.  You cannot use this
// function to install or uninstall the cache service.
bool RestoreStateInternal(const cache_service::Win32ServiceState &state) {
  ScopedSCHandle service_handle;
  constexpr DWORD kSCRights = SC_MANAGER_CONNECT;
  constexpr DWORD kServiceRights =
      GENERIC_READ | GENERIC_WRITE | SERVICE_START | SERVICE_STOP;
  if (!GetCacheService(kSCRights, kServiceRights, &service_handle) ||
      !service_handle) {
    return false;
  }

  if (!::ChangeServiceConfig(service_handle.get(), SERVICE_NO_CHANGE,
                             static_cast<DWORD>(state.load_type()),
                             SERVICE_NO_CHANGE, nullptr, nullptr, nullptr,
                             nullptr, nullptr, nullptr, nullptr)) {
    LOG(ERROR) << "ChangeServiceConfig failed: " << ::GetLastError();
    return false;
  }

  // We ignore any failure of advanced configurations because they are
  // *nice-to-have* features.
  SetAdvancedConfig(service_handle);

  const bool now_running = IsServiceRunning(service_handle);

  // If the service was running and is not running now, start it unless the
  // load type is not DISABLED.
  if (state.running() && !now_running &&
      (state.load_type() != cache_service::Win32ServiceState::DISABLED)) {
    std::vector<std::wstring> arguments(state.arguments_size());
    arguments.resize(state.arguments_size());
    for (size_t i = 0; i < state.arguments_size(); ++i) {
      arguments[i] = Utf8ToWide(state.arguments(i));
      if (arguments[i].empty()) {
        return false;
      }
    }
    return StartServiceInternal(service_handle, arguments);
    // If the service is now running and was not running, stop it.
  } else if (!state.running() && now_running) {
    return StopService(service_handle);
    // Nothing to do.
  } else {
    return true;
  }

  assert(0);
  return false;
}

// Returns true if a byte array which contains valid QUERY_SERVICE_CONFIG
// is successfully retrieved.  Some members of QUERY_SERVICE_CONFIG points
// memory addresses inside the retrieved byte array.
bool GetServiceConfig(const ScopedSCHandle &service_handle,
                      std::unique_ptr<char[]> *result) {
  if (result == nullptr) {
    return false;
  }

  if (!service_handle) {
    return false;
  }

  DWORD size = 0;
  if (!::QueryServiceConfig(service_handle.get(), nullptr, 0, &size) &&
      ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
    LOG(ERROR) << "QueryServiceConfig failed: " << ::GetLastError();
    return false;
  }

  if (size == 0) {
    LOG(ERROR) << "buffer size is 0";
    return false;
  }

  std::unique_ptr<char[]> buf(new char[size]);
  LPQUERY_SERVICE_CONFIG service_config =
      reinterpret_cast<LPQUERY_SERVICE_CONFIG>(buf.get());

  if (!::QueryServiceConfig(service_handle.get(), service_config, size,
                            &size)) {
    LOG(ERROR) << "QueryServiceConfig failed: " << ::GetLastError();
    return false;
  }

  *result = std::move(buf);
  return true;
}
}  // namespace

bool CacheServiceManager::IsInstalled() {
  ScopedSCHandle service_handle;
  if (!GetCacheService(SC_MANAGER_CONNECT | GENERIC_READ, SERVICE_QUERY_STATUS,
                       &service_handle) ||
      !service_handle) {
    return false;
  }
  return true;
}

bool CacheServiceManager::IsRunning() {
  ScopedSCHandle service_handle;
  if (!GetCacheService(SC_MANAGER_CONNECT | GENERIC_READ, SERVICE_QUERY_STATUS,
                       &service_handle) ||
      !service_handle) {
    return false;
  }
  return IsServiceRunning(service_handle);
}

bool CacheServiceManager::IsEnabled() {
  ScopedSCHandle service_handle;
  if (!GetCacheService(SC_MANAGER_CONNECT | GENERIC_READ, SERVICE_QUERY_CONFIG,
                       &service_handle) ||
      !service_handle) {
    return false;
  }

  // This byte array will contain both QUERY_SERVICE_CONFIG and some
  // string buffers which are referred by corresponding members of
  // QUERY_SERVICE_CONFIG.  We have to keep the array until we complete
  // all tasks which use the contents of QUERY_SERVICE_CONFIG.
  std::unique_ptr<char[]> buffer;
  if (!GetServiceConfig(service_handle, &buffer)) {
    return false;
  }
  const LPQUERY_SERVICE_CONFIG service_config =
      reinterpret_cast<const LPQUERY_SERVICE_CONFIG>(buffer.get());
  return service_config->dwStartType == SERVICE_AUTO_START;
}

const wchar_t *CacheServiceManager::GetServiceName() {
  return kMozcCacheServiceName;
}

std::wstring CacheServiceManager::GetUnquotedServicePath() {
  const std::string lock_service_path = FileUtil::JoinPath(
      SystemUtil::GetServerDirectory(), kMozcCacheServiceExeName);
  return Utf8ToWide(lock_service_path);
}

std::wstring CacheServiceManager::GetQuotedServicePath() {
  return L"\"" + GetUnquotedServicePath() + L"\"";
}

bool CacheServiceManager::EnableAutostart() {
  if (!IsInstalled()) {
    return false;
  }

  const bool enough = CacheServiceManager::HasEnoughMemory();

  // Fortunately, the expected service settings can be descried by
  // Win32ServiceState so we can use RestoreStateInternal to change the
  // settings and start the service (if needed).
  // If the system does not have enough memory, disable the service
  // to reduce the performance impact on the boot-time.
  cache_service::Win32ServiceState state;
  state.set_version(1);
  state.set_installed(true);
  state.set_load_type(enough ? cache_service::Win32ServiceState::AUTO_START
                             : cache_service::Win32ServiceState::DISABLED);
  state.set_running(enough);
  const bool result = RestoreStateInternal(state);
  return enough ? result : false;
}

bool CacheServiceManager::DisableService() {
  if (!IsInstalled()) {
    return false;
  }

  // Fortunately, the expected service settings can be descried by
  // Win32ServiceState so we can use RestoreStateInternal to change the
  // settings and stop the service (if needed).
  cache_service::Win32ServiceState state;
  state.set_version(1);
  state.set_installed(true);
  state.set_load_type(cache_service::Win32ServiceState::DISABLED);
  state.set_running(false);
  return RestoreStateInternal(state);
}

bool CacheServiceManager::RestartService() {
  ScopedSCHandle service_handle;
  if (!GetCacheService(SC_MANAGER_CONNECT,
                       SERVICE_START | SERVICE_STOP | SERVICE_QUERY_STATUS,
                       &service_handle) ||
      !service_handle) {
    return false;
  }

  SERVICE_STATUS status;
  if (!::ControlService(service_handle.get(), SERVICE_CONTROL_STOP, &status)) {
    LOG(ERROR) << "ControlService failed: " << ::GetLastError();
  }

  constexpr int kNumTrial = 10;
  for (int i = 0; i < kNumTrial; ++i) {
    SERVICE_STATUS service_status = {};
    if (!::QueryServiceStatus(service_handle.get(), &service_status)) {
      LOG(ERROR) << "QueryServiceStatus failed: " << ::GetLastError();
      return false;
    }
    if (service_status.dwCurrentState != SERVICE_RUNNING) {
      break;
    }
    ::Sleep(200);  // wait for 0.2sec
  }

  return StartServiceInternal(service_handle, std::vector<std::wstring>());
}

bool CacheServiceManager::HasEnoughMemory() {
  return SystemUtil::GetTotalPhysicalMemory() >=
         kMinimumRequiredMemorySizeForInstall;
}

bool CacheServiceManager::BackupStateAsString(std::wstring *result) {
  if (result == nullptr) {
    return false;
  }
  result->clear();

  cache_service::Win32ServiceState state;
  state.set_version(1);

  ScopedSCHandle service_handle;
  if (!GetCacheService(SC_MANAGER_CONNECT | GENERIC_READ,
                       SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG,
                       &service_handle)) {
    // If it turns out w/o any error that the cache service is not installed,
    // |GetCacheService| returns true.  We can conclude any other error
    // occurred.
    return false;
  }

  // If the cachse service is actually installed, |service_handle| should have
  // non-nullptr value.
  state.set_installed(nullptr != service_handle.get());
  if (!state.installed()) {
    // The cache service is not installed.
    // Use default settings with setting |installed| flag to false.
    state.set_load_type(cache_service::Win32ServiceState::AUTO_START);
    state.set_running(true);
  } else {
    // This byte array will contain both QUERY_SERVICE_CONFIG and some
    // string buffers which are referred by corresponding members of
    // QUERY_SERVICE_CONFIG.  We have to keep the array until we complete
    // all tasks which use the contents of QUERY_SERVICE_CONFIG.
    std::unique_ptr<char[]> buffer;
    if (!GetServiceConfig(service_handle, &buffer)) {
      return false;
    }
    const LPQUERY_SERVICE_CONFIG service_config =
        reinterpret_cast<const LPQUERY_SERVICE_CONFIG>(buffer.get());
    // retrieves the server load type.
    state.set_load_type(static_cast<cache_service::Win32ServiceState::LoadType>(
        service_config->dwStartType));
    // retrieves the server running status.
    state.set_running(IsServiceRunning(service_handle));
  }

  if (!SerializeToBase64WString(state, result)) {
    return false;
  }

  return true;
}

bool CacheServiceManager::RestoreStateFromString(
    const std::wstring &serialized) {
  cache_service::Win32ServiceState state;
  if (!DeserializeFromBase64WString(serialized, &state)) {
    return false;
  }

  return RestoreStateInternal(state);
}

bool CacheServiceManager::EnsureServiceStopped() {
  ScopedSCHandle service_handle;
  constexpr DWORD kSCRights = SC_MANAGER_CONNECT;
  constexpr DWORD kServiceRights = GENERIC_READ | SERVICE_STOP;
  if (!GetCacheService(kSCRights, kServiceRights, &service_handle)) {
    return false;
  }

  if (!service_handle) {
    return true;
  }

  if (!IsServiceRunning(service_handle)) {
    return true;
  }

  if (!StopService(service_handle)) {
    return false;
  }

  return IsServiceRunning(service_handle);
}

}  // namespace mozc
#endif  // _WIN32
