diff --git a/README.md b/README.md
index 84e665bd9..b63460b76 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-  SSH.NET
+  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);
+ }
+ }
+ }
+}