using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using Bitvise;
using Bitvise.FlowSshNet;


namespace FlowSshNet_Exec
{

    // ExitCodes

    public enum ExitCodes
    {
        Success,		// no exit code received from remote
        UsageError,
        SessionError,	// including connection error 
        FatalError,
        ExitCodeBase = 1000,	// ExitCode = ExitCodeBase + exit code received from remote
    };


    // ExitCode

    public class ExitCode
    {
        public ExitCode() { m_exitCode = ExitCodes.Success; }
        public volatile ExitCodes m_exitCode;
    }


    // CmdLineParams

    public class CmdLineParams
    {
        public bool   m_bOk;
        public string m_sUserName;
        public string m_sPassword;
        public uint   m_nPort;
        public string m_sHost;

        public string m_sPty;
        public string m_sCmd;

        public CmdLineParams(string[] args)
        {
            bool syntaxError = false;
            for (int i = 0; i < args.Length && !syntaxError; ++i)
	        {
                if (args[i].StartsWith("-user="))      m_sUserName = args[i].Substring(6);
                else if (args[i].StartsWith("-pw="))   m_sPassword = args[i].Substring(4);
                else if (args[i].StartsWith("-host=")) m_sHost = args[i].Substring(6);
                else if (args[i].StartsWith("-port=")) m_nPort = uint.Parse(args[i].Substring(6));
                else if (args[i].StartsWith("-pty="))  m_sPty = args[i].Substring(5);
                else if (args[i].StartsWith("-cmd="))  m_sCmd = args[i].Substring(5);
                else syntaxError = true;
	        }

            if (syntaxError)
            {
                Console.WriteLine("FlowSshNet sample Exec client.");
                Console.WriteLine("");
                Console.WriteLine("Parameters:");
                Console.WriteLine(" -host=...  (default localhost)");
                Console.WriteLine(" -port=...  (default 22)");
                Console.WriteLine(" -user=...");
                Console.WriteLine(" -pw=...");
                Console.WriteLine(" -pty=...");
                Console.WriteLine(" -cmd=...   (open shell by default)");
            }
            m_bOk = !syntaxError;
        }
    }


    // ConsoleHelper

    static class ConsoleHelper
    {   
        public static void DisableEchoInput()
        {
            m_stdIn = GetStdHandle(STD_INPUT_HANDLE);

            m_restoreMode = false;
            if (GetFileType(m_stdIn) == FILE_TYPE_CHAR)
                if (m_restoreMode = GetConsoleMode(m_stdIn, ref m_originalMode))
                    m_restoreMode = SetConsoleMode(m_stdIn, m_originalMode & ~(ENABLE_ECHO_INPUT));
        }

        public static void RestoreEchoInput()
        {
            if (m_restoreMode) SetConsoleMode(m_stdIn, m_originalMode);
        }

        private static IntPtr m_stdIn;
        private static uint m_originalMode;
        private static bool m_restoreMode;

        private const uint STD_INPUT_HANDLE = 0xfffffff6;
        private const uint FILE_TYPE_CHAR = 2;
        private const uint ENABLE_ECHO_INPUT = 4;

        [System.Runtime.InteropServices.DllImport("kernel32")]
        private static extern IntPtr GetStdHandle(uint nStdHandle);

        [System.Runtime.InteropServices.DllImport("kernel32")]
        private static extern uint GetFileType(IntPtr hFile);

        [System.Runtime.InteropServices.DllImport("kernel32")]
        private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);

        [System.Runtime.InteropServices.DllImport("kernel32")]
        private static extern bool GetConsoleMode(IntPtr hConsoleHandle, ref uint dwMode);
    };


    // MyClient

    public class MyClient : ClientBase
    {
        public MyClient(ExitCode exitCode)
        {
            m_exitCode = exitCode;
            this.OnSshVersion += new SshVersionEventHandler(this.OnMySshVersion);
            this.OnHostKey += new HostKeyEventHandler(this.OnMyHostKey);
            this.OnFurtherAuth += new FurtherAuthEventHandler(this.OnMyFurtherAuth);
            this.OnPasswordChange += new PasswordChangeEventHandler(this.OnMyPasswordChange);
            this.OnBanner += new BannerEventHandler(this.OnMyBanner);
            this.OnDisconnect += new DisconnectEventHandler(this.OnMyDisconnect);
        }

        public void SetClientSettings(CmdLineParams cmdpar)
        {
            if (cmdpar.m_sUserName != null) SetUserName(cmdpar.m_sUserName);
            if (cmdpar.m_sPassword != null) SetPassword(cmdpar.m_sPassword);
            if (cmdpar.m_nPort > 0)         SetPort(cmdpar.m_nPort);
            if (cmdpar.m_sHost != null)     SetHost(cmdpar.m_sHost);
        }

        private void OnMySshVersion(object sender, string version)
        {
            Console.WriteLine("Server version: {0}", version);
        }

        private bool OnMyHostKey(object sender, PublicKey publicKey)
        {
            Console.WriteLine("Received the following host key:");
            Console.WriteLine("  MD5 Fingerprint: {0}", publicKey.GetMd5());
            Console.WriteLine("  Bubble-Babble: {0}", publicKey.GetBubbleBabble());
            Console.WriteLine("  SHA-256: {0}", publicKey.GetSha256());
            Console.Write("Type 'yes' to accept the key, any other input to exit: ");
            return Console.ReadLine() == "yes";
        }

        private bool OnMyFurtherAuth(object sender, FurtherAuth furtherAuth)
        {
            ConsoleHelper.DisableEchoInput();
            try
            {
                bool success = false;
                if (furtherAuth.IsPasswordRemaining())
                {	// user must provide a password
                    Console.Write("Password: ");

                    string password = Console.ReadLine();
                    if (password.Length > 0)
                    {
                        furtherAuth.SetPassword(password);
                        success = true;
                    }
                    Console.WriteLine("");
                }                
                return success;
            }
            finally { ConsoleHelper.RestoreEchoInput(); }
        }

        private bool OnMyPasswordChange(object sender, PasswordChange passwordChange)
        {
            ConsoleHelper.DisableEchoInput();
            try
            {
	            // Production code should remove volatile characters from prompt first.
	            Console.WriteLine(passwordChange.GetPrompt());
	            Console.WriteLine("New password: ");

                bool success = false;
	            string newPassword = Console.ReadLine();

	            if (newPassword.Length > 0)
	            {
		            Console.WriteLine("\nRepeat password: ");

                    string repeatPassword = Console.ReadLine();
		            if (newPassword == repeatPassword)
		            {
			            passwordChange.SetNewPassword(newPassword);
			            success = true;
		            }
		            else Console.WriteLine("\nPasswords do not match!");
	            }
	            Console.WriteLine("");
	            return success;
            }
            finally { ConsoleHelper.RestoreEchoInput(); }
        }

        void OnMyBanner(object sender, string banner)
	    {	// Production code should remove volatile characters from banner first.
		    Console.WriteLine("\n\nUser authentication banner: {0}", banner);
	    }

        void OnMyDisconnect(object sender, DisconnectReason reason, string desc)
	    {			
		    if (reason != DisconnectReason.ByClient)
		    {
			    Console.WriteLine("\n\nClient disconnecting: {0}", desc);
                if (m_exitCode.m_exitCode == ExitCodes.Success)
                    m_exitCode.m_exitCode = ExitCodes.SessionError;
		    }
	    }

        private ExitCode m_exitCode;
    }


    // MyClientSessionChannel

    public class MyClientSessionChannel : ClientSessionChannel
    {
        public MyClientSessionChannel(ClientBase client, ExitCode exitCode) : base(client)
        {
            m_exitCode = exitCode;
            this.OnExitStatus += new ExitStatusEventHandler(this.OnMyExitStatus);
        }

        private void OnMyExitStatus(object sender, ExitStatus status)
        {
            if (m_exitCode.m_exitCode == ExitCodes.Success)
                m_exitCode.m_exitCode = (ExitCodes)((int)ExitCodes.ExitCodeBase + (int)status.Code);
        }   // i.e. user enters Eof (Ctrl-Z + Enter)

        private ExitCode m_exitCode;
    };


    // StdInput

    public class StdInput
    {
        public byte[] m_data = new byte[BUF_SIZE];
        public int m_dataSize;
        public string m_exceptionMessage;
        public AutoResetEvent m_newData = new AutoResetEvent(false);
        public AutoResetEvent m_threadDone = new AutoResetEvent(false);
        public AutoResetEvent m_continueRead = new AutoResetEvent(false);

        public StdInput()
        {
            m_thread = new Thread(this.ThreadRun);
            m_thread.IsBackground = true;
            m_thread.Start();
        }

        private void ThreadRun()
        {
            while (true)
            {
                try { m_dataSize = m_stdInp.Read(m_data, 0, BUF_SIZE); }
                catch (System.Exception e)
                {
                    m_exceptionMessage = e.Message;
                    break;
                }
                m_newData.Set();
                m_continueRead.WaitOne();
            }
            m_threadDone.Set();
        }

        private const int BUF_SIZE = 1024;
        private Stream m_stdInp = Console.OpenStandardInput();
        private Thread m_thread;
    };


    // ChannelInputThread

    public class ChannelInputThread
    {
        public Thread m_thread;
        public AutoResetEvent m_threadDone = new AutoResetEvent(false);

        public ChannelInputThread(ClientSessionChannel channel, EventWaitHandle outputThreadDone, ExitCode exitCode)
        {
            m_channel = channel;
            m_outputThreadDone = outputThreadDone;
            m_exitCode = exitCode;

            m_thread = new Thread(this.ThreadRun);
            m_thread.Start();
        }

        public void ThreadRun()
        {
	        try
	        {
                StdInput stdInput = new StdInput();
                ProgressHandler progress = new ProgressHandler();

                while (true)
                {
                    WaitHandle[] waitObjects = { m_outputThreadDone, stdInput.m_threadDone, stdInput.m_newData };
                    int dw = WaitHandle.WaitAny(waitObjects);
                    if (dw == 0)
                        break;
                    else if (dw == 1)
                    {	// stdInput::ThreadRun terminated
                        Console.WriteLine("Stream.Read(..) failed with exception: {0}.", stdInput.m_exceptionMessage);
                        if (m_exitCode.m_exitCode == ExitCodes.Success)
                            m_exitCode.m_exitCode = ExitCodes.FatalError;
                        break;
                    }

                    if (stdInput.m_dataSize == 0) // user enters Eof (Ctrl-Z + Enter)?
                        break;

                    m_channel.Send(stdInput.m_data, 0, stdInput.m_dataSize, false, progress);
                    stdInput.m_continueRead.Set();
                    progress.WaitDone();
                }
	        }
            catch (System.Exception e) // includes Bitvise.FlowSshNet.Exception
	        {
                Console.WriteLine(e.Message);
                if (m_exitCode.m_exitCode == ExitCodes.Success)
                    m_exitCode.m_exitCode = ExitCodes.FatalError;
	        }

            // send Eof (implicitly terminates ChannelOutputThread)
            try { m_channel.Send(null, true, null); } catch (System.Exception) {}
            m_threadDone.Set();
        }

        private ClientSessionChannel m_channel;
        private EventWaitHandle m_outputThreadDone;
        private ExitCode m_exitCode;
    }


    // ChannelOutputThread

    public class ChannelOutputThread
    {
        public Thread m_thread;
        public AutoResetEvent m_threadDone = new AutoResetEvent(false);

        public ChannelOutputThread(ClientSessionChannel channel, ExitCode exitCode)
        {
            m_channel = channel;
            m_exitCode = exitCode;

            m_thread = new Thread(this.ThreadRun);
            m_thread.Start();
        }

        public void ThreadRun()
        {
            try
	        {		
                Stream stdErr = Console.OpenStandardError();
                Stream stdOut = Console.OpenStandardOutput();

                ReceiveHandler receiver = new ReceiveHandler();
		        do
		        {
                    m_channel.Receive(receiver);
			        receiver.WaitDone();

			        if (receiver.Success)	// true until ClientSessionChannel is or gets closed
			        {
                        byte[] data = receiver.GetData();
                        if (data != null)
                        {
                            if (receiver.StdErr()) stdErr.Write(data, 0, data.Length); 
                            else                   stdOut.Write(data, 0, data.Length);
				        }
			        }
		        } while (receiver.Success && !receiver.Eof());
	        }
            catch (System.Exception e) // inclusive Bitvise.FlowSshNet.Exception's
	        {
                Console.WriteLine(e.Message);
                if (m_exitCode.m_exitCode == ExitCodes.Success)
                    m_exitCode.m_exitCode = ExitCodes.FatalError;
	        }
            m_threadDone.Set();
        }

        private ClientSessionChannel m_channel;
        private ExitCode m_exitCode;
    }


    // Program

    class Program
    {
        static void OnUncaughtExceptionInEvent(object sender, bool fatal, System.Exception e)
        {
            Console.WriteLine("Error: " + e.ToString());
            System.Environment.Exit((int)ExitCodes.FatalError);
        }

        static int Main(string[] args)
        {
            CmdLineParams cmdpar = new CmdLineParams(args);
            if (!cmdpar.m_bOk)
                return (int)ExitCodes.UsageError;

            ChannelOutputThread outputThread = null;
            ChannelInputThread  inputThread = null;

            ExitCode exitCode = new ExitCode();
            try
            {
                // Register delegate for uncaught exceptions in user-defined events.
                // Example: If there is an uncaught exception in MyClient.OnMyHostKey, 
                //          then this is reported in OnUncaughtExceptionInEvent.
                SshNet.OnExceptionInEvent += new ExceptionInEventEventHandler(OnUncaughtExceptionInEvent);

                // For use in deployed applications where Bitvise SSH Client might not be installed, 
                // provide an activation code using SetActCode to ensure that FlowSsh does not  
                // display an evaluation dialog. On computers with Bitvise SSH Client, use of 
                // FlowSsh is permitted under the same terms as the Client; in this case, there 
                // will be no evaluation dialog, and SetActCode does not have to be called.
                //
                //SshNet.SetActCode("Enter Your Activation Code Here");

                // create client

                MyClient client = new MyClient(exitCode);
                // CHANGE APPLICATION NAME IN PRODUCTION CODE!
                client.SetAppName("FlowSshNet_Exec 1.0");
                client.SetClientSettings(cmdpar);

                // connect client

                ProgressHandler progress = new ProgressHandler();
                client.Connect(progress);
                progress.WaitDone();

                if (!progress.Success)
                {	// Alternatively we could use a ProgressHandler::OnError
                    // event handler and do the logging there.
                    uint step = progress.GetTaskSpecificStep();
                    string auxInfo = progress.GetAuxInfo();
                    Console.WriteLine("{0}", ProgressHandler.DescribeConnectError(step, auxInfo));
                    return (int)ExitCodes.SessionError;
                }

                // open session channel

                MyClientSessionChannel channel = new MyClientSessionChannel(client, exitCode);
                channel.OpenRequest(progress);
		        progress.WaitDone();

		        if (!progress.Success)
		        {
                    string auxInfo = (progress.GetAuxInfo().Length > 0) ? progress.GetAuxInfo() : "(no additional info)";
			        Console.WriteLine("Opening session channel failed: {0}", auxInfo);
                    return (int)ExitCodes.SessionError;
		        }

                // Create output thread - thread for reading channel's incoming data.
		        // Note that data can arrive at any point after channel is opened.

                outputThread = new ChannelOutputThread(channel, exitCode);


                // send requests (pty, exec, shell)

		        if (cmdpar.m_sPty != null)
		        {
                    channel.PtyRequest(cmdpar.m_sPty, 80, 25, progress);
			        progress.WaitDone();
			        if (!progress.Success)
			        {
                        string auxInfo = (progress.GetAuxInfo().Length > 0) ? progress.GetAuxInfo() : "(no additional info)";
				        Console.WriteLine("Pty request failed: {0}", auxInfo);
                        return (int)ExitCodes.SessionError;
			        }
		        }

                if (cmdpar.m_sCmd != null)
		        {
                    channel.ExecRequest(cmdpar.m_sCmd, progress);
			        progress.WaitDone();
                    if (!progress.Success)
			        {
                        string auxInfo = (progress.GetAuxInfo().Length > 0) ? progress.GetAuxInfo() : "(no additional info)";
				        Console.WriteLine("Exec request failed: {0}", auxInfo);
                        return (int)ExitCodes.SessionError;
			        }
		        }
		        else
		        {
                    channel.ShellRequest(progress);
			        progress.WaitDone();
			        if (!progress.Success)
			        {
                        string auxInfo = (progress.GetAuxInfo().Length > 0) ? progress.GetAuxInfo() : "(no additional info)";
                        Console.WriteLine("Shell request failed: {0}", auxInfo);
                        return (int)ExitCodes.SessionError;
			        }
		        }

                // create input thread

                inputThread = new ChannelInputThread(channel, outputThread.m_threadDone, exitCode);
            }
            catch (System.Exception e) // inclusive Bitvise.FlowSshNet.Exception's
            {
                Console.WriteLine("{0}", e.Message);
                SshNet.Shutdown();  // drop connection, close channel
                return (int)ExitCodes.FatalError;
            }
            finally
            {
                // Output thread exits if channel gets closed or if Eof is received.
                // Input thread exits if output thread has finished or if user enters Eof (i.e. Ctrl + Z).
                if (outputThread != null && outputThread.m_thread != null)
                    outputThread.m_thread.Join();

                if (inputThread != null && inputThread.m_thread != null)
                    inputThread.m_thread.Join();

                SshNet.Shutdown();
            }
            return (int)exitCode.m_exitCode;
        }
    } // Program
}