/* Myrtille: A native HTML4/5 Remote Desktop and SSH Protocol client. Copyright(c) 2018 Paul Oliver (Olive Innovations) Copyright(c) 2014-2021 Cedric Coste Licensed 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 System; using System.Diagnostics; using System.Text; using System.Threading; using System.Windows.Forms; using log4net.Config; using Renci.SshNet; using Renci.SshNet.Common; using Myrtille.Services.Contracts; namespace Myrtille.SSH { public class Program { private static PipeMessaging pipeMessaging; private static SshClient client; private static ShellStream shellStream; private static bool loginMessage; private static int Main(string[] args) { // enable the code below for debug; disable otherwise //if (Environment.UserInteractive) //{ // MessageBox.Show("Attach the .NET debugger to the 'SSH Debug' Myrtille.SSH.exe process now for debug. Click OK when ready...", "SSH Debug"); //} //else //{ // Thread.Sleep(10000); //} // logger XmlConfigurator.Configure(); string argKeyValueSeparator = ":"; foreach (string arg in args) { var argParts = arg.Trim().Split(argKeyValueSeparator.ToCharArray(), 2); parseCommandLineArg(argParts[0].ToLower(), (argParts.Length > 1 ? argParts[1] : "")); } if (!ValidConfig) { return (int)RemoteSessionExitCode.InvalidConfiguration; } pipeMessaging = new PipeMessaging(RemoteSessionID); if (pipeMessaging.ConnectPipes()) { pipeMessaging.OnMessageReceivedEvent += PipeMessaging_OnMessageReceivedEvent; try { pipeMessaging.ReadInputsPipe(); } catch (Exception e) { if (ConsoleOutput) { Console.WriteLine(e.Message); } Trace.TraceError("SSH error, remote session {0} ({1})", RemoteSessionID, e); if (e is SshAuthenticationException) { if (e.Message == "Missing Username") return (int)RemoteSessionExitCode.MissingUserName; else if (e.Message == "Missing Password") return (int)RemoteSessionExitCode.MissingPassword; else return (int)RemoteSessionExitCode.InvalidCredentials; } return (int)RemoteSessionExitCode.Unknown; } finally { pipeMessaging.ClosePipes(); DisconnectSSHClient(); } } return (int)RemoteSessionExitCode.Success; } /// /// Handle commands from inputs pipe /// /// /// private static void PipeMessaging_OnMessageReceivedEvent(RemoteSessionCommand command, string data = "") { switch (command) { case RemoteSessionCommand.RequestFullscreenUpdate: WriteOutput(command, data); ClearOrExitTerminal(data); break; case RemoteSessionCommand.SendUserDomain: WriteOutput(command, data); Domain = data; break; case RemoteSessionCommand.SendUserName: WriteOutput(command, data); UserName = string.IsNullOrEmpty(Domain) ? data : string.Format("{0}\\{1}", Domain, data); break; case RemoteSessionCommand.SendServerAddress: WriteOutput(command, data); ServerAddress = data; break; case RemoteSessionCommand.SendUserPassword: WriteOutput(command, "Credentials received"); Password = data; break; case RemoteSessionCommand.ConnectClient: WriteOutput(command, "Connecting to remote host"); ConnectSSHClient(); break; case RemoteSessionCommand.CloseClient: WriteOutput(command, "Disconnecting from remote host"); pipeMessaging.ClosePipes(); break; case RemoteSessionCommand.SendKeyUnicode: WriteOutput(command, data); HandleKeyboardInput(data); break; } } private static void ClearOrExitTerminal(string fsuType) { // initial FSU (page (re)load) if (fsuType == "initial") { // don't clear the terminal on first initial FSU so that the SSH welcome message upon login can be seen if (!loginMessage) { loginMessage = true; return; } // windows if (client.ConnectionInfo.ServerVersion.Contains("Windows")) { // cancel the current command line, if any SendSSHClientData(Encoding.UTF8.GetBytes("\u001b")); // esc (27) // give some time to process data Thread.Sleep(1000); // clear the terminal SendSSHClientData(Encoding.UTF8.GetBytes("cls\r")); // cls + enter (13) } // others else { // cancel the current command line, if any SendSSHClientData(Encoding.UTF8.GetBytes("\u0003")); // break (3) // give some time to process data Thread.Sleep(1000); // clear the terminal SendSSHClientData(Encoding.UTF8.GetBytes("clear\r")); // clear + enter (13) } } // periodical or adaptive FSU // close the ssh client if disconnected else { CheckSSHClientState(); } } #region ssh client /// /// Send data to ssh client /// /// private static void SendSSHClientData(byte[] byteData) { try { shellStream.Write(byteData, 0, byteData.Length); shellStream.Flush(); } catch (Exception e) { Trace.TraceError("Failed to send data to ssh client, remote session {0} ({1})", RemoteSessionID, e); throw; } } /// /// Send data to ssh client /// /// private static void SendSSHClientData(byte byteData) { try { shellStream.WriteByte(byteData); shellStream.Flush(); } catch (Exception e) { Trace.TraceError("Failed to send data to ssh client, remote session {0} ({1})", RemoteSessionID, e); throw; } } /// /// Disconnect SSH client from Host /// private static void DisconnectSSHClient() { try { if (client?.IsConnected ?? false) { client.Disconnect(); } } catch (Exception e) { Trace.TraceError("Failed to disconnect ssh client, remote session {0} ({1})", RemoteSessionID, e); } finally { client?.Dispose(); client = null; } } /// /// Connect SSH client to remote host /// private static void ConnectSSHClient() { try { if (string.IsNullOrEmpty(UserName)) throw new SshAuthenticationException("Missing Username"); if (string.IsNullOrEmpty(Password)) throw new SshAuthenticationException("Missing Password"); Uri serverUri; var serverHost = ServerAddress; var serverPort = 22; if (Uri.TryCreate("tcp://" + ServerAddress, UriKind.Absolute, out serverUri)) { serverHost = serverUri.Host; if(serverUri.Port > 0) serverPort = serverUri.Port; } var connectionInfo = new Renci.SshNet.ConnectionInfo(serverHost, serverPort, UserName, new PasswordAuthenticationMethod(UserName, Password)); connectionInfo.Encoding = Encoding.UTF8; client = new SshClient(connectionInfo); client.Connect(); shellStream = client.CreateShellStream("xterm", Columns, Rows, Width, Height, 1024); shellStream.DataReceived += ShellStream_DataReceived; } catch (Exception e) { if (ConsoleOutput) Console.WriteLine(e.Message); Trace.TraceError("Failed to connect ssh client, remote session {0} ({1})", RemoteSessionID, e); throw; } finally { if (client != null && !client.IsConnected) { client.Dispose(); pipeMessaging.ClosePipes(); } } } /// /// Receive data from SSH client and send to updates pipe for processing by web client /// /// /// private static void ShellStream_DataReceived(object sender, ShellDataEventArgs e) { try { pipeMessaging.SendUpdatesPipeMessage("term|" + Encoding.UTF8.GetString(e.Data)); } catch (Exception exc) { Trace.TraceError("Failed to process terminal updates, remote session {0} ({1})", RemoteSessionID, exc); pipeMessaging.ClosePipes(); } } /// /// Check if ssh client is still connected, if not close pipes and exit program /// private static void CheckSSHClientState() { try { if (client != null && !client.IsConnected) { pipeMessaging.ClosePipes(); } else { try { // dummy write to see if the shellstream is still opened (closed after an exit command, for example) shellStream.Write(null); shellStream.Flush(); } catch { pipeMessaging.ClosePipes(); } } } catch (Exception e) { Trace.TraceError("Failed to check ssh client state, remote session {0} ({1})", RemoteSessionID, e); throw; } } #endregion #region handle keyboard data /// /// Handle received keyboard input and send to ssh client /// /// private static void HandleKeyboardInput(string keyCode) { SendSSHClientData(Encoding.UTF8.GetBytes(keyCode)); } /// /// Write output to console window if required /// /// /// private static void WriteOutput(RemoteSessionCommand command, string data) { var output = string.Format("CMD: {0}, DATA: {1}", (string)RemoteSessionCommandMapping.ToPrefix[command], data); if (ConsoleOutput) Console.WriteLine(output); if (LoggingEnabled) Trace.TraceInformation(output); } #endregion #region configuration private static string Domain { get; set; } // Domain use to create SSH connection private static string UserName { get; set; } // Username use to create SSH connection private static string Password { get; set; } // Password use to create SSH connection private static string ServerAddress { get; set; } //Host to establish SSH connection with private static string RemoteSessionID { get; set; } //Myrtille session ID used for pipe messaging private static bool LoggingEnabled { get; set; } //Myrtille logging parameter private static bool ConsoleOutput { get; set; } //Output comms to console window private static uint Height { get; set; } //Height of ssh terminal private static uint Width { get; set; } //Width of SSH terminal private static uint Columns { get { return (Width / 10); } } //Number of columns within the terminal window private static uint Rows { get { return (Height / 17); } } //Number of rows within the terminal window /// /// Indicate all command line parameters for correct operation have been received. /// private static bool ValidConfig { get { if (string.IsNullOrEmpty(RemoteSessionID)) return false; if (Height == 0) return false; if (Width == 0) return false; return true; } } /// /// Parse command line arguments /// /// /// private static void parseCommandLineArg(string arg, string value) { switch (arg) { case "/myrtille-sid": RemoteSessionID = value.Trim(); break; case "/myrtille-window": ConsoleOutput = true; break; case "/myrtille-log": LoggingEnabled = true; break; case "/h": Height = uint.Parse(value); break; case "/w": Width = uint.Parse(value); break; } } #endregion } }