// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/desktop_session_win.h"
#include <limits>
#include <sddl.h>
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/guid.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_checker.h"
#include "base/timer/timer.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_comptr.h"
#include "base/win/scoped_handle.h"
#include "base/win/windows_version.h"
#include "ipc/ipc_message_macros.h"
#include "ipc/ipc_platform_file.h"
#include "remoting/base/auto_thread_task_runner.h"
// MIDL-generated declarations and definitions.
#include "remoting/host/chromoting_lib.h"
#include "remoting/host/chromoting_messages.h"
#include "remoting/host/daemon_process.h"
#include "remoting/host/desktop_session.h"
#include "remoting/host/host_main.h"
#include "remoting/host/ipc_constants.h"
#include "remoting/host/sas_injector.h"
#include "remoting/host/screen_resolution.h"
#include "remoting/host/win/host_service.h"
#include "remoting/host/win/worker_process_launcher.h"
#include "remoting/host/win/wts_session_process_delegate.h"
#include "remoting/host/win/wts_terminal_monitor.h"
#include "remoting/host/win/wts_terminal_observer.h"
#include "remoting/host/worker_process_ipc_delegate.h"
using base::win::ScopedHandle;
namespace remoting {
namespace {
// The security descriptor of the daemon IPC endpoint. It gives full access
// to SYSTEM and denies access by anyone else.
const wchar_t kDaemonIpcSecurityDescriptor[] =
SDDL_OWNER L":" SDDL_LOCAL_SYSTEM
SDDL_GROUP L":" SDDL_LOCAL_SYSTEM
SDDL_DACL L":("
SDDL_ACCESS_ALLOWED L";;" SDDL_GENERIC_ALL L";;;" SDDL_LOCAL_SYSTEM
L")";
// The command line parameters that should be copied from the service's command
// line to the desktop process.
const char* kCopiedSwitchNames[] = { switches::kV, switches::kVModule };
// The default screen dimensions for an RDP session.
const int kDefaultRdpScreenWidth = 1280;
const int kDefaultRdpScreenHeight = 768;
// RDC 6.1 (W2K8) supports dimensions of up to 4096x2048.
const int kMaxRdpScreenWidth = 4096;
const int kMaxRdpScreenHeight = 2048;
// The minimum effective screen dimensions supported by Windows are 800x600.
const int kMinRdpScreenWidth = 800;
const int kMinRdpScreenHeight = 600;
// Default dots per inch used by RDP is 96 DPI.
const int kDefaultRdpDpi = 96;
// The session attach notification should arrive within 30 seconds.
const int kSessionAttachTimeoutSeconds = 30;
// DesktopSession implementation which attaches to the host's physical console.
// Receives IPC messages from the desktop process, running in the console
// session, via |WorkerProcessIpcDelegate|, and monitors console session
// attach/detach events via |WtsConsoleObserer|.
class ConsoleSession : public DesktopSessionWin {
public:
// Same as DesktopSessionWin().
ConsoleSession(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
WtsTerminalMonitor* monitor);
virtual ~ConsoleSession();
protected:
// DesktopSession overrides.
virtual void SetScreenResolution(const ScreenResolution& resolution) OVERRIDE;
// DesktopSessionWin overrides.
virtual void InjectSas() OVERRIDE;
private:
scoped_ptr<SasInjector> sas_injector_;
DISALLOW_COPY_AND_ASSIGN(ConsoleSession);
};
// DesktopSession implementation which attaches to virtual RDP console.
// Receives IPC messages from the desktop process, running in the console
// session, via |WorkerProcessIpcDelegate|, and monitors console session
// attach/detach events via |WtsConsoleObserer|.
class RdpSession : public DesktopSessionWin {
public:
// Same as DesktopSessionWin().
RdpSession(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
WtsTerminalMonitor* monitor);
virtual ~RdpSession();
// Performs the part of initialization that can fail.
bool Initialize(const ScreenResolution& resolution);
// Mirrors IRdpDesktopSessionEventHandler.
void OnRdpConnected();
void OnRdpClosed();
protected:
// DesktopSession overrides.
virtual void SetScreenResolution(const ScreenResolution& resolution) OVERRIDE;
// DesktopSessionWin overrides.
virtual void InjectSas() OVERRIDE;
private:
// An implementation of IRdpDesktopSessionEventHandler interface that forwards
// notifications to the owning desktop session.
class EventHandler : public IRdpDesktopSessionEventHandler {
public:
explicit EventHandler(base::WeakPtr<RdpSession> desktop_session);
virtual ~EventHandler();
// IUnknown interface.
STDMETHOD_(ULONG, AddRef)() OVERRIDE;
STDMETHOD_(ULONG, Release)() OVERRIDE;
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) OVERRIDE;
// IRdpDesktopSessionEventHandler interface.
STDMETHOD(OnRdpConnected)() OVERRIDE;
STDMETHOD(OnRdpClosed)() OVERRIDE;
private:
ULONG ref_count_;
// Points to the desktop session object receiving OnRdpXxx() notifications.
base::WeakPtr<RdpSession> desktop_session_;
// This class must be used on a single thread.
base::ThreadChecker thread_checker_;
DISALLOW_COPY_AND_ASSIGN(EventHandler);
};
// Used to create an RDP desktop session.
base::win::ScopedComPtr<IRdpDesktopSession> rdp_desktop_session_;
// Used to match |rdp_desktop_session_| with the session it is attached to.
std::string terminal_id_;
base::WeakPtrFactory<RdpSession> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(RdpSession);
};
ConsoleSession::ConsoleSession(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
WtsTerminalMonitor* monitor)
: DesktopSessionWin(caller_task_runner, io_task_runner, daemon_process, id,
monitor) {
StartMonitoring(WtsTerminalMonitor::kConsole);
}
ConsoleSession::~ConsoleSession() {
}
void ConsoleSession::SetScreenResolution(const ScreenResolution& resolution) {
// Do nothing. The screen resolution of the console session is controlled by
// the DesktopSessionAgent instance running in that session.
DCHECK(caller_task_runner()->BelongsToCurrentThread());
}
void ConsoleSession::InjectSas() {
DCHECK(caller_task_runner()->BelongsToCurrentThread());
if (!sas_injector_)
sas_injector_ = SasInjector::Create();
if (!sas_injector_->InjectSas())
LOG(ERROR) << "Failed to inject Secure Attention Sequence.";
}
RdpSession::RdpSession(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
WtsTerminalMonitor* monitor)
: DesktopSessionWin(caller_task_runner, io_task_runner, daemon_process, id,
monitor),
weak_factory_(this) {
}
RdpSession::~RdpSession() {
}
bool RdpSession::Initialize(const ScreenResolution& resolution) {
DCHECK(caller_task_runner()->BelongsToCurrentThread());
// Create the RDP wrapper object.
HRESULT result = rdp_desktop_session_.CreateInstance(
__uuidof(RdpDesktopSession));
if (FAILED(result)) {
LOG(ERROR) << "Failed to create RdpSession object, 0x"
<< std::hex << result << std::dec << ".";
return false;
}
ScreenResolution local_resolution = resolution;
// If the screen resolution is not specified, use the default screen
// resolution.
if (local_resolution.IsEmpty()) {
local_resolution = ScreenResolution(
webrtc::DesktopSize(kDefaultRdpScreenWidth, kDefaultRdpScreenHeight),
webrtc::DesktopVector(kDefaultRdpDpi, kDefaultRdpDpi));
}
// Get the screen dimensions assuming the default DPI.
webrtc::DesktopSize host_size = local_resolution.ScaleDimensionsToDpi(
webrtc::DesktopVector(kDefaultRdpDpi, kDefaultRdpDpi));
// Make sure that the host resolution is within the limits supported by RDP.
host_size = webrtc::DesktopSize(
std::min(kMaxRdpScreenWidth,
std::max(kMinRdpScreenWidth, host_size.width())),
std::min(kMaxRdpScreenHeight,
std::max(kMinRdpScreenHeight, host_size.height())));
// Create an RDP session.
base::win::ScopedComPtr<IRdpDesktopSessionEventHandler> event_handler(
new EventHandler(weak_factory_.GetWeakPtr()));
terminal_id_ = base::GenerateGUID();
base::win::ScopedBstr terminal_id(base::UTF8ToUTF16(terminal_id_).c_str());
result = rdp_desktop_session_->Connect(host_size.width(),
host_size.height(),
terminal_id,
event_handler);
if (FAILED(result)) {
LOG(ERROR) << "RdpSession::Create() failed, 0x"
<< std::hex << result << std::dec << ".";
return false;
}
return true;
}
void RdpSession::OnRdpConnected() {
DCHECK(caller_task_runner()->BelongsToCurrentThread());
StopMonitoring();
StartMonitoring(terminal_id_);
}
void RdpSession::OnRdpClosed() {
DCHECK(caller_task_runner()->BelongsToCurrentThread());
TerminateSession();
}
void RdpSession::SetScreenResolution(const ScreenResolution& resolution) {
DCHECK(caller_task_runner()->BelongsToCurrentThread());
// TODO(alexeypa): implement resize-to-client for RDP sessions here.
// See http://crbug.com/137696.
NOTIMPLEMENTED();
}
void RdpSession::InjectSas() {
DCHECK(caller_task_runner()->BelongsToCurrentThread());
rdp_desktop_session_->InjectSas();
}
RdpSession::EventHandler::EventHandler(
base::WeakPtr<RdpSession> desktop_session)
: ref_count_(0),
desktop_session_(desktop_session) {
}
RdpSession::EventHandler::~EventHandler() {
DCHECK(thread_checker_.CalledOnValidThread());
if (desktop_session_)
desktop_session_->OnRdpClosed();
}
ULONG STDMETHODCALLTYPE RdpSession::EventHandler::AddRef() {
DCHECK(thread_checker_.CalledOnValidThread());
return ++ref_count_;
}
ULONG STDMETHODCALLTYPE RdpSession::EventHandler::Release() {
DCHECK(thread_checker_.CalledOnValidThread());
if (--ref_count_ == 0) {
delete this;
return 0;
}
return ref_count_;
}
STDMETHODIMP RdpSession::EventHandler::QueryInterface(REFIID riid, void** ppv) {
DCHECK(thread_checker_.CalledOnValidThread());
if (riid == IID_IUnknown ||
riid == IID_IRdpDesktopSessionEventHandler) {
*ppv = static_cast<IRdpDesktopSessionEventHandler*>(this);
AddRef();
return S_OK;
}
*ppv = NULL;
return E_NOINTERFACE;
}
STDMETHODIMP RdpSession::EventHandler::OnRdpConnected() {
DCHECK(thread_checker_.CalledOnValidThread());
if (desktop_session_)
desktop_session_->OnRdpConnected();
return S_OK;
}
STDMETHODIMP RdpSession::EventHandler::OnRdpClosed() {
DCHECK(thread_checker_.CalledOnValidThread());
if (!desktop_session_)
return S_OK;
base::WeakPtr<RdpSession> desktop_session = desktop_session_;
desktop_session_.reset();
desktop_session->OnRdpClosed();
return S_OK;
}
} // namespace
// static
scoped_ptr<DesktopSession> DesktopSessionWin::CreateForConsole(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
const ScreenResolution& resolution) {
scoped_ptr<ConsoleSession> session(new ConsoleSession(
caller_task_runner, io_task_runner, daemon_process, id,
HostService::GetInstance()));
return session.PassAs<DesktopSession>();
}
// static
scoped_ptr<DesktopSession> DesktopSessionWin::CreateForVirtualTerminal(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
const ScreenResolution& resolution) {
scoped_ptr<RdpSession> session(new RdpSession(
caller_task_runner, io_task_runner, daemon_process, id,
HostService::GetInstance()));
if (!session->Initialize(resolution))
return scoped_ptr<DesktopSession>();
return session.PassAs<DesktopSession>();
}
DesktopSessionWin::DesktopSessionWin(
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner,
DaemonProcess* daemon_process,
int id,
WtsTerminalMonitor* monitor)
: DesktopSession(daemon_process, id),
caller_task_runner_(caller_task_runner),
io_task_runner_(io_task_runner),
monitor_(monitor),
monitoring_notifications_(false) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
ReportElapsedTime("created");
}
DesktopSessionWin::~DesktopSessionWin() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
StopMonitoring();
}
void DesktopSessionWin::OnSessionAttachTimeout() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
LOG(ERROR) << "Session attach notification didn't arrived within "
<< kSessionAttachTimeoutSeconds << " seconds.";
TerminateSession();
}
void DesktopSessionWin::StartMonitoring(const std::string& terminal_id) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
DCHECK(!monitoring_notifications_);
DCHECK(!session_attach_timer_.IsRunning());
ReportElapsedTime("started monitoring");
session_attach_timer_.Start(
FROM_HERE, base::TimeDelta::FromSeconds(kSessionAttachTimeoutSeconds),
this, &DesktopSessionWin::OnSessionAttachTimeout);
monitoring_notifications_ = true;
monitor_->AddWtsTerminalObserver(terminal_id, this);
}
void DesktopSessionWin::StopMonitoring() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
if (monitoring_notifications_) {
ReportElapsedTime("stopped monitoring");
monitoring_notifications_ = false;
monitor_->RemoveWtsTerminalObserver(this);
}
session_attach_timer_.Stop();
OnSessionDetached();
}
void DesktopSessionWin::TerminateSession() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
StopMonitoring();
// This call will delete |this| so it should be at the very end of the method.
daemon_process()->CloseDesktopSession(id());
}
void DesktopSessionWin::OnChannelConnected(int32 peer_pid) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
ReportElapsedTime("channel connected");
// Obtain the handle of the desktop process. It will be passed to the network
// process to use to duplicate handles of shared memory objects from
// the desktop process.
desktop_process_.Set(OpenProcess(PROCESS_DUP_HANDLE, false, peer_pid));
if (!desktop_process_.IsValid()) {
CrashDesktopProcess(FROM_HERE);
return;
}
VLOG(1) << "IPC: daemon <- desktop (" << peer_pid << ")";
}
bool DesktopSessionWin::OnMessageReceived(const IPC::Message& message) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(DesktopSessionWin, message)
IPC_MESSAGE_HANDLER(ChromotingDesktopDaemonMsg_DesktopAttached,
OnDesktopSessionAgentAttached)
IPC_MESSAGE_HANDLER(ChromotingDesktopDaemonMsg_InjectSas,
InjectSas)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
if (!handled) {
LOG(ERROR) << "Received unexpected IPC type: " << message.type();
CrashDesktopProcess(FROM_HERE);
}
return handled;
}
void DesktopSessionWin::OnPermanentError(int exit_code) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
TerminateSession();
}
void DesktopSessionWin::OnSessionAttached(uint32 session_id) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
DCHECK(!launcher_);
DCHECK(monitoring_notifications_);
ReportElapsedTime("attached");
// Launch elevated on Win8 to be able to inject Alt+Tab.
bool launch_elevated = base::win::GetVersion() >= base::win::VERSION_WIN8;
// Get the name of the executable to run. |kDesktopBinaryName| specifies
// uiAccess="true" in it's manifest.
base::FilePath desktop_binary;
bool result;
if (launch_elevated) {
result = GetInstalledBinaryPath(kDesktopBinaryName, &desktop_binary);
} else {
result = GetInstalledBinaryPath(kHostBinaryName, &desktop_binary);
}
if (!result) {
TerminateSession();
return;
}
session_attach_timer_.Stop();
scoped_ptr<CommandLine> target(new CommandLine(desktop_binary));
target->AppendSwitchASCII(kProcessTypeSwitchName, kProcessTypeDesktop);
// Copy the command line switches enabling verbose logging.
target->CopySwitchesFrom(*CommandLine::ForCurrentProcess(),
kCopiedSwitchNames,
arraysize(kCopiedSwitchNames));
// Create a delegate capable of launching a process in a different session.
scoped_ptr<WtsSessionProcessDelegate> delegate(
new WtsSessionProcessDelegate(io_task_runner_,
target.Pass(),
launch_elevated,
base::WideToUTF8(
kDaemonIpcSecurityDescriptor)));
if (!delegate->Initialize(session_id)) {
TerminateSession();
return;
}
// Create a launcher for the desktop process, using the per-session delegate.
launcher_.reset(new WorkerProcessLauncher(
delegate.PassAs<WorkerProcessLauncher::Delegate>(), this));
}
void DesktopSessionWin::OnSessionDetached() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
launcher_.reset();
if (monitoring_notifications_) {
ReportElapsedTime("detached");
session_attach_timer_.Start(
FROM_HERE, base::TimeDelta::FromSeconds(kSessionAttachTimeoutSeconds),
this, &DesktopSessionWin::OnSessionAttachTimeout);
}
}
void DesktopSessionWin::OnDesktopSessionAgentAttached(
IPC::PlatformFileForTransit desktop_pipe) {
if (!daemon_process()->OnDesktopSessionAgentAttached(id(),
desktop_process_.Get(),
desktop_pipe)) {
CrashDesktopProcess(FROM_HERE);
}
}
void DesktopSessionWin::CrashDesktopProcess(
const tracked_objects::Location& location) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
launcher_->Crash(location);
}
void DesktopSessionWin::ReportElapsedTime(const std::string& event) {
base::Time now = base::Time::Now();
std::string passed;
if (!last_timestamp_.is_null()) {
passed = base::StringPrintf(", %.2fs passed",
(now - last_timestamp_).InSecondsF());
}
base::Time::Exploded exploded;
now.LocalExplode(&exploded);
VLOG(1) << base::StringPrintf("session(%d): %s at %02d:%02d:%02d.%03d%s",
id(),
event.c_str(),
exploded.hour,
exploded.minute,
exploded.second,
exploded.millisecond,
passed.c_str());
last_timestamp_ = now;
}
} // namespace remoting