< Summary

Information
Class: Renci.SshNet.ForwardedPortLocal
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\ForwardedPortLocal.cs
Line coverage
71%
Covered lines: 152
Uncovered lines: 62
Coverable lines: 214
Total lines: 453
Line coverage: 71%
Branch coverage
69%
Covered branches: 36
Total branches: 52
Branch coverage: 69.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
get_BoundHost()100%1100%
get_BoundPort()100%1100%
get_Host()100%1100%
get_Port()100%1100%
get_IsStarted()100%1100%
.ctor(...)100%1100%
.ctor(...)100%1100%
.ctor(...)100%4100%
StartPort()50%250%
StopPort(...)100%2100%
CheckDisposed()100%1100%
Dispose(...)100%2100%
Finalize()100%1100%
InternalStart()100%1100%
StartAccept(...)50%1064%
AcceptCompleted(...)87.5%869.23%
ProcessAccept(...)50%265.62%
InitializePendingChannelCountdown()100%2100%
CloseClientSocket(...)0%20%
StopListener()75%4100%
InternalStop(...)50%257.14%
InternalDispose(...)100%6100%
Session_Disconnected(...)0%20%
Session_ErrorOccured(...)0%20%
Channel_Exception(...)100%10%

File(s)

\home\appveyor\projects\ssh-net\src\Renci.SshNet\ForwardedPortLocal.cs

#LineLine coverage
 1using System;
 2using System.Net;
 3using System.Net.Sockets;
 4using System.Threading;
 5
 6using Renci.SshNet.Abstractions;
 7using Renci.SshNet.Common;
 8
 9namespace Renci.SshNet
 10{
 11    /// <summary>
 12    /// Provides functionality for local port forwarding.
 13    /// </summary>
 14    public partial class ForwardedPortLocal : ForwardedPort
 15    {
 16        private ForwardedPortStatus _status;
 17        private bool _isDisposed;
 18        private Socket _listener;
 19        private CountdownEvent _pendingChannelCountdown;
 20
 21        /// <summary>
 22        /// Gets the bound host.
 23        /// </summary>
 42024        public string BoundHost { get; private set; }
 25
 26        /// <summary>
 27        /// Gets the bound port.
 28        /// </summary>
 58029        public uint BoundPort { get; private set; }
 30
 31        /// <summary>
 32        /// Gets the forwarded host.
 33        /// </summary>
 45834        public string Host { get; private set; }
 35
 36        /// <summary>
 37        /// Gets the forwarded port.
 38        /// </summary>
 45239        public uint Port { get; private set; }
 40
 41        /// <summary>
 42        /// Gets a value indicating whether port forwarding is started.
 43        /// </summary>
 44        /// <value>
 45        /// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
 46        /// </value>
 47        public override bool IsStarted
 48        {
 199549            get { return _status == ForwardedPortStatus.Started; }
 50        }
 51
 52        /// <summary>
 53        /// Initializes a new instance of the <see cref="ForwardedPortLocal"/> class.
 54        /// </summary>
 55        /// <param name="boundPort">The bound port.</param>
 56        /// <param name="host">The host.</param>
 57        /// <param name="port">The port.</param>
 58        /// <exception cref="ArgumentOutOfRangeException"><paramref name="boundPort" /> is greater than <see cref="IPEnd
 59        /// <exception cref="ArgumentNullException"><paramref name="host"/> is <see langword="null"/>.</exception>
 60        /// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint
 61        /// <example>
 62        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\ForwardedPortLocalTest.cs" region="Example SshClient 
 63        /// </example>
 64        public ForwardedPortLocal(uint boundPort, string host, uint port)
 365            : this(string.Empty, boundPort, host, port)
 366        {
 367        }
 68
 69        /// <summary>
 70        /// Initializes a new instance of the <see cref="ForwardedPortLocal"/> class.
 71        /// </summary>
 72        /// <param name="boundHost">The bound host.</param>
 73        /// <param name="host">The host.</param>
 74        /// <param name="port">The port.</param>
 75        /// <exception cref="ArgumentNullException"><paramref name="boundHost"/> is <see langword="null"/>.</exception>
 76        /// <exception cref="ArgumentNullException"><paramref name="host"/> is <see langword="null"/>.</exception>
 77        /// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint
 78        public ForwardedPortLocal(string boundHost, string host, uint port)
 2479            : this(boundHost, 0, host, port)
 2480        {
 2481        }
 82
 83        /// <summary>
 84        /// Initializes a new instance of the <see cref="ForwardedPortLocal"/> class.
 85        /// </summary>
 86        /// <param name="boundHost">The bound host.</param>
 87        /// <param name="boundPort">The bound port.</param>
 88        /// <param name="host">The host.</param>
 89        /// <param name="port">The port.</param>
 90        /// <exception cref="ArgumentNullException"><paramref name="boundHost"/> is <see langword="null"/>.</exception>
 91        /// <exception cref="ArgumentNullException"><paramref name="host"/> is <see langword="null"/>.</exception>
 92        /// <exception cref="ArgumentOutOfRangeException"><paramref name="boundPort" /> is greater than <see cref="IPEnd
 93        /// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint
 25494        public ForwardedPortLocal(string boundHost, uint boundPort, string host, uint port)
 25495        {
 25496            if (boundHost is null)
 397            {
 398                throw new ArgumentNullException(nameof(boundHost));
 99            }
 100
 251101            if (host is null)
 3102            {
 3103                throw new ArgumentNullException(nameof(host));
 104            }
 105
 248106            boundPort.ValidatePort("boundPort");
 248107            port.ValidatePort("port");
 108
 248109            BoundHost = boundHost;
 248110            BoundPort = boundPort;
 248111            Host = host;
 248112            Port = port;
 248113            _status = ForwardedPortStatus.Stopped;
 248114        }
 115
 116        /// <summary>
 117        /// Starts local port forwarding.
 118        /// </summary>
 119        protected override void StartPort()
 166120        {
 166121            if (!ForwardedPortStatus.ToStarting(ref _status))
 0122            {
 0123                return;
 124            }
 125
 126            try
 166127            {
 166128                InternalStart();
 166129            }
 0130            catch (Exception)
 0131            {
 0132                _status = ForwardedPortStatus.Stopped;
 0133                throw;
 134            }
 166135        }
 136
 137        /// <summary>
 138        /// Stops local port forwarding, and waits for the specified timeout until all pending
 139        /// requests are processed.
 140        /// </summary>
 141        /// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
 142        protected override void StopPort(TimeSpan timeout)
 268143        {
 268144            if (!ForwardedPortStatus.ToStopping(ref _status))
 102145            {
 102146                return;
 147            }
 148
 149            // signal existing channels that the port is closing
 166150            base.StopPort(timeout);
 151
 152            // prevent new requests from getting processed
 166153            StopListener();
 154
 155            // wait for open channels to close
 166156            InternalStop(timeout);
 157
 158            // mark port stopped
 166159            _status = ForwardedPortStatus.Stopped;
 268160        }
 161
 162        /// <summary>
 163        /// Ensures the current instance is not disposed.
 164        /// </summary>
 165        /// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
 166        protected override void CheckDisposed()
 220167        {
 168#if NET7_0_OR_GREATER
 148169            ObjectDisposedException.ThrowIf(_isDisposed, this);
 170#else
 72171            if (_isDisposed)
 3172            {
 3173                throw new ObjectDisposedException(GetType().FullName);
 174            }
 175#endif // NET7_0_OR_GREATER
 211176        }
 177
 178        /// <summary>
 179        /// Releases unmanaged and - optionally - managed resources.
 180        /// </summary>
 181        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor
 182        protected override void Dispose(bool disposing)
 365183        {
 365184            if (_isDisposed)
 111185            {
 111186                return;
 187            }
 188
 254189            base.Dispose(disposing);
 254190            InternalDispose(disposing);
 254191            _isDisposed = true;
 365192        }
 193
 194        /// <summary>
 195        /// Finalizes an instance of the <see cref="ForwardedPortLocal"/> class.
 196        /// </summary>
 197        ~ForwardedPortLocal()
 48198        {
 24199            Dispose(disposing: false);
 48200        }
 201
 202        private void InternalStart()
 166203        {
 166204            var addr = DnsAbstraction.GetHostAddresses(BoundHost)[0];
 166205            var ep = new IPEndPoint(addr, (int) BoundPort);
 206
 166207            _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
 166208            _listener.Bind(ep);
 166209            _listener.Listen(5);
 210
 211            // update bound port (in case original was passed as zero)
 166212            BoundPort = (uint) ((IPEndPoint) _listener.LocalEndPoint).Port;
 213
 166214            Session.ErrorOccured += Session_ErrorOccured;
 166215            Session.Disconnected += Session_Disconnected;
 216
 166217            InitializePendingChannelCountdown();
 218
 219            // consider port started when we're listening for inbound connections
 166220            _status = ForwardedPortStatus.Started;
 221
 166222            StartAccept(e: null);
 166223        }
 224
 225        private void StartAccept(SocketAsyncEventArgs e)
 232226        {
 232227            if (e is null)
 166228            {
 229#pragma warning disable CA2000 // Dispose objects before losing scope
 166230                e = new SocketAsyncEventArgs();
 231#pragma warning restore CA2000 // Dispose objects before losing scope
 166232                e.Completed += AcceptCompleted;
 166233            }
 234            else
 66235            {
 236                // clear the socket as we're reusing the context object
 66237                e.AcceptSocket = null;
 66238            }
 239
 240            // only accept new connections while we are started
 232241            if (IsStarted)
 232242            {
 243                try
 232244                {
 232245                    if (!_listener.AcceptAsync(e))
 0246                    {
 0247                        AcceptCompleted(sender: null, e);
 0248                    }
 232249                }
 0250                catch (ObjectDisposedException)
 0251                {
 0252                    if (_status == ForwardedPortStatus.Stopping || _status == ForwardedPortStatus.Stopped)
 0253                    {
 254                        // ignore ObjectDisposedException while stopping or stopped
 0255                        return;
 256                    }
 257
 0258                    throw;
 259                }
 232260            }
 232261        }
 262
 263        private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
 232264        {
 232265            if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
 166266            {
 267                // server was stopped
 166268                return;
 269            }
 270
 271            // capture client socket
 66272            var clientSocket = e.AcceptSocket;
 273
 66274            if (e.SocketError != SocketError.Success)
 0275            {
 276                // accept new connection
 0277                StartAccept(e);
 278
 279                // dispose broken client socket
 0280                CloseClientSocket(clientSocket);
 0281                return;
 282            }
 283
 284            // accept new connection
 66285            StartAccept(e);
 286
 287            // process connection
 66288            ProcessAccept(clientSocket);
 232289        }
 290
 291        private void ProcessAccept(Socket clientSocket)
 66292        {
 293            // close the client socket if we're no longer accepting new connections
 66294            if (!IsStarted)
 0295            {
 0296                CloseClientSocket(clientSocket);
 0297                return;
 298            }
 299
 300            // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
 301            // that same instance; the instance field for the countdown event is re-initialized when the port is restart
 302            // and at that time there may still be pending requests
 66303            var pendingChannelCountdown = _pendingChannelCountdown;
 304
 66305            pendingChannelCountdown.AddCount();
 306
 307            try
 66308            {
 66309                var originatorEndPoint = (IPEndPoint) clientSocket.RemoteEndPoint;
 310
 66311                RaiseRequestReceived(originatorEndPoint.Address.ToString(),
 66312                    (uint) originatorEndPoint.Port);
 313
 66314                using (var channel = Session.CreateChannelDirectTcpip())
 66315                {
 66316                    channel.Exception += Channel_Exception;
 66317                    channel.Open(Host, Port, this, clientSocket);
 66318                    channel.Bind();
 66319                }
 66320            }
 0321            catch (Exception exp)
 0322            {
 0323                RaiseExceptionEvent(exp);
 0324                CloseClientSocket(clientSocket);
 0325            }
 326            finally
 66327            {
 328                // take into account that CountdownEvent has since been disposed; when stopping the port we
 329                // wait for a given time for the channels to close, but once that timeout period has elapsed
 330                // the CountdownEvent will be disposed
 331                try
 66332                {
 66333                    _ = pendingChannelCountdown.Signal();
 66334                }
 0335                catch (ObjectDisposedException)
 0336                {
 337                    // Ignore any ObjectDisposedException
 0338                }
 66339            }
 66340        }
 341
 342        /// <summary>
 343        /// Initializes the <see cref="CountdownEvent"/>.
 344        /// </summary>
 345        /// <remarks>
 346        /// <para>
 347        /// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
 348        /// of <c>1</c>.
 349        /// </para>
 350        /// <para>
 351        /// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
 352        /// initial count of <c>1</c>.
 353        /// </para>
 354        /// </remarks>
 355        private void InitializePendingChannelCountdown()
 166356        {
 166357            var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
 166358            original?.Dispose();
 166359        }
 360
 361        private static void CloseClientSocket(Socket clientSocket)
 0362        {
 0363            if (clientSocket.Connected)
 0364            {
 365                try
 0366                {
 0367                    clientSocket.Shutdown(SocketShutdown.Send);
 0368                }
 0369                catch (Exception)
 0370                {
 371                    // ignore exception when client socket was already closed
 0372                }
 0373            }
 374
 0375            clientSocket.Dispose();
 0376        }
 377
 378        /// <summary>
 379        /// Interrupts the listener, and unsubscribes from <see cref="Session"/> events.
 380        /// </summary>
 381        private void StopListener()
 166382        {
 383            // close listener socket
 166384            _listener?.Dispose();
 385
 386            // unsubscribe from session events
 166387            var session = Session;
 166388            if (session != null)
 166389            {
 166390                session.ErrorOccured -= Session_ErrorOccured;
 166391                session.Disconnected -= Session_Disconnected;
 166392            }
 166393        }
 394
 395        /// <summary>
 396        /// Waits for pending channels to close.
 397        /// </summary>
 398        /// <param name="timeout">The maximum time to wait for the pending channels to close.</param>
 399        private void InternalStop(TimeSpan timeout)
 166400        {
 166401            _ = _pendingChannelCountdown.Signal();
 402
 166403            if (!_pendingChannelCountdown.Wait(timeout))
 0404            {
 405                // TODO: log as warning
 0406                DiagnosticAbstraction.Log("Timeout waiting for pending channels in local forwarded port to close.");
 0407            }
 166408        }
 409
 410        private void InternalDispose(bool disposing)
 254411        {
 254412            if (disposing)
 230413            {
 230414                var listener = _listener;
 230415                if (listener is not null)
 151416                {
 151417                    _listener = null;
 151418                    listener.Dispose();
 151419                }
 420
 230421                var pendingRequestsCountdown = _pendingChannelCountdown;
 230422                if (pendingRequestsCountdown is not null)
 151423                {
 151424                    _pendingChannelCountdown = null;
 151425                    pendingRequestsCountdown.Dispose();
 151426                }
 230427            }
 254428        }
 429
 430        private void Session_Disconnected(object sender, EventArgs e)
 0431        {
 0432            var session = Session;
 0433            if (session is not null)
 0434            {
 0435                StopPort(session.ConnectionInfo.Timeout);
 0436            }
 0437        }
 438
 439        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
 0440        {
 0441            var session = Session;
 0442            if (session is not null)
 0443            {
 0444                StopPort(session.ConnectionInfo.Timeout);
 0445            }
 0446        }
 447
 448        private void Channel_Exception(object sender, ExceptionEventArgs e)
 0449        {
 0450            RaiseExceptionEvent(e.Exception);
 0451        }
 452    }
 453}