Skip to content

Instantly share code, notes, and snippets.

@blenderfreaky
Last active April 8, 2021 09:46
Show Gist options
  • Save blenderfreaky/3d2d03a9af6eaa30ec534437774c779d to your computer and use it in GitHub Desktop.
Save blenderfreaky/3d2d03a9af6eaa30ec534437774c779d to your computer and use it in GitHub Desktop.
A hack for writing data to SshCommands
var envHome = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "USERPROFILE" : "HOME";
var home = Environment.GetEnvironmentVariable(envHome);
var ssh = new SshClient(new ConnectionInfo("localhost", Environment.UserName,
new PrivateKeyAuthenticationMethod(
Environment.UserName,
new PrivateKeyFile(home + "/.ssh/id_ed25519"))));
var command = ssh.CreateCommand("path/to/server");
command.BeginExecute();
var inStream = command.OutputStream;
var outStream = new SshOutStream(command);
// Read from inStream and write to outStream
// outStream does not receive anything written to inStream,
// unlike in a regular ssh terminal (I assume that's because this is running in the
// exec channel of ssh instead of the shell one)
// These streams can handle arbirtrary input and not just text
var inStream = Console.OpenStandardInput();
var outStream = Console.OpenStandardOutput();
// Read from inStream and write to outStream
// Don't use Console.Write/Read to avoid problems with Encoding
using Renci.SshNet;
using System;
using System.Linq.Expressions;
using System.Text;
/// <summary>
/// Extensions for the <see cref="SshCommand"/> class that allow sending data into the stream.
/// </summary>
public static class SshCommandExtensions
{
// HACK: This is a house of cards that relies on private variables.
// Although these are unlikely to change, this means minor version changes for Renci.SshNet can cause breaking changes.
// Prepare parameters for the lambdas below
private static readonly ParameterExpression _sshCommand = Expression.Parameter(typeof(SshCommand), "sshCommand");
private static readonly ParameterExpression _data = Expression.Parameter(typeof(byte[]), "data");
private static readonly ParameterExpression _size = Expression.Parameter(typeof(int), "size");
private static readonly ParameterExpression _offset = Expression.Parameter(typeof(int), "offset");
// Store the access to _channel to access the type after
private static readonly Expression _channelSession = Expression.PropertyOrField(_sshCommand, "_channel");
// Get the IChannel interface to allow calling SendData (it doesn't work when calling on IChannelSession)
private static readonly Type _iChannelType = _channelSession.Type.GetInterface("IChannel")!;
// void IChannel.SendData(byte[] data);
private static readonly Action<SshCommand, byte[]> _sendData1 =
Expression.Lambda<Action<SshCommand, byte[]>>(
Expression.Call(
Expression.Convert(_channelSession, _iChannelType), // Convert to allow calling SendData
"SendData", Array.Empty<Type>(), _data),
_sshCommand, _data).Compile();
// void IChannel.SendData(byte[] data, int offset, int size);
private static readonly Action<SshCommand, byte[], int, int> _sendData3 =
Expression.Lambda<Action<SshCommand, byte[], int, int>>(
Expression.Call(
Expression.Convert(_channelSession, _iChannelType),
"SendData", Array.Empty<Type>(), _data, _offset, _size),
_sshCommand, _data, _offset, _size).Compile();
/// <summary>
/// Sends a SSH_MSG_CHANNEL_DATA message with the specified payload.
/// </summary>
/// <param name="sshCommand">The <see cref="SshCommand"/> to send for.</param>
/// <param name="data">The payload to send.</param>
public static void SendData(this SshCommand sshCommand, byte[] data) => _sendData1(sshCommand, data);
/// <summary>
/// Sends a SSH_MSG_CHANNEL_DATA message with the specified payload.
/// </summary>
/// <param name="sshCommand">The <see cref="SshCommand"/> to send for.</param>
/// <param name="data">An array of System.Byte containing the payload to send.</param>
/// <param name="offset">The zero-based offset in data at which to begin taking data from.</param>
/// <param name="size">The number of bytes of data to send.</param>
/// <remarks>
/// When the size of the data to send exceeds the maximum packet size or the remote
/// window size does not allow the full data to be sent, then this method will send
/// the data in multiple chunks and will wait for the remote window size to be adjusted
/// when it's zero.
/// This is done to support SSH servers will a small window size that do not agressively
/// increase their window size. We need to take into account that there may be SSH
/// servers that only increase their window size when it has reached zero.
/// </remarks>
public static void SendData(this SshCommand sshCommand, byte[] data, int offset, int size) => _sendData3(sshCommand, data, offset, size);
/// <summary>
/// Writes <paramref name="text"/> into the stdin of <paramref name="sshCommand"/>.
/// </summary>
/// <param name="sshCommand">The <see cref="SshCommand"/> to send for.</param>
/// <param name="text">The text to send.</param>
public static void Write(this SshCommand sshCommand, string text) => sshCommand.SendData(Encoding.UTF8.GetBytes(text));
/// <summary>
/// Writes <paramref name="text"/> and a newline into the stdin of <paramref name="sshCommand"/>.
/// </summary>
/// <param name="sshCommand">The <see cref="SshCommand"/> to send for.</param>
/// <param name="text">The text to send.</param>
public static void WriteLine(this SshCommand sshCommand, string text) => sshCommand.Write(text + Environment.NewLine);
}
using Renci.SshNet;
using System;
using System.IO;
/// <summary>
/// A stream for writing data into an <see cref="SshCommand"/>s stdin.
/// </summary>
public class SshOutStream : Stream
{
/// <summary>
/// The command the stream is for.
/// </summary>
public SshCommand Command { get; init; }
/// <summary>
/// Initializes a new instance of the class <see cref="SshOutStream"/>.
/// </summary>
/// <param name="command">The command the stream is for.</param>
public SshOutStream(SshCommand command) => Command = command;
/// <inheritdoc/>
public override bool CanRead => false;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => true;
/// <inheritdoc/>
public override long Length => 0;
/// <inheritdoc/>
public override long Position { get; set; } = 0;
/// <inheritdoc/>
public override void Flush()
{
// Auto-Flush on write for now.
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
Command.SendData(buffer, offset, count);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment