jetty.project icon indicating copy to clipboard operation
jetty.project copied to clipboard

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

Open MohammadNC opened this issue 1 year ago • 3 comments
trafficstars

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;
    }


}

MohammadNC avatar Jul 18 '24 08:07 MohammadNC

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.

sbordet avatar Jul 18 '24 12:07 sbordet

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

MohammadNC avatar Jul 18 '24 13:07 MohammadNC

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

sbordet avatar Jul 18 '24 13:07 sbordet

This issue has been automatically marked as stale because it has been a full year without activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Aug 23 '25 00:08 github-actions[bot]

Closed as answered.

sbordet avatar Aug 24 '25 08:08 sbordet