Merge partial classes and complete IForwardedPort interface (#1223)

* Merge partial classes.
Complete IForwardedPort interface.
Expoe Closing event publicly on ForwardedPort.
Disable CA1030 (use events where appropriate).

* Merge partial classes.
Complete IForwardedPort interface.
Expoe Closing event publicly on ForwardedPort.
Disable CA1030 (use events where appropriate).

* Use local function and remove suppression.

* Compile regular expressions.

* Fix build, pfff.

---------

Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
This commit is contained in:
Gert Driesen
2023-10-25 22:14:56 +02:00
committed by GitHub
parent 066f9982e3
commit 42ff9206d2
14 changed files with 1440 additions and 1442 deletions
+4
View File
@@ -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
+24 -19
View File
@@ -1,4 +1,5 @@
using System;
using Renci.SshNet.Common;
namespace Renci.SshNet
@@ -16,28 +17,19 @@ namespace Renci.SshNet
/// </value>
internal ISession Session { get; set; }
/// <summary>
/// The <see cref="Closing"/> event occurs as the forwarded port is being stopped.
/// </summary>
internal event EventHandler Closing;
/// <summary>
/// The <see cref="IForwardedPort.Closing"/> event occurs as the forwarded port is being stopped.
/// </summary>
event EventHandler IForwardedPort.Closing
{
add { Closing += value; }
remove { Closing -= value; }
}
/// <summary>
/// Gets a value indicating whether port forwarding is started.
/// </summary>
/// <value>
/// <c>true</c> if port forwarding is started; otherwise, <c>false</c>.
/// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
/// </value>
public abstract bool IsStarted { get; }
/// <summary>
/// The <see cref="Closing"/> event occurs as the forwarded port is being stopped.
/// </summary>
public event EventHandler Closing;
/// <summary>
/// Occurs when an exception is thrown.
/// </summary>
@@ -51,6 +43,8 @@ namespace Renci.SshNet
/// <summary>
/// Starts port forwarding.
/// </summary>
/// <exception cref="InvalidOperationException">The current <see cref="ForwardedPort"/> is already started -or- is not linked to a SSH session.</exception>
/// <exception cref="SshConnectionException">The client is not connected.</exception>
public virtual void Start()
{
CheckDisposed();
@@ -77,7 +71,9 @@ namespace Renci.SshNet
/// <summary>
/// Stops port forwarding.
/// </summary>
#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 @@ namespace Renci.SshNet
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Starts port forwarding.
/// </summary>
@@ -100,22 +105,22 @@ namespace Renci.SshNet
RaiseClosing();
var session = Session;
if (session != null)
if (session is not null)
{
session.ErrorOccured -= Session_ErrorOccured;
}
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
/// <param name="disposing"><see langowrd="true"/> to release both managed and unmanaged resources; <see langowrd="false"/> to release only unmanaged resources.</param>
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;
@@ -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)
{
}
}
}
/// <summary>
/// Initializes the <see cref="CountdownEvent"/>.
/// </summary>
/// <remarks>
/// <para>
/// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
/// of <c>1</c>.
/// </para>
/// <para>
/// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
/// initial count of <c>1</c>.
/// </para>
/// </remarks>
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();
}
/// <summary>
/// Interrupts the listener, and unsubscribes from <see cref="Session"/> events.
/// </summary>
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;
}
}
/// <summary>
/// Waits for pending channels to close.
/// </summary>
/// <param name="timeout">The maximum time to wait for the pending channels to close.</param>
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;
}
/// <summary>
/// Reads a null terminated string from a socket.
/// </summary>
/// <param name="socket">The <see cref="Socket"/> to read from.</param>
/// <param name="timeout">The timeout to apply to individual reads.</param>
/// <returns>
/// The <see cref="string"/> read, or <c>null</c> when the socket was closed.
/// </returns>
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();
}
}
}
+609 -28
View File
@@ -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.
/// </summary>
public partial class ForwardedPortDynamic : ForwardedPort
public class ForwardedPortDynamic : ForwardedPort
{
private ForwardedPortStatus _status;
@@ -14,7 +24,7 @@ namespace Renci.SshNet
/// Holds a value indicating whether the current instance is disposed.
/// </summary>
/// <value>
/// <c>true</c> if the current instance is disposed; otherwise, <c>false</c>.
/// <see langword="true"/> if the current instance is disposed; otherwise, <see langword="false"/>.
/// </value>
private bool _isDisposed;
@@ -28,11 +38,14 @@ namespace Renci.SshNet
/// </summary>
public uint BoundPort { get; }
private Socket _listener;
private CountdownEvent _pendingChannelCountdown;
/// <summary>
/// Gets a value indicating whether port forwarding is started.
/// </summary>
/// <value>
/// <c>true</c> if port forwarding is started; otherwise, <c>false</c>.
/// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
/// </value>
public override bool IsStarted
{
@@ -112,40 +125,20 @@ namespace Renci.SshNet
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
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();
/// <summary>
/// Stops the listener.
/// </summary>
partial void StopListener();
/// <summary>
/// Waits for pending requests to finish, and channels to close.
/// </summary>
/// <param name="timeout">The maximum time to wait for the forwarded port to stop.</param>
partial void InternalStop(TimeSpan timeout);
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
partial void InternalDispose(bool disposing);
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
/// <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)
{
if (_isDisposed)
@@ -159,6 +152,594 @@ namespace Renci.SshNet
_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
}
}
}
/// <summary>
/// Initializes the <see cref="CountdownEvent"/>.
/// </summary>
/// <remarks>
/// <para>
/// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
/// of <c>1</c>.
/// </para>
/// <para>
/// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
/// initial count of <c>1</c>.
/// </para>
/// </remarks>
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();
}
/// <summary>
/// Interrupts the listener, and unsubscribes from <see cref="Session"/> events.
/// </summary>
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;
}
}
/// <summary>
/// Waits for pending channels to close.
/// </summary>
/// <param name="timeout">The maximum time to wait for the pending channels to close.</param>
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)
{
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;
}
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;
}
/// <summary>
/// Reads a null terminated string from a socket.
/// </summary>
/// <param name="socket">The <see cref="Socket"/> to read from.</param>
/// <param name="timeout">The timeout to apply to individual reads.</param>
/// <returns>
/// The <see cref="string"/> read, or <see langword="null"/> when the socket was closed.
/// </returns>
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();
}
/// <summary>
/// Finalizes an instance of the <see cref="ForwardedPortDynamic"/> class.
/// </summary>
-264
View File
@@ -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)
{
}
}
}
/// <summary>
/// Initializes the <see cref="CountdownEvent"/>.
/// </summary>
/// <remarks>
/// <para>
/// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
/// of <c>1</c>.
/// </para>
/// <para>
/// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
/// initial count of <c>1</c>.
/// </para>
/// </remarks>
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();
}
/// <summary>
/// Interrupts the listener, and unsubscribes from <see cref="Session"/> events.
/// </summary>
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;
}
}
/// <summary>
/// Waits for pending channels to close.
/// </summary>
/// <param name="timeout">The maximum time to wait for the pending channels to close.</param>
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);
}
}
}
+268 -32
View File
@@ -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
/// <summary>
/// Provides functionality for local port forwarding.
/// </summary>
public partial class ForwardedPortLocal : ForwardedPort, IDisposable
public partial class ForwardedPortLocal : ForwardedPort
{
private ForwardedPortStatus _status;
private bool _isDisposed;
private Socket _listener;
private CountdownEvent _pendingChannelCountdown;
/// <summary>
/// Gets the bound host.
@@ -37,7 +42,7 @@ namespace Renci.SshNet
/// Gets a value indicating whether port forwarding is started.
/// </summary>
/// <value>
/// <c>true</c> if port forwarding is started; otherwise, <c>false</c>.
/// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
/// </value>
public override bool IsStarted
{
@@ -51,7 +56,7 @@ namespace Renci.SshNet
/// <param name="host">The host.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="boundPort" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\ForwardedPortLocalTest.cs" region="Example SshClient AddForwardedPort Start Stop ForwardedPortLocal" language="C#" title="Local port forwarding" />
@@ -67,8 +72,8 @@ namespace Renci.SshNet
/// <param name="boundHost">The bound host.</param>
/// <param name="host">The host.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentNullException"><paramref name="boundHost"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="boundHost"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
public ForwardedPortLocal(string boundHost, string host, uint port)
: this(boundHost, 0, host, port)
@@ -82,8 +87,8 @@ namespace Renci.SshNet
/// <param name="boundPort">The bound port.</param>
/// <param name="host">The host.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentNullException"><paramref name="boundHost"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="boundHost"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="host"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="boundPort" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
public ForwardedPortLocal(string boundHost, uint boundPort, string host, uint port)
@@ -160,39 +165,20 @@ namespace Renci.SshNet
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
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();
/// <summary>
/// Interrupts the listener, and waits for the listener loop to finish.
/// </summary>
/// <remarks>
/// When the forwarded port is stopped, then any further action is skipped.
/// </remarks>
partial void StopListener();
partial void InternalStop(TimeSpan timeout);
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
partial void InternalDispose(bool disposing);
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
/// <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)
{
if (_isDisposed)
@@ -201,7 +187,6 @@ namespace Renci.SshNet
}
base.Dispose(disposing);
InternalDispose(disposing);
_isDisposed = true;
}
@@ -213,5 +198,256 @@ namespace Renci.SshNet
{
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)
{
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)
{
// Ignore any ObjectDisposedException
}
}
}
/// <summary>
/// Initializes the <see cref="CountdownEvent"/>.
/// </summary>
/// <remarks>
/// <para>
/// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
/// of <c>1</c>.
/// </para>
/// <para>
/// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
/// initial count of <c>1</c>.
/// </para>
/// </remarks>
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();
}
/// <summary>
/// Interrupts the listener, and unsubscribes from <see cref="Session"/> events.
/// </summary>
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;
}
}
/// <summary>
/// Waits for pending channels to close.
/// </summary>
/// <param name="timeout">The maximum time to wait for the pending channels to close.</param>
private 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.");
}
}
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);
}
}
}
+11 -15
View File
@@ -12,7 +12,7 @@ namespace Renci.SshNet
/// <summary>
/// Provides functionality for remote port forwarding.
/// </summary>
public class ForwardedPortRemote : ForwardedPort, IDisposable
public class ForwardedPortRemote : ForwardedPort
{
private ForwardedPortStatus _status;
private bool _requestStatus;
@@ -24,7 +24,7 @@ namespace Renci.SshNet
/// Gets a value indicating whether port forwarding is started.
/// </summary>
/// <value>
/// <c>true</c> if port forwarding is started; otherwise, <c>false</c>.
/// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
/// </value>
public override bool IsStarted
{
@@ -80,8 +80,8 @@ namespace Renci.SshNet
/// <param name="boundPort">The bound port.</param>
/// <param name="hostAddress">The host address.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentNullException"><paramref name="boundHostAddress"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="hostAddress"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="boundHostAddress"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="hostAddress"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="boundPort" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint.MaxPort" />.</exception>
public ForwardedPortRemote(IPAddress boundHostAddress, uint boundPort, IPAddress hostAddress, uint port)
@@ -229,10 +229,14 @@ namespace Renci.SshNet
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
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<ChannelOpenMessage> e)
@@ -285,6 +289,7 @@ namespace Renci.SshNet
}
catch (ObjectDisposedException)
{
// Ignore any ObjectDisposedException
}
}
});
@@ -335,18 +340,9 @@ namespace Renci.SshNet
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
/// <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)
{
if (_isDisposed)
+31 -1
View File
@@ -1,15 +1,45 @@
using System;
using Renci.SshNet.Common;
namespace Renci.SshNet
{
/// <summary>
/// Supports port forwarding functionality.
/// </summary>
public interface IForwardedPort
public interface IForwardedPort : IDisposable
{
/// <summary>
/// The <see cref="Closing"/> event occurs as the forwarded port is being stopped.
/// </summary>
event EventHandler Closing;
/// <summary>
/// Occurs when an exception is thrown.
/// </summary>
event EventHandler<ExceptionEventArgs> Exception;
/// <summary>
/// Occurs when a port forwarding request is received.
/// </summary>
event EventHandler<PortForwardEventArgs> RequestReceived;
/// <summary>
/// Gets a value indicating whether port forwarding is started.
/// </summary>
/// <value>
/// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
/// </value>
bool IsStarted { get; }
/// <summary>
/// Starts port forwarding.
/// </summary>
void Start();
/// <summary>
/// Stops port forwarding.
/// </summary>
void Stop();
}
}
-18
View File
@@ -1,18 +0,0 @@
using Renci.SshNet.NetConf;
namespace Renci.SshNet
{
internal partial interface IServiceFactory
{
/// <summary>
/// Creates a new <see cref="INetConfSession"/> in a given <see cref="ISession"/>
/// and with the specified operation timeout.
/// </summary>
/// <param name="session">The <see cref="ISession"/> to create the <see cref="INetConfSession"/> in.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
/// <returns>
/// An <see cref="INetConfSession"/>.
/// </returns>
INetConfSession CreateNetConfSession(ISession session, int operationTimeout);
}
}
+40 -5
View File
@@ -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
/// </summary>
internal partial interface IServiceFactory
{
/// <summary>
/// Creates an <see cref="IClientAuthentication"/>.
/// </summary>
/// <returns>
/// An <see cref="IClientAuthentication"/>.
/// </returns>
IClientAuthentication CreateClientAuthentication();
/// <summary>
/// Creates a new <see cref="INetConfSession"/> in a given <see cref="ISession"/>
/// and with the specified operation timeout.
/// </summary>
/// <param name="session">The <see cref="ISession"/> to create the <see cref="INetConfSession"/> in.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.</param>
/// <returns>
/// An <see cref="INetConfSession"/>.
/// </returns>
INetConfSession CreateNetConfSession(ISession session, int operationTimeout);
/// <summary>
/// Creates a new <see cref="ISession"/> with the specified <see cref="ConnectionInfo"/> and
/// <see cref="ISocketFactory"/>.
@@ -25,8 +44,8 @@ namespace Renci.SshNet
/// <returns>
/// An <see cref="ISession"/> for the specified <see cref="ConnectionInfo"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="socketFactory"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="socketFactory"/> is <see langword="null"/>.</exception>
ISession CreateSession(ConnectionInfo connectionInfo, ISocketFactory socketFactory);
/// <summary>
@@ -34,7 +53,7 @@ namespace Renci.SshNet
/// the specified operation timeout and encoding.
/// </summary>
/// <param name="session">The <see cref="ISession"/> to create the <see cref="ISftpSession"/> in.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.</param>
/// <param name="encoding">The encoding.</param>
/// <param name="sftpMessageFactory">The factory to use for creating SFTP messages.</param>
/// <returns>
@@ -59,13 +78,29 @@ namespace Renci.SshNet
/// <returns>
/// A <see cref="IKeyExchange"/> that was negotiated between client and server.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="clientAlgorithms"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serverAlgorithms"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="clientAlgorithms"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serverAlgorithms"/> is <see langword="null"/>.</exception>
/// <exception cref="SshConnectionException">No key exchange algorithm is supported by both client and server.</exception>
IKeyExchange CreateKeyExchange(IDictionary<string, Type> clientAlgorithms, string[] serverAlgorithms);
/// <summary>
/// Creates an <see cref="ISftpFileReader"/> for the specified file and with the specified
/// buffer size.
/// </summary>
/// <param name="fileName">The file to read.</param>
/// <param name="sftpSession">The SFTP session to use.</param>
/// <param name="bufferSize">The size of buffer.</param>
/// <returns>
/// An <see cref="ISftpFileReader"/>.
/// </returns>
ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
/// <summary>
/// Creates a new <see cref="ISftpResponseFactory"/> instance.
/// </summary>
/// <returns>
/// An <see cref="ISftpResponseFactory"/>.
/// </returns>
ISftpResponseFactory CreateSftpResponseFactory();
/// <summary>
-354
View File
@@ -1,354 +0,0 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
namespace Renci.SshNet
{
/// <summary>
/// Provides SCP client functionality.
/// </summary>
public partial class ScpClient
{
private static readonly Regex DirectoryInfoRe = new Regex(@"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
private static readonly Regex TimestampRe = new Regex(@"T(?<mtime>\d+) 0 (?<atime>\d+) 0");
/// <summary>
/// Uploads the specified file to the remote host.
/// </summary>
/// <param name="fileInfo">The file system info.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileInfo" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
}
/// <summary>
/// Uploads the specified directory to the remote host.
/// </summary>
/// <param name="directoryInfo">The directory info.</param>
/// <param name="path">A relative or absolute path for the remote directory.</param>
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length string.</exception>
/// <exception cref="ScpException"><paramref name="path"/> does not exist on the remote host, is not a directory or the user does not have the required permission.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
/// <summary>
/// Downloads the specified file from the remote host to local file.
/// </summary>
/// <param name="filename">Remote host file name.</param>
/// <param name="fileInfo">Local file information.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or empty.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
/// <summary>
/// Downloads the specified directory from the remote host to local directory.
/// </summary>
/// <param name="directoryName">Remote host directory name.</param>
/// <param name="directoryInfo">Local directory information.</param>
/// <exception cref="ArgumentException"><paramref name="directoryName"/> is <c>null</c> or empty.</exception>
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <c>null</c>.</exception>
/// <exception cref="ScpException">File or directory with the specified path does not exist on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
/// <summary>
/// Uploads the <see cref="FileSystemInfo.LastWriteTimeUtc"/> and <see cref="FileSystemInfo.LastAccessTimeUtc"/>
/// of the next file or directory to upload.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="fileOrDirectory">The file or directory to upload.</param>
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);
}
/// <summary>
/// Upload the files and subdirectories in the specified directory.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="directoryInfo">The directory to upload.</param>
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);
}
/// <summary>
/// Sets mode and name of the directory being upload.
/// </summary>
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));
}
}
}
}
+414 -74
View File
@@ -16,8 +16,7 @@ namespace Renci.SshNet
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
private static readonly Regex FileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)", RegexOptions.Compiled);
private static readonly byte[] SuccessConfirmationCode = { 0 };
private static readonly byte[] ErrorConfirmationCode = { 1 };
private static readonly Regex DirectoryInfoRe = new Regex(@"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)", RegexOptions.Compiled);
private static readonly Regex TimestampRe = new Regex(@"T(?<mtime>\d+) 0 (?<atime>\d+) 0", RegexOptions.Compiled);
private IRemotePathTransformation _remotePathTransformation;
@@ -61,7 +62,7 @@ namespace Renci.SshNet
/// <value>
/// The transformation to apply to remote paths. The default is <see cref="RemotePathTransformation.DoubleQuote"/>.
/// </value>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This transformation is applied to the remote file or directory path that is passed to the
@@ -74,7 +75,10 @@ namespace Renci.SshNet
/// </remarks>
public IRemotePathTransformation RemotePathTransformation
{
get { return _remotePathTransformation; }
get
{
return _remotePathTransformation;
}
set
{
if (value is null)
@@ -100,7 +104,7 @@ namespace Renci.SshNet
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="connectionInfo">The connection info.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
public ScpClient(ConnectionInfo connectionInfo)
: this(connectionInfo, ownsConnectionInfo: false)
{
@@ -113,8 +117,8 @@ namespace Renci.SshNet
/// <param name="port">Connection port.</param>
/// <param name="username">Authentication username.</param>
/// <param name="password">Authentication password.</param>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
[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 @@ namespace Renci.SshNet
/// <param name="host">Connection host.</param>
/// <param name="username">Authentication username.</param>
/// <param name="password">Authentication password.</param>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
public ScpClient(string host, string username, string password)
: this(host, ConnectionInfo.DefaultPort, username, password)
{
@@ -142,8 +146,8 @@ namespace Renci.SshNet
/// <param name="port">Connection port.</param>
/// <param name="username">Authentication username.</param>
/// <param name="keyFiles">Authentication private key file(s) .</param>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
[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 @@ namespace Renci.SshNet
/// <param name="host">Connection host.</param>
/// <param name="username">Authentication username.</param>
/// <param name="keyFiles">Authentication private key file(s) .</param>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
public ScpClient(string host, string username, params IPrivateKeySource[] keyFiles)
: this(host, ConnectionInfo.DefaultPort, username, keyFiles)
{
@@ -169,9 +173,9 @@ namespace Renci.SshNet
/// </summary>
/// <param name="connectionInfo">The connection info.</param>
/// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
/// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
/// connection info will be disposed when this instance is disposed.
/// </remarks>
private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
@@ -185,10 +189,10 @@ namespace Renci.SshNet
/// <param name="connectionInfo">The connection info.</param>
/// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
/// <param name="serviceFactory">The factory to use for creating new services.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
/// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
/// connection info will be disposed when this instance is disposed.
/// </remarks>
internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
@@ -204,7 +208,7 @@ namespace Renci.SshNet
/// </summary>
/// <param name="source">The <see cref="Stream"/> to upload.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
@@ -232,13 +236,185 @@ namespace Renci.SshNet
}
}
/// <summary>
/// Uploads the specified file to the remote host.
/// </summary>
/// <param name="fileInfo">The file system info.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileInfo" /> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
}
/// <summary>
/// Uploads the specified directory to the remote host.
/// </summary>
/// <param name="directoryInfo">The directory info.</param>
/// <param name="path">A relative or absolute path for the remote directory.</param>
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length string.</exception>
/// <exception cref="ScpException"><paramref name="path"/> does not exist on the remote host, is not a directory or the user does not have the required permission.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
/// <summary>
/// Downloads the specified file from the remote host to local file.
/// </summary>
/// <param name="filename">Remote host file name.</param>
/// <param name="fileInfo">Local file information.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <see langword="null"/> or empty.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
/// <summary>
/// Downloads the specified directory from the remote host to local directory.
/// </summary>
/// <param name="directoryName">Remote host directory name.</param>
/// <param name="directoryInfo">Local directory information.</param>
/// <exception cref="ArgumentException"><paramref name="directoryName"/> is <see langword="null"/> or empty.</exception>
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ScpException">File or directory with the specified path does not exist on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
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);
}
}
/// <summary>
/// Downloads the specified file from the remote host to the stream.
/// </summary>
/// <param name="filename">A relative or absolute path for the remote file.</param>
/// <param name="destination">The <see cref="Stream"/> to download the remote file to.</param>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="destination"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="destination"/> is <see langword="null"/>.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Download(string filename, Stream destination)
@@ -287,6 +463,33 @@ namespace Renci.SshNet
}
}
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.");
}
/// <summary>
/// Sets mode, size and name of file being upload.
/// </summary>
@@ -342,34 +545,6 @@ namespace Renci.SshNet
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 @@ namespace Renci.SshNet
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;
}
/// <summary>
/// Read a LF-terminated string from the <see cref="Stream"/>.
/// </summary>
@@ -470,9 +623,196 @@ namespace Renci.SshNet
return ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length);
}
private static SshException SecureExecutionRequestRejectedException()
/// <summary>
/// Uploads the <see cref="FileSystemInfo.LastWriteTimeUtc"/> and <see cref="FileSystemInfo.LastAccessTimeUtc"/>
/// of the next file or directory to upload.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="fileOrDirectory">The file or directory to upload.</param>
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);
}
/// <summary>
/// Upload the files and subdirectories in the specified directory.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="directoryInfo">The directory to upload.</param>
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);
}
/// <summary>
/// Sets mode and name of the directory being upload.
/// </summary>
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));
}
}
}
}
-21
View File
@@ -1,21 +0,0 @@
using Renci.SshNet.NetConf;
namespace Renci.SshNet
{
internal partial class ServiceFactory
{
/// <summary>
/// Creates a new <see cref="INetConfSession"/> in a given <see cref="ISession"/>
/// and with the specified operation timeout.
/// </summary>
/// <param name="session">The <see cref="ISession"/> to create the <see cref="INetConfSession"/> in.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
/// <returns>
/// An <see cref="INetConfSession"/>.
/// </returns>
public INetConfSession CreateNetConfSession(ISession session, int operationTimeout)
{
return new NetConfSession(session, operationTimeout);
}
}
}
+39 -8
View File
@@ -8,6 +8,7 @@ using Renci.SshNet.Abstractions;
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 @@ namespace Renci.SshNet
/// Defines the number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/>
/// can result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
/// </summary>
private static readonly int PartialSuccessLimit = 5;
private const int PartialSuccessLimit = 5;
/// <summary>
/// Creates a <see cref="IClientAuthentication"/>.
/// Creates an <see cref="IClientAuthentication"/>.
/// </summary>
/// <returns>
/// A <see cref="IClientAuthentication"/>.
/// An <see cref="IClientAuthentication"/>.
/// </returns>
public IClientAuthentication CreateClientAuthentication()
{
@@ -44,8 +45,8 @@ namespace Renci.SshNet
/// <returns>
/// An <see cref="ISession"/> for the specified <see cref="ConnectionInfo"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="socketFactory"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="socketFactory"/> is <see langword="null"/>.</exception>
public ISession CreateSession(ConnectionInfo connectionInfo, ISocketFactory socketFactory)
{
return new Session(connectionInfo, this, socketFactory);
@@ -56,7 +57,7 @@ namespace Renci.SshNet
/// the specified operation timeout and encoding.
/// </summary>
/// <param name="session">The <see cref="ISession"/> to create the <see cref="ISftpSession"/> in.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.</param>
/// <param name="encoding">The encoding.</param>
/// <param name="sftpMessageFactory">The factory to use for creating SFTP messages.</param>
/// <returns>
@@ -87,8 +88,8 @@ namespace Renci.SshNet
/// <returns>
/// A <see cref="IKeyExchange"/> that was negotiated between client and server.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="clientAlgorithms"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serverAlgorithms"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="clientAlgorithms"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serverAlgorithms"/> is <see langword="null"/>.</exception>
/// <exception cref="SshConnectionException">No key exchange algorithms are supported by both client and server.</exception>
public IKeyExchange CreateKeyExchange(IDictionary<string, Type> clientAlgorithms, string[] serverAlgorithms)
{
@@ -116,6 +117,30 @@ namespace Renci.SshNet
return keyExchangeAlgorithmType.CreateInstance<IKeyExchange>();
}
/// <summary>
/// Creates a new <see cref="INetConfSession"/> in a given <see cref="ISession"/>
/// and with the specified operation timeout.
/// </summary>
/// <param name="session">The <see cref="ISession"/> to create the <see cref="INetConfSession"/> in.</param>
/// <param name="operationTimeout">The number of milliseconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.</param>
/// <returns>
/// An <see cref="INetConfSession"/>.
/// </returns>
public INetConfSession CreateNetConfSession(ISession session, int operationTimeout)
{
return new NetConfSession(session, operationTimeout);
}
/// <summary>
/// Creates an <see cref="ISftpFileReader"/> for the specified file and with the specified
/// buffer size.
/// </summary>
/// <param name="fileName">The file to read.</param>
/// <param name="sftpSession">The SFTP session to use.</param>
/// <param name="bufferSize">The size of buffer.</param>
/// <returns>
/// An <see cref="ISftpFileReader"/>.
/// </returns>
public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize)
{
const int defaultMaxPendingReads = 3;
@@ -151,6 +176,12 @@ namespace Renci.SshNet
return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
}
/// <summary>
/// Creates a new <see cref="ISftpResponseFactory"/> instance.
/// </summary>
/// <returns>
/// An <see cref="ISftpResponseFactory"/>.
/// </returns>
public ISftpResponseFactory CreateSftpResponseFactory()
{
return new SftpResponseFactory();