How to write to stdin/read from stdout of an SSH-c

2019-05-10 07:29发布

问题:

I want to connect with an SSH server which, upon connection, runs a custom command which expects XML as input (UTF-8 encoded, multiple lines, with a special line at the end of each XML document marking its end) and outputs XML (UTF-8 encoded, multiple lines, with same special line at the end to signify end-of-document).

There currently is a mechanism using plink.exe (of PuTTy fame) that works (meaning: the server-side does work as specified), but I am in the process of eliminating the need for an external process.

I'm giving a shot to the Renci.SshNet library, but I find the documentation inadequate, and can't find examples for what I need.

My attempt so far:

using System;
using Renci.SshNet;

namespace ServerConnect
{
    public class ServerConnection
    {
        private const string EOC= "*-* COMMAND DELIMITER *-*";
        private SshClient scon;
        private System.IO.Stream stdin, stdout, stderr;
        private System.IO.StreamWriter stdin_writer;
        private System.IO.StreamReader stdout_reader;
        private Shell shell;

        public ServerConnection (string keyFileName, string keyFilePassphrase)
        {
            ConnectionInfo ci= new ConnectionInfo("my.server.local", 22, "datamgr",
                                                  new PrivateKeyAuthenticationMethod("datamgr", new PrivateKeyFile(keyFileName, keyFilePassphrase)));
            this.scon= new SshClient(ci);
        }

        public ServerConnection (string keyFilePassphrase): this("client_id_rsa", keyFilePassphrase) {}

        public void Connect() {
            this.scon.Connect();
            this.stdin= new System.IO.MemoryStream();
            this.stdin_writer= new System.IO.StreamWriter(stdin);
            this.stdout= new System.IO.MemoryStream();
            this.stdout_reader= new System.IO.StreamReader(stdout);
            this.stderr= new System.IO.MemoryStream();
            this.shell= this.scon.CreateShell(this.stdin, this.stdout, this.stderr);
            this.shell.Start();
        }

        public void Disconnect() {
            this.scon.Disconnect();
        }

        public string Command(string text) {
            Console.WriteLine("sending command");
            this.stdin_writer.WriteLine(text);
            Console.WriteLine("sending EOC");
            this.stdin_writer.WriteLine(EOC);
            this.stdin_writer.Flush();
            for (;;) {
                string line;
                line= this.stdout_reader.ReadLine();
                if (line == null) {
                    Console.WriteLine("got null");
                    break;
                }
                Console.WriteLine("READ");
                Console.WriteLine(line);
                if (line.StartsWith(EOC)) break;
            }
            return ""; // yes, this should return the built output
        }
    }
}

Given this as an assembly, I'm at the stage that I try to connect to the server (tests run through booish for ease):

>>> import ServerConnect
>>> a= ServerConnection("passphrase")
ServerConnect.ServerConnection
>>> a.Connect()
>>> a.Command("<i command=\"ping\"/>")
sending command
sending EOC
got null
''

I can see (in the server's auth.log) that the connection is established after the a.Connect() call. However, it seems that whatever I write to stdin_writer does not get sent to the server.

How can I accomplish this mechanism? Send data to the server, then send the delimiter, and subsequently read data until I read the delimiter? The cycle will be repeated many times during a connection.

Please note that I am not an experienced C# programmer, so it's possible that I have a lack of basic understanding of the C# stream model.

PS I have seen this answer, but it seems that the CreateShellStream call blocks, probably in a PseudoTty allocation (the process has no need for pseudo-ttys, just input/output).

PS2 I downloaded and compiled the most current version of Renci.SshNet from codeplex.com, and now I am at this point (excerpt from the Connect method):

this.scon.Connect();
this.stream= this.scon.CreateShellStream("dumb", 80, 24, 800, 600, 1024);
this.stdin_writer= new System.IO.StreamWriter(this.stream);
this.stdout_reader= new System.IO.StreamReader(this.stream);
this.shell= this.scon.CreateShell(this.stream, this.stream, this.stream);
this.shell.Start();

However, any writes to the stdin_writer (with subsequent flushes) do not seem to go through.

回答1:

Sorry, after typing this up I noticed I am necro posting...

You can use the solution given here, which is what I used when I wanted to do something similar to you:

Renci ssh.net - Create console screen from form when connected

You can ditch using any of the System.IO objects and manipulate the SSH session IO using the ShellStream object.

ShellStream.Expect will read the output, and look for a specific string. ShellStream.WriteLine will send input to the shell.

        string reply = string.Empty;
        ShellStream ss = scon.CreateShellStream("dumb", 80, 24, 800, 600, 1024);

        reply = ss.Expect("Press enter to continue");  //The prompt I get after login
        ParseOutput(reply);

        ss.WriteLine("./myscript.sh")
        reply = ss.Expect("$"); //My prompt, when the script has finished.
        ParseOutput(reply);

ParseOutput is a routine I wrote to split the "reply" string at each linefeed character and add each line to a listbox on a form.



回答2:

This is not possible with Renci.SshNet (yet).

The first response does not address the original question of piping data into the standard input of the command that is being executed remotely. A typical example would be to stream the contents of a compressed file to a decompressor to be executed on the target host.

Only recently (Sept 11 2016) was this enhancement proposed for the SSH.NET project in GitHub. You can see it here:

Support writing to stdin when executing a remote command



标签: c# ssh