/// <summary> /// Handle upgrade to websocket /// </summary> private async Task handleWebSocketUpgrade(SessionEventArgs args, HttpClientStream clientStream, TcpServerConnection serverConnection, CancellationTokenSource cancellationTokenSource, CancellationToken cancellationToken) { await serverConnection.Stream.WriteRequestAsync(args.HttpClient.Request, cancellationToken); var httpStatus = await serverConnection.Stream.ReadResponseStatus(cancellationToken); var response = args.HttpClient.Response; response.HttpVersion = httpStatus.Version; response.StatusCode = httpStatus.StatusCode; response.StatusDescription = httpStatus.Description; await HeaderParser.ReadHeaders(serverConnection.Stream, response.Headers, cancellationToken); if (!args.IsTransparent) { await clientStream.WriteResponseAsync(response, cancellationToken); } // If user requested call back then do it if (!args.HttpClient.Response.Locked) { await onBeforeResponse(args); } await TcpHelper.SendRaw(clientStream, serverConnection.Stream, BufferPool, args.OnDataSent, args.OnDataReceived, cancellationTokenSource, ExceptionFunc); }
/// <summary> /// Handle upgrade to websocket /// </summary> private async Task handleWebSocketUpgrade(string httpCmd, SessionEventArgs args, Request request, Response response, CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, TcpServerConnection serverConnection, CancellationTokenSource cancellationTokenSource, CancellationToken cancellationToken) { // prepare the prefix content await serverConnection.StreamWriter.WriteLineAsync(httpCmd, cancellationToken); await serverConnection.StreamWriter.WriteHeadersAsync(request.Headers, cancellationToken : cancellationToken); string httpStatus; try { httpStatus = await serverConnection.Stream.ReadLineAsync(cancellationToken); if (httpStatus == null) { throw new ServerConnectionException("Server connection was closed."); } } catch (Exception e) when(!(e is ServerConnectionException)) { throw new ServerConnectionException("Server connection was closed.", e); } Version responseVersion; int responseStatusCode; string responseStatusDescription; Response.ParseResponseLine(httpStatus, out responseVersion, out responseStatusCode, out responseStatusDescription); response.HttpVersion = responseVersion; response.StatusCode = responseStatusCode; response.StatusDescription = responseStatusDescription; await HeaderParser.ReadHeaders(serverConnection.Stream, response.Headers, cancellationToken); if (!args.IsTransparent) { await clientStreamWriter.WriteResponseAsync(response, cancellationToken : cancellationToken); } // If user requested call back then do it if (!args.WebSession.Response.Locked) { await invokeBeforeResponse(args); } await TcpHelper.SendRaw(clientStream, serverConnection.Stream, BufferPool, BufferSize, (buffer, offset, count) => { args.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); }, cancellationTokenSource, ExceptionFunc); }
/// <summary> /// Handle upgrade to websocket /// </summary> private async Task handleWebSocketUpgrade(string requestHttpMethod, string requestHttpUrl, Version requestVersion, SessionEventArgs args, Request request, Response response, CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, TcpServerConnection serverConnection, CancellationTokenSource cancellationTokenSource, CancellationToken cancellationToken) { // prepare the prefix content var headerBuilder = new HeaderBuilder(); headerBuilder.WriteRequestLine(requestHttpMethod, requestHttpUrl, requestVersion); headerBuilder.WriteHeaders(request.Headers); await serverConnection.StreamWriter.WriteHeadersAsync(headerBuilder, cancellationToken); string httpStatus; try { httpStatus = await serverConnection.Stream.ReadLineAsync(cancellationToken) ?? throw new ServerConnectionException("Server connection was closed."); } catch (Exception e) when(!(e is ServerConnectionException)) { throw new ServerConnectionException("Server connection was closed.", e); } Response.ParseResponseLine(httpStatus, out var responseVersion, out int responseStatusCode, out string responseStatusDescription); response.HttpVersion = responseVersion; response.StatusCode = responseStatusCode; response.StatusDescription = responseStatusDescription; await HeaderParser.ReadHeaders(serverConnection.Stream, response.Headers, cancellationToken); if (!args.IsTransparent) { await clientStreamWriter.WriteResponseAsync(response, cancellationToken : cancellationToken); } // If user requested call back then do it if (!args.HttpClient.Response.Locked) { await onBeforeResponse(args); } await TcpHelper.SendRaw(clientStream, serverConnection.Stream, BufferPool, args.OnDataSent, args.OnDataReceived, cancellationTokenSource, ExceptionFunc); }
/// <summary> /// This is called when the request is PUT/POST/PATCH to read the body /// </summary> /// <returns></returns> internal async Task CopyRequestBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken) { var request = WebSession.Request; long contentLength = request.ContentLength; // send the request body bytes to server if (contentLength > 0 && hasMulipartEventSubscribers && request.IsMultipartFormData) { var reader = getStreamReader(true); string boundary = HttpHelper.GetBoundaryFromContentType(request.ContentType); using (var copyStream = new CopyStream(reader, writer, bufferPool, bufferSize)) { while (contentLength > copyStream.ReadBytes) { long read = await readUntilBoundaryAsync(copyStream, contentLength, boundary, cancellationToken); if (read == 0) { break; } if (contentLength > copyStream.ReadBytes) { var headers = new HeaderCollection(); await HeaderParser.ReadHeaders(copyStream, headers, cancellationToken); OnMultipartRequestPartSent(boundary, headers); } } await copyStream.FlushAsync(cancellationToken); } } else { await copyBodyAsync(true, false, writer, transformation, OnDataSent, cancellationToken); } }
/// <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(); } }
/// <summary> /// This is the core request handler method for a particular connection from client. /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination. /// </summary> /// <param name="endPoint">The proxy endpoint.</param> /// <param name="clientConnection">The client connection.</param> /// <param name="clientStream">The client stream.</param> /// <param name="clientStreamWriter">The client stream writer.</param> /// <param name="cancellationTokenSource">The cancellation token source for this async task.</param> /// <param name="httpsConnectHostname"> /// The https hostname as appeared in CONNECT request if this is a HTTPS request from /// explicit endpoint. /// </param> /// <param name="connectRequest">The Connect request if this is a HTTPS request from explicit endpoint.</param> private async Task HandleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection, CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, CancellationTokenSource cancellationTokenSource, string httpsConnectHostname, ConnectRequest connectRequest) { var cancellationToken = cancellationTokenSource.Token; TcpServerConnection serverConnection = null; bool serverConnectionClose = false; try { // Loop through each subsequest request on this particular client connection // (assuming HTTP connection is kept alive by client) while (true) { // read the request line string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(httpCmd)) { return; } var args = new SessionEventArgs(BufferSize, endPoint, cancellationTokenSource, ExceptionFunc) { ProxyClient = { ClientConnection = clientConnection }, WebSession = { ConnectRequest = connectRequest } }; try { try { Request.ParseRequestLine(httpCmd, out string httpMethod, out string httpUrl, out var version); // Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStream, args.WebSession.Request.Headers, cancellationToken); Uri httpRemoteUri; if (uriSchemeRegex.IsMatch(httpUrl)) { try { httpRemoteUri = new Uri(httpUrl); } catch (Exception ex) { throw new Exception($"Invalid URI: '{httpUrl}'", ex); } } else { string host = args.WebSession.Request.Host ?? httpsConnectHostname; string hostAndPath = host; if (httpUrl.StartsWith("/")) { hostAndPath += httpUrl; } string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://", hostAndPath); try { httpRemoteUri = new Uri(url); } catch (Exception ex) { throw new Exception($"Invalid URI: '{url}'", ex); } } var request = args.WebSession.Request; request.RequestUri = httpRemoteUri; request.OriginalUrl = httpUrl; request.Method = httpMethod; request.HttpVersion = version; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamWriter = clientStreamWriter; if (!args.IsTransparent) { // proxy authorization check if (httpsConnectHostname == null && await CheckAuthorization(args) == false) { await InvokeBeforeResponse(args); // send the response await clientStreamWriter.WriteResponseAsync(args.WebSession.Response, cancellationToken : cancellationToken); return; } PrepareRequestHeaders(request.Headers); request.Host = request.RequestUri.Authority; } // if win auth is enabled // we need a cache of request body // so that we can send it after authentication in WinAuthHandler.cs if (isWindowsAuthenticationEnabledAndSupported && request.HasBody) { await args.GetRequestBody(cancellationToken); } request.OriginalHasBody = request.HasBody; // If user requested interception do it await InvokeBeforeRequest(args); var response = args.WebSession.Response; if (request.CancelRequest) { // syphon out the request body from client before setting the new body await args.SyphonOutBodyAsync(true, cancellationToken); await HandleHttpSessionResponse(args); if (!response.KeepAlive) { return; } continue; } // create a new connection if hostname/upstream end point changes if (serverConnection != null && (!serverConnection.HostName.EqualsIgnoreCase(request.RequestUri.Host) || args.WebSession.UpStreamEndPoint?.Equals(serverConnection.UpStreamEndPoint) == false)) { tcpConnectionFactory.Release(serverConnection, true); serverConnection = null; } if (serverConnection == null) { serverConnection = await GetServerConnection(args, false, clientConnection.NegotiatedApplicationProtocol, cancellationToken); } // if upgrading to websocket then relay the requet without reading the contents if (request.UpgradeToWebSocket) { // prepare the prefix content await serverConnection.StreamWriter.WriteLineAsync(httpCmd, cancellationToken); await serverConnection.StreamWriter.WriteHeadersAsync(request.Headers, cancellationToken : cancellationToken); string httpStatus = await serverConnection.Stream.ReadLineAsync(cancellationToken); Response.ParseResponseLine(httpStatus, out var responseVersion, out int responseStatusCode, out string responseStatusDescription); response.HttpVersion = responseVersion; response.StatusCode = responseStatusCode; response.StatusDescription = responseStatusDescription; await HeaderParser.ReadHeaders(serverConnection.Stream, response.Headers, cancellationToken); if (!args.IsTransparent) { await clientStreamWriter.WriteResponseAsync(response, cancellationToken : cancellationToken); } // If user requested call back then do it if (!args.WebSession.Response.Locked) { await InvokeBeforeResponse(args); } await TcpHelper.SendRaw(clientStream, serverConnection.Stream, BufferSize, (buffer, offset, count) => { args.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); }, cancellationTokenSource, ExceptionFunc); return; } // construct the web request that we are going to issue on behalf of the client. await HandleHttpSessionRequestInternal(serverConnection, args); if (args.WebSession.ServerConnection == null) { return; } // if connection is closing exit if (!response.KeepAlive) { serverConnectionClose = true; return; } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } } catch (Exception e) when(!(e is ProxyHttpException)) { throw new ProxyHttpException("Error occured whilst handling session request", e, args); } } catch (Exception e) { args.Exception = e; serverConnectionClose = true; throw; } finally { await InvokeAfterResponse(args); args.Dispose(); } } } finally { tcpConnectionFactory.Release(serverConnection, serverConnectionClose || !EnableConnectionPool); } }
/// <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); 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(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.GetClientHelloInfo(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 { var alpnStream = AlpnEnabled ? (Stream) new ServerHelloAlpnAdderStream(clientStream) : clientStream; sslStream = new SslStream(alpnStream); 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); } 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)) { 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.GetServerHelloInfo(connection.Stream); ((ConnectResponse)connectArgs.WebSession.Response).ServerHelloInfo = serverHelloInfo; } await TcpHelper.SendRaw(clientStream, connection.Stream, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }); 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 the core request handler method for a particular connection from client /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination /// </summary> /// <param name="client"></param> /// <param name="httpCmd"></param> /// <param name="clientStream"></param> /// <param name="clientStreamReader"></param> /// <param name="clientStreamWriter"></param> /// <param name="httpsConnectHostname"></param> /// <param name="endPoint"></param> /// <param name="connectRequest"></param> /// <param name="isTransparentEndPoint"></param> /// <returns></returns> private async Task <bool> HandleHttpSessionRequest(TcpClient client, string httpCmd, CustomBufferedStream clientStream, CustomBinaryReader clientStreamReader, HttpResponseWriter clientStreamWriter, string httpsConnectHostname, ProxyEndPoint endPoint, ConnectRequest connectRequest, bool isTransparentEndPoint = false) { bool disposed = false; TcpConnection connection = null; //Loop through each subsequest request on this particular client connection //(assuming HTTP connection is kept alive by client) while (true) { if (string.IsNullOrEmpty(httpCmd)) { break; } var args = new SessionEventArgs(BufferSize, endPoint, HandleHttpSessionResponse) { ProxyClient = { TcpClient = client }, WebSession = { ConnectRequest = connectRequest } }; try { string httpMethod; string httpUrl; Version version; Request.ParseRequestLine(httpCmd, out httpMethod, out httpUrl, out version); //Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.RequestHeaders); var httpRemoteUri = new Uri(httpsConnectHostname == null ? isTransparentEndPoint ? string.Concat("http://", args.WebSession.Request.Host, httpUrl) : httpUrl : string.Concat("https://", args.WebSession.Request.Host ?? httpsConnectHostname, httpUrl)); args.WebSession.Request.RequestUri = httpRemoteUri; args.WebSession.Request.OriginalRequestUrl = httpUrl; args.WebSession.Request.Method = httpMethod; args.WebSession.Request.HttpVersion = version; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamReader = clientStreamReader; args.ProxyClient.ClientStreamWriter = clientStreamWriter; //proxy authorization check if (httpsConnectHostname == null && await CheckAuthorization(clientStreamWriter, args) == false) { args.Dispose(); break; } PrepareRequestHeaders(args.WebSession.Request.RequestHeaders); args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority; #if NET45 //if win auth is enabled //we need a cache of request body //so that we can send it after authentication in WinAuthHandler.cs if (EnableWinAuth && !RunTime.IsRunningOnMono && args.WebSession.Request.HasBody) { await args.GetRequestBody(); } #endif //If user requested interception do it if (BeforeRequest != null) { await BeforeRequest.InvokeParallelAsync(this, args, ExceptionFunc); } if (args.WebSession.Request.CancelRequest) { args.Dispose(); break; } //create a new connection if hostname changes if (connection != null && !connection.HostName.Equals(args.WebSession.Request.RequestUri.Host, StringComparison.OrdinalIgnoreCase)) { connection.Dispose(); UpdateServerConnectionCount(false); connection = null; } if (connection == null) { connection = await GetServerConnection(args, false); } //if upgrading to websocket then relay the requet without reading the contents if (args.WebSession.Request.UpgradeToWebSocket) { //prepare the prefix content var requestHeaders = args.WebSession.Request.RequestHeaders; byte[] requestBytes; using (var ms = new MemoryStream()) using (var writer = new HttpRequestWriter(ms)) { writer.WriteLine(httpCmd); writer.WriteHeaders(requestHeaders); requestBytes = ms.ToArray(); } await connection.Stream.WriteAsync(requestBytes, 0, requestBytes.Length); string httpStatus = await connection.StreamReader.ReadLineAsync(); Version responseVersion; int responseStatusCode; string responseStatusDescription; Response.ParseResponseLine(httpStatus, out responseVersion, out responseStatusCode, out responseStatusDescription); args.WebSession.Response.HttpVersion = responseVersion; args.WebSession.Response.ResponseStatusCode = responseStatusCode; args.WebSession.Response.ResponseStatusDescription = responseStatusDescription; await HeaderParser.ReadHeaders(connection.StreamReader, args.WebSession.Response.ResponseHeaders); await clientStreamWriter.WriteResponseAsync(args.WebSession.Response); //If user requested call back then do it if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked) { await BeforeResponse.InvokeParallelAsync(this, args, ExceptionFunc); } await TcpHelper.SendRaw(clientStream, connection.Stream, (buffer, offset, count) => { args.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); }); args.Dispose(); break; } //construct the web request that we are going to issue on behalf of the client. disposed = await HandleHttpSessionRequestInternal(connection, args, false); if (disposed) { //already disposed inside above method args.Dispose(); break; } //if connection is closing exit if (args.WebSession.Response.ResponseKeepAlive == false) { args.Dispose(); break; } args.Dispose(); // read the next request httpCmd = await clientStreamReader.ReadLineAsync(); } catch (Exception e) { ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request", e, args)); break; } } if (!disposed) { Dispose(clientStream, clientStreamReader, clientStreamWriter, connection); } return(true); }
/// <summary> /// This is the core request handler method for a particular connection from client /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination /// </summary> /// <param name="client"></param> /// <param name="httpCmd"></param> /// <param name="clientStream"></param> /// <param name="clientStreamReader"></param> /// <param name="clientStreamWriter"></param> /// <param name="httpsHostName"></param> /// <param name="endPoint"></param> /// <param name="connectHeaders"></param> /// <returns></returns> private async Task <bool> HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream, CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsHostName, ProxyEndPoint endPoint, List <HttpHeader> connectHeaders) { bool disposed = false; TcpConnection connection = null; //Loop through each subsequest request on this particular client connection //(assuming HTTP connection is kept alive by client) while (true) { if (string.IsNullOrEmpty(httpCmd)) { break; } var args = new SessionEventArgs(BufferSize, HandleHttpSessionResponse) { ProxyClient = { TcpClient = client }, WebSession = { ConnectHeaders = connectHeaders } }; args.WebSession.ProcessId = new Lazy <int>(() => { var remoteEndPoint = (IPEndPoint)args.ProxyClient.TcpClient.Client.RemoteEndPoint; //If client is localhost get the process id if (NetworkHelper.IsLocalIpAddress(remoteEndPoint.Address)) { return(NetworkHelper.GetProcessIdFromPort(remoteEndPoint.Port, endPoint.IpV6Enabled)); } //can't access process Id of remote request from remote machine return(-1); }); try { //break up the line into three components (method, remote URL & Http Version) var httpCmdSplit = httpCmd.Split(ProxyConstants.SpaceSplit, 3); var httpMethod = httpCmdSplit[0]; //find the request HTTP version var httpVersion = HttpHeader.Version11; if (httpCmdSplit.Length == 3) { var httpVersionString = httpCmdSplit[2].Trim(); if (string.Equals(httpVersionString, "HTTP/1.0", StringComparison.OrdinalIgnoreCase)) { httpVersion = HttpHeader.Version10; } } //Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.NonUniqueRequestHeaders, args.WebSession.Request.RequestHeaders); var httpRemoteUri = new Uri(httpsHostName == null ? httpCmdSplit[1] : string.Concat("https://", args.WebSession.Request.Host ?? httpsHostName, httpCmdSplit[1])); args.WebSession.Request.RequestUri = httpRemoteUri; args.WebSession.Request.Method = httpMethod.Trim().ToUpper(); args.WebSession.Request.HttpVersion = httpVersion; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamReader = clientStreamReader; args.ProxyClient.ClientStreamWriter = clientStreamWriter; if (httpsHostName == null && await CheckAuthorization(clientStreamWriter, args.WebSession.Request.RequestHeaders.Values) == false) { args.Dispose(); break; } PrepareRequestHeaders(args.WebSession.Request.RequestHeaders, args.WebSession); args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority; //if win auth is enabled //we need a cache of request body //so that we can send it after authentication in WinAuthHandler.cs if (EnableWinAuth && !RunTime.IsRunningOnMono && args.WebSession.Request.HasBody) { await args.GetRequestBody(); } //If user requested interception do it if (BeforeRequest != null) { await BeforeRequest.InvokeParallelAsync(this, args); } //if upgrading to websocket then relay the requet without reading the contents if (args.WebSession.Request.UpgradeToWebSocket) { await TcpHelper.SendRaw(this, httpRemoteUri.Host, httpRemoteUri.Port, httpCmd, httpVersion, args.WebSession.Request.RequestHeaders, args.IsHttps, clientStream, tcpConnectionFactory, connection); args.Dispose(); break; } if (connection == null) { connection = await GetServerConnection(args); } //construct the web request that we are going to issue on behalf of the client. disposed = await HandleHttpSessionRequestInternal(connection, args, false); if (disposed) { //already disposed inside above method args.Dispose(); break; } if (args.WebSession.Request.CancelRequest) { args.Dispose(); break; } //if connection is closing exit if (args.WebSession.Response.ResponseKeepAlive == false) { args.Dispose(); break; } args.Dispose(); // read the next request httpCmd = await clientStreamReader.ReadLineAsync(); } catch (Exception e) { ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request", e, args)); break; } } if (!disposed) { Dispose(clientStream, clientStreamReader, clientStreamWriter, connection); } return(true); }
/// <summary> /// This is the core request handler method for a particular connection from client. /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination. /// </summary> /// <param name="endPoint">The proxy endpoint.</param> /// <param name="clientConnection">The client connection.</param> /// <param name="clientStream">The client stream.</param> /// <param name="clientStreamWriter">The client stream writer.</param> /// <param name="cancellationTokenSource">The cancellation token source for this async task.</param> /// <param name="httpsConnectHostname"> /// The https hostname as appeared in CONNECT request if this is a HTTPS request from /// explicit endpoint. /// </param> /// <param name="connectRequest">The Connect request if this is a HTTPS request from explicit endpoint.</param> /// <param name="prefetchConnectionTask">Prefetched server connection for current client using Connect/SNI headers.</param> private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection, CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, CancellationTokenSource cancellationTokenSource, string httpsConnectHostname, TunnelConnectSessionEventArgs connectArgs, Task <TcpServerConnection> prefetchConnectionTask = null) { var connectRequest = connectArgs?.HttpClient.ConnectRequest; var prefetchTask = prefetchConnectionTask; TcpServerConnection connection = null; bool closeServerConnection = false; try { var cancellationToken = cancellationTokenSource.Token; // Loop through each subsequest request on this particular client connection // (assuming HTTP connection is kept alive by client) while (true) { // read the request line string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(httpCmd)) { return; } var args = new SessionEventArgs(this, endPoint, cancellationTokenSource) { ProxyClient = { Connection = clientConnection }, HttpClient = { ConnectRequest = connectRequest }, UserData = connectArgs?.UserData }; try { try { Request.ParseRequestLine(httpCmd, out string httpMethod, out string httpUrl, out var version); // Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStream, args.HttpClient.Request.Headers, cancellationToken); Uri httpRemoteUri; if (ProxyConstants.UriSchemeRegex.IsMatch(httpUrl)) { try { httpRemoteUri = new Uri(httpUrl); } catch (Exception ex) { throw new Exception($"Invalid URI: '{httpUrl}'", ex); } } else { string host = args.HttpClient.Request.Host ?? httpsConnectHostname; string hostAndPath = host; if (httpUrl.StartsWith("/")) { hostAndPath += httpUrl; } string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://", hostAndPath); try { httpRemoteUri = new Uri(url); } catch (Exception ex) { throw new Exception($"Invalid URI: '{url}'", ex); } } var request = args.HttpClient.Request; request.RequestUri = httpRemoteUri; request.OriginalUrl = httpUrl; request.Method = httpMethod; request.HttpVersion = version; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamWriter = clientStreamWriter; if (!args.IsTransparent) { // proxy authorization check if (httpsConnectHostname == null && await checkAuthorization(args) == false) { await invokeBeforeResponse(args); // send the response await clientStreamWriter.WriteResponseAsync(args.HttpClient.Response, cancellationToken : cancellationToken); return; } prepareRequestHeaders(request.Headers); request.Host = request.RequestUri.Authority; } // if win auth is enabled // we need a cache of request body // so that we can send it after authentication in WinAuthHandler.cs if (isWindowsAuthenticationEnabledAndSupported && request.HasBody) { await args.GetRequestBody(cancellationToken); } //we need this to syphon out data from connection if API user changes them. request.SetOriginalHeaders(); args.TimeLine["Request Received"] = DateTime.Now; // If user requested interception do it await invokeBeforeRequest(args); var response = args.HttpClient.Response; if (request.CancelRequest) { if (!(Enable100ContinueBehaviour && request.ExpectContinue)) { // syphon out the request body from client before setting the new body await args.SyphonOutBodyAsync(true, cancellationToken); } await handleHttpSessionResponse(args); if (!response.KeepAlive) { return; } continue; } //If prefetch task is available. if (connection == null && prefetchTask != null) { connection = await prefetchTask; prefetchTask = null; } // create a new connection if cache key changes. // only gets hit when connection pool is disabled. // or when prefetch task has a unexpectedly different connection. if (connection != null && (await tcpConnectionFactory.GetConnectionCacheKey(this, args, clientConnection.NegotiatedApplicationProtocol) != connection.CacheKey)) { await tcpConnectionFactory.Release(connection); connection = null; } var result = await handleHttpSessionRequest(httpCmd, args, connection, clientConnection.NegotiatedApplicationProtocol, cancellationToken, cancellationTokenSource); //update connection to latest used connection = result.LatestConnection; closeServerConnection = !result.Continue; //throw if exception happened if (!result.IsSuccess) { throw result.Exception; } if (!result.Continue) { return; } //user requested if (args.HttpClient.CloseServerConnection) { closeServerConnection = true; return; } // if connection is closing exit if (!response.KeepAlive) { closeServerConnection = true; return; } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } //Release server connection for each HTTP session instead of per client connection. //This will be more efficient especially when client is idly holding server connection //between sessions without using it. //Do not release authenticated connections for performance reasons. //Otherwise it will keep authenticating per session. if (EnableConnectionPool && connection != null && !connection.IsWinAuthenticated) { await tcpConnectionFactory.Release(connection); connection = null; } } catch (Exception e) when(!(e is ProxyHttpException)) { throw new ProxyHttpException("Error occured whilst handling session request", e, args); } } catch (Exception e) { args.Exception = e; closeServerConnection = true; throw; } finally { await invokeAfterResponse(args); args.Dispose(); } } } finally { await tcpConnectionFactory.Release(connection, closeServerConnection); await tcpConnectionFactory.Release(prefetchTask, closeServerConnection); } }
/// <summary> /// This is the core request handler method for a particular connection from client. /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination. /// </summary> /// <param name="endPoint">The proxy endpoint.</param> /// <param name="clientStream">The client stream.</param> /// <param name="cancellationTokenSource">The cancellation token source for this async task.</param> /// <param name="connectArgs">The Connect request if this is a HTTPS request from explicit endpoint.</param> /// <param name="prefetchConnectionTask">Prefetched server connection for current client using Connect/SNI headers.</param> /// <param name="isHttps">Is HTTPS</param> private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, HttpClientStream clientStream, CancellationTokenSource cancellationTokenSource, TunnelConnectSessionEventArgs?connectArgs = null, Task <TcpServerConnection>?prefetchConnectionTask = null, bool isHttps = false) { var connectRequest = connectArgs?.HttpClient.ConnectRequest; var prefetchTask = prefetchConnectionTask; TcpServerConnection?connection = null; bool closeServerConnection = false; try { var cancellationToken = cancellationTokenSource.Token; // Loop through each subsequent request on this particular client connection // (assuming HTTP connection is kept alive by client) while (true) { if (clientStream.IsClosed) { return; } // read the request line var requestLine = await clientStream.ReadRequestLine(cancellationToken); if (requestLine.IsEmpty()) { return; } var args = new SessionEventArgs(this, endPoint, clientStream, connectRequest, cancellationTokenSource) { UserData = connectArgs?.UserData }; var request = args.HttpClient.Request; if (isHttps) { request.IsHttps = true; } try { try { // Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStream, args.HttpClient.Request.Headers, cancellationToken); if (connectRequest != null) { request.IsHttps = connectRequest.IsHttps; request.Authority = connectRequest.Authority; } request.RequestUriString8 = requestLine.RequestUri; request.Method = requestLine.Method; request.HttpVersion = requestLine.Version; // we need this to syphon out data from connection if API user changes them. request.SetOriginalHeaders(); // If user requested interception do it await onBeforeRequest(args); if (!args.IsTransparent && !args.IsSocks) { // proxy authorization check if (connectRequest == null && await checkAuthorization(args) == false) { await onBeforeResponse(args); // send the response await clientStream.WriteResponseAsync(args.HttpClient.Response, cancellationToken); return; } prepareRequestHeaders(request.Headers); request.Host = request.RequestUri.Authority; } // if win auth is enabled // we need a cache of request body // so that we can send it after authentication in WinAuthHandler.cs if (args.EnableWinAuth && request.HasBody) { await args.GetRequestBody(cancellationToken); } var response = args.HttpClient.Response; if (request.CancelRequest) { if (!(Enable100ContinueBehaviour && request.ExpectContinue)) { // syphon out the request body from client before setting the new body await args.SyphonOutBodyAsync(true, cancellationToken); } await handleHttpSessionResponse(args); if (!response.KeepAlive) { return; } continue; } // If prefetch task is available. if (connection == null && prefetchTask != null) { try { connection = await prefetchTask; } catch (SocketException e) { if (e.SocketErrorCode != SocketError.HostNotFound) { throw; } } prefetchTask = null; } if (connection != null) { var socket = connection.TcpSocket; bool part1 = socket.Poll(1000, SelectMode.SelectRead); bool part2 = socket.Available == 0; if (part1 & part2) { //connection is closed await tcpConnectionFactory.Release(connection, true); connection = null; } } // create a new connection if cache key changes. // only gets hit when connection pool is disabled. // or when prefetch task has a unexpectedly different connection. if (connection != null && (await tcpConnectionFactory.GetConnectionCacheKey(this, args, clientStream.Connection.NegotiatedApplicationProtocol) != connection.CacheKey)) { await tcpConnectionFactory.Release(connection); connection = null; } var result = await handleHttpSessionRequest(args, connection, clientStream.Connection.NegotiatedApplicationProtocol, cancellationToken, cancellationTokenSource); // update connection to latest used connection = result.LatestConnection; closeServerConnection = !result.Continue; // throw if exception happened if (result.Exception != null) { throw result.Exception; } if (!result.Continue) { return; } // user requested if (args.HttpClient.CloseServerConnection) { closeServerConnection = true; return; } // if connection is closing exit if (!response.KeepAlive) { closeServerConnection = true; return; } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } // Release server connection for each HTTP session instead of per client connection. // This will be more efficient especially when client is idly holding server connection // between sessions without using it. // Do not release authenticated connections for performance reasons. // Otherwise it will keep authenticating per session. if (EnableConnectionPool && connection != null && !connection.IsWinAuthenticated) { await tcpConnectionFactory.Release(connection); connection = null; } } catch (Exception e) when(!(e is ProxyHttpException)) { throw new ProxyHttpException("Error occured whilst handling session request", e, args); } } catch (Exception e) { args.Exception = e; closeServerConnection = true; throw; } finally { await onAfterResponse(args); args.Dispose(); } } } finally { if (connection != null) { await tcpConnectionFactory.Release(connection, closeServerConnection); } await tcpConnectionFactory.Release(prefetchTask, closeServerConnection); } }
/// <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 the core request handler method for a particular connection from client /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination /// </summary> /// <param name="client"></param> /// <param name="clientStream"></param> /// <param name="clientStreamReader"></param> /// <param name="clientStreamWriter"></param> /// <param name="httpsConnectHostname"></param> /// <param name="endPoint"></param> /// <param name="connectRequest"></param> /// <param name="isTransparentEndPoint"></param> /// <returns></returns> private async Task HandleHttpSessionRequest(TcpClient client, CustomBufferedStream clientStream, CustomBinaryReader clientStreamReader, HttpResponseWriter clientStreamWriter, string httpsConnectHostname, ProxyEndPoint endPoint, ConnectRequest connectRequest, bool isTransparentEndPoint = false) { TcpConnection connection = null; try { //Loop through each subsequest request on this particular client connection //(assuming HTTP connection is kept alive by client) while (true) { // read the request line string httpCmd = await clientStreamReader.ReadLineAsync(); if (string.IsNullOrEmpty(httpCmd)) { break; } var args = new SessionEventArgs(BufferSize, endPoint, ExceptionFunc) { ProxyClient = { TcpClient = client }, WebSession = { ConnectRequest = connectRequest } }; try { Request.ParseRequestLine(httpCmd, out string httpMethod, out string httpUrl, out var version); //Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.Headers); Uri httpRemoteUri; if (uriSchemeRegex.IsMatch(httpUrl)) { try { httpRemoteUri = new Uri(httpUrl); } catch (Exception ex) { throw new Exception($"Invalid URI: '{httpUrl}'", ex); } } else { string host = args.WebSession.Request.Host ?? httpsConnectHostname; string hostAndPath = host; if (httpUrl.StartsWith("/")) { hostAndPath += httpUrl; } string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://", hostAndPath); try { httpRemoteUri = new Uri(url); } catch (Exception ex) { throw new Exception($"Invalid URI: '{url}'", ex); } } args.WebSession.Request.RequestUri = httpRemoteUri; args.WebSession.Request.OriginalUrl = httpUrl; args.WebSession.Request.Method = httpMethod; args.WebSession.Request.HttpVersion = version; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamReader = clientStreamReader; args.ProxyClient.ClientStreamWriter = clientStreamWriter; //proxy authorization check if (!args.IsTransparent && httpsConnectHostname == null && await CheckAuthorization(clientStreamWriter, args) == false) { break; } if (!isTransparentEndPoint) { PrepareRequestHeaders(args.WebSession.Request.Headers); args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority; } //if win auth is enabled //we need a cache of request body //so that we can send it after authentication in WinAuthHandler.cs if (isWindowsAuthenticationEnabledAndSupported && args.WebSession.Request.HasBody) { await args.GetRequestBody(); } //If user requested interception do it if (BeforeRequest != null) { await BeforeRequest.InvokeAsync(this, args, ExceptionFunc); } var response = args.WebSession.Response; if (args.WebSession.Request.CancelRequest) { await HandleHttpSessionResponse(args); if (!response.KeepAlive) { break; } continue; } //create a new connection if hostname/upstream end point changes if (connection != null && (!connection.HostName.Equals(args.WebSession.Request.RequestUri.Host, StringComparison.OrdinalIgnoreCase) || (args.WebSession.UpStreamEndPoint != null && !args.WebSession.UpStreamEndPoint.Equals(connection.UpStreamEndPoint)))) { connection.Dispose(); connection = null; } if (connection == null) { connection = await GetServerConnection(args, false); } //if upgrading to websocket then relay the requet without reading the contents if (args.WebSession.Request.UpgradeToWebSocket) { //prepare the prefix content var requestHeaders = args.WebSession.Request.Headers; await connection.StreamWriter.WriteLineAsync(httpCmd); await connection.StreamWriter.WriteHeadersAsync(requestHeaders); string httpStatus = await connection.StreamReader.ReadLineAsync(); Response.ParseResponseLine(httpStatus, out var responseVersion, out int responseStatusCode, out string responseStatusDescription); response.HttpVersion = responseVersion; response.StatusCode = responseStatusCode; response.StatusDescription = responseStatusDescription; await HeaderParser.ReadHeaders(connection.StreamReader, response.Headers); if (!args.IsTransparent) { await clientStreamWriter.WriteResponseAsync(response); } //If user requested call back then do it if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked) { await BeforeResponse.InvokeAsync(this, args, ExceptionFunc); } await TcpHelper.SendRaw(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { args.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); }, ExceptionFunc); break; } //construct the web request that we are going to issue on behalf of the client. await HandleHttpSessionRequestInternal(connection, args); //if connection is closing exit if (!response.KeepAlive) { break; } } catch (Exception e) when(!(e is ProxyHttpException)) { throw new ProxyHttpException("Error occured whilst handling session request", e, args); } finally { args.Dispose(); } } } finally { connection?.Dispose(); } }
/// <summary> /// This is the core request handler method for a particular connection from client. /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination. /// </summary> /// <param name="endPoint">The proxy endpoint.</param> /// <param name="clientConnection">The client connection.</param> /// <param name="clientStream">The client stream.</param> /// <param name="clientStreamWriter">The client stream writer.</param> /// <param name="cancellationTokenSource">The cancellation token source for this async task.</param> /// <param name="httpsConnectHostname"> /// The https hostname as appeared in CONNECT request if this is a HTTPS request from /// explicit endpoint. /// </param> /// <param name="connectRequest">The Connect request if this is a HTTPS request from explicit endpoint.</param> /// <param name="prefetchConnectionTask">Prefetched server connection for current client using Connect/SNI headers.</param> private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection, CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, CancellationTokenSource cancellationTokenSource, string httpsConnectHostname, ConnectRequest connectRequest, Task <TcpServerConnection> prefetchConnectionTask = null) { var prefetchTask = prefetchConnectionTask; TcpServerConnection connection = null; bool closeServerConnection = false; try { var cancellationToken = cancellationTokenSource.Token; System.Diagnostics.Debug.WriteLine("{2}:HttpSessionRequestHandler: ReqURL: {0}{1}", connectRequest == null ? string.Empty : connectRequest.RequestUri.ToString(), string.Empty, System.Threading.Thread.CurrentThread.ManagedThreadId); // Loop through each subsequest request on this particular client connection // (assuming HTTP connection is kept alive by client) while (true) { // read the request line string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(httpCmd)) { return; } var args = new SessionEventArgs(this, endPoint, cancellationTokenSource) { ProxyClient = { ClientConnection = clientConnection }, WebSession = { ConnectRequest = connectRequest } }; try { try { string httpMethod; string httpUrl; Version version; Request.ParseRequestLine(httpCmd, out httpMethod, out httpUrl, out version); // Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStream, args.WebSession.Request.Headers, cancellationToken); Uri httpRemoteUri; if (uriSchemeRegex.IsMatch(httpUrl)) { try { httpRemoteUri = new Uri(httpUrl); } catch (Exception ex) { throw new Exception($"Invalid URI: '{httpUrl}'", ex); } } else { string host = args.WebSession.Request.Host ?? httpsConnectHostname; string hostAndPath = host; if (httpUrl.StartsWith("/")) { hostAndPath += httpUrl; } string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://", hostAndPath); try { httpRemoteUri = new Uri(url); } catch (Exception ex) { throw new Exception($"Invalid URI: '{url}'", ex); } } var request = args.WebSession.Request; request.RequestUri = httpRemoteUri; request.OriginalUrl = httpUrl; request.Method = httpMethod; request.HttpVersion = version; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamWriter = clientStreamWriter; if (!args.IsTransparent) { // proxy authorization check if (httpsConnectHostname == null && await checkAuthorization(args) == false) { await invokeBeforeResponse(args); // send the response await clientStreamWriter.WriteResponseAsync(args.WebSession.Response, cancellationToken : cancellationToken); return; } prepareRequestHeaders(request.Headers); request.Host = request.RequestUri.Authority; } // if win auth is enabled // we need a cache of request body // so that we can send it after authentication in WinAuthHandler.cs if (isWindowsAuthenticationEnabledAndSupported && request.HasBody) { await args.GetRequestBody(cancellationToken); } //we need this to syphon out data from connection if API user changes them. request.SetOriginalHeaders(); // If user requested interception do it await invokeBeforeRequest(args); var response = args.WebSession.Response; if (request.CancelRequest) { // syphon out the request body from client before setting the new body await args.SyphonOutBodyAsync(true, cancellationToken); await handleHttpSessionResponse(args); if (!response.KeepAlive) { return; } continue; } //If prefetch task is available. if (connection == null && prefetchTask != null) { connection = await prefetchTask; prefetchTask = null; } // create a new connection if cache key changes. // only gets hit when connection pool is disabled. // or when prefetch task has a unexpectedly different connection. if (connection != null && (await tcpConnectionFactory.GetConnectionCacheKey(this, args, clientConnection.NegotiatedApplicationProtocol) != connection.CacheKey)) { await tcpConnectionFactory.Release(connection); connection = null; } //a connection generator task with captured parameters via closure. Func <Task <TcpServerConnection> > generator = () => tcpConnectionFactory.GetServerConnection(this, args, isConnect: false, applicationProtocol: clientConnection.NegotiatedApplicationProtocol, noCache: false, cancellationToken: cancellationToken); //for connection pool, retry fails until cache is exhausted. var result = await retryPolicy <ServerConnectionException>().ExecuteAsync(async(serverConnection) => { // if upgrading to websocket then relay the request without reading the contents if (request.UpgradeToWebSocket) { await handleWebSocketUpgrade(httpCmd, args, request, response, clientStream, clientStreamWriter, serverConnection, cancellationTokenSource, cancellationToken); closeServerConnection = true; return(false); } // construct the web request that we are going to issue on behalf of the client. await handleHttpSessionRequestInternal(serverConnection, args); return(true); }, generator, connection); //update connection to latest used connection = result.LatestConnection; //throw if exception happened if (!result.IsSuccess) { throw result.Exception; } if (!result.Continue) { return; } //user requested if (args.WebSession.CloseServerConnection) { closeServerConnection = true; return; } // if connection is closing exit if (!response.KeepAlive) { closeServerConnection = true; return; } if (cancellationTokenSource.IsCancellationRequested) { throw new Exception("Session was terminated by user."); } //Get/release server connection for each HTTP session instead of per client connection. //This will be more efficient especially when client is idly holding server connection //between sessions without using it. //Do not release authenticated connections for performance reasons. //Otherwise it will keep authenticating per session. if (EnableConnectionPool && connection != null && !connection.IsWinAuthenticated) { await tcpConnectionFactory.Release(connection); connection = null; } } catch (Exception e) when(!(e is ProxyHttpException)) { throw new ProxyHttpException("Error occured whilst handling session request", e, args); } } catch (Exception e) { args.Exception = e; closeServerConnection = true; throw; } finally { await invokeAfterResponse(args); args.Dispose(); } } } finally { await tcpConnectionFactory.Release(connection, closeServerConnection); await tcpConnectionFactory.Release(prefetchTask, closeServerConnection); } }
/// <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(); } } }