diff --git a/README.md b/README.md index 84e665bd9..b63460b76 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - ![Logo](https://raw.githubusercontent.com/sshnet/SSH.NET/develop/images/logo/png/SS-NET-icon-h50.png) SSH.NET + ![Logo](https://raw.githubusercontent.com/sshnet/SSH.NET/develop/images/logo/png/SS-NET-icon-h50.png) SSH.NET ======= SSH.NET is a Secure Shell (SSH-2) library for .NET, optimized for parallelism. @@ -120,6 +120,13 @@ Private keys can be encrypted using one of the following cipher methods: * hmac-sha2-256-etm@openssh.com * hmac-sha2-512-etm@openssh.com + +## Compression + +**SSH.NET** supports the following compression algorithms: +* none (default) +* zlib@openssh.com (.NET 6 and higher) + ## Framework Support **SSH.NET** supports the following target frameworks: * .NETFramework 4.6.2 (and higher) diff --git a/src/Renci.SshNet/Compression/CompressionMode.cs b/src/Renci.SshNet/Compression/CompressionMode.cs deleted file mode 100644 index b0b6f6b94..000000000 --- a/src/Renci.SshNet/Compression/CompressionMode.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Renci.SshNet.Compression -{ - /// - /// Specifies compression modes. - /// - public enum CompressionMode - { - /// - /// Specifies that content should be compressed. - /// - Compress = 0, - - /// - /// Specifies that content should be decompressed. - /// - Decompress = 1, - } -} diff --git a/src/Renci.SshNet/Compression/Compressor.cs b/src/Renci.SshNet/Compression/Compressor.cs index 63eb3e336..876144962 100644 --- a/src/Renci.SshNet/Compression/Compressor.cs +++ b/src/Renci.SshNet/Compression/Compressor.cs @@ -1,6 +1,6 @@ using System; -using System.IO; +using Renci.SshNet.Messages.Authentication; using Renci.SshNet.Security; namespace Renci.SshNet.Compression @@ -10,35 +10,23 @@ namespace Renci.SshNet.Compression /// public abstract class Compressor : Algorithm, IDisposable { - private readonly ZlibStream _compressor; - private readonly ZlibStream _decompressor; - private MemoryStream _compressorStream; - private MemoryStream _decompressorStream; - private bool _isDisposed; - - /// - /// Gets or sets a value indicating whether compression is active. - /// - /// - /// if compression is active; otherwise, . - /// - protected bool IsActive { get; set; } + private readonly bool _delayedCompression; - /// - /// Gets the session. - /// - protected Session Session { get; private set; } + private bool _isActive; + private Session _session; + private bool _isDisposed; /// /// Initializes a new instance of the class. /// - protected Compressor() + /// + /// to start compression after receiving SSH_MSG_NEWKEYS. + /// to delay compression util receiving SSH_MSG_USERAUTH_SUCCESS. + /// . + /// + protected Compressor(bool delayedCompression) { - _compressorStream = new MemoryStream(); - _decompressorStream = new MemoryStream(); - - _compressor = new ZlibStream(_compressorStream, CompressionMode.Compress); - _decompressor = new ZlibStream(_decompressorStream, CompressionMode.Decompress); + _delayedCompression = delayedCompression; } /// @@ -47,7 +35,15 @@ protected Compressor() /// The session. public virtual void Init(Session session) { - Session = session; + if (_delayedCompression) + { + _session = session; + _session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived; + } + else + { + _isActive = true; + } } /// @@ -57,7 +53,7 @@ public virtual void Init(Session session) /// /// The compressed data. /// - public virtual byte[] Compress(byte[] data) + public byte[] Compress(byte[] data) { return Compress(data, 0, data.Length); } @@ -73,7 +69,7 @@ public virtual byte[] Compress(byte[] data) /// public virtual byte[] Compress(byte[] data, int offset, int length) { - if (!IsActive) + if (!_isActive) { if (offset == 0 && length == data.Length) { @@ -85,13 +81,20 @@ public virtual byte[] Compress(byte[] data, int offset, int length) return buffer; } - _compressorStream.SetLength(0); - - _compressor.Write(data, offset, length); - - return _compressorStream.ToArray(); + return CompressCore(data, offset, length); } + /// + /// Compresses the specified data. + /// + /// Data to compress. + /// The zero-based byte offset in at which to begin reading the data to compress. + /// The number of bytes to be compressed. + /// + /// The compressed data. + /// + protected abstract byte[] CompressCore(byte[] data, int offset, int length); + /// /// Decompresses the specified data. /// @@ -99,7 +102,7 @@ public virtual byte[] Compress(byte[] data, int offset, int length) /// /// The decompressed data. /// - public virtual byte[] Decompress(byte[] data) + public byte[] Decompress(byte[] data) { return Decompress(data, 0, data.Length); } @@ -115,7 +118,7 @@ public virtual byte[] Decompress(byte[] data) /// public virtual byte[] Decompress(byte[] data, int offset, int length) { - if (!IsActive) + if (!_isActive) { if (offset == 0 && length == data.Length) { @@ -127,11 +130,24 @@ public virtual byte[] Decompress(byte[] data, int offset, int length) return buffer; } - _decompressorStream.SetLength(0); + return DecompressCore(data, offset, length); + } - _decompressor.Write(data, offset, length); + /// + /// Decompresses the specified data. + /// + /// Compressed data. + /// The zero-based byte offset in at which to begin reading the data to decompress. + /// The number of bytes to be read from the compressed data. + /// + /// The decompressed data. + /// + protected abstract byte[] DecompressCore(byte[] data, int offset, int length); - return _decompressorStream.ToArray(); + private void Session_UserAuthenticationSuccessReceived(object sender, MessageEventArgs e) + { + _isActive = true; + _session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived; } /// @@ -156,20 +172,6 @@ protected virtual void Dispose(bool disposing) if (disposing) { - var compressorStream = _compressorStream; - if (compressorStream != null) - { - compressorStream.Dispose(); - _compressorStream = null; - } - - var decompressorStream = _decompressorStream; - if (decompressorStream != null) - { - decompressorStream.Dispose(); - _decompressorStream = null; - } - _isDisposed = true; } } diff --git a/src/Renci.SshNet/Compression/Zlib.cs b/src/Renci.SshNet/Compression/Zlib.cs index 504697fd7..29fe8aacb 100644 --- a/src/Renci.SshNet/Compression/Zlib.cs +++ b/src/Renci.SshNet/Compression/Zlib.cs @@ -1,10 +1,35 @@ -namespace Renci.SshNet.Compression +#if NET6_0_OR_GREATER +using System.IO; +using System.IO.Compression; + +namespace Renci.SshNet.Compression { /// /// Represents "zlib" compression implementation. /// - internal sealed class Zlib : Compressor + internal class Zlib : Compressor { + private readonly ZLibStream _compressor; + private readonly ZLibStream _decompressor; + private MemoryStream _compressorStream; + private MemoryStream _decompressorStream; + private bool _isDisposed; + + public Zlib() + : this(delayedCompression: false) + { + } + + protected Zlib(bool delayedCompression) + : base(delayedCompression) + { + _compressorStream = new MemoryStream(); + _decompressorStream = new MemoryStream(); + + _compressor = new ZLibStream(_compressorStream, CompressionMode.Compress); + _decompressor = new ZLibStream(_decompressorStream, CompressionMode.Decompress); + } + /// /// Gets algorithm name. /// @@ -13,15 +38,61 @@ public override string Name get { return "zlib"; } } + protected override byte[] CompressCore(byte[] data, int offset, int length) + { + _compressorStream.SetLength(0); + + _compressor.Write(data, offset, length); + _compressor.Flush(); + + return _compressorStream.ToArray(); + } + + protected override byte[] DecompressCore(byte[] data, int offset, int length) + { + _decompressorStream.Write(data, offset, length); + _decompressorStream.Position = 0; + + using var outputStream = new MemoryStream(); + _decompressor.CopyTo(outputStream); + + _decompressorStream.SetLength(0); + + return outputStream.ToArray(); + } + /// - /// Initializes the algorithm. + /// Releases unmanaged and - optionally - managed resources. /// - /// The session. - public override void Init(Session session) + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected override void Dispose(bool disposing) { - base.Init(session); + base.Dispose(disposing); + + if (_isDisposed) + { + return; + } + + if (disposing) + { + var compressorStream = _compressorStream; + if (compressorStream != null) + { + compressorStream.Dispose(); + _compressorStream = null; + } + + var decompressorStream = _decompressorStream; + if (decompressorStream != null) + { + decompressorStream.Dispose(); + _decompressorStream = null; + } - IsActive = true; + _isDisposed = true; + } } } } +#endif diff --git a/src/Renci.SshNet/Compression/ZlibOpenSsh.cs b/src/Renci.SshNet/Compression/ZlibOpenSsh.cs index 45bf1165f..6ece09254 100644 --- a/src/Renci.SshNet/Compression/ZlibOpenSsh.cs +++ b/src/Renci.SshNet/Compression/ZlibOpenSsh.cs @@ -1,35 +1,17 @@ -using Renci.SshNet.Messages.Authentication; - +#if NET6_0_OR_GREATER namespace Renci.SshNet.Compression { - /// - /// Represents "zlib@openssh.org" compression implementation. - /// - public class ZlibOpenSsh : Compressor + internal sealed class ZlibOpenSsh : Zlib { - /// - /// Gets algorithm name. - /// - public override string Name + public ZlibOpenSsh() + : base(delayedCompression: true) { - get { return "zlib@openssh.org"; } } - /// - /// Initializes the algorithm. - /// - /// The session. - public override void Init(Session session) - { - base.Init(session); - - session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived; - } - - private void Session_UserAuthenticationSuccessReceived(object sender, MessageEventArgs e) + public override string Name { - IsActive = true; - Session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived; + get { return "zlib@openssh.com"; } } } } +#endif diff --git a/src/Renci.SshNet/Compression/ZlibStream.cs b/src/Renci.SshNet/Compression/ZlibStream.cs deleted file mode 100644 index fb2e8089e..000000000 --- a/src/Renci.SshNet/Compression/ZlibStream.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.IO; - -#pragma warning disable S125 // Sections of code should not be commented out -#pragma warning disable SA1005 // Single line comments should begin with single space - -namespace Renci.SshNet.Compression -{ - /// - /// Implements Zlib compression algorithm. - /// -#pragma warning disable CA1711 // Identifiers should not have incorrect suffix - public class ZlibStream -#pragma warning restore CA1711 // Identifiers should not have incorrect suffix - { - //private readonly Ionic.Zlib.ZlibStream _baseStream; - - /// - /// Initializes a new instance of the class. - /// - /// The stream. - /// The mode. -#pragma warning disable IDE0060 // Remove unused parameter - public ZlibStream(Stream stream, CompressionMode mode) -#pragma warning restore IDE0060 // Remove unused parameter - { - //switch (mode) - //{ - // case CompressionMode.Compress: - // this._baseStream = new Ionic.Zlib.ZlibStream(stream, Ionic.Zlib.CompressionMode.Compress, Ionic.Zlib.CompressionLevel.Default); - // break; - // case CompressionMode.Decompress: - // this._baseStream = new Ionic.Zlib.ZlibStream(stream, Ionic.Zlib.CompressionMode.Decompress, Ionic.Zlib.CompressionLevel.Default); - // break; - // default: - // break; - //} - - //this._baseStream.FlushMode = Ionic.Zlib.FlushType.Partial; - } - - /// - /// Writes the specified buffer. - /// - /// The buffer. - /// The offset. - /// The count. -#pragma warning disable IDE0060 // Remove unused parameter - public void Write(byte[] buffer, int offset, int count) -#pragma warning restore IDE0060 // Remove unused parameter - { - //this._baseStream.Write(buffer, offset, count); - } -#pragma warning restore SA1005 // Single line comments should begin with single space - } -} - -#pragma warning restore S125 // Sections of code should not be commented out diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs index 7584816ff..2a0a76a33 100644 --- a/src/Renci.SshNet/ConnectionInfo.cs +++ b/src/Renci.SshNet/ConnectionInfo.cs @@ -439,6 +439,9 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy CompressionAlgorithms = new Dictionary> { { "none", null }, +#if NET6_0_OR_GREATER + { "zlib@openssh.com", () => new ZlibOpenSsh() }, +#endif }; ChannelRequests = new Dictionary diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs index 10f7e0f8a..1dd09ea97 100644 --- a/src/Renci.SshNet/Security/KeyExchange.cs +++ b/src/Renci.SshNet/Security/KeyExchange.cs @@ -124,7 +124,7 @@ from a in message.MacAlgorithmsServerToClient var compressionAlgorithmName = (from b in session.ConnectionInfo.CompressionAlgorithms.Keys from a in message.CompressionAlgorithmsClientToServer where a == b - select a).LastOrDefault(); + select a).FirstOrDefault(); if (string.IsNullOrEmpty(compressionAlgorithmName)) { throw new SshConnectionException("Compression algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -136,7 +136,7 @@ from a in message.CompressionAlgorithmsClientToServer var decompressionAlgorithmName = (from b in session.ConnectionInfo.CompressionAlgorithms.Keys from a in message.CompressionAlgorithmsServerToClient where a == b - select a).LastOrDefault(); + select a).FirstOrDefault(); if (string.IsNullOrEmpty(decompressionAlgorithmName)) { throw new SshConnectionException("Decompression algorithm not found", DisconnectReason.KeyExchangeFailed); diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index 0c067ec2d..dd20f34c9 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -2132,6 +2132,20 @@ protected virtual void Dispose(bool disposing) _clientMac = null; } + var serverDecompression = _serverDecompression; + if (serverDecompression != null) + { + serverDecompression.Dispose(); + _serverDecompression = null; + } + + var clientCompression = _clientCompression; + if (clientCompression != null) + { + clientCompression.Dispose(); + _clientCompression = null; + } + var keyExchange = _keyExchange; if (keyExchange != null) { diff --git a/test/Renci.SshNet.IntegrationTests/CompressionTests.cs b/test/Renci.SshNet.IntegrationTests/CompressionTests.cs new file mode 100644 index 000000000..68cf1b237 --- /dev/null +++ b/test/Renci.SshNet.IntegrationTests/CompressionTests.cs @@ -0,0 +1,64 @@ +using Renci.SshNet.Compression; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class CompressionTests : IntegrationTestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + } + + [TestMethod] + public void None() + { + DoTest(new KeyValuePair>("none", null)); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void ZlibOpenSsh() + { + DoTest(new KeyValuePair>("zlib@openssh.com", () => new ZlibOpenSsh())); + } +#endif + + private void DoTest(KeyValuePair> compressor) + { + using (var scpClient = new ScpClient(_connectionInfoFactory.Create())) + { + scpClient.ConnectionInfo.CompressionAlgorithms.Clear(); + scpClient.ConnectionInfo.CompressionAlgorithms.Add(compressor); + + scpClient.Connect(); + + Assert.AreEqual(compressor.Key, scpClient.ConnectionInfo.CurrentClientCompressionAlgorithm); + Assert.AreEqual(compressor.Key, scpClient.ConnectionInfo.CurrentServerCompressionAlgorithm); + + var file = $"/tmp/{Guid.NewGuid()}.txt"; + + var sb = new StringBuilder(); + for (var i = 0; i < 100; i++) + { + _ = sb.Append("Repeating"); + } + + var fileContent = sb.ToString(); + + using var uploadStream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent)); + scpClient.Upload(uploadStream, file); + + using var downloadStream = new MemoryStream(); + scpClient.Download(file, downloadStream); + + var result = Encoding.UTF8.GetString(downloadStream.ToArray()); + + Assert.AreEqual(fileContent, result); + } + } + } +}