4

I am new to SSH.NET and I am using it in a project that I am currently working on.

I have to run a sudo command using SSH.NET and that is why I am using ShellStream to run the command and provide authentication for the sudo command. Now I am trying to run a sudo command that finds a file in a directory on the server that I ssh to. The command is as follows:

sudo -S find -name 29172

This command is supposed to output a path that indicates where the file is located as follows:

./output/directory/29172

The problem that I am having now is that when I do this through shell stream I do not know how to get the output. And even when I try to read the ShellStream I end up with this:

var client = new SshClient(IP, username, password);

var stream = client.CreateShellStream("input", 0, 0, 0, 0, 1000000);

stream.WriteLine("sudo -S find . -name 29172");

stream.WriteLine("\"" +password+"\"");

var output = stream.ReadToEnd();

What output usually gives is a description of when I logged into the server using SSH.NET and then the commands that I provided to the system:

"Last login: Mon Sep 23 15:23:35 2019 from 100.00.00.00\r\r\nserver@ubuntu-dev4:~$ sudo -S find . -name 29172\r\n[sudo] password for server: \r\n"

I am not looking for this output, rather I am looking for the actual output of the command such as "./output/directory/29172" from the ShellStream. Would anyone know how to do this? Thank you for reading and I hope to hear from you soon.

8
  • 1
    You created a string for input, do you need to also for output? Commented Sep 23, 2019 at 15:54
  • stream.WriteLine("\"" +password+"\""); – Why the double-quotes? It should be simply stream.WriteLine(password); Commented Sep 23, 2019 at 16:36
  • And in general, automating sudo is a bad approach. See Allowing automatic command execution as root on Linux using SSH. Commented Sep 23, 2019 at 16:37
  • As an aside, make sure you're deterministically disposing of stream and client by employing the using construct. Commented Sep 23, 2019 at 17:37
  • 1
    And yes @Jesse C.Slicer I am using the using keyword thank you for the tip. I just did not put it because I thought it was not necessary for my example. Commented Sep 24, 2019 at 18:27

1 Answer 1

3

My solution is quite lengthy but it does a few other necessary things to reliably run commands over ssh:

  • automatically respond to authentication requests
  • captures error codes and throw on failures

To automate sudo over SSH we can use Expect - this is like the linux tool of the same name and lets you respond to interactively. It waits until there is some output that matches a pattern e.g. a password prompt.

If you have a series of sudo operations you can be caught by the unpredictable amount of time until sudo requires reauthentication so sudo might or might not need authentication, we can't be sure.

A big issue when automating is to know whether a command fails or not. The only way to know is to get the last error over the shell. I throw on non-zero.

The regex to match the shell prompt might need to be customized for your configuration. All sorts of things might be injected into the prompt.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Renci.SshNet;
using Renci.SshNet.Common;

namespace Example
{
    public class Log
    {
        public static void Verbose(string message) =>
            Console.WriteLine(message);

        public static void Error(string message) =>
            Console.WriteLine(message);
    }

    public static class StringExt
    {
        public static string StringBeforeLastRegEx(this string str, Regex regex)
        {
            var matches = regex.Matches(str);

            return matches.Count > 0
                ? str.Substring(0, matches.Last().Index)
                : str;

        }

        public static bool EndsWithRegEx(this string str, Regex regex)
        {
            var matches = regex.Matches(str);

            return
                matches.Count > 0 &&
                str.Length == (matches.Last().Index + matches.Last().Length);
        }

        public static string StringAfter(this string str, string substring)
        {
            var index = str.IndexOf(substring, StringComparison.Ordinal);

            return index >= 0
                ? str.Substring(index + substring.Length)
                : "";
        }

        public static string[] GetLines(this string str) =>
            Regex.Split(str, "\r\n|\r|\n");
    }

    public static class UtilExt
    {
        public static void ForEach<T>(this IEnumerable<T> sequence, Action<T> func) 
        {
            foreach (var item in sequence)
            {
                func(item);
            }
        }
    }

    public class Ssh
    {
        SshClient sshClient;
        ShellStream shell;
        string pwd = "";
        string lastCommand = "";

        static Regex prompt = new Regex("[a-zA-Z0-9_.-]*\\@[a-zA-Z0-9_.-]*\\:\\~[#$] ", RegexOptions.Compiled);
        static Regex pwdPrompt = new Regex("password for .*\\:", RegexOptions.Compiled);
        static Regex promptOrPwd = new Regex(Ssh.prompt + "|" + Ssh.pwdPrompt);

        public void Connect(string url, int port, string user, string pwd)
        {
            Log.Verbose($"Connect Ssh: {user}@{pwd}:{port}");

            var connectionInfo =
                new ConnectionInfo(
                    url,
                    port,
                    user,
                    new PasswordAuthenticationMethod(user, pwd));

            this.pwd = pwd;
            this.sshClient = new SshClient(connectionInfo);
            this.sshClient.Connect();

            var terminalMode = new Dictionary<TerminalModes, uint>();
            terminalMode.Add(TerminalModes.ECHO, 53);

            this.shell = this.sshClient.CreateShellStream("", 0, 0, 0, 0, 4096, terminalMode);

            try
            {
                this.Expect(Ssh.prompt);
            }
            catch (Exception ex)
            {
                Log.Error("Exception - " + ex.Message);
                throw;
            }
        }

        public void Disconnect()
        {
            Log.Verbose($"Ssh Disconnect");

            this.sshClient?.Disconnect();
            this.sshClient = null;
        }

        void Write(string commandLine)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Log.Verbose("> " + commandLine);
            Console.ResetColor(); 

            this.lastCommand = commandLine;

            this.shell.WriteLine(commandLine);
        }

        string Expect(Regex expect, double timeoutSeconds = 60.0)
        {
            var result = this.shell.Expect(expect, TimeSpan.FromSeconds(timeoutSeconds));

            if (result == null)
            {
                throw new Exception($"Timeout {timeoutSeconds}s executing {this.lastCommand}");
            }

            result = result.Contains(this.lastCommand) ? result.StringAfter(this.lastCommand) : result;
            result = result.StringBeforeLastRegEx(Ssh.prompt);
            result = result.Trim();

            result.GetLines().ForEach(x => Log.Verbose(x));

            return result;
        }

        public string Execute(string commandLine, double timeoutSeconds = 30.0)
        {
            Exception exception = null;
            var result = "";
            var errorMessage = "failed";
            var errorCode = "exception";

            try
            {
                this.Write(commandLine);
                result = this.Expect(Ssh.promptOrPwd);

                if (result.EndsWithRegEx(pwdPrompt))
                {
                    this.Write(this.pwd);
                    this.Expect(Ssh.prompt);
                }

                this.Write("echo $?");
                errorCode = this.Expect(Ssh.prompt);

                if (errorCode == "0")
                {
                    return result;    
                }
                else if (result.Length > 0)
                {
                    errorMessage = result;
                }
            }
            catch (Exception ex)
            {
                exception = ex;
                errorMessage = ex.Message;
            }

            throw new Exception($"Ssh error: {errorMessage}, code: {errorCode}, command: {commandLine}", exception);
        }
    }
}

And then use it like this:


var client = new Ssh(IP, 22, username, password);

var output = client.Execute("sudo -S find . -name 29172");
Sign up to request clarification or add additional context in comments.

2 Comments

ShellStream.Expect returns a string, but your Expect method calls var result = this.shell.Expect and then uses the result.StringAfter() and result.StringBeforeLastRegEx() method, which don't exist. What is this attempting to do here?
See the StringExt class in the example. They are string extensions for extracting substrings to e.g. strip shell prompts, strip the echoed command and identify password prompts.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.