// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <objbase.h>

#include <windows.h>

#include <shlobj.h>
#include <wrl/client.h>

#include <string_view>
#include <utility>

#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/types/expected.h"
#include "base/win/elevation_util.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/win_util.h"
#include "chrome/windows_services/elevated_tracing_service/system_tracing_session.h"
#include "chrome/windows_services/elevated_tracing_service/tracing_service_idl.h"
#include "chrome/windows_services/elevated_tracing_service/with_child_test.h"
#include "chrome/windows_services/service_program/test_support/scoped_medium_integrity.h"
#include "chrome/windows_services/service_program/test_support/service_environment.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/system/invitation.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "mojo/public/cpp/system/simple_watcher.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace {

void TestGetServiceProcessHandle() {
  Microsoft::WRL::ComPtr<IUnknown> unknown;
  ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
      elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
      CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));

  Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
  ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session));
  unknown.Reset();

  ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
      trace_session.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING));

  mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
  mojo::OutgoingInvitation invitation;
  invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
  mojo::ScopedMessagePipeHandle pipe = invitation.AttachMessagePipe(/*name=*/0);

  DWORD pid = base::kNullProcessId;
  ASSERT_HRESULT_SUCCEEDED(
      trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid));
  ASSERT_NE(pid, base::kNullProcessId);

  mojo::OutgoingInvitation::Send(std::move(invitation),
                                 /*target_process=*/base::kNullProcessHandle,
                                 channel.TakeServerEndpoint());

  // Wait for the service to connect to the pipe.
  mojo::SimpleWatcher pipe_watcher(FROM_HERE,
                                   mojo::SimpleWatcher::ArmingPolicy::MANUAL);
  base::RunLoop run_loop;
  ASSERT_EQ(
      pipe_watcher.Watch(
          pipe.get(),
          MOJO_HANDLE_SIGNAL_WRITABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED,
          MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED,
          base::BindLambdaForTesting(
              [quit_closure = run_loop.QuitClosure()](
                  MojoResult result, const mojo::HandleSignalsState& state) {
                EXPECT_EQ(result, MOJO_RESULT_OK);
                EXPECT_EQ(state.satisfied_signals, MOJO_HANDLE_SIGNAL_WRITABLE);
                quit_closure.Run();
              })),
      MOJO_RESULT_OK);
  pipe_watcher.ArmOrNotify();
  run_loop.Run();

  // Releasing `trace_session` will trigger termination in the service.
}

// Tests that AcceptInvitation refuses a second invitation from a client.
void TestOnlyOneInvitation() {
  // Create a session and activate it by sending an invitation.
  Microsoft::WRL::ComPtr<IUnknown> unknown;
  ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
      elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
      CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));

  Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
  ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session));
  unknown.Reset();

  ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
      trace_session.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING));

  mojo::ScopedMessagePipeHandle pipe;
  {
    mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
    mojo::OutgoingInvitation invitation;
    invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
    pipe = invitation.AttachMessagePipe(/*name=*/0);

    DWORD pid = base::kNullProcessId;
    ASSERT_HRESULT_SUCCEEDED(
        trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid));
    ASSERT_NE(pid, base::kNullProcessId);

    mojo::OutgoingInvitation::Send(std::move(invitation),
                                   /*target_process=*/base::kNullProcessHandle,
                                   channel.TakeServerEndpoint());
  }

  {
    mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
    mojo::OutgoingInvitation invitation;
    invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
    mojo::ScopedMessagePipeHandle pipe2 =
        invitation.AttachMessagePipe(/*name=*/0);

    DWORD pid = base::kNullProcessId;
    ASSERT_EQ(
        trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid),
        kErrorSessionAlreadyActive);
  }
}

// Tests that a second session is refused while a first is active.
void TestOnlyOneSession() {
  // Create a session and activate it by sending an invitation.
  Microsoft::WRL::ComPtr<IUnknown> unknown;
  ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
      elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
      CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));

  Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
  ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session));
  unknown.Reset();

  ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
      trace_session.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING));

  mojo::ScopedMessagePipeHandle pipe;
  {
    mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
    mojo::OutgoingInvitation invitation;
    invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
    pipe = invitation.AttachMessagePipe(/*name=*/0);

    DWORD pid = base::kNullProcessId;
    ASSERT_HRESULT_SUCCEEDED(
        trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid));
    ASSERT_NE(pid, base::kNullProcessId);

    mojo::OutgoingInvitation::Send(std::move(invitation),
                                   /*target_process=*/base::kNullProcessHandle,
                                   channel.TakeServerEndpoint());
  }

  // Now a second client is refused.
  ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
      elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
      CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));

  Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session2;
  ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session2));
  unknown.Reset();

  ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
      trace_session2.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING));

  mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
  mojo::OutgoingInvitation invitation;
  invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
  mojo::ScopedMessagePipeHandle pipe2 =
      invitation.AttachMessagePipe(/*name=*/0);

  DWORD pid = base::kNullProcessId;
  ASSERT_EQ(
      trace_session2->AcceptInvitation(channel.GetServerName().c_str(), &pid),
      kErrorSessionInProgress);
}

MULTIPROCESS_TEST_MAIN(GetServiceProcessHandleInChild) {
  // base::debug::WaitForDebugger(30, /*silent=*/false);

  base::win::ScopedCOMInitializer com_initializer;
  if (!com_initializer.Succeeded()) {
    return 1;
  }

  base::test::SingleThreadTaskEnvironment task_environment;

  TestGetServiceProcessHandle();

  return ::testing::Test::HasFailure() ? 1 : 0;
}

MULTIPROCESS_TEST_MAIN(OnlyOneInvitationInChild) {
  // base::debug::WaitForDebugger(30, /*silent=*/false);

  base::win::ScopedCOMInitializer com_initializer;
  if (!com_initializer.Succeeded()) {
    return 1;
  }

  TestOnlyOneInvitation();

  return ::testing::Test::HasFailure() ? 1 : 0;
}

MULTIPROCESS_TEST_MAIN(OnlyOneSessionInChild) {
  // base::debug::WaitForDebugger(30, /*silent=*/false);

  base::win::ScopedCOMInitializer com_initializer;
  if (!com_initializer.Succeeded()) {
    return 1;
  }

  TestOnlyOneSession();

  return ::testing::Test::HasFailure() ? 1 : 0;
}

// Relaunches the test process de-elevated; using the multi-process test
// facilitied to run the named function.
base::expected<base::Process, DWORD> RunInChildDeElevated(
    std::string_view function_name) {
  base::CommandLine command_line =
      base::GetMultiProcessTestChildBaseCommandLine();
  command_line.AppendSwitchASCII(switches::kTestChildProcess, function_name);

  return base::win::RunDeElevated(command_line);
}

}  // namespace

// A test harness that installs the tracing service so that tests can call into
// it. The service's log output is redirected to the test process's stderr.
class TracingServiceTest : public elevated_tracing_service::WithChildTest {
 protected:
  static void SetUpTestSuite() {
    if (!::IsUserAnAdmin()) {
      GTEST_SKIP() << "Test requires admin rights";
    }
    service_environment_ = new ServiceEnvironment(
        L"Test Tracing Service",
        FILE_PATH_LITERAL("elevated_tracing_service.exe"),
        base::span_from_ref(
            std::string_view(elevated_tracing_service::switches::
                                 kSystemTracingClsIdForTestingSwitch)),
        elevated_tracing_service::kTestSystemTracingSessionClsid,
        __uuidof(ISystemTraceSession));
    ASSERT_TRUE(service_environment_->is_valid());
  }

  static void TearDownTestSuite() {
    delete std::exchange(service_environment_, nullptr);
  }

  // Returns a handle to the service process if it is running, or an invalid
  // process otherwise.
  base::Process GetRunningService() {
    return service_environment_->GetRunningService();
  }

 private:
  static ServiceEnvironment* service_environment_;
};

// static
ServiceEnvironment* TracingServiceTest::service_environment_ = nullptr;

// Tests calling into the service from the main test process itself after
// dropping to medium integrity level.
TEST_F(TracingServiceTest, GetServiceProcessHandle) {
  base::win::ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());

  ScopedMediumIntegrity medium_integrity;
  ASSERT_TRUE(medium_integrity.Succeeded());

  TestGetServiceProcessHandle();
}

// Tests calling into the service from the main test process itself after
// dropping to medium integrity level.
TEST_F(TracingServiceTest, OnlyOneInvitation) {
  base::win::ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());

  ScopedMediumIntegrity medium_integrity;
  ASSERT_TRUE(medium_integrity.Succeeded());

  TestOnlyOneInvitation();
}

// Tests calling into the service from the main test process itself after
// dropping to medium integrity level.
TEST_F(TracingServiceTest, OnlyOneSession) {
  base::win::ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());

  ScopedMediumIntegrity medium_integrity;
  ASSERT_TRUE(medium_integrity.Succeeded());

  TestOnlyOneSession();
}

// Tests calling into the service from a de-elevated child process. This test is
// skipped if UAC is disabled.
TEST_F(TracingServiceTest, GetServiceProcessHandleInChild) {
  // Pro tip: Add --show-error-dialogs to the test's command line and put
  // base::debug::WaitForDebugger(30, /*silent=*/false); in
  // `GetServiceProcessHandleInChild()` to more easily debug in the child
  // process.
  auto child_or_error = RunInChildDeElevated("GetServiceProcessHandleInChild");

  if (!child_or_error.has_value()) {
    GTEST_SKIP() << "Cannot de-elevate when UAC is disabled.";
  }
  int exit_code;
  ASSERT_TRUE(child_or_error->WaitForExit(&exit_code));
  ASSERT_EQ(exit_code, 0);
}

// Tests calling into the service from a de-elevated child process. This test is
// skipped if UAC is disabled.
TEST_F(TracingServiceTest, OnlyOneInvitationInChild) {
  // Pro tip: Add --show-error-dialogs to the test's command line and put
  // base::debug::WaitForDebugger(30, /*silent=*/false); in
  // `GetServiceProcessHandleInChild()` to more easily debug in the child
  // process.
  auto child_or_error = RunInChildDeElevated("OnlyOneInvitationInChild");

  if (!child_or_error.has_value()) {
    GTEST_SKIP() << "Cannot de-elevate when UAC is disabled.";
  }
  int exit_code;
  ASSERT_TRUE(child_or_error->WaitForExit(&exit_code));
  ASSERT_EQ(exit_code, 0);
}

// Tests calling into the service from a de-elevated child process. This test is
// skipped if UAC is disabled.
TEST_F(TracingServiceTest, OnlyOneSessionInChild) {
  // Pro tip: Add --show-error-dialogs to the test's command line and put
  // base::debug::WaitForDebugger(30, /*silent=*/false); in
  // `GetServiceProcessHandleInChild()` to more easily debug in the child
  // process.
  auto child_or_error = RunInChildDeElevated("OnlyOneSessionInChild");

  if (!child_or_error.has_value()) {
    GTEST_SKIP() << "Cannot de-elevate when UAC is disabled.";
  }
  int exit_code;
  ASSERT_TRUE(child_or_error->WaitForExit(&exit_code));
  ASSERT_EQ(exit_code, 0);
}

MULTIPROCESS_TEST_MAIN(ConnectToServiceAndTerminateInChild) {
  base::win::ScopedCOMInitializer com_initializer;
  if (!com_initializer.Succeeded()) {
    return 1;
  }

  // Create a session and activate it by sending an invitation.
  Microsoft::WRL::ComPtr<IUnknown> unknown;
  CHECK(SUCCEEDED(::CoCreateInstance(
      elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
      CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown))));

  Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
  CHECK(SUCCEEDED(unknown.As(&trace_session)));
  unknown.Reset();

  CHECK(SUCCEEDED(::CoSetProxyBlanket(
      trace_session.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING)));

  mojo::ScopedMessagePipeHandle pipe;
  {
    mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
    mojo::OutgoingInvitation invitation;
    invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
    pipe = invitation.AttachMessagePipe(/*name=*/0);

    DWORD pid = base::kNullProcessId;
    CHECK(SUCCEEDED(trace_session->AcceptInvitation(
        channel.GetServerName().c_str(), &pid)));
    CHECK_NE(pid, base::kNullProcessId);

    mojo::OutgoingInvitation::Send(std::move(invitation),
                                   /*target_process=*/base::kNullProcessHandle,
                                   channel.TakeServerEndpoint());
  }

  // A session is now active. Tell the test that the child has started. The test
  // will monitor the service and then release the child.
  elevated_tracing_service::WithChildTest::SignalChildStart();

  // Wait for the test to release the child for termination.
  elevated_tracing_service::WithChildTest::WaitForChildTermination();

  base::Process::TerminateCurrentProcessImmediately(1);
}

// Tests that the service terminates promptly when the child process terminates
// while holding a session.
TEST_F(TracingServiceTest, ServiceTerminatesOnChildTermination) {
  auto child =
      SpawnChildWithEventHandles("ConnectToServiceAndTerminateInChild");
  ASSERT_TRUE(child.IsValid());
  absl::Cleanup child_stopper = [this] { SignalChildTermination(); };

  // Wait for the child to attach to the service.
  WaitForChildStart();

  // Get a handle to the service process.
  auto service_process = GetRunningService();
  ASSERT_TRUE(service_process.IsValid());

  // Tell the child to terminate.
  std::move(child_stopper).Invoke();

  // Wait for the service to terminate.
  ASSERT_EQ(::WaitForSingleObject(
                service_process.Handle(),
                TestTimeouts::action_max_timeout().InMilliseconds()),
            WAIT_OBJECT_0);
}
