sources/Google.Solutions.IapDesktop.Extensions.Session/ToolWindows/Session/RdpView.cs (296 lines of code) (raw):
//
// Copyright 2020 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF 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.
//
using Google.Solutions.Apis.Locator;
using Google.Solutions.Common.Diagnostics;
using Google.Solutions.Common.Linq;
using Google.Solutions.Common.Security;
using Google.Solutions.Common.Util;
using Google.Solutions.IapDesktop.Application;
using Google.Solutions.IapDesktop.Application.Profile.Settings;
using Google.Solutions.IapDesktop.Application.Windows;
using Google.Solutions.IapDesktop.Application.Windows.Dialog;
using Google.Solutions.IapDesktop.Core.ObjectModel;
using Google.Solutions.IapDesktop.Extensions.Session.Properties;
using Google.Solutions.IapDesktop.Extensions.Session.Protocol.Rdp;
using Google.Solutions.Mvvm.Binding;
using Google.Solutions.Settings.Collection;
using Google.Solutions.Terminal.Controls;
using System;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Google.Solutions.IapDesktop.Extensions.Session.ToolWindows.Session
{
[Service]
public class RdpView
: ClientViewBase<RdpClient>, IRdpSession, IView<RdpViewModel>
{
/// <summary>
/// Hotkey to toggle full-screen.
/// </summary>
public const Keys ToggleFullScreenHotKey = Keys.Control | Keys.Alt | Keys.F11;
private readonly IRepository<IApplicationSettings> settingsRepository;
private Bound<RdpViewModel> viewModel;
// For testing only.
internal event EventHandler? AuthenticationWarningDisplayed;
private bool IsRdsSessionHostRedirectionError(Exception e)
{
try
{
//
// When connecting to an RDSH in non-admin mode, we might
// be redirected to a different RDSH. This redirect always
// fails because it's using the internal IP address, not
// the tunnel address.
//
// The control sets the Server property to the redirect
// address, and we can use that to detect this situation.
//
return e is RdpDisconnectedException disconnected &&
disconnected.DisconnectReason == 516 && // Unable to establish a connection
this.Client!.Server != this.viewModel.Value.Server;
}
catch
{
return false;
}
}
//---------------------------------------------------------------------
// Ctor.
//---------------------------------------------------------------------
public RdpView(
IMainWindow mainWindow,
IRepository<IApplicationSettings> settingsRepository,
ToolWindowStateRepository stateRepository,
IEventQueue eventQueue,
IExceptionDialog exceptionDialog,
IBindingContext bindingContext)
: base(
mainWindow,
stateRepository,
eventQueue,
exceptionDialog,
bindingContext)
{
this.settingsRepository = settingsRepository;
this.Icon = Resources.ComputerBlue_16;
}
public void Bind(
RdpViewModel viewModel,
IBindingContext bindingContext)
{
this.viewModel.Value = viewModel;
}
//---------------------------------------------------------------------
// Overrides.
//---------------------------------------------------------------------
public override InstanceLocator Instance
{
get => this.viewModel.Value.Instance!;
}
public override string Text
{
get => this.viewModel.TryGet()?.Instance?.Name ?? "Remote Desktop";
set { }
}
protected override void OnFatalError(Exception e)
{
if (IsRdsSessionHostRedirectionError(e))
{
base.OnFatalError(new RdsRedirectException(
"The server initiated a redirect to a different " +
"server. IAP Desktop does not support redirects.\n\n" +
"To connect to a RD Session Host, change your connection settings " +
"to use an 'Admin' session.",
e));
}
else
{
base.OnFatalError(e);
}
}
protected override void ConnectCore()
{
var viewModel = this.viewModel.Value;
using (ApplicationTraceSource.Log.TraceMethod().WithParameters(
viewModel.Server,
viewModel.Port,
viewModel.Parameters!.ConnectionTimeout))
{
this.Client!.MainWindow = (Form)this.MainWindow;
this.Client.ServerAuthenticationWarningDisplayed += (_, args)
=> this.AuthenticationWarningDisplayed?.Invoke(this, args);
//
// Basic connection settings.
//
this.Client.Server = viewModel.Server;
this.Client.Domain = viewModel.Credential!.Domain;
this.Client.Username = viewModel.Credential.User;
this.Client.ServerPort = viewModel.Port!.Value;
this.Client.ConnectionTimeout = viewModel.Parameters.ConnectionTimeout;
this.Client.EnableAdminMode = viewModel.Parameters.SessionType == RdpSessionType.Admin;
//
// Connection security settings.
//
switch (viewModel.Parameters.AuthenticationLevel)
{
case RdpAuthenticationLevel.NoServerAuthentication:
this.Client.ServerAuthenticationLevel = 0;
break;
case RdpAuthenticationLevel.RequireServerAuthentication:
this.Client.ServerAuthenticationLevel = 1;
break;
case RdpAuthenticationLevel.AttemptServerAuthentication:
this.Client.ServerAuthenticationLevel = 2;
break;
}
switch (viewModel.Parameters.UserAuthenticationBehavior)
{
case RdpAutomaticLogon.Enabled:
//
// Use stored credentials, but allow prompting in case
// they're incomplete or wrong.
//
this.Client.EnableCredentialPrompt = true;
this.Client.Password =
viewModel.Credential.Password?.ToClearText() ?? string.Empty;
break;
case RdpAutomaticLogon.Disabled:
//
// Allow (and expect) a prompt.
//
// We "shouldn't" have a stored password -- but we might
// have one anyway:
//
// - Automatic logons might be auto-disabled by group policy,
// but the user might have stored a credential before that
// group policy took effect (or before IAP Desktop started
// considering that group policy).
// - Automatic logons might be auto-disabled by group policy
// (causing prompts to be suppressed), but the user might
// have stored credentialsmanually.
//
// So if there is a password, use it.
//
this.Client.EnableCredentialPrompt = true;
this.Client.Password =
viewModel.Credential.Password?.ToClearText() ?? string.Empty;
break;
case RdpAutomaticLogon.LegacyAbortOnFailure:
this.Client.EnableCredentialPrompt = false;
this.Client.Password =
viewModel.Credential.Password?.ToClearText() ?? string.Empty;
break;
}
this.Client.EnableNetworkLevelAuthentication =
viewModel.Parameters.NetworkLevelAuthentication != RdpNetworkLevelAuthentication.Disabled;
this.Client.EnableRestrictedAdminMode =
viewModel.Parameters.RestrictedAdminMode == RdpRestrictedAdminMode.Enabled;
//
// Connection bar settings.
//
this.Client.EnableConnectionBar =
viewModel.Parameters.ConnectionBar != RdpConnectionBarState.Off;
this.Client.EnableConnectionBarMinimizeButton = true;
this.Client.EnableConnectionBarPin =
viewModel.Parameters.ConnectionBar == RdpConnectionBarState.Pinned;
this.Client.ConnectionBarText = this.Instance.Name;
//
// Local resources settings.
//
this.Client.EnableClipboardRedirection =
viewModel.Parameters.RedirectClipboard == RdpRedirectClipboard.Enabled;
this.Client.EnablePrinterRedirection =
viewModel.Parameters.RedirectPrinter == RdpRedirectPrinter.Enabled;
this.Client.EnableSmartCardRedirection =
viewModel.Parameters.RedirectSmartCard == RdpRedirectSmartCard.Enabled;
this.Client.EnablePortRedirection =
viewModel.Parameters.RedirectPort == RdpRedirectPort.Enabled;
this.Client.EnableDriveRedirection =
viewModel.Parameters.RedirectDrive == RdpRedirectDrive.Enabled;
this.Client.EnableDeviceRedirection =
viewModel.Parameters.RedirectDevice == RdpRedirectDevice.Enabled;
this.Client.EnableAudioCaptureRedirection =
viewModel.Parameters.AudioInput == RdpAudioInput.Enabled;
switch (viewModel.Parameters.AudioPlayback)
{
case RdpAudioPlayback.PlayLocally:
this.Client.AudioRedirectionMode = 0;
break;
case RdpAudioPlayback.PlayOnServer:
this.Client.AudioRedirectionMode = 1;
break;
case RdpAudioPlayback.DoNotPlay:
this.Client.AudioRedirectionMode = 2;
break;
}
//
// Display settings.
//
this.Client.EnableDpiScaling =
viewModel.Parameters.DpiScaling == RdpDpiScaling.Enabled;
this.Client.EnableAutoResize =
viewModel.Parameters.DesktopSize == RdpDesktopSize.AutoAdjust;
switch (viewModel.Parameters.ColorDepth)
{
case RdpColorDepth.HighColor:
this.Client.ColorDepth = 16;
break;
case RdpColorDepth.TrueColor:
this.Client.ColorDepth = 24;
break;
case RdpColorDepth.DeepColor:
this.Client.ColorDepth = 32;
break;
}
//
// Keyboard settings.
//
this.Client.KeyboardHookMode =
(int)viewModel.Parameters.HookWindowsKeys;
//
// Set hotkey to trigger OnFocusReleasedEvent. This should be
// the same as the main window uses to move the focus to the
// control.
//
this.Client.FocusHotKey = ToggleFocusHotKey;
this.Client.FullScreenHotKey = ToggleFullScreenHotKey;
this.Client.EnableWebAuthnRedirection =
viewModel.Parameters.RedirectWebAuthn == RdpRedirectWebAuthn.Enabled;
//
// Start establishing a connection and react to events.
//
this.Client.Connect();
}
}
protected override void OnSizeChanged(EventArgs e)
{
if (this.Client != null && this.Client.IsFullScreen)
{
//
// Ignore, any attempted size change might
// just screw up full-screen mode.
//
return;
}
base.OnSizeChanged(e);
}
//---------------------------------------------------------------------
// IRdpSession.
//---------------------------------------------------------------------
public bool CanEnterFullScreen
{
get => this.Client != null && this.Client.CanEnterFullScreen;
}
public bool TrySetFullscreen(FullScreenMode mode)
{
this.Client.ExpectNotNull("Client connected");
Rectangle? customBounds;
if (mode == FullScreenMode.SingleScreen)
{
//
// Normal full screen.
//
customBounds = null;
}
else
{
//
// Use all configured screns.
//
// NB. The list of devices might include devices that
// do not exist anymore.
//
var selectedDevices = (this.settingsRepository.GetSettings()
.FullScreenDevices.Value ?? string.Empty)
.Split(ApplicationSettingsRepository.FullScreenDevicesSeparator)
.ToHashSet();
var screens = Screen.AllScreens
.Where(s => selectedDevices.Contains(s.DeviceName));
if (!screens.Any())
{
//
// Default to all screens.
//
screens = Screen.AllScreens;
}
var r = new Rectangle();
foreach (var s in screens)
{
r = Rectangle.Union(r, s.Bounds);
}
customBounds = r;
}
return this.Client!.TryEnterFullScreen(customBounds);
}
public void ShowSecurityScreen()
{
this.Client
.ExpectNotNull("Client connected")
.ShowSecurityScreen();
}
public void ShowTaskManager()
{
this.Client
.ExpectNotNull("Client connected")
.ShowTaskManager();
}
public void Logoff()
{
this.Client
.ExpectNotNull("Client connected")
.Logoff();
}
public void Reconnect()
{
this.Client
.ExpectNotNull("Client connected")
.Reconnect();
}
public void SendText(string text)
{
this.Client
.ExpectNotNull("Client connected")
.SendText(text);
}
public bool CanTransferFiles
{
get => this.viewModel
.Value
.Parameters!
.RedirectClipboard == RdpRedirectClipboard.Enabled;
}
public Task TransferFilesAsync()
{
ShowTooltip(
"Copy and paste files here",
"Use copy and paste to transfer files between " +
"your local computer and the VM.");
return Task.CompletedTask;
}
//---------------------------------------------------------------------
// Inner classes.
//---------------------------------------------------------------------
private class RdsRedirectException : RdpException
{
public RdsRedirectException(string message, Exception inner)
: base(message, inner)
{
}
}
}
}