/// <summary> /// This is called when client is aware of proxy /// So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy /// </summary> /// <param name="endPoint">The explicit endpoint.</param> /// <param name="clientConnection">The client connection.</param> /// <returns>The task.</returns> private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnection clientConnection) { var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; var clientStream = new CustomBufferedStream(clientConnection.GetStream(), BufferPool, BufferSize); var clientStreamWriter = new HttpResponseWriter(clientStream, BufferPool, BufferSize); Task <TcpServerConnection> prefetchConnectionTask = null; bool closeServerConnection = false; bool calledRequestHandler = false; SslStream sslStream = null; try { string connectHostname = null; TunnelConnectSessionEventArgs connectArgs = null; // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) if (await HttpHelper.IsConnectMethod(clientStream, BufferPool, BufferSize, cancellationToken) == 1) { // read the first line HTTP command string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(httpCmd)) { return; } Request.ParseRequestLine(httpCmd, out string _, out string httpUrl, out var version); var httpRemoteUri = new Uri("http://" + httpUrl); connectHostname = httpRemoteUri.Host; var connectRequest = new ConnectRequest { RequestUri = httpRemoteUri, OriginalUrl = httpUrl, HttpVersion = version }; await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken); connectArgs = new TunnelConnectSessionEventArgs(this, endPoint, connectRequest, cancellationTokenSource); connectArgs.ProxyClient.Connection = clientConnection; connectArgs.ProxyClient.ClientStream = clientStream; await endPoint.InvokeBeforeTunnelConnectRequest(this, connectArgs, ExceptionFunc); // filter out excluded host names bool decryptSsl = endPoint.DecryptSsl && connectArgs.DecryptSsl; if (connectArgs.DenyConnect) { if (connectArgs.HttpClient.Response.StatusCode == 0) { connectArgs.HttpClient.Response = new Response { HttpVersion = HttpHeader.Version11, StatusCode = (int)HttpStatusCode.Forbidden, StatusDescription = "Forbidden" }; } // send the response await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken : cancellationToken); return; } if (await checkAuthorization(connectArgs) == false) { await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc); // send the response await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken : cancellationToken); return; } // write back successful CONNECT response var response = ConnectResponse.CreateSuccessfulConnectResponse(version); // Set ContentLength explicitly to properly handle HTTP 1.0 response.ContentLength = 0; response.Headers.FixProxyHeaders(); connectArgs.HttpClient.Response = response; await clientStreamWriter.WriteResponseAsync(response, cancellationToken : cancellationToken); var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken); bool isClientHello = clientHelloInfo != null; if (isClientHello) { connectRequest.TunnelType = TunnelType.Https; connectRequest.ClientHelloInfo = clientHelloInfo; } await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc, isClientHello); if (decryptSsl && isClientHello) { connectRequest.RequestUri = new Uri("https://" + httpUrl); bool http2Supported = false; var alpn = clientHelloInfo.GetAlpn(); if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2)) { // test server HTTP/2 support try { // todo: this is a hack, because Titanium does not support HTTP protocol changing currently var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, isConnect : true, applicationProtocols : SslExtensions.Http2ProtocolAsList, noCache : true, cancellationToken : cancellationToken); http2Supported = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2; //release connection back to pool instead of closing when connection pool is enabled. await tcpConnectionFactory.Release(connection, true); } catch (Exception) { // ignore } } if (EnableTcpServerConnectionPrefetch) { IPAddress[] ipAddresses = null; try { //make sure the host can be resolved before creating the prefetch task ipAddresses = await Dns.GetHostAddressesAsync(connectArgs.HttpClient.Request.RequestUri.Host); } catch (SocketException) { } if (ipAddresses != null && ipAddresses.Length > 0) { //don't pass cancellation token here //it could cause floating server connections when client exits prefetchConnectionTask = tcpConnectionFactory.GetServerConnection(this, connectArgs, isConnect: true, applicationProtocols: null, noCache: false, cancellationToken: CancellationToken.None); } } X509Certificate2 certificate = null; try { sslStream = new SslStream(clientStream, false); string certName = HttpHelper.GetWildCardDomainName(connectHostname); certificate = endPoint.GenericCertificate ?? await CertificateManager.CreateServerCertificate(certName); // Successfully managed to authenticate the client using the fake certificate var options = new SslServerAuthenticationOptions(); if (EnableHttp2 && http2Supported) { options.ApplicationProtocols = clientHelloInfo.GetAlpn(); if (options.ApplicationProtocols == null || options.ApplicationProtocols.Count == 0) { options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList; } } options.ServerCertificate = certificate; options.ClientCertificateRequired = false; options.EnabledSslProtocols = SupportedSslProtocols; options.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; await sslStream.AuthenticateAsServerAsync(options, cancellationToken); #if NETCOREAPP2_1 clientConnection.NegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol; #endif // HTTPS server created - we can now decrypt the client's traffic clientStream = new CustomBufferedStream(sslStream, BufferPool, BufferSize); clientStreamWriter = new HttpResponseWriter(clientStream, BufferPool, BufferSize); } catch (Exception e) { var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false); throw new ProxyConnectException( $"Couldn't authenticate host '{connectHostname}' with certificate '{certName}'.", e, connectArgs); } if (await HttpHelper.IsConnectMethod(clientStream, BufferPool, BufferSize, cancellationToken) == -1) { decryptSsl = false; } if (!decryptSsl) { await tcpConnectionFactory.Release(prefetchConnectionTask, true); prefetchConnectionTask = null; } } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } // Hostname is excluded or it is not an HTTPS connect if (!decryptSsl || !isClientHello) { if (!isClientHello) { connectRequest.TunnelType = TunnelType.Websocket; } // create new connection to server. // If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then // this connection should not be HTTPS. var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, isConnect : true, applicationProtocols : SslExtensions.Http2ProtocolAsList, noCache : true, cancellationToken : cancellationToken); try { if (isClientHello) { int available = clientStream.Available; if (available > 0) { // send the buffered data var data = BufferPool.GetBuffer(BufferSize); try { await clientStream.ReadAsync(data, 0, available, cancellationToken); // clientStream.Available should be at most BufferSize because it is using the same buffer size await connection.StreamWriter.WriteAsync(data, 0, available, true, cancellationToken); } finally { BufferPool.ReturnBuffer(data); } } var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream, BufferPool, cancellationToken); ((ConnectResponse)connectArgs.HttpClient.Response).ServerHelloInfo = serverHelloInfo; } await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }, connectArgs.CancellationTokenSource, ExceptionFunc); } finally { await tcpConnectionFactory.Release(connection, true); } return; } } if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream, BufferPool, BufferSize, cancellationToken) == 1) { // todo string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (httpCmd == "PRI * HTTP/2.0") { connectArgs.HttpClient.ConnectRequest.TunnelType = TunnelType.Http2; // HTTP/2 Connection Preface string line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) { throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } line = await clientStream.ReadLineAsync(cancellationToken); if (line != "SM") { throw new Exception($"HTTP/2 Protocol violation. 'SM' expected, '{line}' received"); } line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) { throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, isConnect : true, applicationProtocols : SslExtensions.Http2ProtocolAsList, noCache : true, cancellationToken : cancellationToken); try { await connection.StreamWriter.WriteLineAsync("PRI * HTTP/2.0", cancellationToken); await connection.StreamWriter.WriteLineAsync(cancellationToken); await connection.StreamWriter.WriteLineAsync("SM", cancellationToken); await connection.StreamWriter.WriteLineAsync(cancellationToken); #if NETCOREAPP2_1 await Http2Helper.SendHttp2(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }, () => new SessionEventArgs(this, endPoint, cancellationTokenSource) { ProxyClient = { Connection = clientConnection }, HttpClient = { ConnectRequest = connectArgs?.HttpClient.ConnectRequest }, UserData = connectArgs?.UserData }, async args => { await invokeBeforeRequest(args); }, async args => { await invokeBeforeResponse(args); }, connectArgs.CancellationTokenSource, clientConnection.Id, ExceptionFunc); #endif } finally { await tcpConnectionFactory.Release(connection, true); } } } calledRequestHandler = true; // Now create the request await handleHttpSessionRequest(endPoint, clientConnection, clientStream, clientStreamWriter, cancellationTokenSource, connectHostname, connectArgs, prefetchConnectionTask); } catch (ProxyException e) { closeServerConnection = true; onException(clientStream, e); } catch (IOException e) { closeServerConnection = true; onException(clientStream, new Exception("Connection was aborted", e)); } catch (SocketException e) { closeServerConnection = true; onException(clientStream, new Exception("Could not connect", e)); } catch (Exception e) { closeServerConnection = true; onException(clientStream, new Exception("Error occured in whilst handling the client", e)); } finally { if (!calledRequestHandler) { await tcpConnectionFactory.Release(prefetchConnectionTask, closeServerConnection); } sslStream?.Dispose(); clientStream.Dispose(); if (!cancellationTokenSource.IsCancellationRequested) { cancellationTokenSource.Cancel(); } } }
/// <summary> /// This is called when client is aware of proxy /// So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy /// </summary> /// <param name="endPoint">The explicit endpoint.</param> /// <param name="clientConnection">The client connection.</param> /// <returns>The task.</returns> private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnection clientConnection) { var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; var clientStream = new HttpClientStream(clientConnection, clientConnection.GetStream(), BufferPool, cancellationToken); Task <TcpServerConnection>?prefetchConnectionTask = null; bool closeServerConnection = false; bool calledRequestHandler = false; try { TunnelConnectSessionEventArgs?connectArgs = null; var method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken); if (clientStream.IsClosed) { return; } // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) if (method == KnownMethod.Connect) { // read the first line HTTP command var requestLine = await clientStream.ReadRequestLine(cancellationToken); if (requestLine.IsEmpty()) { return; } var connectRequest = new ConnectRequest(requestLine.RequestUri) { RequestUriString8 = requestLine.RequestUri, HttpVersion = requestLine.Version }; await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken); connectArgs = new TunnelConnectSessionEventArgs(this, endPoint, connectRequest, clientStream, cancellationTokenSource); clientStream.DataRead += (o, args) => connectArgs.OnDataSent(args.Buffer, args.Offset, args.Count); clientStream.DataWrite += (o, args) => connectArgs.OnDataReceived(args.Buffer, args.Offset, args.Count); await endPoint.InvokeBeforeTunnelConnectRequest(this, connectArgs, ExceptionFunc); // filter out excluded host names bool decryptSsl = endPoint.DecryptSsl && connectArgs.DecryptSsl; bool sendRawData = !decryptSsl; if (connectArgs.DenyConnect) { if (connectArgs.HttpClient.Response.StatusCode == 0) { connectArgs.HttpClient.Response = new Response { HttpVersion = HttpHeader.Version11, StatusCode = (int)HttpStatusCode.Forbidden, StatusDescription = "Forbidden" }; } // send the response await clientStream.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken); return; } if (await checkAuthorization(connectArgs) == false) { await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc); // send the response await clientStream.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken); return; } // write back successful CONNECT response var response = ConnectResponse.CreateSuccessfulConnectResponse(requestLine.Version); // Set ContentLength explicitly to properly handle HTTP 1.0 response.ContentLength = 0; response.Headers.FixProxyHeaders(); connectArgs.HttpClient.Response = response; await clientStream.WriteResponseAsync(response, cancellationToken); var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken); if (clientStream.IsClosed) { return; } bool isClientHello = clientHelloInfo != null; if (clientHelloInfo != null) { connectRequest.TunnelType = TunnelType.Https; connectRequest.ClientHelloInfo = clientHelloInfo; } await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc, isClientHello); if (decryptSsl && clientHelloInfo != null) { connectRequest.IsHttps = true; // todo: move this line to the previous "if" clientStream.Connection.SslProtocol = clientHelloInfo.SslProtocol; bool http2Supported = false; if (EnableHttp2) { var alpn = clientHelloInfo.GetAlpn(); if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2)) { // test server HTTP/2 support try { // todo: this is a hack, because Titanium does not support HTTP protocol changing currently var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, true, SslExtensions.Http2ProtocolAsList, true, cancellationToken); http2Supported = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2; // release connection back to pool instead of closing when connection pool is enabled. await tcpConnectionFactory.Release(connection, true); } catch (Exception) { // ignore } } } if (EnableTcpServerConnectionPrefetch) { IPAddress[]? ipAddresses = null; try { // make sure the host can be resolved before creating the prefetch task ipAddresses = await Dns.GetHostAddressesAsync(connectArgs.HttpClient.Request.RequestUri.Host); } catch (SocketException) { } if (ipAddresses != null && ipAddresses.Length > 0) { // don't pass cancellation token here // it could cause floating server connections when client exits prefetchConnectionTask = tcpConnectionFactory.GetServerConnection(this, connectArgs, true, null, false, CancellationToken.None); } } string connectHostname = requestLine.RequestUri.GetString(); int idx = connectHostname.IndexOf(":"); if (idx >= 0) { connectHostname = connectHostname.Substring(0, idx); } X509Certificate2?certificate = null; SslStream? sslStream = null; try { sslStream = new SslStream(clientStream, false); string certName = HttpHelper.GetWildCardDomainName(connectHostname); certificate = endPoint.GenericCertificate ?? await CertificateManager.CreateServerCertificate(certName); // Successfully managed to authenticate the client using the fake certificate var options = new SslServerAuthenticationOptions(); if (EnableHttp2 && http2Supported) { options.ApplicationProtocols = clientHelloInfo.GetAlpn(); if (options.ApplicationProtocols == null || options.ApplicationProtocols.Count == 0) { options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList; } } options.ServerCertificate = certificate; options.ClientCertificateRequired = false; options.EnabledSslProtocols = SupportedSslProtocols; options.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; await sslStream.AuthenticateAsServerAsync(options, cancellationToken); #if NETSTANDARD2_1 clientStream.Connection.NegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol; #endif // HTTPS server created - we can now decrypt the client's traffic clientStream = new HttpClientStream(clientStream.Connection, sslStream, BufferPool, cancellationToken); sslStream = null; // clientStream was created, no need to keep SSL stream reference clientStream.DataRead += (o, args) => connectArgs.OnDecryptedDataSent(args.Buffer, args.Offset, args.Count); clientStream.DataWrite += (o, args) => connectArgs.OnDecryptedDataReceived(args.Buffer, args.Offset, args.Count); } catch (Exception e) { sslStream?.Dispose(); var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false); throw new ProxyConnectException( $"Couldn't authenticate host '{connectHostname}' with certificate '{certName}'.", e, connectArgs); } method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken); if (clientStream.IsClosed) { return; } if (method == KnownMethod.Invalid) { sendRawData = true; await tcpConnectionFactory.Release(prefetchConnectionTask, true); prefetchConnectionTask = null; } } else if (clientHelloInfo == null) { method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken); if (clientStream.IsClosed) { return; } } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } if (method == KnownMethod.Invalid) { sendRawData = true; } // Hostname is excluded or it is not an HTTPS connect if (sendRawData) { // create new connection to server. // If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then // this connection should not be HTTPS. var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, true, null, true, cancellationToken); try { if (isClientHello) { int available = clientStream.Available; if (available > 0) { // send the buffered data var data = BufferPool.GetBuffer(); try { // clientStream.Available should be at most BufferSize because it is using the same buffer size int read = await clientStream.ReadAsync(data, 0, available, cancellationToken); if (read != available) { throw new Exception("Internal error."); } await connection.Stream.WriteAsync(data, 0, available, true, cancellationToken); } finally { BufferPool.ReturnBuffer(data); } } var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream, BufferPool, cancellationToken); ((ConnectResponse)connectArgs.HttpClient.Response).ServerHelloInfo = serverHelloInfo; } if (!clientStream.IsClosed && !connection.Stream.IsClosed) { await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, null, null, connectArgs.CancellationTokenSource, ExceptionFunc); } } finally { await tcpConnectionFactory.Release(connection, true); } return; } } if (connectArgs != null && method == KnownMethod.Pri) { // todo string?httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (httpCmd == "PRI * HTTP/2.0") { connectArgs.HttpClient.ConnectRequest !.TunnelType = TunnelType.Http2; // HTTP/2 Connection Preface string?line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) { throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } line = await clientStream.ReadLineAsync(cancellationToken); if (line != "SM") { throw new Exception($"HTTP/2 Protocol violation. 'SM' expected, '{line}' received"); } line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) { throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, true, SslExtensions.Http2ProtocolAsList, true, cancellationToken); try { #if NETSTANDARD2_1 var connectionPreface = new ReadOnlyMemory <byte>(Http2Helper.ConnectionPreface); await connection.Stream.WriteAsync(connectionPreface, cancellationToken); await Http2Helper.SendHttp2(clientStream, connection.Stream, () => new SessionEventArgs(this, endPoint, clientStream, connectArgs?.HttpClient.ConnectRequest, cancellationTokenSource) { UserData = connectArgs?.UserData }, async args => { await onBeforeRequest(args); }, async args => { await onBeforeResponse(args); }, connectArgs.CancellationTokenSource, clientStream.Connection.Id, ExceptionFunc); #endif } finally { await tcpConnectionFactory.Release(connection, true); } } } calledRequestHandler = true; // Now create the request await handleHttpSessionRequest(endPoint, clientStream, cancellationTokenSource, connectArgs, prefetchConnectionTask); } catch (ProxyException e) { closeServerConnection = true; onException(clientStream, e); } catch (IOException e) { closeServerConnection = true; onException(clientStream, new Exception("Connection was aborted", e)); } catch (SocketException e) { closeServerConnection = true; onException(clientStream, new Exception("Could not connect", e)); } catch (Exception e) { closeServerConnection = true; onException(clientStream, new Exception("Error occured in whilst handling the client", e)); } finally { if (!cancellationTokenSource.IsCancellationRequested) { cancellationTokenSource.Cancel(); } if (!calledRequestHandler) { await tcpConnectionFactory.Release(prefetchConnectionTask, closeServerConnection); } clientStream.Dispose(); } }
public override void Write(byte[] buffer, int offset, int count) { if (called) { stream.Write(buffer, offset, count); return; } called = true; var ms = new MemoryStream(buffer, offset, count); //this can be non async, because reads from a memory stream var cts = new CancellationTokenSource(); var serverHello = SslTools.PeekServerHello(new CustomBufferedStream(ms, bufferPool, (int)ms.Length), bufferPool, cts.Token).Result; if (serverHello != null) { // 0x00 0x10: ALPN identifier // 0x00 0x0e: length of ALPN data // 0x00 0x0c: length of ALPN data again:) var dataToAdd = new byte[] { 0x0, 0x10, 0x0, 0x5, 0x0, 0x3, 2, (byte)'h', (byte)'2' }; int newByteCount = serverHello.Extensions == null ? dataToAdd.Length + 2 : dataToAdd.Length; var buffer2 = new byte[buffer.Length + newByteCount]; for (int i = 0; i < buffer.Length; i++) { buffer2[i] = buffer[i]; } //this is a hacky solution, but works int length = (buffer[offset + 3] << 8) + buffer[offset + 4]; length += newByteCount; buffer2[offset + 3] = (byte)(length >> 8); buffer2[offset + 4] = (byte)length; length = (buffer[offset + 6] << 16) + (buffer[offset + 7] << 8) + buffer[offset + 8]; length += newByteCount; buffer2[offset + 6] = (byte)(length >> 16); buffer2[offset + 7] = (byte)(length >> 8); buffer2[offset + 8] = (byte)length; int pos = offset + serverHello.EntensionsStartPosition; int endPos = offset + serverHello.ServerHelloLength; if (serverHello.Extensions != null) { // update ALPN length length = (buffer[pos] << 8) + buffer[pos + 1]; length += newByteCount; buffer2[pos] = (byte)(length >> 8); buffer2[pos + 1] = (byte)length; } else { // add ALPN length length = dataToAdd.Length; buffer2[pos] = (byte)(length >> 8); buffer2[pos + 1] = (byte)length; endPos += 2; } for (int i = 0; i < dataToAdd.Length; i++) { buffer2[endPos + i] = dataToAdd[i]; } // copy the reamining data if any for (int i = serverHello.ServerHelloLength; i < count; i++) { buffer2[offset + newByteCount + i] = buffer[offset + i]; } buffer = buffer2; count += newByteCount; } stream.Write(buffer, offset, count); }
/// <summary> /// This is called when client is aware of proxy /// So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy /// </summary> /// <param name="endPoint"></param> /// <param name="tcpClient"></param> /// <returns></returns> private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpClient) { bool disposed = false; var clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); var clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); var clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize); Uri httpRemoteUri; try { //read the first line HTTP command string httpCmd = await clientStreamReader.ReadLineAsync(); if (string.IsNullOrEmpty(httpCmd)) { return; } string httpMethod; string httpUrl; Version version; Request.ParseRequestLine(httpCmd, out httpMethod, out httpUrl, out version); httpRemoteUri = httpMethod == "CONNECT" ? new Uri("http://" + httpUrl) : new Uri(httpUrl); //filter out excluded host names bool excluded = false; if (endPoint.ExcludedHttpsHostNameRegex != null) { excluded = endPoint.ExcludedHttpsHostNameRegexList.Any(x => x.IsMatch(httpRemoteUri.Host)); } if (endPoint.IncludedHttpsHostNameRegex != null) { excluded = !endPoint.IncludedHttpsHostNameRegexList.Any(x => x.IsMatch(httpRemoteUri.Host)); } ConnectRequest connectRequest = null; //Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) if (httpMethod == "CONNECT") { connectRequest = new ConnectRequest { RequestUri = httpRemoteUri, OriginalRequestUrl = httpUrl, HttpVersion = version, Method = httpMethod, }; await HeaderParser.ReadHeaders(clientStreamReader, connectRequest.RequestHeaders); var connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint); connectArgs.WebSession.Request = connectRequest; connectArgs.ProxyClient.TcpClient = tcpClient; connectArgs.ProxyClient.ClientStream = clientStream; if (TunnelConnectRequest != null) { await TunnelConnectRequest.InvokeParallelAsync(this, connectArgs, ExceptionFunc); } if (!excluded && await CheckAuthorization(clientStreamWriter, connectArgs) == false) { if (TunnelConnectResponse != null) { await TunnelConnectResponse.InvokeParallelAsync(this, connectArgs, ExceptionFunc); } return; } //write back successfull CONNECT response connectArgs.WebSession.Response = ConnectResponse.CreateSuccessfullConnectResponse(version); await clientStreamWriter.WriteResponseAsync(connectArgs.WebSession.Response); var clientHelloInfo = await SslTools.PeekClientHello(clientStream); bool isClientHello = clientHelloInfo != null; if (isClientHello) { connectRequest.ClientHelloInfo = clientHelloInfo; } if (TunnelConnectResponse != null) { connectArgs.IsHttpsConnect = isClientHello; await TunnelConnectResponse.InvokeParallelAsync(this, connectArgs, ExceptionFunc); } if (!excluded && isClientHello) { httpRemoteUri = new Uri("https://" + httpUrl); connectRequest.RequestUri = httpRemoteUri; SslStream sslStream = null; try { sslStream = new SslStream(clientStream); string certName = HttpHelper.GetWildCardDomainName(httpRemoteUri.Host); var certificate = endPoint.GenericCertificate ?? CertificateManager.CreateCertificate(certName, false); //Successfully managed to authenticate the client using the fake certificate await sslStream.AuthenticateAsServerAsync(certificate, false, SupportedSslProtocols, false); //HTTPS server created - we can now decrypt the client's traffic clientStream = new CustomBufferedStream(sslStream, BufferSize); clientStreamReader.Dispose(); clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize); } catch { sslStream?.Dispose(); return; } //Now read the actual HTTPS request line httpCmd = await clientStreamReader.ReadLineAsync(); } //Hostname is excluded or it is not an HTTPS connect else { //create new connection using (var connection = await GetServerConnection(connectArgs, true)) { try { if (isClientHello) { if (clientStream.Available > 0) { //send the buffered data var data = new byte[clientStream.Available]; await clientStream.ReadAsync(data, 0, data.Length); await connection.Stream.WriteAsync(data, 0, data.Length); await connection.Stream.FlushAsync(); } var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream); ((ConnectResponse)connectArgs.WebSession.Response).ServerHelloInfo = serverHelloInfo; } await TcpHelper.SendRaw(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }); } finally { UpdateServerConnectionCount(false); } } return; } } //Now create the request disposed = await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, httpRemoteUri.Scheme == UriSchemeHttps?httpRemoteUri.Host : null, endPoint, connectRequest); } catch (Exception e) { ExceptionFunc(new Exception("Error whilst authorizing request", e)); } finally { if (!disposed) { Dispose(clientStream, clientStreamReader, clientStreamWriter, null); } } }
/// <summary> /// This is called when client is aware of proxy /// So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy /// </summary> /// <param name="endPoint"></param> /// <param name="tcpClient"></param> /// <returns></returns> private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpClient) { var clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); var clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); var clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize); try { string connectHostname = null; ConnectRequest connectRequest = null; //Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) if (await HttpHelper.IsConnectMethod(clientStream) == 1) { //read the first line HTTP command string httpCmd = await clientStreamReader.ReadLineAsync(); if (string.IsNullOrEmpty(httpCmd)) { return; } Request.ParseRequestLine(httpCmd, out string _, out string httpUrl, out var version); var httpRemoteUri = new Uri("http://" + httpUrl); connectHostname = httpRemoteUri.Host; //filter out excluded host names bool excluded = false; if (endPoint.ExcludedHttpsHostNameRegex != null) { excluded = endPoint.ExcludedHttpsHostNameRegexList.Any(x => x.IsMatch(connectHostname)); } if (endPoint.IncludedHttpsHostNameRegex != null) { excluded = !endPoint.IncludedHttpsHostNameRegexList.Any(x => x.IsMatch(connectHostname)); } if (endPoint.BeforeTunnelConnect != null) { excluded = await endPoint.BeforeTunnelConnect(connectHostname); } connectRequest = new ConnectRequest { RequestUri = httpRemoteUri, OriginalUrl = httpUrl, HttpVersion = version, }; await HeaderParser.ReadHeaders(clientStreamReader, connectRequest.Headers); var connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint, connectRequest, ExceptionFunc); connectArgs.ProxyClient.TcpClient = tcpClient; connectArgs.ProxyClient.ClientStream = clientStream; await endPoint.InvokeTunnectConnectRequest(this, connectArgs, ExceptionFunc); if (await CheckAuthorization(clientStreamWriter, connectArgs) == false) { await endPoint.InvokeTunnectConnectResponse(this, connectArgs, ExceptionFunc); return; } //write back successfull CONNECT response var response = ConnectResponse.CreateSuccessfullConnectResponse(version); response.Headers.FixProxyHeaders(); connectArgs.WebSession.Response = response; await clientStreamWriter.WriteResponseAsync(response); var clientHelloInfo = await SslTools.PeekClientHello(clientStream); bool isClientHello = clientHelloInfo != null; if (isClientHello) { connectRequest.ClientHelloInfo = clientHelloInfo; } await endPoint.InvokeTunnectConnectResponse(this, connectArgs, ExceptionFunc, isClientHello); if (!excluded && isClientHello) { connectRequest.RequestUri = new Uri("https://" + httpUrl); SslStream sslStream = null; try { sslStream = new SslStream(clientStream); string certName = HttpHelper.GetWildCardDomainName(connectHostname); var certificate = endPoint.GenericCertificate ?? await CertificateManager.CreateCertificateAsync(certName); //Successfully managed to authenticate the client using the fake certificate await sslStream.AuthenticateAsServerAsync(certificate, false, SupportedSslProtocols, false); //HTTPS server created - we can now decrypt the client's traffic clientStream = new CustomBufferedStream(sslStream, BufferSize); clientStreamReader.Dispose(); clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize); } catch { sslStream?.Dispose(); return; } if (await HttpHelper.IsConnectMethod(clientStream) == -1) { // It can be for example some Google (Cloude Messaging for Chrome) magic excluded = true; } } //Hostname is excluded or it is not an HTTPS connect if (excluded || !isClientHello) { //create new connection using (var connection = await GetServerConnection(connectArgs, true)) { if (isClientHello) { int available = clientStream.Available; if (available > 0) { //send the buffered data var data = BufferPool.GetBuffer(BufferSize); try { // clientStream.Available sbould be at most BufferSize because it is using the same buffer size await clientStream.ReadAsync(data, 0, available); await connection.StreamWriter.WriteAsync(data, 0, available, true); } finally { BufferPool.ReturnBuffer(data); } } var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream); ((ConnectResponse)connectArgs.WebSession.Response).ServerHelloInfo = serverHelloInfo; } await TcpHelper.SendRaw(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }, ExceptionFunc); } return; } } //Now create the request await HandleHttpSessionRequest(tcpClient, clientStream, clientStreamReader, clientStreamWriter, connectHostname, endPoint, connectRequest); } catch (ProxyHttpException e) { ExceptionFunc(e); } catch (IOException e) { ExceptionFunc(new Exception("Connection was aborted", e)); } catch (SocketException e) { ExceptionFunc(new Exception("Could not connect", e)); } catch (Exception e) { ExceptionFunc(new Exception("Error occured in whilst handling the client", e)); } finally { clientStreamReader.Dispose(); clientStream.Dispose(); } }
/// <summary> /// This is called when client is aware of proxy /// So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy /// </summary> /// <param name="endPoint">The explicit endpoint.</param> /// <param name="clientConnection">The client connection.</param> /// <returns>The task.</returns> private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClientConnection clientConnection) { var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; var clientStream = new CustomBufferedStream(clientConnection.GetStream(), BufferSize); var clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize); try { string connectHostname = null; TunnelConnectSessionEventArgs connectArgs = null; // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) if (await HttpHelper.IsConnectMethod(clientStream) == 1) { // read the first line HTTP command string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(httpCmd)) { return; } Request.ParseRequestLine(httpCmd, out string _, out string httpUrl, out var version); var httpRemoteUri = new Uri("http://" + httpUrl); connectHostname = httpRemoteUri.Host; var connectRequest = new ConnectRequest { RequestUri = httpRemoteUri, OriginalUrl = httpUrl, HttpVersion = version }; await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken); connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint, connectRequest, cancellationTokenSource, ExceptionFunc); connectArgs.ProxyClient.ClientConnection = clientConnection; connectArgs.ProxyClient.ClientStream = clientStream; await endPoint.InvokeBeforeTunnelConnectRequest(this, connectArgs, ExceptionFunc); // filter out excluded host names bool decryptSsl = endPoint.DecryptSsl && connectArgs.DecryptSsl; if (connectArgs.DenyConnect) { if (connectArgs.WebSession.Response.StatusCode == 0) { connectArgs.WebSession.Response = new Response { HttpVersion = HttpHeader.Version11, StatusCode = (int)HttpStatusCode.Forbidden, StatusDescription = "Forbidden" }; } // send the response await clientStreamWriter.WriteResponseAsync(connectArgs.WebSession.Response, cancellationToken : cancellationToken); return; } if (await CheckAuthorization(connectArgs) == false) { await endPoint.InvokeBeforeTunnectConnectResponse(this, connectArgs, ExceptionFunc); // send the response await clientStreamWriter.WriteResponseAsync(connectArgs.WebSession.Response, cancellationToken : cancellationToken); return; } // write back successfull CONNECT response var response = ConnectResponse.CreateSuccessfullConnectResponse(version); // Set ContentLength explicitly to properly handle HTTP 1.0 response.ContentLength = 0; response.Headers.FixProxyHeaders(); connectArgs.WebSession.Response = response; await clientStreamWriter.WriteResponseAsync(response, cancellationToken : cancellationToken); var clientHelloInfo = await SslTools.PeekClientHello(clientStream, cancellationToken); bool isClientHello = clientHelloInfo != null; if (isClientHello) { connectRequest.ClientHelloInfo = clientHelloInfo; } await endPoint.InvokeBeforeTunnectConnectResponse(this, connectArgs, ExceptionFunc, isClientHello); if (decryptSsl && isClientHello) { connectRequest.RequestUri = new Uri("https://" + httpUrl); bool http2Supproted = false; var alpn = clientHelloInfo.GetAlpn(); if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2)) { // test server HTTP/2 support // todo: this is a hack, because Titanium does not support HTTP protocol changing currently using (var connection = await GetServerConnection(connectArgs, true, SslExtensions.Http2ProtocolAsList, cancellationToken)) { http2Supproted = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2; } } SslStream sslStream = null; try { sslStream = new SslStream(clientStream); string certName = HttpHelper.GetWildCardDomainName(connectHostname); var certificate = endPoint.GenericCertificate ?? await CertificateManager.CreateCertificateAsync(certName); // Successfully managed to authenticate the client using the fake certificate var options = new SslServerAuthenticationOptions(); if (http2Supproted) { options.ApplicationProtocols = clientHelloInfo.GetAlpn(); if (options.ApplicationProtocols == null || options.ApplicationProtocols.Count == 0) { options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList; } } options.ServerCertificate = certificate; options.ClientCertificateRequired = false; options.EnabledSslProtocols = SupportedSslProtocols; options.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; await sslStream.AuthenticateAsServerAsync(options, cancellationToken); #if NETCOREAPP2_1 clientConnection.NegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol; #endif // HTTPS server created - we can now decrypt the client's traffic clientStream = new CustomBufferedStream(sslStream, BufferSize); clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize); } catch (Exception e) { sslStream?.Dispose(); throw new ProxyConnectException( $"Could'nt authenticate client '{connectHostname}' with fake certificate.", e, connectArgs); } if (await HttpHelper.IsConnectMethod(clientStream) == -1) { decryptSsl = false; } } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } // Hostname is excluded or it is not an HTTPS connect if (!decryptSsl || !isClientHello) { // create new connection using (var connection = await GetServerConnection(connectArgs, true, clientConnection.NegotiatedApplicationProtocol, cancellationToken)) { if (isClientHello) { int available = clientStream.Available; if (available > 0) { // send the buffered data var data = BufferPool.GetBuffer(BufferSize); try { // clientStream.Available sbould be at most BufferSize because it is using the same buffer size await clientStream.ReadAsync(data, 0, available, cancellationToken); await connection.StreamWriter.WriteAsync(data, 0, available, true, cancellationToken); } finally { BufferPool.ReturnBuffer(data); } } var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream, cancellationToken); ((ConnectResponse)connectArgs.WebSession.Response).ServerHelloInfo = serverHelloInfo; } await TcpHelper.SendRaw(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }, connectArgs.CancellationTokenSource, ExceptionFunc); } return; } } if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream) == 1) { // todo string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (httpCmd == "PRI * HTTP/2.0") { // HTTP/2 Connection Preface string line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) { throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } line = await clientStream.ReadLineAsync(cancellationToken); if (line != "SM") { throw new Exception($"HTTP/2 Protocol violation. 'SM' expected, '{line}' received"); } line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) { throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } // create new connection using (var connection = await GetServerConnection(connectArgs, true, SslExtensions.Http2ProtocolAsList, cancellationToken)) { await connection.StreamWriter.WriteLineAsync("PRI * HTTP/2.0", cancellationToken); await connection.StreamWriter.WriteLineAsync(cancellationToken); await connection.StreamWriter.WriteLineAsync("SM", cancellationToken); await connection.StreamWriter.WriteLineAsync(cancellationToken); #if NETCOREAPP2_1 await Http2Helper.SendHttp2(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }, connectArgs.CancellationTokenSource, clientConnection.Id, ExceptionFunc); #endif } } } // Now create the request await HandleHttpSessionRequest(endPoint, clientConnection, clientStream, clientStreamWriter, cancellationTokenSource, connectHostname, connectArgs?.WebSession.ConnectRequest); } catch (ProxyException e) { OnException(clientStream, e); } catch (IOException e) { OnException(clientStream, new Exception("Connection was aborted", e)); } catch (SocketException e) { OnException(clientStream, new Exception("Could not connect", e)); } catch (Exception e) { OnException(clientStream, new Exception("Error occured in whilst handling the client", e)); } finally { clientStream.Dispose(); if (!cancellationTokenSource.IsCancellationRequested) { cancellationTokenSource.Cancel(); } } }