diff --git a/src/Renci.SshNet/.editorconfig b/src/Renci.SshNet/.editorconfig
index 5da8db715..97f5eb12b 100644
--- a/src/Renci.SshNet/.editorconfig
+++ b/src/Renci.SshNet/.editorconfig
@@ -21,6 +21,10 @@ MA0053.public_class_should_be_sealed = false
#### .NET Compiler Platform analysers rules ####
+# CA1030: Use events where appropriate
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1030
+dotnet_diagnostic.CA10310.severity = none
+
# CA1031: Do not catch general exception types
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1031
dotnet_diagnostic.CA1031.severity = none
diff --git a/src/Renci.SshNet/ForwardedPort.cs b/src/Renci.SshNet/ForwardedPort.cs
index f4544094c..50157e1c1 100644
--- a/src/Renci.SshNet/ForwardedPort.cs
+++ b/src/Renci.SshNet/ForwardedPort.cs
@@ -1,4 +1,5 @@
using System;
+
using Renci.SshNet.Common;
namespace Renci.SshNet
@@ -16,28 +17,19 @@ public abstract class ForwardedPort : IForwardedPort
///
internal ISession Session { get; set; }
- ///
- /// The event occurs as the forwarded port is being stopped.
- ///
- internal event EventHandler Closing;
-
- ///
- /// The event occurs as the forwarded port is being stopped.
- ///
- event EventHandler IForwardedPort.Closing
- {
- add { Closing += value; }
- remove { Closing -= value; }
- }
-
///
/// Gets a value indicating whether port forwarding is started.
///
///
- /// true if port forwarding is started; otherwise, false.
+ /// if port forwarding is started; otherwise, .
///
public abstract bool IsStarted { get; }
+ ///
+ /// The event occurs as the forwarded port is being stopped.
+ ///
+ public event EventHandler Closing;
+
///
/// Occurs when an exception is thrown.
///
@@ -51,6 +43,8 @@ event EventHandler IForwardedPort.Closing
///
/// Starts port forwarding.
///
+ /// The current is already started -or- is not linked to a SSH session.
+ /// The client is not connected.
public virtual void Start()
{
CheckDisposed();
@@ -77,7 +71,9 @@ public virtual void Start()
///
/// Stops port forwarding.
///
+#pragma warning disable CA1716 // Identifiers should not match keywords
public virtual void Stop()
+#pragma warning restore CA1716 // Identifiers should not match keywords
{
if (IsStarted)
{
@@ -85,6 +81,15 @@ public virtual void Stop()
}
}
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
///
/// Starts port forwarding.
///
@@ -100,22 +105,22 @@ protected virtual void StopPort(TimeSpan timeout)
RaiseClosing();
var session = Session;
- if (session != null)
+ if (session is not null)
{
session.ErrorOccured -= Session_ErrorOccured;
}
}
///
- /// Releases unmanaged and - optionally - managed resources
+ /// Releases unmanaged and - optionally - managed resources.
///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ /// to release both managed and unmanaged resources; to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
var session = Session;
- if (session != null)
+ if (session is not null)
{
StopPort(session.ConnectionInfo.Timeout);
Session = null;
diff --git a/src/Renci.SshNet/ForwardedPortDynamic.NET.cs b/src/Renci.SshNet/ForwardedPortDynamic.NET.cs
deleted file mode 100644
index 0ae0f4319..000000000
--- a/src/Renci.SshNet/ForwardedPortDynamic.NET.cs
+++ /dev/null
@@ -1,603 +0,0 @@
-using System;
-using System.Linq;
-using System.Net;
-using System.Net.Sockets;
-using System.Text;
-using System.Threading;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Channels;
-using Renci.SshNet.Common;
-
-namespace Renci.SshNet
-{
- public partial class ForwardedPortDynamic
- {
- private Socket _listener;
- private CountdownEvent _pendingChannelCountdown;
-
- partial void InternalStart()
- {
- InitializePendingChannelCountdown();
-
- var ip = IPAddress.Any;
- if (!string.IsNullOrEmpty(BoundHost))
- {
- ip = DnsAbstraction.GetHostAddresses(BoundHost)[0];
- }
-
- var ep = new IPEndPoint(ip, (int) BoundPort);
-
- _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {NoDelay = true};
- _listener.Bind(ep);
- _listener.Listen(5);
-
- Session.ErrorOccured += Session_ErrorOccured;
- Session.Disconnected += Session_Disconnected;
-
- // consider port started when we're listening for inbound connections
- _status = ForwardedPortStatus.Started;
-
- StartAccept(e: null);
- }
-
- private void StartAccept(SocketAsyncEventArgs e)
- {
- if (e is null)
- {
- e = new SocketAsyncEventArgs();
- e.Completed += AcceptCompleted;
- }
- else
- {
- // clear the socket as we're reusing the context object
- e.AcceptSocket = null;
- }
-
- // only accept new connections while we are started
- if (IsStarted)
- {
- try
- {
- if (!_listener.AcceptAsync(e))
- {
- AcceptCompleted(sender: null, e);
- }
- }
- catch (ObjectDisposedException)
- {
- if (_status == ForwardedPortStatus.Stopping || _status == ForwardedPortStatus.Stopped)
- {
- // ignore ObjectDisposedException while stopping or stopped
- return;
- }
-
- throw;
- }
- }
- }
-
- private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
- {
- if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
- {
- // server was stopped
- return;
- }
-
- // capture client socket
- var clientSocket = e.AcceptSocket;
-
- if (e.SocketError != SocketError.Success)
- {
- // accept new connection
- StartAccept(e);
-
- // dispose broken client socket
- CloseClientSocket(clientSocket);
- return;
- }
-
- // accept new connection
- StartAccept(e);
-
- // process connection
- ProcessAccept(clientSocket);
- }
-
- private void ProcessAccept(Socket clientSocket)
- {
- // close the client socket if we're no longer accepting new connections
- if (!IsStarted)
- {
- CloseClientSocket(clientSocket);
- return;
- }
-
- // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
- // that same instance; the instance field for the countdown event is re-initialized when the port is restarted
- // and at that time there may still be pending requests
- var pendingChannelCountdown = _pendingChannelCountdown;
-
- pendingChannelCountdown.AddCount();
-
- try
- {
- using (var channel = Session.CreateChannelDirectTcpip())
- {
- channel.Exception += Channel_Exception;
-
- if (!HandleSocks(channel, clientSocket, Session.ConnectionInfo.Timeout))
- {
- CloseClientSocket(clientSocket);
- return;
- }
-
- // start receiving from client socket (and sending to server)
- channel.Bind();
- }
- }
- catch (Exception exp)
- {
- RaiseExceptionEvent(exp);
- CloseClientSocket(clientSocket);
- }
- finally
- {
- // take into account that CountdownEvent has since been disposed; when stopping the port we
- // wait for a given time for the channels to close, but once that timeout period has elapsed
- // the CountdownEvent will be disposed
- try
- {
- _ = pendingChannelCountdown.Signal();
- }
- catch (ObjectDisposedException)
- {
- }
- }
- }
-
- ///
- /// Initializes the .
- ///
- ///
- ///
- /// When the port is started for the first time, a is created with an initial count
- /// of 1.
- ///
- ///
- /// On subsequent (re)starts, we'll dispose the current and create a new one with
- /// initial count of 1.
- ///
- ///
- private void InitializePendingChannelCountdown()
- {
- var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
- original?.Dispose();
- }
-
- private bool HandleSocks(IChannelDirectTcpip channel, Socket clientSocket, TimeSpan timeout)
- {
-
-#pragma warning disable IDE0039 // Use lambda instead of local function to reduce allocations
- // Create eventhandler which is to be invoked to interrupt a blocking receive
- // when we're closing the forwarded port.
- EventHandler closeClientSocket = (_, args) => CloseClientSocket(clientSocket);
-#pragma warning restore IDE0039 // Use lambda instead of local function to reduce allocations
-
- Closing += closeClientSocket;
-
- try
- {
- var version = SocketAbstraction.ReadByte(clientSocket, timeout);
- switch (version)
- {
- case -1:
- // SOCKS client closed connection
- return false;
- case 4:
- return HandleSocks4(clientSocket, channel, timeout);
- case 5:
- return HandleSocks5(clientSocket, channel, timeout);
- default:
- throw new NotSupportedException(string.Format("SOCKS version {0} is not supported.", version));
- }
- }
- catch (SocketException ex)
- {
- // ignore exception thrown by interrupting the blocking receive as part of closing
- // the forwarded port
-#if NETFRAMEWORK
- if (ex.SocketErrorCode != SocketError.Interrupted)
- {
- RaiseExceptionEvent(ex);
- }
-#else
- // Since .NET 5 the exception has been changed.
- // more info https://github.com/dotnet/runtime/issues/41585
- if (ex.SocketErrorCode != SocketError.ConnectionAborted)
- {
- RaiseExceptionEvent(ex);
- }
-#endif
- return false;
- }
- finally
- {
- // interrupt of blocking receive is now handled by channel (SOCKS4 and SOCKS5)
- // or no longer necessary
- Closing -= closeClientSocket;
- }
-
- }
-
- private static void CloseClientSocket(Socket clientSocket)
- {
- if (clientSocket.Connected)
- {
- try
- {
- clientSocket.Shutdown(SocketShutdown.Send);
- }
- catch (Exception)
- {
- // ignore exception when client socket was already closed
- }
- }
-
- clientSocket.Dispose();
- }
-
- ///
- /// Interrupts the listener, and unsubscribes from events.
- ///
- partial void StopListener()
- {
- // close listener socket
- _listener?.Dispose();
-
- // unsubscribe from session events
- var session = Session;
- if (session != null)
- {
- session.ErrorOccured -= Session_ErrorOccured;
- session.Disconnected -= Session_Disconnected;
- }
- }
-
- ///
- /// Waits for pending channels to close.
- ///
- /// The maximum time to wait for the pending channels to close.
- partial void InternalStop(TimeSpan timeout)
- {
- _ = _pendingChannelCountdown.Signal();
-
- if (!_pendingChannelCountdown.Wait(timeout))
- {
- // TODO: log as warning
- DiagnosticAbstraction.Log("Timeout waiting for pending channels in dynamic forwarded port to close.");
- }
-
- }
-
- partial void InternalDispose(bool disposing)
- {
- if (disposing)
- {
- var listener = _listener;
- if (listener != null)
- {
- _listener = null;
- listener.Dispose();
- }
-
- var pendingRequestsCountdown = _pendingChannelCountdown;
- if (pendingRequestsCountdown != null)
- {
- _pendingChannelCountdown = null;
- pendingRequestsCountdown.Dispose();
- }
- }
- }
-
- private void Session_Disconnected(object sender, EventArgs e)
- {
- var session = Session;
- if (session != null)
- {
- StopPort(session.ConnectionInfo.Timeout);
- }
- }
-
- private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
- {
- var session = Session;
- if (session != null)
- {
- StopPort(session.ConnectionInfo.Timeout);
- }
- }
-
- private void Channel_Exception(object sender, ExceptionEventArgs e)
- {
- RaiseExceptionEvent(e.Exception);
- }
-
- private bool HandleSocks4(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
- {
- var commandCode = SocketAbstraction.ReadByte(socket, timeout);
- if (commandCode == -1)
- {
- // SOCKS client closed connection
- return false;
- }
-
- // TODO: See what need to be done depends on the code
-
- var portBuffer = new byte[2];
- if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var port = Pack.BigEndianToUInt16(portBuffer);
-
- var ipBuffer = new byte[4];
- if (SocketAbstraction.Read(socket, ipBuffer, 0, ipBuffer.Length, timeout) == 0)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var ipAddress = new IPAddress(ipBuffer);
-
- var username = ReadString(socket, timeout);
- if (username is null)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var host = ipAddress.ToString();
-
- RaiseRequestReceived(host, port);
-
- channel.Open(host, port, this, socket);
-
- SocketAbstraction.SendByte(socket, 0x00);
-
- if (channel.IsOpen)
- {
- SocketAbstraction.SendByte(socket, 0x5a);
- SocketAbstraction.Send(socket, portBuffer, 0, portBuffer.Length);
- SocketAbstraction.Send(socket, ipBuffer, 0, ipBuffer.Length);
- return true;
- }
-
- // signal that request was rejected or failed
- SocketAbstraction.SendByte(socket, 0x5b);
- return false;
- }
-
- private bool HandleSocks5(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
- {
- var authenticationMethodsCount = SocketAbstraction.ReadByte(socket, timeout);
- if (authenticationMethodsCount == -1)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var authenticationMethods = new byte[authenticationMethodsCount];
- if (SocketAbstraction.Read(socket, authenticationMethods, 0, authenticationMethods.Length, timeout) == 0)
- {
- // SOCKS client closed connection
- return false;
- }
-
- if (authenticationMethods.Min() == 0)
- {
- // no user authentication is one of the authentication methods supported
- // by the SOCKS client
- SocketAbstraction.Send(socket, new byte[] { 0x05, 0x00 }, 0, 2);
- }
- else
- {
- // the SOCKS client requires authentication, which we currently do not support
- SocketAbstraction.Send(socket, new byte[] { 0x05, 0xFF }, 0, 2);
-
- // we continue business as usual but expect the client to close the connection
- // so one of the subsequent reads should return -1 signaling that the client
- // has effectively closed the connection
- }
-
- var version = SocketAbstraction.ReadByte(socket, timeout);
- if (version == -1)
- {
- // SOCKS client closed connection
- return false;
- }
-
- if (version != 5)
- {
- throw new ProxyException("SOCKS5: Version 5 is expected.");
- }
-
- var commandCode = SocketAbstraction.ReadByte(socket, timeout);
- if (commandCode == -1)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var reserved = SocketAbstraction.ReadByte(socket, timeout);
- if (reserved == -1)
- {
- // SOCKS client closed connection
- return false;
- }
-
- if (reserved != 0)
- {
- throw new ProxyException("SOCKS5: 0 is expected for reserved byte.");
- }
-
- var addressType = SocketAbstraction.ReadByte(socket, timeout);
- if (addressType == -1)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var host = GetSocks5Host(addressType, socket, timeout);
- if (host is null)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var portBuffer = new byte[2];
- if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0)
- {
- // SOCKS client closed connection
- return false;
- }
-
- var port = Pack.BigEndianToUInt16(portBuffer);
-
- RaiseRequestReceived(host, port);
-
- channel.Open(host, port, this, socket);
-
- var socksReply = CreateSocks5Reply(channel.IsOpen);
-
- SocketAbstraction.Send(socket, socksReply, 0, socksReply.Length);
-
- return true;
- }
-
- private static string GetSocks5Host(int addressType, Socket socket, TimeSpan timeout)
- {
- switch (addressType)
- {
- case 0x01: // IPv4
- {
- var addressBuffer = new byte[4];
- if (SocketAbstraction.Read(socket, addressBuffer, 0, 4, timeout) == 0)
- {
- // SOCKS client closed connection
- return null;
- }
-
- var ipv4 = new IPAddress(addressBuffer);
- return ipv4.ToString();
- }
- case 0x03: // Domain name
- {
- var length = SocketAbstraction.ReadByte(socket, timeout);
- if (length == -1)
- {
- // SOCKS client closed connection
- return null;
- }
- var addressBuffer = new byte[length];
- if (SocketAbstraction.Read(socket, addressBuffer, 0, addressBuffer.Length, timeout) == 0)
- {
- // SOCKS client closed connection
- return null;
- }
-
- var hostName = SshData.Ascii.GetString(addressBuffer, 0, addressBuffer.Length);
- return hostName;
- }
- case 0x04: // IPv6
- {
- var addressBuffer = new byte[16];
- if (SocketAbstraction.Read(socket, addressBuffer, 0, 16, timeout) == 0)
- {
- // SOCKS client closed connection
- return null;
- }
-
- var ipv6 = new IPAddress(addressBuffer);
- return ipv6.ToString();
- }
- default:
- throw new ProxyException(string.Format("SOCKS5: Address type '{0}' is not supported.", addressType));
- }
- }
-
- private static byte[] CreateSocks5Reply(bool channelOpen)
- {
- var socksReply = new byte
- [
- // SOCKS version
- 1 +
- // Reply field
- 1 +
- // Reserved; fixed: 0x00
- 1 +
- // Address type; fixed: 0x01
- 1 +
- // IPv4 server bound address; fixed: {0x00, 0x00, 0x00, 0x00}
- 4 +
- // server bound port; fixed: {0x00, 0x00}
- 2
- ];
-
- socksReply[0] = 0x05;
-
- if (channelOpen)
- {
- socksReply[1] = 0x00; // succeeded
- }
- else
- {
- socksReply[1] = 0x01; // general SOCKS server failure
- }
-
- // reserved
- socksReply[2] = 0x00;
-
- // IPv4 address type
- socksReply[3] = 0x01;
-
- return socksReply;
- }
-
- ///
- /// Reads a null terminated string from a socket.
- ///
- /// The to read from.
- /// The timeout to apply to individual reads.
- ///
- /// The read, or null when the socket was closed.
- ///
- private static string ReadString(Socket socket, TimeSpan timeout)
- {
- var text = new StringBuilder();
- var buffer = new byte[1];
- while (true)
- {
- if (SocketAbstraction.Read(socket, buffer, 0, 1, timeout) == 0)
- {
- // SOCKS client closed connection
- return null;
- }
-
- var byteRead = buffer[0];
- if (byteRead == 0)
- {
- // end of the string
- break;
- }
-
- _ = text.Append((char) byteRead);
- }
-
- return text.ToString();
- }
- }
-}
diff --git a/src/Renci.SshNet/ForwardedPortDynamic.cs b/src/Renci.SshNet/ForwardedPortDynamic.cs
index 1331557a8..be67f6289 100644
--- a/src/Renci.SshNet/ForwardedPortDynamic.cs
+++ b/src/Renci.SshNet/ForwardedPortDynamic.cs
@@ -1,4 +1,14 @@
using System;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Common;
namespace Renci.SshNet
{
@@ -6,7 +16,7 @@ namespace Renci.SshNet
/// Provides functionality for forwarding connections from the client to destination servers via the SSH server,
/// also known as dynamic port forwarding.
///
- public partial class ForwardedPortDynamic : ForwardedPort
+ public class ForwardedPortDynamic : ForwardedPort
{
private ForwardedPortStatus _status;
@@ -14,7 +24,7 @@ public partial class ForwardedPortDynamic : ForwardedPort
/// Holds a value indicating whether the current instance is disposed.
///
///
- /// true if the current instance is disposed; otherwise, false.
+ /// if the current instance is disposed; otherwise, .
///
private bool _isDisposed;
@@ -28,11 +38,14 @@ public partial class ForwardedPortDynamic : ForwardedPort
///
public uint BoundPort { get; }
+ private Socket _listener;
+ private CountdownEvent _pendingChannelCountdown;
+
///
/// Gets a value indicating whether port forwarding is started.
///
///
- /// true if port forwarding is started; otherwise, false.
+ /// if port forwarding is started; otherwise, .
///
public override bool IsStarted
{
@@ -112,51 +125,619 @@ protected override void StopPort(TimeSpan timeout)
/// The current instance is disposed.
protected override void CheckDisposed()
{
+#if NET7_0_OR_GREATER
+ ObjectDisposedException.ThrowIf(_isDisposed, this);
+#else
if (_isDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
+#endif // NET7_0_OR_GREATER
}
- partial void InternalStart();
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// to release both managed and unmanaged resources; to release only unmanaged resources.
+ protected override void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ base.Dispose(disposing);
+
+ InternalDispose(disposing);
+ _isDisposed = true;
+ }
+
+ private void InternalStart()
+ {
+ InitializePendingChannelCountdown();
+
+ var ip = IPAddress.Any;
+ if (!string.IsNullOrEmpty(BoundHost))
+ {
+ ip = DnsAbstraction.GetHostAddresses(BoundHost)[0];
+ }
+
+ var ep = new IPEndPoint(ip, (int) BoundPort);
+
+ _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
+ _listener.Bind(ep);
+ _listener.Listen(5);
+
+ Session.ErrorOccured += Session_ErrorOccured;
+ Session.Disconnected += Session_Disconnected;
+
+ // consider port started when we're listening for inbound connections
+ _status = ForwardedPortStatus.Started;
+
+ StartAccept(e: null);
+ }
+
+ private void StartAccept(SocketAsyncEventArgs e)
+ {
+ if (e is null)
+ {
+#pragma warning disable CA2000 // Dispose objects before losing scope
+ e = new SocketAsyncEventArgs();
+#pragma warning restore CA2000 // Dispose objects before losing scope
+ e.Completed += AcceptCompleted;
+ }
+ else
+ {
+ // clear the socket as we're reusing the context object
+ e.AcceptSocket = null;
+ }
+
+ // only accept new connections while we are started
+ if (IsStarted)
+ {
+ try
+ {
+ if (!_listener.AcceptAsync(e))
+ {
+ AcceptCompleted(sender: null, e);
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ if (_status == ForwardedPortStatus.Stopping || _status == ForwardedPortStatus.Stopped)
+ {
+ // ignore ObjectDisposedException while stopping or stopped
+ return;
+ }
+
+ throw;
+ }
+ }
+ }
+
+ private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
+ {
+ if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
+ {
+ // server was stopped
+ return;
+ }
+
+ // capture client socket
+ var clientSocket = e.AcceptSocket;
+
+ if (e.SocketError != SocketError.Success)
+ {
+ // accept new connection
+ StartAccept(e);
+
+ // dispose broken client socket
+ CloseClientSocket(clientSocket);
+ return;
+ }
+
+ // accept new connection
+ StartAccept(e);
+
+ // process connection
+ ProcessAccept(clientSocket);
+ }
+
+ private void ProcessAccept(Socket clientSocket)
+ {
+ // close the client socket if we're no longer accepting new connections
+ if (!IsStarted)
+ {
+ CloseClientSocket(clientSocket);
+ return;
+ }
+
+ // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
+ // that same instance; the instance field for the countdown event is re-initialized when the port is restarted
+ // and at that time there may still be pending requests
+ var pendingChannelCountdown = _pendingChannelCountdown;
+
+ pendingChannelCountdown.AddCount();
+
+ try
+ {
+ using (var channel = Session.CreateChannelDirectTcpip())
+ {
+ channel.Exception += Channel_Exception;
+
+ if (!HandleSocks(channel, clientSocket, Session.ConnectionInfo.Timeout))
+ {
+ CloseClientSocket(clientSocket);
+ return;
+ }
+
+ // start receiving from client socket (and sending to server)
+ channel.Bind();
+ }
+ }
+ catch (Exception exp)
+ {
+ RaiseExceptionEvent(exp);
+ CloseClientSocket(clientSocket);
+ }
+ finally
+ {
+ // take into account that CountdownEvent has since been disposed; when stopping the port we
+ // wait for a given time for the channels to close, but once that timeout period has elapsed
+ // the CountdownEvent will be disposed
+ try
+ {
+ _ = pendingChannelCountdown.Signal();
+ }
+ catch (ObjectDisposedException)
+ {
+ // Ignore any ObjectDisposedException
+ }
+ }
+ }
///
- /// Stops the listener.
+ /// Initializes the .
///
- partial void StopListener();
+ ///
+ ///
+ /// When the port is started for the first time, a is created with an initial count
+ /// of 1.
+ ///
+ ///
+ /// On subsequent (re)starts, we'll dispose the current and create a new one with
+ /// initial count of 1.
+ ///
+ ///
+ private void InitializePendingChannelCountdown()
+ {
+ var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
+ original?.Dispose();
+ }
+
+ private bool HandleSocks(IChannelDirectTcpip channel, Socket clientSocket, TimeSpan timeout)
+ {
+ Closing += closeClientSocket;
+
+ try
+ {
+ var version = SocketAbstraction.ReadByte(clientSocket, timeout);
+ switch (version)
+ {
+ case -1:
+ // SOCKS client closed connection
+ return false;
+ case 4:
+ return HandleSocks4(clientSocket, channel, timeout);
+ case 5:
+ return HandleSocks5(clientSocket, channel, timeout);
+ default:
+ throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "SOCKS version {0} is not supported.", version));
+ }
+ }
+ catch (SocketException ex)
+ {
+ // ignore exception thrown by interrupting the blocking receive as part of closing
+ // the forwarded port
+#if NETFRAMEWORK
+ if (ex.SocketErrorCode != SocketError.Interrupted)
+ {
+ RaiseExceptionEvent(ex);
+ }
+#else
+ // Since .NET 5 the exception has been changed.
+ // more info https://github.com/dotnet/runtime/issues/41585
+ if (ex.SocketErrorCode != SocketError.ConnectionAborted)
+ {
+ RaiseExceptionEvent(ex);
+ }
+#endif
+ return false;
+ }
+ finally
+ {
+ // interrupt of blocking receive is now handled by channel (SOCKS4 and SOCKS5)
+ // or no longer necessary
+ Closing -= closeClientSocket;
+ }
+
+ void closeClientSocket(object _, EventArgs args)
+ {
+ CloseClientSocket(clientSocket);
+ };
+ }
+
+ private static void CloseClientSocket(Socket clientSocket)
+ {
+ if (clientSocket.Connected)
+ {
+ try
+ {
+ clientSocket.Shutdown(SocketShutdown.Send);
+ }
+ catch (Exception)
+ {
+ // ignore exception when client socket was already closed
+ }
+ }
+
+ clientSocket.Dispose();
+ }
///
- /// Waits for pending requests to finish, and channels to close.
+ /// Interrupts the listener, and unsubscribes from events.
///
- /// The maximum time to wait for the forwarded port to stop.
- partial void InternalStop(TimeSpan timeout);
+ private void StopListener()
+ {
+ // close listener socket
+ _listener?.Dispose();
+
+ // unsubscribe from session events
+ var session = Session;
+ if (session is not null)
+ {
+ session.ErrorOccured -= Session_ErrorOccured;
+ session.Disconnected -= Session_Disconnected;
+ }
+ }
///
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// Waits for pending channels to close.
///
- public void Dispose()
+ /// The maximum time to wait for the pending channels to close.
+ private void InternalStop(TimeSpan timeout)
+ {
+ _ = _pendingChannelCountdown.Signal();
+
+ if (!_pendingChannelCountdown.Wait(timeout))
+ {
+ // TODO: log as warning
+ DiagnosticAbstraction.Log("Timeout waiting for pending channels in dynamic forwarded port to close.");
+ }
+ }
+
+ private void InternalDispose(bool disposing)
+ {
+ if (disposing)
+ {
+ var listener = _listener;
+ if (listener is not null)
+ {
+ _listener = null;
+ listener.Dispose();
+ }
+
+ var pendingRequestsCountdown = _pendingChannelCountdown;
+ if (pendingRequestsCountdown is not null)
+ {
+ _pendingChannelCountdown = null;
+ pendingRequestsCountdown.Dispose();
+ }
+ }
+ }
+
+ private void Session_Disconnected(object sender, EventArgs e)
+ {
+ var session = Session;
+ if (session is not null)
+ {
+ StopPort(session.ConnectionInfo.Timeout);
+ }
+ }
+
+ private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
+ {
+ var session = Session;
+ if (session is not null)
+ {
+ StopPort(session.ConnectionInfo.Timeout);
+ }
+ }
+
+ private void Channel_Exception(object sender, ExceptionEventArgs e)
{
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
+ RaiseExceptionEvent(e.Exception);
}
- partial void InternalDispose(bool disposing);
+ private bool HandleSocks4(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
+ {
+ var commandCode = SocketAbstraction.ReadByte(socket, timeout);
+ if (commandCode == -1)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var portBuffer = new byte[2];
+ if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var port = Pack.BigEndianToUInt16(portBuffer);
+
+ var ipBuffer = new byte[4];
+ if (SocketAbstraction.Read(socket, ipBuffer, 0, ipBuffer.Length, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var ipAddress = new IPAddress(ipBuffer);
+
+ var username = ReadString(socket, timeout);
+ if (username is null)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var host = ipAddress.ToString();
+
+ RaiseRequestReceived(host, port);
+
+ channel.Open(host, port, this, socket);
+
+ SocketAbstraction.SendByte(socket, 0x00);
+
+ if (channel.IsOpen)
+ {
+ SocketAbstraction.SendByte(socket, 0x5a);
+ SocketAbstraction.Send(socket, portBuffer, 0, portBuffer.Length);
+ SocketAbstraction.Send(socket, ipBuffer, 0, ipBuffer.Length);
+ return true;
+ }
+
+ // signal that request was rejected or failed
+ SocketAbstraction.SendByte(socket, 0x5b);
+ return false;
+ }
+
+ private bool HandleSocks5(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
+ {
+ var authenticationMethodsCount = SocketAbstraction.ReadByte(socket, timeout);
+ if (authenticationMethodsCount == -1)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var authenticationMethods = new byte[authenticationMethodsCount];
+ if (SocketAbstraction.Read(socket, authenticationMethods, 0, authenticationMethods.Length, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ if (authenticationMethods.Min() == 0)
+ {
+ // no user authentication is one of the authentication methods supported
+ // by the SOCKS client
+ SocketAbstraction.Send(socket, new byte[] { 0x05, 0x00 }, 0, 2);
+ }
+ else
+ {
+ // the SOCKS client requires authentication, which we currently do not support
+ SocketAbstraction.Send(socket, new byte[] { 0x05, 0xFF }, 0, 2);
+
+ // we continue business as usual but expect the client to close the connection
+ // so one of the subsequent reads should return -1 signaling that the client
+ // has effectively closed the connection
+ }
+
+ var version = SocketAbstraction.ReadByte(socket, timeout);
+ if (version == -1)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ if (version != 5)
+ {
+ throw new ProxyException("SOCKS5: Version 5 is expected.");
+ }
+
+ var commandCode = SocketAbstraction.ReadByte(socket, timeout);
+ if (commandCode == -1)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var reserved = SocketAbstraction.ReadByte(socket, timeout);
+ if (reserved == -1)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ if (reserved != 0)
+ {
+ throw new ProxyException("SOCKS5: 0 is expected for reserved byte.");
+ }
+
+ var addressType = SocketAbstraction.ReadByte(socket, timeout);
+ if (addressType == -1)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var host = GetSocks5Host(addressType, socket, timeout);
+ if (host is null)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var portBuffer = new byte[2];
+ if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return false;
+ }
+
+ var port = Pack.BigEndianToUInt16(portBuffer);
+
+ RaiseRequestReceived(host, port);
+
+ channel.Open(host, port, this, socket);
+
+ var socksReply = CreateSocks5Reply(channel.IsOpen);
+
+ SocketAbstraction.Send(socket, socksReply, 0, socksReply.Length);
+
+ return true;
+ }
+
+ private static string GetSocks5Host(int addressType, Socket socket, TimeSpan timeout)
+ {
+ switch (addressType)
+ {
+ case 0x01: // IPv4
+ {
+ var addressBuffer = new byte[4];
+ if (SocketAbstraction.Read(socket, addressBuffer, 0, 4, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return null;
+ }
+
+ var ipv4 = new IPAddress(addressBuffer);
+ return ipv4.ToString();
+ }
+
+ case 0x03: // Domain name
+ {
+ var length = SocketAbstraction.ReadByte(socket, timeout);
+ if (length == -1)
+ {
+ // SOCKS client closed connection
+ return null;
+ }
+
+ var addressBuffer = new byte[length];
+ if (SocketAbstraction.Read(socket, addressBuffer, 0, addressBuffer.Length, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return null;
+ }
+
+ var hostName = SshData.Ascii.GetString(addressBuffer, 0, addressBuffer.Length);
+ return hostName;
+ }
+
+ case 0x04: // IPv6
+ {
+ var addressBuffer = new byte[16];
+ if (SocketAbstraction.Read(socket, addressBuffer, 0, 16, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return null;
+ }
+
+ var ipv6 = new IPAddress(addressBuffer);
+ return ipv6.ToString();
+ }
+
+ default:
+ throw new ProxyException(string.Format(CultureInfo.InvariantCulture, "SOCKS5: Address type '{0}' is not supported.", addressType));
+ }
+ }
+
+ private static byte[] CreateSocks5Reply(bool channelOpen)
+ {
+ var socksReply = new byte[// SOCKS version
+ 1 +
+
+ // Reply field
+ 1 +
+
+ // Reserved; fixed: 0x00
+ 1 +
+
+ // Address type; fixed: 0x01
+ 1 +
+
+ // IPv4 server bound address; fixed: {0x00, 0x00, 0x00, 0x00}
+ 4 +
+
+ // server bound port; fixed: {0x00, 0x00}
+ 2];
+
+ socksReply[0] = 0x05;
+
+ if (channelOpen)
+ {
+ socksReply[1] = 0x00; // succeeded
+ }
+ else
+ {
+ socksReply[1] = 0x01; // general SOCKS server failure
+ }
+
+ // reserved
+ socksReply[2] = 0x00;
+
+ // IPv4 address type
+ socksReply[3] = 0x01;
+
+ return socksReply;
+ }
///
- /// Releases unmanaged and - optionally - managed resources.
+ /// Reads a null terminated string from a socket.
///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected override void Dispose(bool disposing)
+ /// The to read from.
+ /// The timeout to apply to individual reads.
+ ///
+ /// The read, or when the socket was closed.
+ ///
+ private static string ReadString(Socket socket, TimeSpan timeout)
{
- if (_isDisposed)
+ var text = new StringBuilder();
+ var buffer = new byte[1];
+
+ while (true)
{
- return;
- }
+ if (SocketAbstraction.Read(socket, buffer, 0, 1, timeout) == 0)
+ {
+ // SOCKS client closed connection
+ return null;
+ }
- base.Dispose(disposing);
+ var byteRead = buffer[0];
+ if (byteRead == 0)
+ {
+ // end of the string
+ break;
+ }
- InternalDispose(disposing);
- _isDisposed = true;
+ _ = text.Append((char) byteRead);
+ }
+
+ return text.ToString();
}
///
diff --git a/src/Renci.SshNet/ForwardedPortLocal.NET.cs b/src/Renci.SshNet/ForwardedPortLocal.NET.cs
deleted file mode 100644
index ca7ec6261..000000000
--- a/src/Renci.SshNet/ForwardedPortLocal.NET.cs
+++ /dev/null
@@ -1,264 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Sockets;
-using System.Threading;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Common;
-
-namespace Renci.SshNet
-{
- public partial class ForwardedPortLocal
- {
- private Socket _listener;
- private CountdownEvent _pendingChannelCountdown;
-
- partial void InternalStart()
- {
- var addr = DnsAbstraction.GetHostAddresses(BoundHost)[0];
- var ep = new IPEndPoint(addr, (int) BoundPort);
-
- _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {NoDelay = true};
- _listener.Bind(ep);
- _listener.Listen(5);
-
- // update bound port (in case original was passed as zero)
- BoundPort = (uint)((IPEndPoint)_listener.LocalEndPoint).Port;
-
- Session.ErrorOccured += Session_ErrorOccured;
- Session.Disconnected += Session_Disconnected;
-
- InitializePendingChannelCountdown();
-
- // consider port started when we're listening for inbound connections
- _status = ForwardedPortStatus.Started;
-
- StartAccept(e: null);
- }
-
- private void StartAccept(SocketAsyncEventArgs e)
- {
- if (e is null)
- {
- e = new SocketAsyncEventArgs();
- e.Completed += AcceptCompleted;
- }
- else
- {
- // clear the socket as we're reusing the context object
- e.AcceptSocket = null;
- }
-
- // only accept new connections while we are started
- if (IsStarted)
- {
- try
- {
- if (!_listener.AcceptAsync(e))
- {
- AcceptCompleted(sender: null, e);
- }
- }
- catch (ObjectDisposedException)
- {
- if (_status == ForwardedPortStatus.Stopping || _status == ForwardedPortStatus.Stopped)
- {
- // ignore ObjectDisposedException while stopping or stopped
- return;
- }
-
- throw;
- }
- }
- }
-
- private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
- {
- if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
- {
- // server was stopped
- return;
- }
-
- // capture client socket
- var clientSocket = e.AcceptSocket;
-
- if (e.SocketError != SocketError.Success)
- {
- // accept new connection
- StartAccept(e);
-
- // dispose broken client socket
- CloseClientSocket(clientSocket);
- return;
- }
-
- // accept new connection
- StartAccept(e);
-
- // process connection
- ProcessAccept(clientSocket);
- }
-
- private void ProcessAccept(Socket clientSocket)
- {
- // close the client socket if we're no longer accepting new connections
- if (!IsStarted)
- {
- CloseClientSocket(clientSocket);
- return;
- }
-
- // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
- // that same instance; the instance field for the countdown event is re-initialized when the port is restarted
- // and at that time there may still be pending requests
- var pendingChannelCountdown = _pendingChannelCountdown;
-
- pendingChannelCountdown.AddCount();
-
- try
- {
- var originatorEndPoint = (IPEndPoint) clientSocket.RemoteEndPoint;
-
- RaiseRequestReceived(originatorEndPoint.Address.ToString(),
- (uint)originatorEndPoint.Port);
-
- using (var channel = Session.CreateChannelDirectTcpip())
- {
- channel.Exception += Channel_Exception;
- channel.Open(Host, Port, this, clientSocket);
- channel.Bind();
- }
- }
- catch (Exception exp)
- {
- RaiseExceptionEvent(exp);
- CloseClientSocket(clientSocket);
- }
- finally
- {
- // take into account that CountdownEvent has since been disposed; when stopping the port we
- // wait for a given time for the channels to close, but once that timeout period has elapsed
- // the CountdownEvent will be disposed
- try
- {
- _ = pendingChannelCountdown.Signal();
- }
- catch (ObjectDisposedException)
- {
- }
- }
- }
-
- ///
- /// Initializes the .
- ///
- ///
- ///
- /// When the port is started for the first time, a is created with an initial count
- /// of 1.
- ///
- ///
- /// On subsequent (re)starts, we'll dispose the current and create a new one with
- /// initial count of 1.
- ///
- ///
- private void InitializePendingChannelCountdown()
- {
- var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
- original?.Dispose();
- }
-
- private static void CloseClientSocket(Socket clientSocket)
- {
- if (clientSocket.Connected)
- {
- try
- {
- clientSocket.Shutdown(SocketShutdown.Send);
- }
- catch (Exception)
- {
- // ignore exception when client socket was already closed
- }
- }
-
- clientSocket.Dispose();
- }
-
- ///
- /// Interrupts the listener, and unsubscribes from events.
- ///
- partial void StopListener()
- {
- // close listener socket
- _listener?.Dispose();
-
- // unsubscribe from session events
- var session = Session;
- if (session != null)
- {
- session.ErrorOccured -= Session_ErrorOccured;
- session.Disconnected -= Session_Disconnected;
- }
- }
-
- ///
- /// Waits for pending channels to close.
- ///
- /// The maximum time to wait for the pending channels to close.
- partial void InternalStop(TimeSpan timeout)
- {
- _ = _pendingChannelCountdown.Signal();
-
- if (!_pendingChannelCountdown.Wait(timeout))
- {
- // TODO: log as warning
- DiagnosticAbstraction.Log("Timeout waiting for pending channels in local forwarded port to close.");
- }
- }
-
- partial void InternalDispose(bool disposing)
- {
- if (disposing)
- {
- var listener = _listener;
- if (listener != null)
- {
- _listener = null;
- listener.Dispose();
- }
-
- var pendingRequestsCountdown = _pendingChannelCountdown;
- if (pendingRequestsCountdown != null)
- {
- _pendingChannelCountdown = null;
- pendingRequestsCountdown.Dispose();
- }
- }
- }
-
- private void Session_Disconnected(object sender, EventArgs e)
- {
- var session = Session;
- if (session != null)
- {
- StopPort(session.ConnectionInfo.Timeout);
- }
- }
-
- private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
- {
- var session = Session;
- if (session != null)
- {
- StopPort(session.ConnectionInfo.Timeout);
- }
- }
-
- private void Channel_Exception(object sender, ExceptionEventArgs e)
- {
- RaiseExceptionEvent(e.Exception);
- }
- }
-}
diff --git a/src/Renci.SshNet/ForwardedPortLocal.cs b/src/Renci.SshNet/ForwardedPortLocal.cs
index 8dabb09fb..3d5b58768 100644
--- a/src/Renci.SshNet/ForwardedPortLocal.cs
+++ b/src/Renci.SshNet/ForwardedPortLocal.cs
@@ -1,6 +1,9 @@
using System;
using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using Renci.SshNet.Abstractions;
using Renci.SshNet.Common;
namespace Renci.SshNet
@@ -8,10 +11,12 @@ namespace Renci.SshNet
///
/// Provides functionality for local port forwarding.
///
- public partial class ForwardedPortLocal : ForwardedPort, IDisposable
+ public partial class ForwardedPortLocal : ForwardedPort
{
private ForwardedPortStatus _status;
private bool _isDisposed;
+ private Socket _listener;
+ private CountdownEvent _pendingChannelCountdown;
///
/// Gets the bound host.
@@ -37,7 +42,7 @@ public partial class ForwardedPortLocal : ForwardedPort, IDisposable
/// Gets a value indicating whether port forwarding is started.
///
///
- /// true if port forwarding is started; otherwise, false.
+ /// if port forwarding is started; otherwise, .
///
public override bool IsStarted
{
@@ -51,7 +56,7 @@ public override bool IsStarted
/// The host.
/// The port.
/// is greater than .
- /// is null.
+ /// is .
/// is greater than .
///
///
@@ -67,8 +72,8 @@ public ForwardedPortLocal(uint boundPort, string host, uint port)
/// The bound host.
/// The host.
/// The port.
- /// is null.
- /// is null.
+ /// is .
+ /// is .
/// is greater than .
public ForwardedPortLocal(string boundHost, string host, uint port)
: this(boundHost, 0, host, port)
@@ -82,8 +87,8 @@ public ForwardedPortLocal(string boundHost, string host, uint port)
/// The bound port.
/// The host.
/// The port.
- /// is null.
- /// is null.
+ /// is .
+ /// is .
/// is greater than .
/// is greater than .
public ForwardedPortLocal(string boundHost, uint boundPort, string host, uint port)
@@ -160,58 +165,289 @@ protected override void StopPort(TimeSpan timeout)
/// The current instance is disposed.
protected override void CheckDisposed()
{
+#if NET7_0_OR_GREATER
+ ObjectDisposedException.ThrowIf(_isDisposed, this);
+#else
if (_isDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
+#endif // NET7_0_OR_GREATER
}
- partial void InternalStart();
-
///
- /// Interrupts the listener, and waits for the listener loop to finish.
+ /// Releases unmanaged and - optionally - managed resources.
///
- ///
- /// When the forwarded port is stopped, then any further action is skipped.
- ///
- partial void StopListener();
+ /// to release both managed and unmanaged resources; to release only unmanaged resources.
+ protected override void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
- partial void InternalStop(TimeSpan timeout);
+ base.Dispose(disposing);
+ InternalDispose(disposing);
+ _isDisposed = true;
+ }
///
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// Finalizes an instance of the class.
///
- public void Dispose()
+ ~ForwardedPortLocal()
+ {
+ Dispose(disposing: false);
+ }
+
+ private void InternalStart()
+ {
+ var addr = DnsAbstraction.GetHostAddresses(BoundHost)[0];
+ var ep = new IPEndPoint(addr, (int) BoundPort);
+
+ _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
+ _listener.Bind(ep);
+ _listener.Listen(5);
+
+ // update bound port (in case original was passed as zero)
+ BoundPort = (uint) ((IPEndPoint) _listener.LocalEndPoint).Port;
+
+ Session.ErrorOccured += Session_ErrorOccured;
+ Session.Disconnected += Session_Disconnected;
+
+ InitializePendingChannelCountdown();
+
+ // consider port started when we're listening for inbound connections
+ _status = ForwardedPortStatus.Started;
+
+ StartAccept(e: null);
+ }
+
+ private void StartAccept(SocketAsyncEventArgs e)
+ {
+ if (e is null)
+ {
+#pragma warning disable CA2000 // Dispose objects before losing scope
+ e = new SocketAsyncEventArgs();
+#pragma warning restore CA2000 // Dispose objects before losing scope
+ e.Completed += AcceptCompleted;
+ }
+ else
+ {
+ // clear the socket as we're reusing the context object
+ e.AcceptSocket = null;
+ }
+
+ // only accept new connections while we are started
+ if (IsStarted)
+ {
+ try
+ {
+ if (!_listener.AcceptAsync(e))
+ {
+ AcceptCompleted(sender: null, e);
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ if (_status == ForwardedPortStatus.Stopping || _status == ForwardedPortStatus.Stopped)
+ {
+ // ignore ObjectDisposedException while stopping or stopped
+ return;
+ }
+
+ throw;
+ }
+ }
+ }
+
+ private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
{
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
+ if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
+ {
+ // server was stopped
+ return;
+ }
+
+ // capture client socket
+ var clientSocket = e.AcceptSocket;
+
+ if (e.SocketError != SocketError.Success)
+ {
+ // accept new connection
+ StartAccept(e);
+
+ // dispose broken client socket
+ CloseClientSocket(clientSocket);
+ return;
+ }
+
+ // accept new connection
+ StartAccept(e);
+
+ // process connection
+ ProcessAccept(clientSocket);
}
- partial void InternalDispose(bool disposing);
+ private void ProcessAccept(Socket clientSocket)
+ {
+ // close the client socket if we're no longer accepting new connections
+ if (!IsStarted)
+ {
+ CloseClientSocket(clientSocket);
+ return;
+ }
+
+ // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
+ // that same instance; the instance field for the countdown event is re-initialized when the port is restarted
+ // and at that time there may still be pending requests
+ var pendingChannelCountdown = _pendingChannelCountdown;
+
+ pendingChannelCountdown.AddCount();
+
+ try
+ {
+ var originatorEndPoint = (IPEndPoint) clientSocket.RemoteEndPoint;
+
+ RaiseRequestReceived(originatorEndPoint.Address.ToString(),
+ (uint) originatorEndPoint.Port);
+
+ using (var channel = Session.CreateChannelDirectTcpip())
+ {
+ channel.Exception += Channel_Exception;
+ channel.Open(Host, Port, this, clientSocket);
+ channel.Bind();
+ }
+ }
+ catch (Exception exp)
+ {
+ RaiseExceptionEvent(exp);
+ CloseClientSocket(clientSocket);
+ }
+ finally
+ {
+ // take into account that CountdownEvent has since been disposed; when stopping the port we
+ // wait for a given time for the channels to close, but once that timeout period has elapsed
+ // the CountdownEvent will be disposed
+ try
+ {
+ _ = pendingChannelCountdown.Signal();
+ }
+ catch (ObjectDisposedException)
+ {
+ // Ignore any ObjectDisposedException
+ }
+ }
+ }
///
- /// Releases unmanaged and - optionally - managed resources.
+ /// Initializes the .
///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected override void Dispose(bool disposing)
+ ///
+ ///
+ /// When the port is started for the first time, a is created with an initial count
+ /// of 1.
+ ///
+ ///
+ /// On subsequent (re)starts, we'll dispose the current and create a new one with
+ /// initial count of 1.
+ ///
+ ///
+ private void InitializePendingChannelCountdown()
{
- if (_isDisposed)
+ var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
+ original?.Dispose();
+ }
+
+ private static void CloseClientSocket(Socket clientSocket)
+ {
+ if (clientSocket.Connected)
{
- return;
+ try
+ {
+ clientSocket.Shutdown(SocketShutdown.Send);
+ }
+ catch (Exception)
+ {
+ // ignore exception when client socket was already closed
+ }
}
- base.Dispose(disposing);
+ clientSocket.Dispose();
+ }
- InternalDispose(disposing);
- _isDisposed = true;
+ ///
+ /// Interrupts the listener, and unsubscribes from events.
+ ///
+ private void StopListener()
+ {
+ // close listener socket
+ _listener?.Dispose();
+
+ // unsubscribe from session events
+ var session = Session;
+ if (session != null)
+ {
+ session.ErrorOccured -= Session_ErrorOccured;
+ session.Disconnected -= Session_Disconnected;
+ }
}
///
- /// Finalizes an instance of the class.
+ /// Waits for pending channels to close.
///
- ~ForwardedPortLocal()
+ /// The maximum time to wait for the pending channels to close.
+ private void InternalStop(TimeSpan timeout)
{
- Dispose(disposing: false);
+ _ = _pendingChannelCountdown.Signal();
+
+ if (!_pendingChannelCountdown.Wait(timeout))
+ {
+ // TODO: log as warning
+ DiagnosticAbstraction.Log("Timeout waiting for pending channels in local forwarded port to close.");
+ }
+ }
+
+ private void InternalDispose(bool disposing)
+ {
+ if (disposing)
+ {
+ var listener = _listener;
+ if (listener is not null)
+ {
+ _listener = null;
+ listener.Dispose();
+ }
+
+ var pendingRequestsCountdown = _pendingChannelCountdown;
+ if (pendingRequestsCountdown is not null)
+ {
+ _pendingChannelCountdown = null;
+ pendingRequestsCountdown.Dispose();
+ }
+ }
+ }
+
+ private void Session_Disconnected(object sender, EventArgs e)
+ {
+ var session = Session;
+ if (session is not null)
+ {
+ StopPort(session.ConnectionInfo.Timeout);
+ }
+ }
+
+ private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
+ {
+ var session = Session;
+ if (session is not null)
+ {
+ StopPort(session.ConnectionInfo.Timeout);
+ }
+ }
+
+ private void Channel_Exception(object sender, ExceptionEventArgs e)
+ {
+ RaiseExceptionEvent(e.Exception);
}
}
}
diff --git a/src/Renci.SshNet/ForwardedPortRemote.cs b/src/Renci.SshNet/ForwardedPortRemote.cs
index ce465e12e..af7fd48a2 100644
--- a/src/Renci.SshNet/ForwardedPortRemote.cs
+++ b/src/Renci.SshNet/ForwardedPortRemote.cs
@@ -12,7 +12,7 @@ namespace Renci.SshNet
///
/// Provides functionality for remote port forwarding.
///
- public class ForwardedPortRemote : ForwardedPort, IDisposable
+ public class ForwardedPortRemote : ForwardedPort
{
private ForwardedPortStatus _status;
private bool _requestStatus;
@@ -24,7 +24,7 @@ public class ForwardedPortRemote : ForwardedPort, IDisposable
/// Gets a value indicating whether port forwarding is started.
///
///
- /// true if port forwarding is started; otherwise, false.
+ /// if port forwarding is started; otherwise, .
///
public override bool IsStarted
{
@@ -80,8 +80,8 @@ public string Host
/// The bound port.
/// The host address.
/// The port.
- /// is null.
- /// is null.
+ /// is .
+ /// is .
/// is greater than .
/// is greater than .
public ForwardedPortRemote(IPAddress boundHostAddress, uint boundPort, IPAddress hostAddress, uint port)
@@ -229,10 +229,14 @@ protected override void StopPort(TimeSpan timeout)
/// The current instance is disposed.
protected override void CheckDisposed()
{
+#if NET7_0_OR_GREATER
+ ObjectDisposedException.ThrowIf(_isDisposed, this);
+#else
if (_isDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
+#endif // NET7_0_OR_GREATER
}
private void Session_ChannelOpening(object sender, MessageEventArgs e)
@@ -285,6 +289,7 @@ private void Session_ChannelOpening(object sender, MessageEventArgs
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// Releases unmanaged and - optionally - managed resources.
///
- public void Dispose()
- {
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Releases unmanaged and - optionally - managed resources
- ///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ /// to release both managed and unmanaged resources; to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (_isDisposed)
diff --git a/src/Renci.SshNet/IForwardedPort.cs b/src/Renci.SshNet/IForwardedPort.cs
index e9a2bd56b..173613b38 100644
--- a/src/Renci.SshNet/IForwardedPort.cs
+++ b/src/Renci.SshNet/IForwardedPort.cs
@@ -1,15 +1,45 @@
using System;
+using Renci.SshNet.Common;
+
namespace Renci.SshNet
{
///
/// Supports port forwarding functionality.
///
- public interface IForwardedPort
+ public interface IForwardedPort : IDisposable
{
///
/// The event occurs as the forwarded port is being stopped.
///
event EventHandler Closing;
+
+ ///
+ /// Occurs when an exception is thrown.
+ ///
+ event EventHandler Exception;
+
+ ///
+ /// Occurs when a port forwarding request is received.
+ ///
+ event EventHandler RequestReceived;
+
+ ///
+ /// Gets a value indicating whether port forwarding is started.
+ ///
+ ///
+ /// if port forwarding is started; otherwise, .
+ ///
+ bool IsStarted { get; }
+
+ ///
+ /// Starts port forwarding.
+ ///
+ void Start();
+
+ ///
+ /// Stops port forwarding.
+ ///
+ void Stop();
}
}
diff --git a/src/Renci.SshNet/IServiceFactory.NET.cs b/src/Renci.SshNet/IServiceFactory.NET.cs
deleted file mode 100644
index 0a86e08eb..000000000
--- a/src/Renci.SshNet/IServiceFactory.NET.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Renci.SshNet.NetConf;
-
-namespace Renci.SshNet
-{
- internal partial interface IServiceFactory
- {
- ///
- /// Creates a new in a given
- /// and with the specified operation timeout.
- ///
- /// The to create the in.
- /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
- ///
- /// An .
- ///
- INetConfSession CreateNetConfSession(ISession session, int operationTimeout);
- }
-}
diff --git a/src/Renci.SshNet/IServiceFactory.cs b/src/Renci.SshNet/IServiceFactory.cs
index cb20ae064..8b2c8650b 100644
--- a/src/Renci.SshNet/IServiceFactory.cs
+++ b/src/Renci.SshNet/IServiceFactory.cs
@@ -2,8 +2,10 @@
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
+
using Renci.SshNet.Common;
using Renci.SshNet.Connection;
+using Renci.SshNet.NetConf;
using Renci.SshNet.Security;
using Renci.SshNet.Sftp;
@@ -14,8 +16,25 @@ namespace Renci.SshNet
///
internal partial interface IServiceFactory
{
+ ///
+ /// Creates an .
+ ///
+ ///
+ /// An .
+ ///
IClientAuthentication CreateClientAuthentication();
+ ///
+ /// Creates a new in a given
+ /// and with the specified operation timeout.
+ ///
+ /// The to create the in.
+ /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
+ ///
+ /// An .
+ ///
+ INetConfSession CreateNetConfSession(ISession session, int operationTimeout);
+
///
/// Creates a new with the specified and
/// .
@@ -25,8 +44,8 @@ internal partial interface IServiceFactory
///
/// An for the specified .
///
- /// is null.
- /// is null.
+ /// is .
+ /// is .
ISession CreateSession(ConnectionInfo connectionInfo, ISocketFactory socketFactory);
///
@@ -34,7 +53,7 @@ internal partial interface IServiceFactory
/// the specified operation timeout and encoding.
///
/// The to create the in.
- /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
+ /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
/// The encoding.
/// The factory to use for creating SFTP messages.
///
@@ -59,13 +78,29 @@ internal partial interface IServiceFactory
///
/// A that was negotiated between client and server.
///
- /// is null.
- /// is null.
+ /// is .
+ /// is .
/// No key exchange algorithm is supported by both client and server.
IKeyExchange CreateKeyExchange(IDictionary clientAlgorithms, string[] serverAlgorithms);
+ ///
+ /// Creates an for the specified file and with the specified
+ /// buffer size.
+ ///
+ /// The file to read.
+ /// The SFTP session to use.
+ /// The size of buffer.
+ ///
+ /// An .
+ ///
ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// An .
+ ///
ISftpResponseFactory CreateSftpResponseFactory();
///
diff --git a/src/Renci.SshNet/ScpClient.NET.cs b/src/Renci.SshNet/ScpClient.NET.cs
deleted file mode 100644
index ede001942..000000000
--- a/src/Renci.SshNet/ScpClient.NET.cs
+++ /dev/null
@@ -1,354 +0,0 @@
-using System;
-using System.IO;
-using System.Text.RegularExpressions;
-
-using Renci.SshNet.Channels;
-using Renci.SshNet.Common;
-
-namespace Renci.SshNet
-{
- ///
- /// Provides SCP client functionality.
- ///
- public partial class ScpClient
- {
- private static readonly Regex DirectoryInfoRe = new Regex(@"D(?\d{4}) (?\d+) (?.+)");
- private static readonly Regex TimestampRe = new Regex(@"T(?\d+) 0 (?\d+) 0");
-
- ///
- /// Uploads the specified file to the remote host.
- ///
- /// The file system info.
- /// A relative or absolute path for the remote file.
- /// is null.
- /// is null.
- /// is a zero-length .
- /// A directory with the specified path exists on the remote host.
- /// The secure copy execution request was rejected by the server.
- public void Upload(FileInfo fileInfo, string path)
- {
- if (fileInfo is null)
- {
- throw new ArgumentNullException(nameof(fileInfo));
- }
-
- var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
-
- using (var input = ServiceFactory.CreatePipeStream())
- using (var channel = Session.CreateChannelSession())
- {
- channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
- channel.Open();
-
- // Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
- // that we expect the target to be a directory.
- if (!channel.SendExecRequest(string.Format("scp -t -d {0}", _remotePathTransformation.Transform(posixPath.Directory))))
- {
- throw SecureExecutionRequestRejectedException();
- }
-
- CheckReturnCode(input);
-
- using (var source = fileInfo.OpenRead())
- {
- UploadTimes(channel, input, fileInfo);
- UploadFileModeAndName(channel, input, source.Length, posixPath.File);
- UploadFileContent(channel, input, source, fileInfo.Name);
- }
- }
- }
-
- ///
- /// Uploads the specified directory to the remote host.
- ///
- /// The directory info.
- /// A relative or absolute path for the remote directory.
- /// is null.
- /// is null.
- /// is a zero-length string.
- /// does not exist on the remote host, is not a directory or the user does not have the required permission.
- /// The secure copy execution request was rejected by the server.
- public void Upload(DirectoryInfo directoryInfo, string path)
- {
- if (directoryInfo is null)
- {
- throw new ArgumentNullException(nameof(directoryInfo));
- }
-
- if (path is null)
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- if (path.Length == 0)
- {
- throw new ArgumentException("The path cannot be a zero-length string.", nameof(path));
- }
-
- using (var input = ServiceFactory.CreatePipeStream())
- using (var channel = Session.CreateChannelSession())
- {
- channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
- channel.Open();
-
- // start copy with the following options:
- // -p preserve modification and access times
- // -r copy directories recursively
- // -d expect path to be a directory
- // -t copy to remote
- if (!channel.SendExecRequest(string.Format("scp -r -p -d -t {0}", _remotePathTransformation.Transform(path))))
- {
- throw SecureExecutionRequestRejectedException();
- }
-
- CheckReturnCode(input);
-
- UploadDirectoryContent(channel, input, directoryInfo);
- }
- }
-
- ///
- /// Downloads the specified file from the remote host to local file.
- ///
- /// Remote host file name.
- /// Local file information.
- /// is null.
- /// is null or empty.
- /// exists on the remote host, and is not a regular file.
- /// The secure copy execution request was rejected by the server.
- public void Download(string filename, FileInfo fileInfo)
- {
- if (string.IsNullOrEmpty(filename))
- {
- throw new ArgumentException("filename");
- }
-
- if (fileInfo is null)
- {
- throw new ArgumentNullException(nameof(fileInfo));
- }
-
- using (var input = ServiceFactory.CreatePipeStream())
- using (var channel = Session.CreateChannelSession())
- {
- channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
- channel.Open();
-
- // Send channel command request
- if (!channel.SendExecRequest(string.Format("scp -pf {0}", _remotePathTransformation.Transform(filename))))
- {
- throw SecureExecutionRequestRejectedException();
- }
-
- // Send reply
- SendSuccessConfirmation(channel);
-
- InternalDownload(channel, input, fileInfo);
- }
- }
-
- ///
- /// Downloads the specified directory from the remote host to local directory.
- ///
- /// Remote host directory name.
- /// Local directory information.
- /// is null or empty.
- /// is null.
- /// File or directory with the specified path does not exist on the remote host.
- /// The secure copy execution request was rejected by the server.
- public void Download(string directoryName, DirectoryInfo directoryInfo)
- {
- if (string.IsNullOrEmpty(directoryName))
- {
- throw new ArgumentException("directoryName");
- }
-
- if (directoryInfo is null)
- {
- throw new ArgumentNullException(nameof(directoryInfo));
- }
-
- using (var input = ServiceFactory.CreatePipeStream())
- using (var channel = Session.CreateChannelSession())
- {
- channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
- channel.Open();
-
- // Send channel command request
- if (!channel.SendExecRequest(string.Format("scp -prf {0}", _remotePathTransformation.Transform(directoryName))))
- {
- throw SecureExecutionRequestRejectedException();
- }
-
- // Send reply
- SendSuccessConfirmation(channel);
-
- InternalDownload(channel, input, directoryInfo);
- }
- }
-
- ///
- /// Uploads the and
- /// of the next file or directory to upload.
- ///
- /// The channel to perform the upload in.
- /// A from which any feedback from the server can be read.
- /// The file or directory to upload.
- private void UploadTimes(IChannelSession channel, Stream input, FileSystemInfo fileOrDirectory)
- {
- var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
- var modificationSeconds = (long) (fileOrDirectory.LastWriteTimeUtc - zeroTime).TotalSeconds;
- var accessSeconds = (long) (fileOrDirectory.LastAccessTimeUtc - zeroTime).TotalSeconds;
- SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
- CheckReturnCode(input);
- }
-
- ///
- /// Upload the files and subdirectories in the specified directory.
- ///
- /// The channel to perform the upload in.
- /// A from which any feedback from the server can be read.
- /// The directory to upload.
- private void UploadDirectoryContent(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
- {
- // Upload files
- var files = directoryInfo.GetFiles();
- foreach (var file in files)
- {
- using (var source = file.OpenRead())
- {
- UploadTimes(channel, input, file);
- UploadFileModeAndName(channel, input, source.Length, file.Name);
- UploadFileContent(channel, input, source, file.Name);
- }
- }
-
- // Upload directories
- var directories = directoryInfo.GetDirectories();
- foreach (var directory in directories)
- {
- UploadTimes(channel, input, directory);
- UploadDirectoryModeAndName(channel, input, directory.Name);
- UploadDirectoryContent(channel, input, directory);
- }
-
- // Mark upload of current directory complete
- SendData(channel, "E\n");
- CheckReturnCode(input);
- }
-
- ///
- /// Sets mode and name of the directory being upload.
- ///
- private void UploadDirectoryModeAndName(IChannelSession channel, Stream input, string directoryName)
- {
- SendData(channel, string.Format("D0755 0 {0}\n", directoryName));
- CheckReturnCode(input);
- }
-
- private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
- {
- var modifiedTime = DateTime.Now;
- var accessedTime = DateTime.Now;
-
- var startDirectoryFullName = fileSystemInfo.FullName;
- var currentDirectoryFullName = startDirectoryFullName;
- var directoryCounter = 0;
-
- while (true)
- {
- var message = ReadString(input);
-
- if (message == "E")
- {
- SendSuccessConfirmation(channel); // Send reply
-
- directoryCounter--;
-
- currentDirectoryFullName = new DirectoryInfo(currentDirectoryFullName).Parent.FullName;
-
- if (directoryCounter == 0)
- {
- break;
- }
-
- continue;
- }
-
- var match = DirectoryInfoRe.Match(message);
- if (match.Success)
- {
- SendSuccessConfirmation(channel); // Send reply
-
- // Read directory
- var filename = match.Result("${filename}");
-
- DirectoryInfo newDirectoryInfo;
- if (directoryCounter > 0)
- {
- newDirectoryInfo = Directory.CreateDirectory(Path.Combine(currentDirectoryFullName, filename));
- newDirectoryInfo.LastAccessTime = accessedTime;
- newDirectoryInfo.LastWriteTime = modifiedTime;
- }
- else
- {
- // Don't create directory for first level
- newDirectoryInfo = fileSystemInfo as DirectoryInfo;
- }
-
- directoryCounter++;
-
- currentDirectoryFullName = newDirectoryInfo.FullName;
- continue;
- }
-
- match = FileInfoRe.Match(message);
- if (match.Success)
- {
- // Read file
- SendSuccessConfirmation(channel); // Send reply
-
- var length = long.Parse(match.Result("${length}"));
- var fileName = match.Result("${filename}");
-
- if (fileSystemInfo is not FileInfo fileInfo)
- {
- fileInfo = new FileInfo(Path.Combine(currentDirectoryFullName, fileName));
- }
-
- using (var output = fileInfo.OpenWrite())
- {
- InternalDownload(channel, input, output, fileName, length);
- }
-
- fileInfo.LastAccessTime = accessedTime;
- fileInfo.LastWriteTime = modifiedTime;
-
- if (directoryCounter == 0)
- {
- break;
- }
-
- continue;
- }
-
- match = TimestampRe.Match(message);
- if (match.Success)
- {
- // Read timestamp
- SendSuccessConfirmation(channel); // Send reply
-
- var mtime = long.Parse(match.Result("${mtime}"));
- var atime = long.Parse(match.Result("${atime}"));
-
- var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
- modifiedTime = zeroTime.AddSeconds(mtime);
- accessedTime = zeroTime.AddSeconds(atime);
- continue;
- }
-
- SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
- }
- }
- }
-}
diff --git a/src/Renci.SshNet/ScpClient.cs b/src/Renci.SshNet/ScpClient.cs
index 177a83bb5..1413a12d9 100644
--- a/src/Renci.SshNet/ScpClient.cs
+++ b/src/Renci.SshNet/ScpClient.cs
@@ -16,8 +16,7 @@ namespace Renci.SshNet
///
///
///
- /// More information on the SCP protocol is available here:
- /// https://github.com/net-ssh/net-scp/blob/master/lib/net/scp.rb
+ /// More information on the SCP protocol is available here: https://github.com/net-ssh/net-scp/blob/master/lib/net/scp.rb.
///
///
/// Known issues in OpenSSH:
@@ -32,9 +31,11 @@ namespace Renci.SshNet
public partial class ScpClient : BaseClient
{
private const string Message = "filename";
- private static readonly Regex FileInfoRe = new Regex(@"C(?\d{4}) (?\d+) (?.+)");
+ private static readonly Regex FileInfoRe = new Regex(@"C(?\d{4}) (?\d+) (?.+)", RegexOptions.Compiled);
private static readonly byte[] SuccessConfirmationCode = { 0 };
private static readonly byte[] ErrorConfirmationCode = { 1 };
+ private static readonly Regex DirectoryInfoRe = new Regex(@"D(?\d{4}) (?\d+) (?.+)", RegexOptions.Compiled);
+ private static readonly Regex TimestampRe = new Regex(@"T(?\d+) 0 (?\d+) 0", RegexOptions.Compiled);
private IRemotePathTransformation _remotePathTransformation;
@@ -61,7 +62,7 @@ public partial class ScpClient : BaseClient
///
/// The transformation to apply to remote paths. The default is .
///
- /// is null.
+ /// is .
///
///
/// This transformation is applied to the remote file or directory path that is passed to the
@@ -74,7 +75,10 @@ public partial class ScpClient : BaseClient
///
public IRemotePathTransformation RemotePathTransformation
{
- get { return _remotePathTransformation; }
+ get
+ {
+ return _remotePathTransformation;
+ }
set
{
if (value is null)
@@ -100,7 +104,7 @@ public IRemotePathTransformation RemotePathTransformation
/// Initializes a new instance of the class.
///
/// The connection info.
- /// is null.
+ /// is .
public ScpClient(ConnectionInfo connectionInfo)
: this(connectionInfo, ownsConnectionInfo: false)
{
@@ -113,8 +117,8 @@ public ScpClient(ConnectionInfo connectionInfo)
/// Connection port.
/// Authentication username.
/// Authentication password.
- /// is null.
- /// is invalid, or is null or contains only whitespace characters.
+ /// is .
+ /// is invalid, or is or contains only whitespace characters.
/// is not within and .
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
public ScpClient(string host, int port, string username, string password)
@@ -128,8 +132,8 @@ public ScpClient(string host, int port, string username, string password)
/// Connection host.
/// Authentication username.
/// Authentication password.
- /// is null.
- /// is invalid, or is null or contains only whitespace characters.
+ /// is .
+ /// is invalid, or is or contains only whitespace characters.
public ScpClient(string host, string username, string password)
: this(host, ConnectionInfo.DefaultPort, username, password)
{
@@ -142,8 +146,8 @@ public ScpClient(string host, string username, string password)
/// Connection port.
/// Authentication username.
/// Authentication private key file(s) .
- /// is null.
- /// is invalid, -or- is null or contains only whitespace characters.
+ /// is .
+ /// is invalid, -or- is or contains only whitespace characters.
/// is not within and .
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
public ScpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles)
@@ -157,8 +161,8 @@ public ScpClient(string host, int port, string username, params IPrivateKeySourc
/// Connection host.
/// Authentication username.
/// Authentication private key file(s) .
- /// is null.
- /// is invalid, -or- is null or contains only whitespace characters.
+ /// is .
+ /// is invalid, -or- is or contains only whitespace characters.
public ScpClient(string host, string username, params IPrivateKeySource[] keyFiles)
: this(host, ConnectionInfo.DefaultPort, username, keyFiles)
{
@@ -169,9 +173,9 @@ public ScpClient(string host, string username, params IPrivateKeySource[] keyFil
///
/// The connection info.
/// Specified whether this instance owns the connection info.
- /// is null.
+ /// is .
///
- /// If is true, then the
+ /// If is , then the
/// connection info will be disposed when this instance is disposed.
///
private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
@@ -185,10 +189,10 @@ private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
/// The connection info.
/// Specified whether this instance owns the connection info.
/// The factory to use for creating new services.
- /// is null.
- /// is null.
+ /// is .
+ /// is .
///
- /// If is true, then the
+ /// If is , then the
/// connection info will be disposed when this instance is disposed.
///
internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
@@ -204,7 +208,7 @@ internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServ
///
/// The to upload.
/// A relative or absolute path for the remote file.
- /// is null.
+ /// is .
/// is a zero-length .
/// A directory with the specified path exists on the remote host.
/// The secure copy execution request was rejected by the server.
@@ -232,13 +236,185 @@ public void Upload(Stream source, string path)
}
}
+ ///
+ /// Uploads the specified file to the remote host.
+ ///
+ /// The file system info.
+ /// A relative or absolute path for the remote file.
+ /// is .
+ /// is .
+ /// is a zero-length .
+ /// A directory with the specified path exists on the remote host.
+ /// The secure copy execution request was rejected by the server.
+ public void Upload(FileInfo fileInfo, string path)
+ {
+ if (fileInfo is null)
+ {
+ throw new ArgumentNullException(nameof(fileInfo));
+ }
+
+ var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
+
+ using (var input = ServiceFactory.CreatePipeStream())
+ using (var channel = Session.CreateChannelSession())
+ {
+ channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
+ channel.Open();
+
+ // Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
+ // that we expect the target to be a directory.
+ if (!channel.SendExecRequest($"scp -t -d {_remotePathTransformation.Transform(posixPath.Directory)}"))
+ {
+ throw SecureExecutionRequestRejectedException();
+ }
+
+ CheckReturnCode(input);
+
+ using (var source = fileInfo.OpenRead())
+ {
+ UploadTimes(channel, input, fileInfo);
+ UploadFileModeAndName(channel, input, source.Length, posixPath.File);
+ UploadFileContent(channel, input, source, fileInfo.Name);
+ }
+ }
+ }
+
+ ///
+ /// Uploads the specified directory to the remote host.
+ ///
+ /// The directory info.
+ /// A relative or absolute path for the remote directory.
+ /// is .
+ /// is .
+ /// is a zero-length string.
+ /// does not exist on the remote host, is not a directory or the user does not have the required permission.
+ /// The secure copy execution request was rejected by the server.
+ public void Upload(DirectoryInfo directoryInfo, string path)
+ {
+ if (directoryInfo is null)
+ {
+ throw new ArgumentNullException(nameof(directoryInfo));
+ }
+
+ if (path is null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ if (path.Length == 0)
+ {
+ throw new ArgumentException("The path cannot be a zero-length string.", nameof(path));
+ }
+
+ using (var input = ServiceFactory.CreatePipeStream())
+ using (var channel = Session.CreateChannelSession())
+ {
+ channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
+ channel.Open();
+
+ // start copy with the following options:
+ // -p preserve modification and access times
+ // -r copy directories recursively
+ // -d expect path to be a directory
+ // -t copy to remote
+ if (!channel.SendExecRequest($"scp -r -p -d -t {_remotePathTransformation.Transform(path)}"))
+ {
+ throw SecureExecutionRequestRejectedException();
+ }
+
+ CheckReturnCode(input);
+
+ UploadDirectoryContent(channel, input, directoryInfo);
+ }
+ }
+
+ ///
+ /// Downloads the specified file from the remote host to local file.
+ ///
+ /// Remote host file name.
+ /// Local file information.
+ /// is .
+ /// is or empty.
+ /// exists on the remote host, and is not a regular file.
+ /// The secure copy execution request was rejected by the server.
+ public void Download(string filename, FileInfo fileInfo)
+ {
+ if (string.IsNullOrEmpty(filename))
+ {
+ throw new ArgumentException("filename");
+ }
+
+ if (fileInfo is null)
+ {
+ throw new ArgumentNullException(nameof(fileInfo));
+ }
+
+ using (var input = ServiceFactory.CreatePipeStream())
+ using (var channel = Session.CreateChannelSession())
+ {
+ channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
+ channel.Open();
+
+ // Send channel command request
+ if (!channel.SendExecRequest($"scp -pf {_remotePathTransformation.Transform(filename)}"))
+ {
+ throw SecureExecutionRequestRejectedException();
+ }
+
+ // Send reply
+ SendSuccessConfirmation(channel);
+
+ InternalDownload(channel, input, fileInfo);
+ }
+ }
+
+ ///
+ /// Downloads the specified directory from the remote host to local directory.
+ ///
+ /// Remote host directory name.
+ /// Local directory information.
+ /// is or empty.
+ /// is .
+ /// File or directory with the specified path does not exist on the remote host.
+ /// The secure copy execution request was rejected by the server.
+ public void Download(string directoryName, DirectoryInfo directoryInfo)
+ {
+ if (string.IsNullOrEmpty(directoryName))
+ {
+ throw new ArgumentException("directoryName");
+ }
+
+ if (directoryInfo is null)
+ {
+ throw new ArgumentNullException(nameof(directoryInfo));
+ }
+
+ using (var input = ServiceFactory.CreatePipeStream())
+ using (var channel = Session.CreateChannelSession())
+ {
+ channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
+ channel.Open();
+
+ // Send channel command request
+ if (!channel.SendExecRequest($"scp -prf {_remotePathTransformation.Transform(directoryName)}"))
+ {
+ throw SecureExecutionRequestRejectedException();
+ }
+
+ // Send reply
+ SendSuccessConfirmation(channel);
+
+ InternalDownload(channel, input, directoryInfo);
+ }
+ }
+
///
/// Downloads the specified file from the remote host to the stream.
///
/// A relative or absolute path for the remote file.
/// The to download the remote file to.
- /// is null or contains only whitespace characters.
- /// is null.
+ /// is or contains only whitespace characters.
+ /// is .
/// exists on the remote host, and is not a regular file.
/// The secure copy execution request was rejected by the server.
public void Download(string filename, Stream destination)
@@ -287,6 +463,33 @@ public void Download(string filename, Stream destination)
}
}
+ private static void SendData(IChannel channel, byte[] buffer, int length)
+ {
+ channel.SendData(buffer, 0, length);
+ }
+
+ private static void SendData(IChannel channel, byte[] buffer)
+ {
+ channel.SendData(buffer);
+ }
+
+ private static int ReadByte(Stream stream)
+ {
+ var b = stream.ReadByte();
+
+ if (b == -1)
+ {
+ throw new SshException("Stream has been closed.");
+ }
+
+ return b;
+ }
+
+ private static SshException SecureExecutionRequestRejectedException()
+ {
+ throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
+ }
+
///
/// Sets mode, size and name of file being upload.
///
@@ -342,34 +545,6 @@ private void UploadFileContent(IChannelSession channel, Stream input, Stream sou
CheckReturnCode(input);
}
- private void InternalDownload(IChannel channel, Stream input, Stream output, string filename, long length)
- {
- var buffer = new byte[Math.Min(length, BufferSize)];
- var needToRead = length;
-
- do
- {
- var read = input.Read(buffer, 0, (int) Math.Min(needToRead, BufferSize));
-
- output.Write(buffer, 0, read);
-
- RaiseDownloadingEvent(filename, length, length - needToRead);
-
- needToRead -= read;
- }
- while (needToRead > 0);
-
- output.Flush();
-
- // Raise one more time when file downloaded
- RaiseDownloadingEvent(filename, length, length - needToRead);
-
- // Send confirmation byte after last data byte was read
- SendSuccessConfirmation(channel);
-
- CheckReturnCode(input);
- }
-
private void RaiseDownloadingEvent(string filename, long size, long downloaded)
{
Downloading?.Invoke(this, new ScpDownloadEventArgs(filename, size, downloaded));
@@ -412,28 +587,6 @@ private void SendData(IChannel channel, string command)
channel.SendData(ConnectionInfo.Encoding.GetBytes(command));
}
- private static void SendData(IChannel channel, byte[] buffer, int length)
- {
- channel.SendData(buffer, 0, length);
- }
-
- private static void SendData(IChannel channel, byte[] buffer)
- {
- channel.SendData(buffer);
- }
-
- private static int ReadByte(Stream stream)
- {
- var b = stream.ReadByte();
-
- if (b == -1)
- {
- throw new SshException("Stream has been closed.");
- }
-
- return b;
- }
-
///
/// Read a LF-terminated string from the .
///
@@ -470,9 +623,196 @@ private string ReadString(Stream stream)
return ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length);
}
- private static SshException SecureExecutionRequestRejectedException()
+ ///
+ /// Uploads the and
+ /// of the next file or directory to upload.
+ ///
+ /// The channel to perform the upload in.
+ /// A from which any feedback from the server can be read.
+ /// The file or directory to upload.
+ private void UploadTimes(IChannelSession channel, Stream input, FileSystemInfo fileOrDirectory)
{
- throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
+ var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+ var modificationSeconds = (long) (fileOrDirectory.LastWriteTimeUtc - zeroTime).TotalSeconds;
+ var accessSeconds = (long) (fileOrDirectory.LastAccessTimeUtc - zeroTime).TotalSeconds;
+ SendData(channel, string.Format(CultureInfo.InvariantCulture, "T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
+ CheckReturnCode(input);
+ }
+
+ ///
+ /// Upload the files and subdirectories in the specified directory.
+ ///
+ /// The channel to perform the upload in.
+ /// A from which any feedback from the server can be read.
+ /// The directory to upload.
+ private void UploadDirectoryContent(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
+ {
+ // Upload files
+ var files = directoryInfo.GetFiles();
+ foreach (var file in files)
+ {
+ using (var source = file.OpenRead())
+ {
+ UploadTimes(channel, input, file);
+ UploadFileModeAndName(channel, input, source.Length, file.Name);
+ UploadFileContent(channel, input, source, file.Name);
+ }
+ }
+
+ // Upload directories
+ var directories = directoryInfo.GetDirectories();
+ foreach (var directory in directories)
+ {
+ UploadTimes(channel, input, directory);
+ UploadDirectoryModeAndName(channel, input, directory.Name);
+ UploadDirectoryContent(channel, input, directory);
+ }
+
+ // Mark upload of current directory complete
+ SendData(channel, "E\n");
+ CheckReturnCode(input);
+ }
+
+ ///
+ /// Sets mode and name of the directory being upload.
+ ///
+ private void UploadDirectoryModeAndName(IChannelSession channel, Stream input, string directoryName)
+ {
+ SendData(channel, string.Format("D0755 0 {0}\n", directoryName));
+ CheckReturnCode(input);
+ }
+
+ private void InternalDownload(IChannel channel, Stream input, Stream output, string filename, long length)
+ {
+ var buffer = new byte[Math.Min(length, BufferSize)];
+ var needToRead = length;
+
+ do
+ {
+ var read = input.Read(buffer, 0, (int) Math.Min(needToRead, BufferSize));
+
+ output.Write(buffer, 0, read);
+
+ RaiseDownloadingEvent(filename, length, length - needToRead);
+
+ needToRead -= read;
+ }
+ while (needToRead > 0);
+
+ output.Flush();
+
+ // Raise one more time when file downloaded
+ RaiseDownloadingEvent(filename, length, length - needToRead);
+
+ // Send confirmation byte after last data byte was read
+ SendSuccessConfirmation(channel);
+
+ CheckReturnCode(input);
+ }
+
+ private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
+ {
+ var modifiedTime = DateTime.Now;
+ var accessedTime = DateTime.Now;
+
+ var startDirectoryFullName = fileSystemInfo.FullName;
+ var currentDirectoryFullName = startDirectoryFullName;
+ var directoryCounter = 0;
+
+ while (true)
+ {
+ var message = ReadString(input);
+
+ if (message == "E")
+ {
+ SendSuccessConfirmation(channel); // Send reply
+
+ directoryCounter--;
+
+ currentDirectoryFullName = new DirectoryInfo(currentDirectoryFullName).Parent.FullName;
+
+ if (directoryCounter == 0)
+ {
+ break;
+ }
+
+ continue;
+ }
+
+ var match = DirectoryInfoRe.Match(message);
+ if (match.Success)
+ {
+ SendSuccessConfirmation(channel); // Send reply
+
+ // Read directory
+ var filename = match.Result("${filename}");
+
+ DirectoryInfo newDirectoryInfo;
+ if (directoryCounter > 0)
+ {
+ newDirectoryInfo = Directory.CreateDirectory(Path.Combine(currentDirectoryFullName, filename));
+ newDirectoryInfo.LastAccessTime = accessedTime;
+ newDirectoryInfo.LastWriteTime = modifiedTime;
+ }
+ else
+ {
+ // Don't create directory for first level
+ newDirectoryInfo = fileSystemInfo as DirectoryInfo;
+ }
+
+ directoryCounter++;
+
+ currentDirectoryFullName = newDirectoryInfo.FullName;
+ continue;
+ }
+
+ match = FileInfoRe.Match(message);
+ if (match.Success)
+ {
+ // Read file
+ SendSuccessConfirmation(channel); // Send reply
+
+ var length = long.Parse(match.Result("${length}"), CultureInfo.InvariantCulture);
+ var fileName = match.Result("${filename}");
+
+ if (fileSystemInfo is not FileInfo fileInfo)
+ {
+ fileInfo = new FileInfo(Path.Combine(currentDirectoryFullName, fileName));
+ }
+
+ using (var output = fileInfo.OpenWrite())
+ {
+ InternalDownload(channel, input, output, fileName, length);
+ }
+
+ fileInfo.LastAccessTime = accessedTime;
+ fileInfo.LastWriteTime = modifiedTime;
+
+ if (directoryCounter == 0)
+ {
+ break;
+ }
+
+ continue;
+ }
+
+ match = TimestampRe.Match(message);
+ if (match.Success)
+ {
+ // Read timestamp
+ SendSuccessConfirmation(channel); // Send reply
+
+ var mtime = long.Parse(match.Result("${mtime}"), CultureInfo.InvariantCulture);
+ var atime = long.Parse(match.Result("${atime}"), CultureInfo.InvariantCulture);
+
+ var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+ modifiedTime = zeroTime.AddSeconds(mtime);
+ accessedTime = zeroTime.AddSeconds(atime);
+ continue;
+ }
+
+ SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
+ }
}
}
}
diff --git a/src/Renci.SshNet/ServiceFactory.NET.cs b/src/Renci.SshNet/ServiceFactory.NET.cs
deleted file mode 100644
index 881d71ccf..000000000
--- a/src/Renci.SshNet/ServiceFactory.NET.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Renci.SshNet.NetConf;
-
-namespace Renci.SshNet
-{
- internal partial class ServiceFactory
- {
- ///
- /// Creates a new in a given
- /// and with the specified operation timeout.
- ///
- /// The to create the in.
- /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
- ///
- /// An .
- ///
- public INetConfSession CreateNetConfSession(ISession session, int operationTimeout)
- {
- return new NetConfSession(session, operationTimeout);
- }
- }
-}
diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs
index 16568faf2..c41c3679e 100644
--- a/src/Renci.SshNet/ServiceFactory.cs
+++ b/src/Renci.SshNet/ServiceFactory.cs
@@ -8,6 +8,7 @@
using Renci.SshNet.Common;
using Renci.SshNet.Connection;
using Renci.SshNet.Messages.Transport;
+using Renci.SshNet.NetConf;
using Renci.SshNet.Security;
using Renci.SshNet.Sftp;
@@ -22,13 +23,13 @@ internal sealed partial class ServiceFactory : IServiceFactory
/// Defines the number of times an authentication attempt with any given
/// can result in before it is disregarded.
///
- private static readonly int PartialSuccessLimit = 5;
+ private const int PartialSuccessLimit = 5;
///
- /// Creates a .
+ /// Creates an .
///
///
- /// A .
+ /// An .
///
public IClientAuthentication CreateClientAuthentication()
{
@@ -44,8 +45,8 @@ public IClientAuthentication CreateClientAuthentication()
///
/// An for the specified .
///
- /// is null.
- /// is null.
+ /// is .
+ /// is .
public ISession CreateSession(ConnectionInfo connectionInfo, ISocketFactory socketFactory)
{
return new Session(connectionInfo, this, socketFactory);
@@ -56,7 +57,7 @@ public ISession CreateSession(ConnectionInfo connectionInfo, ISocketFactory sock
/// the specified operation timeout and encoding.
///
/// The to create the in.
- /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
+ /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
/// The encoding.
/// The factory to use for creating SFTP messages.
///
@@ -87,8 +88,8 @@ public PipeStream CreatePipeStream()
///
/// A that was negotiated between client and server.
///
- /// is null.
- /// is null.
+ /// is .
+ /// is .
/// No key exchange algorithms are supported by both client and server.
public IKeyExchange CreateKeyExchange(IDictionary clientAlgorithms, string[] serverAlgorithms)
{
@@ -116,6 +117,30 @@ from s in serverAlgorithms
return keyExchangeAlgorithmType.CreateInstance();
}
+ ///
+ /// Creates a new in a given
+ /// and with the specified operation timeout.
+ ///
+ /// The to create the in.
+ /// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
+ ///
+ /// An .
+ ///
+ public INetConfSession CreateNetConfSession(ISession session, int operationTimeout)
+ {
+ return new NetConfSession(session, operationTimeout);
+ }
+
+ ///
+ /// Creates an for the specified file and with the specified
+ /// buffer size.
+ ///
+ /// The file to read.
+ /// The SFTP session to use.
+ /// The size of buffer.
+ ///
+ /// An .
+ ///
public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize)
{
const int defaultMaxPendingReads = 3;
@@ -151,6 +176,12 @@ public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSe
return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
}
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// An .
+ ///
public ISftpResponseFactory CreateSftpResponseFactory()
{
return new SftpResponseFactory();