Add support for Zlib compression (.NET 6.0 onward only) (#1326)

* Integrate `ZLibStream` from .NET 6.0+ with SSH.NET.

* OpenSSH server does not support zlib (pre-auth); OpenSSH client still supports zlib (pre-auth)

* Correct compression algorithm name; Update README.md

* Integrate `ZLibStream` from .NET 6.0+ with SSH.NET.

* OpenSSH server does not support zlib (pre-auth); OpenSSH client still supports zlib (pre-auth)

* Correct compression algorithm name; Update README.md

* Test the compression by upload/download file

* Refactor compression.

* Move delayed compression logic to base class.

* seal Zlib

* update unit test

* update unit test to see if it can trigger integration test

* Flush zlibStream

* Fix integration test

* update test

* Update ConnectionInfo.cs

Co-authored-by: Rob Hague <rob.hague00@gmail.com>

* Update README.md

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
This commit is contained in:
Scott Xu
2024-04-18 03:39:27 +08:00
committed by GitHub
parent dd36d5b98a
commit b553f81f8a
10 changed files with 233 additions and 165 deletions
+8 -1
View File
@@ -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.
@@ -123,6 +123,13 @@ Private keys can be encrypted using one of the following cipher methods:
* hmac-sha2-256-etm<span></span>@openssh.com
* hmac-sha2-512-etm<span></span>@openssh.com
## Compression
**SSH.NET** supports the following compression algorithms:
* none (default)
* zlib<span></span>@openssh.com (.NET 6 and higher)
## Framework Support
**SSH.NET** supports the following target frameworks:
@@ -1,18 +0,0 @@
namespace Renci.SshNet.Compression
{
/// <summary>
/// Specifies compression modes.
/// </summary>
public enum CompressionMode
{
/// <summary>
/// Specifies that content should be compressed.
/// </summary>
Compress = 0,
/// <summary>
/// Specifies that content should be decompressed.
/// </summary>
Decompress = 1,
}
}
+53 -51
View File
@@ -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
/// </summary>
public abstract class Compressor : Algorithm, IDisposable
{
private readonly ZlibStream _compressor;
private readonly ZlibStream _decompressor;
private MemoryStream _compressorStream;
private MemoryStream _decompressorStream;
private readonly bool _delayedCompression;
private bool _isActive;
private Session _session;
private bool _isDisposed;
/// <summary>
/// Gets or sets a value indicating whether compression is active.
/// </summary>
/// <value>
/// <see langword="true"/> if compression is active; otherwise, <see langword="false"/>.
/// </value>
protected bool IsActive { get; set; }
/// <summary>
/// Gets the session.
/// </summary>
protected Session Session { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="Compressor"/> class.
/// </summary>
protected Compressor()
/// <param name="delayedCompression">
/// <see langword="false"/> to start compression after receiving SSH_MSG_NEWKEYS.
/// <see langword="true"/> to delay compression util receiving SSH_MSG_USERAUTH_SUCCESS.
/// <see href="https://www.openssh.com/txt/draft-miller-secsh-compression-delayed-00.txt"/>.
/// </param>
protected Compressor(bool delayedCompression)
{
_compressorStream = new MemoryStream();
_decompressorStream = new MemoryStream();
_compressor = new ZlibStream(_compressorStream, CompressionMode.Compress);
_decompressor = new ZlibStream(_decompressorStream, CompressionMode.Decompress);
_delayedCompression = delayedCompression;
}
/// <summary>
@@ -47,7 +35,15 @@ namespace Renci.SshNet.Compression
/// <param name="session">The session.</param>
public virtual void Init(Session session)
{
Session = session;
if (_delayedCompression)
{
_session = session;
_session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived;
}
else
{
_isActive = true;
}
}
/// <summary>
@@ -57,7 +53,7 @@ namespace Renci.SshNet.Compression
/// <returns>
/// The compressed data.
/// </returns>
public virtual byte[] Compress(byte[] data)
public byte[] Compress(byte[] data)
{
return Compress(data, 0, data.Length);
}
@@ -73,7 +69,7 @@ namespace Renci.SshNet.Compression
/// </returns>
public virtual byte[] Compress(byte[] data, int offset, int length)
{
if (!IsActive)
if (!_isActive)
{
if (offset == 0 && length == data.Length)
{
@@ -85,13 +81,20 @@ namespace Renci.SshNet.Compression
return buffer;
}
_compressorStream.SetLength(0);
_compressor.Write(data, offset, length);
return _compressorStream.ToArray();
return CompressCore(data, offset, length);
}
/// <summary>
/// Compresses the specified data.
/// </summary>
/// <param name="data">Data to compress.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="data"/> at which to begin reading the data to compress. </param>
/// <param name="length">The number of bytes to be compressed. </param>
/// <returns>
/// The compressed data.
/// </returns>
protected abstract byte[] CompressCore(byte[] data, int offset, int length);
/// <summary>
/// Decompresses the specified data.
/// </summary>
@@ -99,7 +102,7 @@ namespace Renci.SshNet.Compression
/// <returns>
/// The decompressed data.
/// </returns>
public virtual byte[] Decompress(byte[] data)
public byte[] Decompress(byte[] data)
{
return Decompress(data, 0, data.Length);
}
@@ -115,7 +118,7 @@ namespace Renci.SshNet.Compression
/// </returns>
public virtual byte[] Decompress(byte[] data, int offset, int length)
{
if (!IsActive)
if (!_isActive)
{
if (offset == 0 && length == data.Length)
{
@@ -127,11 +130,24 @@ namespace Renci.SshNet.Compression
return buffer;
}
_decompressorStream.SetLength(0);
return DecompressCore(data, offset, length);
}
_decompressor.Write(data, offset, length);
/// <summary>
/// Decompresses the specified data.
/// </summary>
/// <param name="data">Compressed data.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="data"/> at which to begin reading the data to decompress. </param>
/// <param name="length">The number of bytes to be read from the compressed data. </param>
/// <returns>
/// The decompressed data.
/// </returns>
protected abstract byte[] DecompressCore(byte[] data, int offset, int length);
return _decompressorStream.ToArray();
private void Session_UserAuthenticationSuccessReceived(object sender, MessageEventArgs<SuccessMessage> e)
{
_isActive = true;
_session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived;
}
/// <summary>
@@ -156,20 +172,6 @@ namespace Renci.SshNet.Compression
if (disposing)
{
var compressorStream = _compressorStream;
if (compressorStream != null)
{
compressorStream.Dispose();
_compressorStream = null;
}
var decompressorStream = _decompressorStream;
if (decompressorStream != null)
{
decompressorStream.Dispose();
_decompressorStream = null;
}
_isDisposed = true;
}
}
+80 -9
View File
@@ -1,10 +1,35 @@
namespace Renci.SshNet.Compression
#if NET6_0_OR_GREATER
using System.IO;
using System.IO.Compression;
namespace Renci.SshNet.Compression
{
/// <summary>
/// Represents "zlib" compression implementation.
/// </summary>
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);
}
/// <summary>
/// Gets algorithm name.
/// </summary>
@@ -13,15 +38,61 @@
get { return "zlib"; }
}
/// <summary>
/// Initializes the algorithm.
/// </summary>
/// <param name="session">The session.</param>
public override void Init(Session session)
protected override byte[] CompressCore(byte[] data, int offset, int length)
{
base.Init(session);
_compressorStream.SetLength(0);
IsActive = true;
_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();
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
protected override void Dispose(bool disposing)
{
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;
}
_isDisposed = true;
}
}
}
}
#endif
+9 -27
View File
@@ -1,35 +1,17 @@
using Renci.SshNet.Messages.Authentication;
#if NET6_0_OR_GREATER
namespace Renci.SshNet.Compression
{
/// <summary>
/// Represents "zlib@openssh.org" compression implementation.
/// </summary>
public class ZlibOpenSsh : Compressor
internal sealed class ZlibOpenSsh : Zlib
{
/// <summary>
/// Gets algorithm name.
/// </summary>
public ZlibOpenSsh()
: base(delayedCompression: true)
{
}
public override string Name
{
get { return "zlib@openssh.org"; }
}
/// <summary>
/// Initializes the algorithm.
/// </summary>
/// <param name="session">The session.</param>
public override void Init(Session session)
{
base.Init(session);
session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived;
}
private void Session_UserAuthenticationSuccessReceived(object sender, MessageEventArgs<SuccessMessage> e)
{
IsActive = true;
Session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived;
get { return "zlib@openssh.com"; }
}
}
}
#endif
@@ -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
{
/// <summary>
/// Implements Zlib compression algorithm.
/// </summary>
#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;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibStream" /> class.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="mode">The mode.</param>
#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;
}
/// <summary>
/// Writes the specified buffer.
/// </summary>
/// <param name="buffer">The buffer.</param>
/// <param name="offset">The offset.</param>
/// <param name="count">The count.</param>
#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
+3
View File
@@ -439,6 +439,9 @@ namespace Renci.SshNet
CompressionAlgorithms = new Dictionary<string, Func<Compressor>>
{
{ "none", null },
#if NET6_0_OR_GREATER
{ "zlib@openssh.com", () => new ZlibOpenSsh() },
#endif
};
ChannelRequests = new Dictionary<string, RequestInfo>
+2 -2
View File
@@ -124,7 +124,7 @@ namespace Renci.SshNet.Security
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 @@ namespace Renci.SshNet.Security
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);
+14
View File
@@ -2132,6 +2132,20 @@ namespace Renci.SshNet
_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)
{
@@ -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<string, Func<Compressor>>("none", null));
}
#if NET6_0_OR_GREATER
[TestMethod]
public void ZlibOpenSsh()
{
DoTest(new KeyValuePair<string, Func<Compressor>>("zlib@openssh.com", () => new ZlibOpenSsh()));
}
#endif
private void DoTest(KeyValuePair<string, Func<Compressor>> 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);
}
}
}
}