Last active
April 8, 2021 09:46
-
-
Save blenderfreaky/3d2d03a9af6eaa30ec534437774c779d to your computer and use it in GitHub Desktop.
A hack for writing data to SshCommands
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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