/// <summary> /// This is called when this proxy acts as a reverse proxy (like a real http server) /// So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client /// </summary> /// <param name="endPoint"></param> /// <param name="tcpClient"></param> /// <returns></returns> private async Task HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient) { bool disposed = false; var clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); CustomBinaryReader clientStreamReader = null; HttpResponseWriter clientStreamWriter = null; try { if (endPoint.EnableSsl) { var clientSslHelloInfo = await SslTools.GetClientHelloInfo(clientStream); if (clientSslHelloInfo != null) { var sslStream = new SslStream(clientStream); clientStream = new CustomBufferedStream(sslStream, BufferSize); string sniHostName = clientSslHelloInfo.Extensions?.FirstOrDefault(x => x.Name == "server_name")?.Data; string certName = HttpHelper.GetWildCardDomainName(sniHostName ?? endPoint.GenericCertificateName); var certificate = CertificateManager.CreateCertificate(certName, false); //Successfully managed to authenticate the client using the fake certificate await sslStream.AuthenticateAsServerAsync(certificate, false, SslProtocols.Tls, false); } //HTTPS server created - we can now decrypt the client's traffic } clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); clientStreamWriter = new HttpResponseWriter(clientStream); //now read the request line string httpCmd = await clientStreamReader.ReadLineAsync(); //Now create the request disposed = await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, endPoint.EnableSsl?endPoint.GenericCertificateName : null, endPoint, null, true); } 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) { 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); } } }
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 clientHello = SslTools.GetClientHelloInfo(new CustomBufferedStream(ms, (int)ms.Length)).Result; if (clientHello != 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, 0xE, 0x0, 0xC, 2, (byte)'h', (byte)'2', 8, (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'1' }; int newByteCount = clientHello.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 + clientHello.EntensionsStartPosition; int endPos = offset + clientHello.ClientHelloLength; if (clientHello.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 = clientHello.ClientHelloLength; i < count; i++) { buffer2[offset + newByteCount + i] = buffer[offset + i]; } buffer = buffer2; count += newByteCount; } stream.Write(buffer, offset, count); }