/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 https://mozilla.org/MPL/2.0/. */

#define MOZ_USE_LAUNCHER_ERROR

#include "LaunchUnelevated.h"

#include "mozilla/Assertions.h"
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/mscom/ProcessRuntime.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ShellHeaderOnlyUtils.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "nsWindowsHelpers.h"

#include <windows.h>

static mozilla::LauncherResult<bool> IsHighIntegrity(
    const nsAutoHandle& aToken) {
  DWORD reqdLen;
  if (!::GetTokenInformation(aToken.get(), TokenIntegrityLevel, nullptr, 0,
                             &reqdLen)) {
    DWORD err = ::GetLastError();
    if (err != ERROR_INSUFFICIENT_BUFFER) {
      return LAUNCHER_ERROR_FROM_WIN32(err);
    }
  }

  auto buf = mozilla::MakeUnique<char[]>(reqdLen);

  if (!::GetTokenInformation(aToken.get(), TokenIntegrityLevel, buf.get(),
                             reqdLen, &reqdLen)) {
    return LAUNCHER_ERROR_FROM_LAST();
  }

  auto tokenLabel = reinterpret_cast<PTOKEN_MANDATORY_LABEL>(buf.get());

  DWORD subAuthCount = *::GetSidSubAuthorityCount(tokenLabel->Label.Sid);
  DWORD integrityLevel =
      *::GetSidSubAuthority(tokenLabel->Label.Sid, subAuthCount - 1);
  return integrityLevel > SECURITY_MANDATORY_MEDIUM_RID;
}

static mozilla::LauncherResult<HANDLE> GetMediumIntegrityToken(
    const nsAutoHandle& aProcessToken) {
  HANDLE rawResult;
  if (!::DuplicateTokenEx(aProcessToken.get(), 0, nullptr,
                          SecurityImpersonation, TokenPrimary, &rawResult)) {
    return LAUNCHER_ERROR_FROM_LAST();
  }

  nsAutoHandle result(rawResult);

  BYTE mediumIlSid[SECURITY_MAX_SID_SIZE];
  DWORD mediumIlSidSize = sizeof(mediumIlSid);
  if (!::CreateWellKnownSid(WinMediumLabelSid, nullptr, mediumIlSid,
                            &mediumIlSidSize)) {
    return LAUNCHER_ERROR_FROM_LAST();
  }

  TOKEN_MANDATORY_LABEL integrityLevel = {};
  integrityLevel.Label.Attributes = SE_GROUP_INTEGRITY;
  integrityLevel.Label.Sid = reinterpret_cast<PSID>(mediumIlSid);

  if (!::SetTokenInformation(rawResult, TokenIntegrityLevel, &integrityLevel,
                             sizeof(integrityLevel))) {
    return LAUNCHER_ERROR_FROM_LAST();
  }

  return result.disown();
}

static mozilla::LauncherResult<bool> IsAdminByAppCompat(
    HKEY aRootKey, const wchar_t* aExecutablePath) {
  static const wchar_t kPathToLayers[] =
      L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\"
      L"AppCompatFlags\\Layers";

  DWORD dataLength = 0;
  LSTATUS status = ::RegGetValueW(aRootKey, kPathToLayers, aExecutablePath,
                                  RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY,
                                  nullptr, nullptr, &dataLength);
  if (status == ERROR_FILE_NOT_FOUND) {
    return false;
  } else if (status != ERROR_SUCCESS) {
    return LAUNCHER_ERROR_FROM_WIN32(status);
  }

  auto valueData = mozilla::MakeUnique<wchar_t[]>(dataLength);
  if (!valueData) {
    return LAUNCHER_ERROR_FROM_WIN32(ERROR_OUTOFMEMORY);
  }

  status = ::RegGetValueW(aRootKey, kPathToLayers, aExecutablePath,
                          RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, nullptr,
                          valueData.get(), &dataLength);
  if (status != ERROR_SUCCESS) {
    return LAUNCHER_ERROR_FROM_WIN32(status);
  }

  const wchar_t kRunAsAdmin[] = L"RUNASADMIN";
  const wchar_t kDelimiters[] = L" ";
  wchar_t* tokenContext = nullptr;
  const wchar_t* token = wcstok_s(valueData.get(), kDelimiters, &tokenContext);
  while (token) {
    if (!_wcsnicmp(token, kRunAsAdmin, mozilla::ArrayLength(kRunAsAdmin))) {
      return true;
    }
    token = wcstok_s(nullptr, kDelimiters, &tokenContext);
  }

  return false;
}

namespace mozilla {

// If we're running at an elevated integrity level, re-run ourselves at the
// user's normal integrity level. We do this by locating the active explorer
// shell, and then asking it to do a ShellExecute on our behalf. We do it this
// way to ensure that the child process runs as the original user in the active
// session; an elevated process could be running with different credentials than
// those of the session.
// See https://blogs.msdn.microsoft.com/oldnewthing/20131118-00/?p=2643

LauncherVoidResult LaunchUnelevated(int aArgc, wchar_t* aArgv[]) {
  // We need COM to talk to Explorer. Using ProcessRuntime so that
  // process-global COM configuration is done correctly
  mozilla::mscom::ProcessRuntime mscom(GeckoProcessType_Default);
  if (!mscom) {
    return LAUNCHER_ERROR_FROM_HRESULT(mscom.GetHResult());
  }

  // Omit argv[0] because ShellExecute doesn't need it in params
  UniquePtr<wchar_t[]> cmdLine(MakeCommandLine(aArgc - 1, aArgv + 1));
  if (!cmdLine) {
    return LAUNCHER_ERROR_GENERIC();
  }

  _bstr_t exe(aArgv[0]);
  _variant_t args(cmdLine.get());
  _variant_t operation(L"open");
  _variant_t directory;
  _variant_t showCmd(SW_SHOWNORMAL);
  return ShellExecuteByExplorer(exe, args, operation, directory, showCmd);
}

LauncherResult<ElevationState> GetElevationState(
    const wchar_t* aExecutablePath, mozilla::LauncherFlags aFlags,
    nsAutoHandle& aOutMediumIlToken) {
  aOutMediumIlToken.reset();

  const DWORD tokenFlags = TOKEN_QUERY | TOKEN_DUPLICATE |
                           TOKEN_ADJUST_DEFAULT | TOKEN_ASSIGN_PRIMARY;
  HANDLE rawToken;
  if (!::OpenProcessToken(::GetCurrentProcess(), tokenFlags, &rawToken)) {
    return LAUNCHER_ERROR_FROM_LAST();
  }

  nsAutoHandle token(rawToken);

  LauncherResult<TOKEN_ELEVATION_TYPE> elevationType = GetElevationType(token);
  if (elevationType.isErr()) {
    return elevationType.propagateErr();
  }

  Maybe<ElevationState> elevationState;
  switch (elevationType.unwrap()) {
    case TokenElevationTypeLimited:
      return ElevationState::eNormalUser;
    case TokenElevationTypeFull:
      elevationState = Some(ElevationState::eElevated);
      break;
    case TokenElevationTypeDefault: {
      // In this case, UAC is disabled. We do not yet know whether or not we
      // are running at high integrity. If we are at high integrity, we can't
      // relaunch ourselves in a non-elevated state via Explorer, as we would
      // just end up in an infinite loop of launcher processes re-launching
      // themselves.
      LauncherResult<bool> isHighIntegrity = IsHighIntegrity(token);
      if (isHighIntegrity.isErr()) {
        return isHighIntegrity.propagateErr();
      }

      if (!isHighIntegrity.unwrap()) {
        return ElevationState::eNormalUser;
      }

      elevationState = Some(ElevationState::eHighIntegrityNoUAC);
      break;
    }
    default:
      MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?");
      return LAUNCHER_ERROR_GENERIC();
  }

  MOZ_ASSERT(elevationState.isSome() &&
                 elevationState.value() != ElevationState::eNormalUser,
             "Should have returned earlier for the eNormalUser case.");

  LauncherResult<bool> isAdminByAppCompat =
      IsAdminByAppCompat(HKEY_CURRENT_USER, aExecutablePath);
  if (isAdminByAppCompat.isErr()) {
    return isAdminByAppCompat.propagateErr();
  }

  if (isAdminByAppCompat.unwrap()) {
    elevationState = Some(ElevationState::eHighIntegrityByAppCompat);
  } else {
    isAdminByAppCompat =
        IsAdminByAppCompat(HKEY_LOCAL_MACHINE, aExecutablePath);
    if (isAdminByAppCompat.isErr()) {
      return isAdminByAppCompat.propagateErr();
    }

    if (isAdminByAppCompat.unwrap()) {
      elevationState = Some(ElevationState::eHighIntegrityByAppCompat);
    }
  }

  // A medium IL token is not needed in the following cases.
  // 1) We keep the process elevated (= LauncherFlags::eNoDeelevate)
  // 2) The process was elevated by UAC (= ElevationState::eElevated)
  //    AND the launcher process doesn't wait for the browser process
  if ((aFlags & mozilla::LauncherFlags::eNoDeelevate) ||
      (elevationState.value() == ElevationState::eElevated &&
       !(aFlags & mozilla::LauncherFlags::eWaitForBrowser))) {
    return elevationState.value();
  }

  LauncherResult<HANDLE> tokenResult = GetMediumIntegrityToken(token);
  if (tokenResult.isOk()) {
    aOutMediumIlToken.own(tokenResult.unwrap());
  } else {
    return tokenResult.propagateErr();
  }

  return elevationState.value();
}

}  // namespace mozilla
