/// <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);
                }
            }
        }
Example #3
0
        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);
        }