Skip to content

Commit

Permalink
Support creating Shell(Stream) without PTY (#1419)
Browse files Browse the repository at this point in the history
* Support creating Shell(Stream) without PTY
Fixes #1418

* Add integration test for "PermitTTY no"

* Fix Integration Test

* Remove duplicate shell request

* Put common operations in a shared constructor. Update xml doc comments.

* Update comments and method overriding

* Update per code review

* Update integration tests

* Renaming

* Make `bufferSize` optional

* Try fix the test

* Update per code review

* try agian

* try again

* docs

* doc

---------

Co-authored-by: Rob Hague <[email protected]>
  • Loading branch information
scott-xu and Rob-Hague authored Jun 16, 2024
1 parent 830e504 commit 9dc7db9
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 26 deletions.
11 changes: 11 additions & 0 deletions src/Renci.SshNet/IServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ ShellStream CreateShellStream(ISession session,
IDictionary<TerminalModes, uint> terminalModeValues,
int bufferSize);

/// <summary>
/// Creates a shell stream without allocating a pseudo terminal.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">Size of the buffer.</param>
/// <returns>
/// The created <see cref="ShellStream"/> instance.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
ShellStream CreateShellStreamNoTerminal(ISession session, int bufferSize);

/// <summary>
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
/// any embedded double quote with a backslash.
Expand Down
6 changes: 6 additions & 0 deletions src/Renci.SshNet/ServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ public ShellStream CreateShellStream(ISession session, string terminalName, uint
return new ShellStream(session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
}

/// <inheritdoc/>
public ShellStream CreateShellStreamNoTerminal(ISession session, int bufferSize)
{
return new ShellStream(session, bufferSize);
}

/// <summary>
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
/// any embedded double quote with a backslash.
Expand Down
67 changes: 61 additions & 6 deletions src/Renci.SshNet/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Renci.SshNet
/// </summary>
public class Shell : IDisposable
{
private const int DefaultBufferSize = 1024;

private readonly ISession _session;
private readonly string _terminalName;
private readonly uint _columns;
Expand All @@ -24,6 +26,7 @@ public class Shell : IDisposable
private readonly Stream _outputStream;
private readonly Stream _extendedOutputStream;
private readonly int _bufferSize;
private readonly bool _noTerminal;
private ManualResetEvent _dataReaderTaskCompleted;
private IChannelSession _channel;
private AutoResetEvent _channelClosedWaitHandle;
Expand Down Expand Up @@ -77,24 +80,66 @@ public class Shell : IDisposable
/// <param name="terminalModes">The terminal modes.</param>
/// <param name="bufferSize">Size of the buffer for output stream.</param>
internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModes, int bufferSize)
: this(session, input, output, extendedOutput, bufferSize, noTerminal: false)
{
_session = session;
_input = input;
_outputStream = output;
_extendedOutputStream = extendedOutput;
_terminalName = terminalName;
_columns = columns;
_rows = rows;
_width = width;
_height = height;
_terminalModes = terminalModes;
}

/// <summary>
/// Initializes a new instance of the <see cref="Shell"/> class.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="input">The input.</param>
/// <param name="output">The output.</param>
/// <param name="extendedOutput">The extended output.</param>
/// <param name="bufferSize">Size of the buffer for output stream.</param>
internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize)
: this(session, input, output, extendedOutput, bufferSize, noTerminal: true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Shell"/> class.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="input">The input.</param>
/// <param name="output">The output.</param>
/// <param name="extendedOutput">The extended output.</param>
/// <param name="bufferSize">Size of the buffer for output stream.</param>
/// <param name="noTerminal">Disables pseudo terminal allocation or not.</param>
private Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize, bool noTerminal)
{
if (bufferSize == -1)
{
bufferSize = DefaultBufferSize;
}
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
#else
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
#endif
_session = session;
_input = input;
_outputStream = output;
_extendedOutputStream = extendedOutput;
_bufferSize = bufferSize;
_noTerminal = noTerminal;
}

/// <summary>
/// Starts this shell.
/// </summary>
/// <exception cref="SshException">Shell is started.</exception>
/// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
public void Start()
{
if (IsStarted)
Expand All @@ -112,8 +157,18 @@ public void Start()
_session.ErrorOccured += Session_ErrorOccured;

_channel.Open();
_ = _channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes);
_ = _channel.SendShellRequest();
if (!_noTerminal)
{
if (!_channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes))
{
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
}
}

if (!_channel.SendShellRequest())
{
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
}

_channelClosedWaitHandle = new AutoResetEvent(initialState: false);

Expand Down
88 changes: 68 additions & 20 deletions src/Renci.SshNet/ShellStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace Renci.SshNet
/// </summary>
public class ShellStream : Stream
{
private const int DefaultBufferSize = 1024;

private readonly ISession _session;
private readonly Encoding _encoding;
private readonly IChannelSession _channel;
Expand All @@ -29,6 +31,7 @@ public class ShellStream : Stream
private readonly object _sync = new object();

private readonly byte[] _writeBuffer;
private readonly bool _noTerminal;
private int _writeLength; // The length of the data in _writeBuffer.

private byte[] _readBuffer;
Expand Down Expand Up @@ -95,7 +98,68 @@ private void AssertValid()
/// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
internal ShellStream(ISession session, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModeValues, int bufferSize)
: this(session, bufferSize, noTerminal: false)
{
try
{
_channel.Open();

if (!_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues))
{
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
}

if (!_channel.SendShellRequest())
{
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
}
}
catch
{
Dispose();
throw;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ShellStream"/> class.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">The size of the buffer.</param>
/// <exception cref="SshException">The channel could not be opened.</exception>
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
internal ShellStream(ISession session, int bufferSize)
: this(session, bufferSize, noTerminal: true)
{
try
{
_channel.Open();

if (!_channel.SendShellRequest())
{
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
}
}
catch
{
Dispose();
throw;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ShellStream"/> class.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">The size of the buffer.</param>
/// <param name="noTerminal">Disables pseudo terminal allocation or not.</param>
/// <exception cref="SshException">The channel could not be opened.</exception>
private ShellStream(ISession session, int bufferSize, bool noTerminal)
{
if (bufferSize == -1)
{
bufferSize = DefaultBufferSize;
}
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
#else
Expand All @@ -119,25 +183,7 @@ internal ShellStream(ISession session, string terminalName, uint columns, uint r
_readBuffer = new byte[bufferSize];
_writeBuffer = new byte[bufferSize];

try
{
_channel.Open();

if (!_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues))
{
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
}

if (!_channel.SendShellRequest())
{
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
}
}
catch
{
Dispose();
throw;
}
_noTerminal = noTerminal;
}

/// <summary>
Expand Down Expand Up @@ -848,7 +894,9 @@ public override void Write(byte[] buffer, int offset, int count)
/// <exception cref="ObjectDisposedException">The stream is closed.</exception>
public void WriteLine(string line)
{
Write(line + "\r");
// By default, the terminal driver translates carriage return to line feed on input.
// See option ICRLF at https://www.man7.org/linux/man-pages/man3/termios.3.html.
Write(line + (_noTerminal ? "\n" : "\r"));
}

/// <inheritdoc/>
Expand Down
35 changes: 35 additions & 0 deletions src/Renci.SshNet/SshClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,25 @@ public Shell CreateShell(Encoding encoding, string input, Stream output, Stream
return CreateShell(encoding, input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
}

/// <summary>
/// Creates the shell without allocating a pseudo terminal,
/// similar to the <c>ssh -T</c> option.
/// </summary>
/// <param name="input">The input.</param>
/// <param name="output">The output.</param>
/// <param name="extendedOutput">The extended output.</param>
/// <param name="bufferSize">Size of the internal read buffer.</param>
/// <returns>
/// Returns a representation of a <see cref="Shell" /> object.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public Shell CreateShellNoTerminal(Stream input, Stream output, Stream extendedOutput, int bufferSize = -1)
{
EnsureSessionIsOpen();

return new Shell(Session, input, output, extendedOutput, bufferSize);
}

/// <summary>
/// Creates the shell stream.
/// </summary>
Expand Down Expand Up @@ -450,6 +469,22 @@ public ShellStream CreateShellStream(string terminalName, uint columns, uint row
return ServiceFactory.CreateShellStream(Session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
}

/// <summary>
/// Creates the shell stream without allocating a pseudo terminal,
/// similar to the <c>ssh -T</c> option.
/// </summary>
/// <param name="bufferSize">The size of the buffer.</param>
/// <returns>
/// The created <see cref="ShellStream"/> instance.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public ShellStream CreateShellStreamNoTerminal(int bufferSize = -1)
{
EnsureSessionIsOpen();

return ServiceFactory.CreateShellStreamNoTerminal(Session, bufferSize);
}

/// <summary>
/// Stops forwarded ports.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ public RemoteSshdConfig PrintMotd(bool? value = true)
return this;
}

/// <summary>
/// Specifies whether TTY is permitted.
/// </summary>
/// <param name="value"><see langword="true"/> to permit TTY.</param>
/// <returns>
/// The current <see cref="RemoteSshdConfig"/> instance.
/// </returns>
public RemoteSshdConfig PermitTTY(bool? value = true)
{
_config.PermitTTY = value;
return this;
}

/// <summary>
/// Specifies whether TCP forwarding is permitted.
/// </summary>
Expand Down
Loading

0 comments on commit 9dc7db9

Please sign in to comment.