< Summary

Information
Class: Renci.SshNet.ForwardedPortRemote
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\ForwardedPortRemote.cs
Line coverage
77%
Covered lines: 148
Uncovered lines: 44
Coverable lines: 192
Total lines: 392
Line coverage: 77%
Branch coverage
76%
Covered branches: 29
Total branches: 38
Branch coverage: 76.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
.ctor(...)50%476.47%
get_IsStarted()100%1100%
get_BoundHostAddress()100%1100%
get_BoundHost()100%1100%
get_BoundPort()100%1100%
get_HostAddress()100%1100%
get_Host()100%10%
get_Port()100%1100%
.ctor(...)100%1100%
.ctor(...)100%1100%
StartPort()50%459.25%
StopPort(...)75%482.35%
CheckDisposed()100%1100%
Session_ChannelOpening(...)100%886.79%
InitializePendingChannelCountdown()100%2100%
Channel_Exception(...)100%10%
Session_RequestFailure(...)100%10%
Session_RequestSuccess(...)25%462.5%
Dispose(...)90%1080%
Finalize()100%1100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.Net;
 4using System.Threading;
 5
 6using Renci.SshNet.Abstractions;
 7using Renci.SshNet.Common;
 8using Renci.SshNet.Messages.Connection;
 9
 10namespace Renci.SshNet
 11{
 12    /// <summary>
 13    /// Provides functionality for remote port forwarding.
 14    /// </summary>
 15    public class ForwardedPortRemote : ForwardedPort
 16    {
 17        private ForwardedPortStatus _status;
 18        private bool _requestStatus;
 20619        private EventWaitHandle _globalRequestResponse = new AutoResetEvent(initialState: false);
 20        private CountdownEvent _pendingChannelCountdown;
 21        private bool _isDisposed;
 22
 23        /// <summary>
 24        /// Gets a value indicating whether port forwarding is started.
 25        /// </summary>
 26        /// <value>
 27        /// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
 28        /// </value>
 29        public override bool IsStarted
 30        {
 128431            get { return _status == ForwardedPortStatus.Started; }
 32        }
 33
 34        /// <summary>
 35        /// Gets the bound host.
 36        /// </summary>
 100437        public IPAddress BoundHostAddress { get; private set; }
 38
 39        /// <summary>
 40        /// Gets the bound host.
 41        /// </summary>
 42        public string BoundHost
 43        {
 44            get
 79845            {
 79846                return BoundHostAddress.ToString();
 79847            }
 48        }
 49
 50        /// <summary>
 51        /// Gets the bound port.
 52        /// </summary>
 122553        public uint BoundPort { get; private set; }
 54
 55        /// <summary>
 56        /// Gets the forwarded host.
 57        /// </summary>
 26858        public IPAddress HostAddress { get; private set; }
 59
 60        /// <summary>
 61        /// Gets the forwarded host.
 62        /// </summary>
 63        public string Host
 64        {
 65            get
 066            {
 067                return HostAddress.ToString();
 068            }
 69        }
 70
 71        /// <summary>
 72        /// Gets the forwarded port.
 73        /// </summary>
 26874        public uint Port { get; private set; }
 75
 76        /// <summary>
 77        /// Initializes a new instance of the <see cref="ForwardedPortRemote" /> class.
 78        /// </summary>
 79        /// <param name="boundHostAddress">The bound host address.</param>
 80        /// <param name="boundPort">The bound port.</param>
 81        /// <param name="hostAddress">The host address.</param>
 82        /// <param name="port">The port.</param>
 83        /// <exception cref="ArgumentNullException"><paramref name="boundHostAddress"/> is <see langword="null"/>.</exce
 84        /// <exception cref="ArgumentNullException"><paramref name="hostAddress"/> is <see langword="null"/>.</exception
 85        /// <exception cref="ArgumentOutOfRangeException"><paramref name="boundPort" /> is greater than <see cref="IPEnd
 86        /// <exception cref="ArgumentOutOfRangeException"><paramref name="port" /> is greater than <see cref="IPEndPoint
 20687        public ForwardedPortRemote(IPAddress boundHostAddress, uint boundPort, IPAddress hostAddress, uint port)
 20688        {
 20689            if (boundHostAddress is null)
 090            {
 091                throw new ArgumentNullException(nameof(boundHostAddress));
 92            }
 93
 20694            if (hostAddress is null)
 095            {
 096                throw new ArgumentNullException(nameof(hostAddress));
 97            }
 98
 20699            boundPort.ValidatePort("boundPort");
 206100            port.ValidatePort("port");
 101
 206102            BoundHostAddress = boundHostAddress;
 206103            BoundPort = boundPort;
 206104            HostAddress = hostAddress;
 206105            Port = port;
 206106            _status = ForwardedPortStatus.Stopped;
 206107        }
 108
 109        /// <summary>
 110        /// Initializes a new instance of the <see cref="ForwardedPortRemote"/> class.
 111        /// </summary>
 112        /// <param name="boundPort">The bound port.</param>
 113        /// <param name="host">The host.</param>
 114        /// <param name="port">The port.</param>
 115        /// <example>
 116        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\ForwardedPortRemoteTest.cs" region="Example SshClient
 117        /// </example>
 118        public ForwardedPortRemote(uint boundPort, string host, uint port)
 3119            : this(string.Empty, boundPort, host, port)
 3120        {
 3121        }
 122
 123        /// <summary>
 124        /// Initializes a new instance of the <see cref="ForwardedPortRemote"/> class.
 125        /// </summary>
 126        /// <param name="boundHost">The bound host.</param>
 127        /// <param name="boundPort">The bound port.</param>
 128        /// <param name="host">The host.</param>
 129        /// <param name="port">The port.</param>
 130        public ForwardedPortRemote(string boundHost, uint boundPort, string host, uint port)
 3131            : this(DnsAbstraction.GetHostAddresses(boundHost)[0],
 3132                   boundPort,
 3133                   DnsAbstraction.GetHostAddresses(host)[0],
 3134                   port)
 3135        {
 3136        }
 137
 138        /// <summary>
 139        /// Starts remote port forwarding.
 140        /// </summary>
 141        protected override void StartPort()
 137142        {
 137143            if (!ForwardedPortStatus.ToStarting(ref _status))
 0144            {
 0145                return;
 146            }
 147
 137148            InitializePendingChannelCountdown();
 149
 150            try
 137151            {
 137152                Session.RegisterMessage("SSH_MSG_REQUEST_FAILURE");
 137153                Session.RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
 137154                Session.RegisterMessage("SSH_MSG_CHANNEL_OPEN");
 155
 137156                Session.RequestSuccessReceived += Session_RequestSuccess;
 137157                Session.RequestFailureReceived += Session_RequestFailure;
 137158                Session.ChannelOpenReceived += Session_ChannelOpening;
 159
 160                // send global request to start forwarding
 137161                Session.SendMessage(new TcpIpForwardGlobalRequestMessage(BoundHost, BoundPort));
 162
 163                // wat for response on global request to start direct tcpip
 137164                Session.WaitOnHandle(_globalRequestResponse);
 165
 137166                if (!_requestStatus)
 0167                {
 0168                    throw new SshException(string.Format(CultureInfo.CurrentCulture, "Port forwarding for '{0}' port '{1
 169                }
 137170            }
 0171            catch (Exception)
 0172            {
 173                // mark port stopped
 0174                _status = ForwardedPortStatus.Stopped;
 175
 176                // when the request to start port forward was rejected or failed, then we're no longer
 177                // interested in these events
 0178                Session.RequestSuccessReceived -= Session_RequestSuccess;
 0179                Session.RequestFailureReceived -= Session_RequestFailure;
 0180                Session.ChannelOpenReceived -= Session_ChannelOpening;
 181
 0182                throw;
 183            }
 184
 137185            _status = ForwardedPortStatus.Started;
 137186        }
 187
 188        /// <summary>
 189        /// Stops remote port forwarding.
 190        /// </summary>
 191        /// <param name="timeout">The maximum amount of time to wait for the port to stop.</param>
 192        protected override void StopPort(TimeSpan timeout)
 224193        {
 224194            if (!ForwardedPortStatus.ToStopping(ref _status))
 87195            {
 87196                return;
 197            }
 198
 137199            base.StopPort(timeout);
 200
 201            // send global request to cancel direct tcpip
 137202            Session.SendMessage(new CancelTcpIpForwardGlobalRequestMessage(BoundHost, BoundPort));
 203
 204            // wait for response on global request to cancel direct tcpip or completion of message
 205            // listener loop (in which case response on global request can never be received)
 137206            _ = WaitHandle.WaitAny(new[] { _globalRequestResponse, Session.MessageListenerCompleted }, timeout);
 207
 208            // unsubscribe from session events as either the tcpip forward is cancelled at the
 209            // server, or our session message loop has completed
 137210            Session.RequestSuccessReceived -= Session_RequestSuccess;
 137211            Session.RequestFailureReceived -= Session_RequestFailure;
 137212            Session.ChannelOpenReceived -= Session_ChannelOpening;
 213
 214            // wait for pending channels to close
 137215            _ = _pendingChannelCountdown.Signal();
 216
 137217            if (!_pendingChannelCountdown.Wait(timeout))
 0218            {
 219                // TODO: log as warning
 0220                DiagnosticAbstraction.Log("Timeout waiting for pending channels in remote forwarded port to close.");
 0221            }
 222
 137223            _status = ForwardedPortStatus.Stopped;
 224224        }
 225
 226        /// <summary>
 227        /// Ensures the current instance is not disposed.
 228        /// </summary>
 229        /// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
 230        protected override void CheckDisposed()
 191231        {
 232#if NET7_0_OR_GREATER
 128233            ObjectDisposedException.ThrowIf(_isDisposed, this);
 234#else
 63235            if (_isDisposed)
 3236            {
 3237                throw new ObjectDisposedException(GetType().FullName);
 238            }
 239#endif // NET7_0_OR_GREATER
 182240        }
 241
 242        private void Session_ChannelOpening(object sender, MessageEventArgs<ChannelOpenMessage> e)
 121243        {
 121244            var channelOpenMessage = e.Message;
 121245            if (channelOpenMessage.Info is ForwardedTcpipChannelInfo info)
 118246            {
 247                // Ensure this is the corresponding request
 118248                if (info.ConnectedAddress == BoundHost && info.ConnectedPort == BoundPort)
 110249                {
 110250                    if (!IsStarted)
 48251                    {
 48252                        Session.SendMessage(new ChannelOpenFailureMessage(channelOpenMessage.LocalChannelNumber,
 48253                                                                          string.Empty,
 48254                                                                          ChannelOpenFailureMessage.AdministrativelyProh
 48255                        return;
 256                    }
 257
 62258                    ThreadAbstraction.ExecuteThread(() =>
 62259                        {
 62260                            // capture the countdown event that we're adding a count to, as we need to make sure that we
 62261                            // that same instance; the instance field for the countdown event is re-initialize when the 
 62262                            // and that time there may still be pending requests
 62263                            var pendingChannelCountdown = _pendingChannelCountdown;
 62264
 62265                            pendingChannelCountdown.AddCount();
 62266
 62267                            try
 62268                            {
 62269                                RaiseRequestReceived(info.OriginatorAddress, info.OriginatorPort);
 62270
 62271                                using (var channel = Session.CreateChannelForwardedTcpip(channelOpenMessage.LocalChannel
 62272                                {
 62273                                    channel.Exception += Channel_Exception;
 62274                                    channel.Bind(new IPEndPoint(HostAddress, (int) Port), this);
 62275                                }
 62276                            }
 0277                            catch (Exception exp)
 0278                            {
 0279                                RaiseExceptionEvent(exp);
 0280                            }
 62281                            finally
 62282                            {
 62283                                // take into account that CountdownEvent has since been disposed; when stopping the port
 62284                                // wait for a given time for the channels to close, but once that timeout period has ela
 62285                                // the CountdownEvent will be disposed
 62286                                try
 62287                                {
 62288                                    _ = pendingChannelCountdown.Signal();
 62289                                }
 0290                                catch (ObjectDisposedException)
 0291                                {
 62292                                    // Ignore any ObjectDisposedException
 0293                                }
 62294                            }
 124295                        });
 62296                }
 70297            }
 121298        }
 299
 300        /// <summary>
 301        /// Initializes the <see cref="CountdownEvent"/>.
 302        /// </summary>
 303        /// <remarks>
 304        /// <para>
 305        /// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
 306        /// of <c>1</c>.
 307        /// </para>
 308        /// <para>
 309        /// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
 310        /// initial count of <c>1</c>.
 311        /// </para>
 312        /// </remarks>
 313        private void InitializePendingChannelCountdown()
 137314        {
 137315            var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
 137316            original?.Dispose();
 137317        }
 318
 319        private void Channel_Exception(object sender, ExceptionEventArgs exceptionEventArgs)
 0320        {
 0321            RaiseExceptionEvent(exceptionEventArgs.Exception);
 0322        }
 323
 324        private void Session_RequestFailure(object sender, EventArgs e)
 0325        {
 0326            _requestStatus = false;
 0327            _ = _globalRequestResponse.Set();
 0328        }
 329
 330        private void Session_RequestSuccess(object sender, MessageEventArgs<RequestSuccessMessage> e)
 223331        {
 223332            _requestStatus = true;
 333
 223334            if (BoundPort == 0)
 0335            {
 0336                BoundPort = (e.Message.BoundPort is null) ? 0 : e.Message.BoundPort.Value;
 0337            }
 338
 223339            _ = _globalRequestResponse.Set();
 223340        }
 341
 342        /// <summary>
 343        /// Releases unmanaged and - optionally - managed resources.
 344        /// </summary>
 345        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor
 346        protected override void Dispose(bool disposing)
 290347        {
 290348            if (_isDisposed)
 84349            {
 84350                return;
 351            }
 352
 206353            base.Dispose(disposing);
 354
 206355            if (disposing)
 201356            {
 201357                var session = Session;
 201358                if (session != null)
 0359                {
 0360                    Session = null;
 0361                    session.RequestSuccessReceived -= Session_RequestSuccess;
 0362                    session.RequestFailureReceived -= Session_RequestFailure;
 0363                    session.ChannelOpenReceived -= Session_ChannelOpening;
 0364                }
 365
 201366                var globalRequestResponse = _globalRequestResponse;
 201367                if (globalRequestResponse != null)
 201368                {
 201369                    _globalRequestResponse = null;
 201370                    globalRequestResponse.Dispose();
 201371                }
 372
 201373                var pendingRequestsCountdown = _pendingChannelCountdown;
 201374                if (pendingRequestsCountdown != null)
 123375                {
 123376                    _pendingChannelCountdown = null;
 123377                    pendingRequestsCountdown.Dispose();
 123378                }
 201379            }
 380
 206381            _isDisposed = true;
 290382        }
 383
 384        /// <summary>
 385        /// Finalizes an instance of the <see cref="ForwardedPortRemote"/> class.
 386        /// </summary>
 387        ~ForwardedPortRemote()
 10388        {
 5389            Dispose(disposing: false);
 10390        }
 391    }
 392}