Forráskód Böngészése

Support creating Shell(Stream) without PTY (#1419)

* Support creating Shell(Stream) without PTY
Fixes https://github.com/sshnet/SSH.NET/issues/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 <rob.hague00@gmail.com>
Scott Xu 1 éve
szülő
commit
9dc7db9de8

+ 11 - 0
src/Renci.SshNet/IServiceFactory.cs

@@ -137,6 +137,17 @@ namespace Renci.SshNet
                                       IDictionary<TerminalModes, uint> terminalModeValues,
                                       IDictionary<TerminalModes, uint> terminalModeValues,
                                       int bufferSize);
                                       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>
         /// <summary>
         /// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
         /// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
         /// any embedded double quote with a backslash.
         /// any embedded double quote with a backslash.

+ 6 - 0
src/Renci.SshNet/ServiceFactory.cs

@@ -206,6 +206,12 @@ namespace Renci.SshNet
             return new ShellStream(session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
             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>
         /// <summary>
         /// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
         /// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
         /// any embedded double quote with a backslash.
         /// any embedded double quote with a backslash.

+ 61 - 6
src/Renci.SshNet/Shell.cs

@@ -14,6 +14,8 @@ namespace Renci.SshNet
     /// </summary>
     /// </summary>
     public class Shell : IDisposable
     public class Shell : IDisposable
     {
     {
+        private const int DefaultBufferSize = 1024;
+
         private readonly ISession _session;
         private readonly ISession _session;
         private readonly string _terminalName;
         private readonly string _terminalName;
         private readonly uint _columns;
         private readonly uint _columns;
@@ -24,6 +26,7 @@ namespace Renci.SshNet
         private readonly Stream _outputStream;
         private readonly Stream _outputStream;
         private readonly Stream _extendedOutputStream;
         private readonly Stream _extendedOutputStream;
         private readonly int _bufferSize;
         private readonly int _bufferSize;
+        private readonly bool _noTerminal;
         private ManualResetEvent _dataReaderTaskCompleted;
         private ManualResetEvent _dataReaderTaskCompleted;
         private IChannelSession _channel;
         private IChannelSession _channel;
         private AutoResetEvent _channelClosedWaitHandle;
         private AutoResetEvent _channelClosedWaitHandle;
@@ -77,24 +80,66 @@ namespace Renci.SshNet
         /// <param name="terminalModes">The terminal modes.</param>
         /// <param name="terminalModes">The terminal modes.</param>
         /// <param name="bufferSize">Size of the buffer for output stream.</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)
         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;
             _terminalName = terminalName;
             _columns = columns;
             _columns = columns;
             _rows = rows;
             _rows = rows;
             _width = width;
             _width = width;
             _height = height;
             _height = height;
             _terminalModes = terminalModes;
             _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;
             _bufferSize = bufferSize;
+            _noTerminal = noTerminal;
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Starts this shell.
         /// Starts this shell.
         /// </summary>
         /// </summary>
         /// <exception cref="SshException">Shell is started.</exception>
         /// <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()
         public void Start()
         {
         {
             if (IsStarted)
             if (IsStarted)
@@ -112,8 +157,18 @@ namespace Renci.SshNet
             _session.ErrorOccured += Session_ErrorOccured;
             _session.ErrorOccured += Session_ErrorOccured;
 
 
             _channel.Open();
             _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);
             _channelClosedWaitHandle = new AutoResetEvent(initialState: false);
 
 

+ 68 - 20
src/Renci.SshNet/ShellStream.cs

@@ -20,6 +20,8 @@ namespace Renci.SshNet
     /// </summary>
     /// </summary>
     public class ShellStream : Stream
     public class ShellStream : Stream
     {
     {
+        private const int DefaultBufferSize = 1024;
+
         private readonly ISession _session;
         private readonly ISession _session;
         private readonly Encoding _encoding;
         private readonly Encoding _encoding;
         private readonly IChannelSession _channel;
         private readonly IChannelSession _channel;
@@ -29,6 +31,7 @@ namespace Renci.SshNet
         private readonly object _sync = new object();
         private readonly object _sync = new object();
 
 
         private readonly byte[] _writeBuffer;
         private readonly byte[] _writeBuffer;
+        private readonly bool _noTerminal;
         private int _writeLength; // The length of the data in _writeBuffer.
         private int _writeLength; // The length of the data in _writeBuffer.
 
 
         private byte[] _readBuffer;
         private byte[] _readBuffer;
@@ -95,7 +98,68 @@ namespace Renci.SshNet
         /// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</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>
         /// <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)
         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
 #if NET8_0_OR_GREATER
             ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
             ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
 #else
 #else
@@ -119,25 +183,7 @@ namespace Renci.SshNet
             _readBuffer = new byte[bufferSize];
             _readBuffer = new byte[bufferSize];
             _writeBuffer = 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>
         /// <summary>
@@ -848,7 +894,9 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
         /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
         public void WriteLine(string line)
         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/>
         /// <inheritdoc/>

+ 35 - 0
src/Renci.SshNet/SshClient.cs

@@ -391,6 +391,25 @@ namespace Renci.SshNet
             return CreateShell(encoding, input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
             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>
         /// <summary>
         /// Creates the shell stream.
         /// Creates the shell stream.
         /// </summary>
         /// </summary>
@@ -450,6 +469,22 @@ namespace Renci.SshNet
             return ServiceFactory.CreateShellStream(Session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
             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>
         /// <summary>
         /// Stops forwarded ports.
         /// Stops forwarded ports.
         /// </summary>
         /// </summary>

+ 13 - 0
test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs

@@ -69,6 +69,19 @@ namespace Renci.SshNet.IntegrationTests
             return this;
             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>
         /// <summary>
         /// Specifies whether TCP forwarding is permitted.
         /// Specifies whether TCP forwarding is permitted.
         /// </summary>
         /// </summary>

+ 54 - 0
test/Renci.SshNet.IntegrationTests/SshTests.cs

@@ -23,6 +23,7 @@ namespace Renci.SshNet.IntegrationTests
 
 
             _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig();
             _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig();
             _remoteSshdConfig.AllowTcpForwarding()
             _remoteSshdConfig.AllowTcpForwarding()
+                             .PermitTTY(true)
                              .PrintMotd(false)
                              .PrintMotd(false)
                              .Update()
                              .Update()
                              .Restart();
                              .Restart();
@@ -77,6 +78,24 @@ namespace Renci.SshNet.IntegrationTests
             }
             }
         }
         }
 
 
+        [TestMethod]
+        public void Ssh_CreateShellStreamNoTerminal()
+        {
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                using (var shellStream = client.CreateShellStreamNoTerminal(bufferSize: 1024))
+                {
+                    var foo = new string('a', 90);
+                    shellStream.WriteLine($"echo {foo}");
+                    var line = shellStream.ReadLine(TimeSpan.FromSeconds(1));
+                    Assert.IsNotNull(line);
+                    Assert.IsTrue(line.EndsWith(foo), line);
+                }
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// https://github.com/sshnet/SSH.NET/issues/63
         /// https://github.com/sshnet/SSH.NET/issues/63
         /// </summary>
         /// </summary>
@@ -171,6 +190,41 @@ namespace Renci.SshNet.IntegrationTests
             }
             }
         }
         }
 
 
+        [TestMethod]
+        public void Ssh_CreateShellNoTerminal()
+        {
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                using (var input = new MemoryStream())
+                using (var output = new MemoryStream())
+                using (var extOutput = new MemoryStream())
+                {
+                    var shell = client.CreateShellNoTerminal(input, output, extOutput, 1024);
+
+                    shell.Start();
+
+                    var inputWriter = new StreamWriter(input, Encoding.ASCII, 1024);
+                    var foo = new string('a', 90);
+                    inputWriter.WriteLine($"echo {foo}");
+                    inputWriter.Flush();
+                    input.Position = 0;
+
+                    Thread.Sleep(1000);
+
+                    output.Position = 0;
+                    var outputReader = new StreamReader(output, Encoding.ASCII, false, 1024);
+                    var outputString = outputReader.ReadLine();
+
+                    Assert.IsNotNull(outputString);
+                    Assert.IsTrue(outputString.EndsWith(foo), outputString);
+
+                    shell.Stop();
+                }
+            }
+        }
+
         [TestMethod]
         [TestMethod]
         public void Ssh_Command_IntermittentOutput_EndExecute()
         public void Ssh_Command_IntermittentOutput_EndExecute()
         {
         {

+ 132 - 0
test/Renci.SshNet.IntegrationTests/SshTests_TTYDisabled.cs

@@ -0,0 +1,132 @@
+using Renci.SshNet.Common;
+using Renci.SshNet.IntegrationTests.Common;
+
+namespace Renci.SshNet.IntegrationTests
+{
+    [TestClass]
+    public class SshTests_TTYDisabled : TestBase
+    {
+        private IConnectionInfoFactory _connectionInfoFactory;
+        private IConnectionInfoFactory _adminConnectionInfoFactory;
+        private RemoteSshdConfig _remoteSshdConfig;
+
+        [TestInitialize]
+        public void SetUp()
+        {
+            _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort);
+            _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort);
+
+            _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig();
+            _remoteSshdConfig.AllowTcpForwarding()
+                             .PermitTTY(false)
+                             .PrintMotd(false)
+                             .Update()
+                             .Restart();
+        }
+
+        [TestCleanup]
+        public void TearDown()
+        {
+            _remoteSshdConfig?.Reset();
+        }
+
+        [TestMethod]
+        public void Ssh_CreateShellStream()
+        {
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                try
+                {
+                    client.CreateShellStream("xterm", 80, 24, 800, 600, 1024, null);
+                    Assert.Fail("Should not be able to create ShellStream with pseudo-terminal settings when PermitTTY is no at server side.");
+                }
+                catch (SshException ex)
+                {
+                    Assert.AreEqual("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.", ex.Message);
+                }
+            }
+        }
+
+        [TestMethod]
+        public void Ssh_CreateShellStreamNoTerminal()
+        {
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                using (var shellStream = client.CreateShellStreamNoTerminal(bufferSize: 1024))
+                {
+                    var foo = new string('a', 90);
+                    shellStream.WriteLine($"echo {foo}");
+                    var line = shellStream.ReadLine(TimeSpan.FromSeconds(1));
+                    Assert.IsNotNull(line);
+                    Assert.IsTrue(line.EndsWith(foo), line);
+                }
+            }
+        }
+
+
+        [TestMethod]
+        public void Ssh_CreateShell()
+        {
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                using (var input = new MemoryStream())
+                using (var output = new MemoryStream())
+                using (var extOutput = new MemoryStream())
+                {
+                    var shell = client.CreateShell(input, output, extOutput, "xterm", 80, 24, 800, 600, null, 1024);
+
+                    try
+                    {
+                        shell.Start();
+                        Assert.Fail("Should not be able to create ShellStream with terminal settings when PermitTTY is no at server side.");
+                    }
+                    catch (SshException ex)
+                    {
+                        Assert.AreEqual("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.", ex.Message);
+                    }
+                }
+            }
+        }
+
+        [TestMethod]
+        public void Ssh_CreateShellNoTerminal()
+        {
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                using (var input = new MemoryStream())
+                using (var output = new MemoryStream())
+                using (var extOutput = new MemoryStream())
+                {
+                    var shell = client.CreateShellNoTerminal(input, output, extOutput, 1024);
+
+                    shell.Start();
+
+                    var inputWriter = new StreamWriter(input, Encoding.ASCII, 1024);
+                    var foo = new string('a', 90);
+                    inputWriter.WriteLine($"echo {foo}");
+                    inputWriter.Flush();
+                    input.Position = 0;
+
+                    Thread.Sleep(1000);
+
+                    output.Position = 0;
+                    var outputReader = new StreamReader(output, Encoding.ASCII, false, 1024);
+                    var outputString = outputReader.ReadLine();
+
+                    Assert.IsNotNull(outputString);
+                    Assert.IsTrue(outputString.EndsWith(foo), outputString);
+
+                    shell.Stop();
+                }
+            }
+        }
+    }
+}

+ 17 - 0
test/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs

@@ -135,6 +135,15 @@ namespace Renci.SshNet.TestTools.OpenSSH
         /// </value>
         /// </value>
         public string Protocol { get; set; }
         public string Protocol { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether TTY is permitted.
+        /// </summary>
+        /// <value>
+        /// <see langword="true"/> to permit and <see langword="false"/> to not permit TTY,
+        /// or <see langword="null"/> if this option is not configured.
+        /// </value>
+        public bool? PermitTTY { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether TCP forwarding is allowed.
         /// Gets or sets a value indicating whether TCP forwarding is allowed.
         /// </summary>
         /// </summary>
@@ -238,6 +247,11 @@ namespace Renci.SshNet.TestTools.OpenSSH
                 writer.WriteLine("KbdInteractiveAuthentication " + _booleanFormatter.Format(KeyboardInteractiveAuthentication.Value));
                 writer.WriteLine("KbdInteractiveAuthentication " + _booleanFormatter.Format(KeyboardInteractiveAuthentication.Value));
             }
             }
 
 
+            if (PermitTTY is not null)
+            {
+                writer.WriteLine("PermitTTY " + _booleanFormatter.Format(PermitTTY.Value));
+            }
+
             if (AllowTcpForwarding is not null)
             if (AllowTcpForwarding is not null)
             {
             {
                 writer.WriteLine("AllowTcpForwarding " + _booleanFormatter.Format(AllowTcpForwarding.Value));
                 writer.WriteLine("AllowTcpForwarding " + _booleanFormatter.Format(AllowTcpForwarding.Value));
@@ -364,6 +378,9 @@ namespace Renci.SshNet.TestTools.OpenSSH
                 case "Protocol":
                 case "Protocol":
                     sshdConfig.Protocol = value;
                     sshdConfig.Protocol = value;
                     break;
                     break;
+                case "PermitTTY":
+                    sshdConfig.PermitTTY = ToBool(value);
+                    break;
                 case "AllowTcpForwarding":
                 case "AllowTcpForwarding":
                     sshdConfig.AllowTcpForwarding = ToBool(value);
                     sshdConfig.AllowTcpForwarding = ToBool(value);
                     break;
                     break;