sources/Google.Solutions.IapDesktop.Extensions.Session/ToolWindows/Session/SshView.cs (226 lines of code) (raw):

// // Copyright 2024 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.Diagnostics; using Google.Solutions.Apis.Locator; using Google.Solutions.Common.Diagnostics; using Google.Solutions.Common.Util; using Google.Solutions.IapDesktop.Application; using Google.Solutions.IapDesktop.Application.Host; 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.Ssh; using Google.Solutions.IapDesktop.Extensions.Session.Settings; using Google.Solutions.Mvvm.Binding; using Google.Solutions.Ssh.Native; using Google.Solutions.Terminal.Controls; using System; using System.Diagnostics; using System.Drawing; using System.Threading.Tasks; using System.Windows.Forms; namespace Google.Solutions.IapDesktop.Extensions.Session.ToolWindows.Session { [Service] public class SshView : ClientViewBase<SshHybridClient>, ISshSession, IView<SshViewModel> { private Bound<SshViewModel> viewModel; private readonly ITerminalSettingsRepository settingsRepository; private readonly IInputDialog inputDialog; private void ApplyTerminalSettings(ITerminalSettings settings) { if (this.Client == null) { return; } var terminal = this.Client.Terminal; terminal.EnableCtrlC = settings.IsCopyPasteUsingCtrlCAndCtrlVEnabled.Value; terminal.EnableCtrlV = settings.IsCopyPasteUsingCtrlCAndCtrlVEnabled.Value; terminal.EnableCtrlInsert = settings.IsCopyPasteUsingShiftInsertAndCtrlInsertEnabled.Value; terminal.EnableShiftInsert = settings.IsCopyPasteUsingShiftInsertAndCtrlInsertEnabled.Value; terminal.EnableTypographicQuoteConversion = settings.IsQuoteConvertionOnPasteEnabled.Value; terminal.EnableBracketedPaste = settings.IsBracketedPasteEnabled.Value; terminal.EnableCtrlHomeEnd = settings.IsScrollingUsingCtrlHomeEndEnabled.Value; terminal.EnableCtrlPageUpDown = settings.IsScrollingUsingCtrlPageUpDownEnabled.Value; terminal.Caret = settings.CaretStyle.Value; terminal.Font = new Font( settings.FontFamily.Value, TerminalSettings.FontSizeFromDword(settings.FontSizeAsDword.Value)); terminal.BackColor = Color.FromArgb(settings.BackgroundColorArgb.Value); terminal.ForeColor = Color.FromArgb(settings.ForegroundColorArgb.Value); } //--------------------------------------------------------------------- // Ctor. //--------------------------------------------------------------------- public SshView( IMainWindow mainWindow, ITerminalSettingsRepository settingsRepository, ToolWindowStateRepository stateRepository, IEventQueue eventQueue, IExceptionDialog exceptionDialog, IInputDialog inputDialog, IBindingContext bindingContext) : base( mainWindow, stateRepository, eventQueue, exceptionDialog, bindingContext) { this.Icon = Resources.ConsoleBlue_16; this.settingsRepository = settingsRepository; this.inputDialog = inputDialog; this.settingsRepository.SettingsChanged += (_, args) => ApplyTerminalSettings(args.Data); } public void Bind( SshViewModel 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 ?? "SSH"; set { } } protected override void ConnectCore() { var viewModel = this.viewModel.Value; var client = this.Client.ExpectNotNull(nameof(this.Client)); using (ApplicationTraceSource.Log.TraceMethod().WithParameters( viewModel.Endpoint, viewModel.Parameters!.ConnectionTimeout)) { // // Identify as IAP-Desktop, not plain libssh2. // Replace dashes as those aren't allowed in SSH // banners. // client.Banner = Install.UserAgent .ToApplicationName() .Replace("-", string.Empty); // // Basic connection settings. // client.ServerEndpoint = viewModel.Endpoint; client.Credential = viewModel.Credential; client.ConnectionTimeout = viewModel.Parameters.ConnectionTimeout; client.Locale = viewModel.Parameters.Language; client.EnableFileBrowser = viewModel.Parameters.EnableFileAccess; client.KeyboardInteractiveHandler = new SshKeyboardInteractiveHandler( this, this.inputDialog, viewModel.Instance!); client.Terminal.Focus(); // // Apply terminal settings. These can change at any // time. // ApplyTerminalSettings(this.settingsRepository.GetSettings()); client.FileBrowsingFailed += (_, args) => OnError("Unable to complete file operation", args.Exception); // // Start establishing a connection and react to events. // client.Connect(); } } protected override void OnFatalError(Exception e) { // // Translate common exceptions to make them more actionable. // if (e.Unwrap() is Libssh2Exception unverifiedEx && unverifiedEx.ErrorCode == LIBSSH2_ERROR.PUBLICKEY_UNVERIFIED && this.viewModel.Value.Credential is PlatformCredential unverifiedPlatformCredential && unverifiedPlatformCredential.AuthorizationMethod == KeyAuthorizationMethods.Oslogin) { OnFatalError(new OsLoginAuthenticationFailedException( "Authenticating to the VM failed. Possible reasons for this " + "error include:\n\n" + " - The VM is configured to require 2-step verification,\n" + " and you haven't set up 2SV for your user account\n" + " - The guest environment is misconfigured or not running", e, HelpTopics.TroubleshootingOsLogin)); } else if (e.Unwrap() is Libssh2Exception authEx && authEx.ErrorCode == LIBSSH2_ERROR.AUTHENTICATION_FAILED && this.viewModel.Value.Credential is PlatformCredential failedPlatformCredential) { if (failedPlatformCredential.AuthorizationMethod == KeyAuthorizationMethods.Oslogin) { var outdatedMessage = failedPlatformCredential.Signer is OsLoginCertificateSigner ? " - The VM is running an outdated version of the guest environment \n" + " that doesn't support certificate-based authentication\n" : string.Empty; var message = "Authenticating to the VM failed. Possible reasons for this " + "error include:\n\n" + " - You don't have sufficient access to log in\n" + outdatedMessage + " - The VM's guest environment is misconfigured or not running\n\n" + "To log in, you need all of the following roles:\n\n" + " 1. 'Compute OS Login' or 'Compute OS Admin Login'\n" + " 2. 'Service Account User' (if the VM uses a service account)\n" + " 3. 'Compute OS Login External User'\n" + " (if the VM belongs to a different GCP organization)\n\n" + "Note that it might take several minutes for IAM policy changes to take effect."; OnFatalError(new OsLoginAuthenticationFailedException( message, e, HelpTopics.GrantingOsLoginRoles)); } else { OnFatalError(new MetadataKeyAuthenticationFailedException( "Authentication failed. Verify that the Compute Engine guest environment " + "is installed on the VM and that the agent is running.", e, HelpTopics.ManagingMetadataAuthorizedKeys)); } } else if (e.Unwrap() is Libssh2Exception kexEx && kexEx.ErrorCode == LIBSSH2_ERROR.KEY_EXCHANGE_FAILURE && Environment.OSVersion.Version.Build <= 10000) { // // Libssh2's CNG support requires Windows 10+. // OnFatalError(new PlatformNotSupportedException( "SSH is not supported on this version of Windows")); } else { base.OnFatalError(e); } } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyData == ToggleFocusHotKey) { // // Release focus and move it to the panel, which ensures // that any other shortcuts start applying again. // this.MainWindow.MainPanel.Focus(); e.Handled = true; } } public bool CanTransferFiles { get => this.Client != null && this.Client.CanShowFileBrowser; } //--------------------------------------------------------------------- // ISshSession. //--------------------------------------------------------------------- public Task TransferFilesAsync() { Debug.Assert(this.CanTransferFiles); this.Client?.BrowseFiles(); return Task.CompletedTask; } //--------------------------------------------------------------------- // Exceptions. //--------------------------------------------------------------------- public class OsLoginAuthenticationFailedException : Exception, IExceptionWithHelpTopic { public IHelpTopic Help { get; } internal OsLoginAuthenticationFailedException( string message, Exception inner, IHelpTopic helpTopic) : base(message, inner) { this.Help = helpTopic; } } public class MetadataKeyAuthenticationFailedException : Exception, IExceptionWithHelpTopic { public IHelpTopic Help { get; } internal MetadataKeyAuthenticationFailedException( string message, Exception inner, IHelpTopic helpTopic) : base(message, inner) { this.Help = helpTopic; } } } }