jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.82k stars 1.91k forks source link

Dynamic update of TLS version for Jetty client and close existing connection gracefully #12059

Open MohammadNC opened 1 month ago

MohammadNC commented 1 month ago

Jetty Version = 12.0.10

Spring Boot Version = 3.2.7

Java Version = 17

Question I am using jetty as a client to send traffic by using the https with TLSv1.2 or TLSv1.3 version.

below are questions.

  1. How to close Existing jetty client connection gracefully.
  2. Need to update the Jetty TLS version without impacting the existing Connections. let's say for Server1 there exists connection and after that I want to update the TLS version dynamically so, that next request to server2 should use the new connection with the latest TLS configuration, but existing connection should remain as is and allow traffic with old TLS config.

below code snippet get the webclient.

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.ssl.SslHandshakeListener;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import org.eclipse.jetty.http.HttpScheme;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SelectableChannel;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;

@Configuration
public class JettyClientConfig {
    private static final Logger logger = LogManager.getLogger(JettyClientConfig.class.getName());

    private static org.eclipse.jetty.client.HttpClient httpsClient;
    public static final String REMOTE_SOCKE_INET_ADDRESS = "org.eclipse.jetty.client.connector.remoteSocketInetAddress";

//    ReloadableX509TrustManager reloadableX509TrustManager;

//    ReloadableX509KeyManager reloadableX509KeyManager;

    @Bean(name = "testWebClient")
    public WebClient getWebClient() throws IOException {
        String extUrl = "http://localhost:9090/dest";
        ClientHttpConnector httpConnector = new JettyClientHttpConnector(getHttpClient());
        return WebClient.builder().clientConnector(httpConnector).baseUrl(extUrl).build();
    }

    // Used for Egress side HTTP over TLS Client
    public org.eclipse.jetty.client.HttpClient getHttpClient() throws IOException {
        SslContextFactory sslContextFactory = new SslContextFactory.Client(true) {
            @Override
            public void customize(SSLEngine sslEngine) {
                sslEngine.setSSLParameters(customize(sslEngine.getSSLParameters()));
                if (logger.isInfoEnabled()) {
                    logger.info("Jetty-H2-Client: SSLEngine: {}", sslEngine);
                }
            }
        };
        ClientConnector clientConnector = new ClientConnector() {
            protected void configure(SelectableChannel selectable) throws IOException {
                super.configure(selectable);
                if (selectable instanceof NetworkChannel) {
                    NetworkChannel channel = (NetworkChannel)selectable;
                    channel.setOption(java.net.StandardSocketOptions.SO_KEEPALIVE,
                            true);
                    // Set keepalive parameters only if it is enabled
/*                    if(tcpConfigOptionProvider.getTcpKeepalive().getEnable()) {
                        channel.setOption(jdk.net.ExtendedSocketOptions.TCP_KEEPIDLE,
                                Integer.parseInt(StringUtils.chop(tcpConfigOptionProvider.getTcpKeepalive().getTime())));
                        channel.setOption(jdk.net.ExtendedSocketOptions.TCP_KEEPINTERVAL,
                                Integer.parseInt(StringUtils.chop(tcpConfigOptionProvider.getTcpKeepalive().getInterval())));
                        channel.setOption(jdk.net.ExtendedSocketOptions.TCP_KEEPCOUNT,
                                tcpConfigOptionProvider.getTcpKeepalive().getProbes());
                    }*/
                }
            }

            protected void connectFailed(Throwable failure, Map<String, Object> context) {
                if (logger.isInfoEnabled()) {
                    logger.info("Jetty-H2-Client: ClientConnector:: connectFailed() context {}",
                            context.get(REMOTE_SOCKE_INET_ADDRESS));
                }

                super.connectFailed(failure, context);
            }

            public void connect(SocketAddress address, Map<String, Object> context) {
                if (logger.isInfoEnabled()) {
                    logger.info("Jetty-H2-Client: Connecting to {}", address);
                }
                if (context != null) {
                    context.put(REMOTE_SOCKE_INET_ADDRESS, address);
                }
                super.connect(address, context);
            }
        };
        clientConnector.setSslContextFactory((SslContextFactory.Client) sslContextFactory);

        sslContextFactory.setEndpointIdentificationAlgorithm(null);
        updateTlsVersionAndCiphers(sslContextFactory);

        HTTP2Client http2Client = new HTTP2Client(clientConnector);
        // HTTP2Client http2Client = new HTTP2Client();
        http2Client.setMaxConcurrentPushedStreams(1000);
        org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2 transport = new org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2(
                http2Client);
        transport.setUseALPN(true);

        org.eclipse.jetty.client.HttpClient httpClient = new org.eclipse.jetty.client.HttpClient(
                transport) {
            @Override
            protected void doStart() throws Exception {
                super.doStart();
            }

            @Override
            public Origin createOrigin(Request request, Origin.Protocol protocol)
            {
                String scheme = request.getScheme();
                if (!HttpScheme.HTTP.is(scheme) && !HttpScheme.HTTPS.is(scheme) &&
                        !HttpScheme.WS.is(scheme) && !HttpScheme.WSS.is(scheme))
                    throw new IllegalArgumentException("Invalid protocol " + scheme);
                scheme = scheme.toLowerCase(Locale.ENGLISH);
                String host = request.getHost();
                host = host.toLowerCase(Locale.ENGLISH);

                List<org.eclipse.jetty.http.HttpCookie> cookies = request.getCookies();
                if (logger.isInfoEnabled()) {
                    logger.info("Jetty-H2-Client: cookies found in request: {}", cookies);
                }
                String ip = getIpFromCookies(cookies);
                if(StringUtils.isNotBlank(ip)) {
                    host = ip;
                }
                /**
                 * Overriding the implementation from jetty client- end
                 */
                int port = request.getPort();
                port = normalizePort(scheme, port);
                return new Origin(scheme, host, port, request.getTag(), protocol);
            }

            private String getIpFromCookies(List<org.eclipse.jetty.http.HttpCookie> cookies) {
                String ip = "";
                if(!cookies.isEmpty()) {
                    Iterator<org.eclipse.jetty.http.HttpCookie> itr = cookies.iterator();
                    while(itr.hasNext()) {
                        HttpCookie httpCookie = itr.next();
                        if(httpCookie.getName().equals("customSource")) {
                            ip = httpCookie.getValue();
                            logger.info("Jetty-H2-Client: cookie found: {}", ip);
                            itr.remove();
                            break;
                        }
                    }
                }
                return ip;
            }
        };

        httpClient.setIdleTimeout(720000);
        httpClient.setMaxRequestsQueuedPerDestination(5000);
        httpClient.setMaxConnectionsPerDestination(4);
        httpClient.setUserAgentField(null);
        httpClient.setConnectTimeout(1000);

        // Add SslHandshakeListener
        httpClient.addBean(new SslHandshakeListener() {
            @Override
            public void handshakeSucceeded(Event event) {
                logger.debug("Handshake is success");
            }
            @Override
            public void handshakeFailed(Event event, Throwable failure) {
                logger.debug("Handshake is Failed");
            }
        });

        try {
            httpClient.start();
        } catch (Exception e) {
            logger.error("exception during client start: {}", e);
        }
        setHttpsclient(httpClient);
        return httpClient;
    }

    public void updateTlsVersionAndCiphers(SslContextFactory sslContextFactory) {
        try {
            String[] ciphers = null;
            String tlsVersion= "";

            TLSConfigurationData tlsCiphersConfigData = getTlsConfigData();
            tlsVersion = tlsCiphersConfigData.getTlsVersion();
            if ("TLSv1.3".equals(tlsVersion)) {
                ciphers = tlsCiphersConfigData.getTls13Ciphers().toArray(new String[0]);
            } else if ("TLSv1.2".equals(tlsVersion)) {
                ciphers = tlsCiphersConfigData.getTls12Ciphers().toArray(new String[0]);
            } else {
                ciphers = Stream.concat(tlsCiphersConfigData.getTls12Ciphers().stream(),
                        tlsCiphersConfigData.getTls13Ciphers().stream()).toList().toArray(new String[0]);
            }
            sslContextFactory.setIncludeCipherSuites(ciphers);
            sslContextFactory.setIncludeProtocols(tlsVersion.split(","));
            sslContextFactory.setSslContext(getSSLContext(tlsVersion));
        } catch (Exception e) {
            logger.error("Excepiton occured :{}", e.getMessage());
        }
    }

    private SSLContext getSSLContext(String tlsVersion) throws Exception {
        SSLContext sslContext = "TLSv1.2".equals(tlsVersion) ?
                SSLContext.getInstance("TLSv1.2") :
                SSLContext.getInstance("TLSv1.3");
        sslContext.init(new KeyManager[] { reloadableX509KeyManager },
                new TrustManager[] { reloadableX509TrustManager }, null);
        return sslContext;
    }

    public static void setHttpsclient(org.eclipse.jetty.client.HttpClient httpsClient) {
        httpsClient = httpsClient;
    }

}
sbordet commented 1 month ago

Need to update the Jetty TLS version without impacting the existing Connections. let's say for Server1 there exists connection and after that I want to update the TLS version dynamically so, that next request to server2 should use the new connection with the latest TLS configuration, but existing connection should remain as is and allow traffic with old TLS config.

I think it's best to use 2 different HttpClient configured differently. You use the first for server1, and the second for server2.

MohammadNC commented 1 month ago

Hi @sbordet , thank you for your response. please let me know. How to close Existing jetty client connection gracefully.

sbordet commented 1 month ago

@MohammadNC why you want to close connections? These are typically managed internally and applications should not worry.