private async Task CopyStreamAsync(bool fromClient, TcpClient tcpIn, TcpClient tcpOut, Stream strIn, Stream strOut, AsyncTimeoutHelper receiveTimeoutHelper, AsyncTimeoutHelper sendTimeoutHelper, AsyncTimeoutHelper oppositeReceiveTimeoutHelper) { try { byte[] buf = new byte[64 * 1024]; // 64 KiB Buffer for optimal efficiency Task waitTask; Func <Task> getWaitTask = () => { Task awaitableTask; if (fromClient) { awaitableTask = waitForSending(sendWaitSemaphore); } else { awaitableTask = waitForReceiving(receiveWaitSemaphore); } return(awaitableTask); }; int read = 0; while (true) { /* OLD: Only handle exceptions that occur in Read() but not the ones that * occur in Write(). * This is because when the other conenction is aborted while it is paused, * Write()s will fail although the other conenction did not yet report * the abortion. * NEW: Also Exceptions that occur in Write() must be handled - otherwise it could happen that * the client half-closes the connection while data is still transmitted from the server to the client, * and then the client aborts (resets) the connection. In this case if Write() would not handle the exection, * we would never raise the ConnectionAborted event. */ Exception readException = null; try { await receiveTimeoutHelper.DoTimeoutableOperationAsync(async() => read = await strIn.ReadAsync(buf, 0, buf.Length)); if (!(read > 0)) { break; } } catch (Exception ex) { if (ExceptionUtils.ShouldExceptionBeRethrown(ex)) { throw; } // The await operator cannot be used in a catch clause. // Also, don't run an async delegate here because this method // might return (and therefore release the Semaphores etc.) before // the async delegate completes. readException = ex; } if (readException != null) { // See if we need to pause the transmission. // This also needs to be done here to ensure that when the code is waiting in ReadAsync() // and then the connection is aborted that we wait correctly before shutting down the // other side. waitTask = getWaitTask(); if (waitTask != null) { await waitTask; } // Abort the connection. AbortConnection(fromClient, readException); return; } // See if we need to pause the transmission. // This is done after reading the next block because it would be // more difficult to reabk the reading operation (when currently // no data arrives). waitTask = getWaitTask(); if (waitTask != null) { await waitTask; } // Check if we need to throttle transfer speed. // TODO: need a better handling for this if (fromClient && forwarder.ThrottleSpeedClient || !fromClient && forwarder.ThrottleSpeedServer) { int speed = fromClient ? forwarder.SpeedClient : forwarder.SpeedServer; SemaphoreSlim sem = fromClient ? sendThrottleSemaphore : receiveThrottleSemaphore; long pauseTime = (long)((double)read * 1000d / (double)speed); long startSwTime = forwarder.StopwatchElapsedMilliseconds; int waitTime; while (true) { waitTime = (int)Math.Min(200, pauseTime - (forwarder.StopwatchElapsedMilliseconds - startSwTime)); if (waitTime <= 0) { break; } await sem.WaitAsync(waitTime); } } // Raise events if (fromClient) { OnDataReceivedLocal(buf, 0, read); } else { OnDataReceivedRemote(buf, 0, read); } // Write the data. try { // Write await sendTimeoutHelper.DoTimeoutableOperationAsync(() => strOut.WriteAsync(buf, 0, read)); } catch (Exception ex) { if (ExceptionUtils.ShouldExceptionBeRethrown(ex)) { throw; } /* The other connection has been aborted or a write time occured. * We must handle the exception. See comment in the Read() method. */ AbortConnection(!fromClient, ex); return; } // We could write data to the other connection, so reset its read timeout as it is still alive. oppositeReceiveTimeoutHelper.ResetTimeout(); // Raise DataForwarded event to indicate the data have actually // been written. if (fromClient) { OnDataForwardedLocal(); } else { OnDataForwardedRemote(); } } // See if we need to pause the transmission. // This also needs to be done here to ensure that when the code is waiting in ReadAsync() // and then the connection is closed that we wait correctly before shutting down the // other side. waitTask = getWaitTask(); if (waitTask != null) { await waitTask; } if (fromClient) { OnLocalConnectionClosed(); } else { OnRemoteConnectionClosed(); } // The connection has been half-closed by tcpIn. Therefore we // need to initiate the shutdown on tcpOut. try { tcpOut.Client.Shutdown(SocketShutdown.Send); } catch (SocketException ex) { System.Diagnostics.Debug.WriteLine(ex.ToString()); // Ignore. This probably means the connection has already // been reset and the other direction will get the error // when reading. } int myShutdownCount = Interlocked.Add(ref shutdownCount, 1); // After the shutdown was initiated for both sides, // deregister this ForwardingConnection from the // TcpConnectionForwarder. if (myShutdownCount == 2) { //forwarder.DeregisterConnection(this); OnConnectionClosedCompletely(); } } finally { // Free resources. if (fromClient) { sendWaitSemaphore.Dispose(); sendThrottleSemaphore.Dispose(); } else { receiveWaitSemaphore.Dispose(); receiveThrottleSemaphore.Dispose(); } } }
private async Task ConnectAsync() { strClient = client.GetStream(); // If the server uses SSL, we try to authenticate as a Server before we open a forwarding connection. if (forwarder.LocalSslCertificate != null) { SslStream sslStream = new SslStream(strClient, true); try { await sslStream.AuthenticateAsServerAsync(forwarder.LocalSslCertificate, false, forwarder.LocalSslProtocols, false); } catch (Exception ex) { // TODO: Use more specific catch clause if (ExceptionUtils.ShouldExceptionBeRethrown(ex)) { throw; } OnConnectionAborted(true, ex); // We could not authenticate the connection. Therefore we need to close the client socket. Cancel(); return; } // Authentication OK. Use the SslStream to send and receive data. strClient = sslStream; OnLocalConnectionAuthenticated(sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.CipherStrength, sslStream.HashAlgorithm, sslStream.HashStrength, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength); } // Try to establish a connection to the server. // First we try to connect using a IPv6 socket; if it fails, we use a IPv4 socket. Exception e = null; bool usedIpv6 = false; for (int i = 0; i < 2; i++) { AddressFamily adrFamily = i == 0 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork; if (addressForBind != null && addressForBind.AddressFamily != adrFamily || addressForConnect != null && addressForConnect.AddressFamily != adrFamily) { continue; } TcpClient c = new TcpClient(adrFamily); try { if (addressForBind != null) { // Use the given addresses to bind and to connect, instead of the remotehost string // (this is used when connecting to localhost addresses, as the forwarder will generate a random address // in the range 127.0.0.1-127.255.255.254 to minimize the risk of re-using a combination of a specific local endpoint to connect // to a specific remote endpoint in a short interval of time, which is not permitted by TCP/IP to prohibit data corruption). c.Client.Bind(new IPEndPoint(addressForBind, 0)); } if (addressForConnect != null) { await c.ConnectAsync(addressForConnect, forwarder.RemotePort); } else { await c.ConnectAsync(forwarder.RemoteHost, forwarder.RemotePort); } } catch (Exception ex) { if (ExceptionUtils.ShouldExceptionBeRethrown(ex)) { throw; } if (e == null) { e = ex; } else { e = ex; // TODO! } c.Close(); continue; } // OK, Connection established usedIpv6 = adrFamily == AddressFamily.InterNetworkV6; server = c; e = null; break; } if (e != null) { // TODO maybe also pause here if pause is enabled for the server. OnConnectionAborted(false, e); // We could not establish the connection. Therefore we need to close the client socket. Cancel(); return; } OnRemoteConnectionEstablished(usedIpv6, (IPEndPoint)server.Client.LocalEndPoint, (IPEndPoint)server.Client.RemoteEndPoint); strServer = server.GetStream(); // If we use client SSL, then create an SSL Stream and authenticate // asynchronously. if (forwarder.RemoteSslHost != null) { SslStream stream = new SslStream(strServer, true); try { await stream.AuthenticateAsClientAsync(forwarder.RemoteSslHost, new System.Security.Cryptography.X509Certificates.X509CertificateCollection(), forwarder.RemoteSslProtocols, false); } catch (Exception ex) { if (ExceptionUtils.ShouldExceptionBeRethrown(ex)) { throw; } // TOODO maybe also pause here if pause is enabled for the server. OnConnectionAborted(false, ex); // We could not authenticate the connection. Therefore we need to close the client and server sockets. Cancel(); return; } // Authentication was OK - now use the SSLStream to transfer data. strServer = stream; System.Security.Cryptography.X509Certificates.X509Certificate2 remoteCert = new System.Security.Cryptography.X509Certificates.X509Certificate2(stream.RemoteCertificate); OnRemoteConnectionAuthenticated(stream.SslProtocol, remoteCert, stream.CipherAlgorithm, stream.CipherStrength, stream.HashAlgorithm, stream.HashStrength, stream.KeyExchangeAlgorithm, stream.KeyExchangeStrength); } // now start to read from the client and pass the data to the server. // Also, start to read from the server and pass the data to the client. // Note: The read timeout should only be used to determine if the remote endpoint is not available any more. // This means, if we can't read data for a long while we still can write data, a timeout should not occur. // Therefore we reset the timeout if we still can write data. string exMsg = "The socket {0} operation exceeded the timeout of {{0}} ms."; AsyncTimeoutHelper clientSendTimeoutHelper = new AsyncTimeoutHelper(sendTimeout, ex => AbortConnection(true, ex), string.Format(exMsg, "write")); AsyncTimeoutHelper clientReceiveTimeoutHelper = new AsyncTimeoutHelper(receiveTimeout, ex => AbortConnection(true, ex), string.Format(exMsg, "read")); AsyncTimeoutHelper serverSendTimeoutHelper = new AsyncTimeoutHelper(sendTimeout, ex => AbortConnection(false, ex), string.Format(exMsg, "write")); AsyncTimeoutHelper serverReceiveTimeoutHelper = new AsyncTimeoutHelper(receiveTimeout, ex => AbortConnection(false, ex), string.Format(exMsg, "read")); // Use ExceptionUtils.WrapTaskForHandlingUnhandledExceptions because we start two tasks and only await t2 after t1 is finished. Task t1 = ExceptionUtils.WrapTaskForHandlingUnhandledExceptions(async() => await CopyStreamAsync(true, client, server, strClient, strServer, clientReceiveTimeoutHelper, serverSendTimeoutHelper, serverReceiveTimeoutHelper)); Task t2 = ExceptionUtils.WrapTaskForHandlingUnhandledExceptions(async() => await CopyStreamAsync(false, server, client, strServer, strClient, serverReceiveTimeoutHelper, clientSendTimeoutHelper, clientReceiveTimeoutHelper)); await t1; await t2; // Dispose the timeout helpers. await clientSendTimeoutHelper.DisposeAsync(); await clientReceiveTimeoutHelper.DisposeAsync(); await serverSendTimeoutHelper.DisposeAsync(); await serverReceiveTimeoutHelper.DisposeAsync(); // Copying the streams has finished. // In this state we should be able to Dispose the Streams and Sockets, as they initiated // shutdown so all data should be sent. // We don't need to lock on lockObjForClosing since every task that might close the sockets // (CopyStreamAsync() method, AsyncTimeoutHelper) has already finished in this state. strClient.Close(); strServer.Close(); client.Close(); server.Close(); }