Add SNI Support based on Host header

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java
index 60c5e83..37183d7 100644
--- a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java
+++ b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -24,6 +24,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.Socket;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -37,6 +38,7 @@
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
+import javax.net.ssl.SSLSocket;
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.client.Client;
 import javax.ws.rs.core.Configuration;
@@ -53,6 +55,7 @@
 import org.glassfish.jersey.client.ClientResponse;
 import org.glassfish.jersey.client.RequestEntityProcessing;
 import org.glassfish.jersey.client.innate.ClientProxy;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
 import org.glassfish.jersey.client.spi.Connector;
 import org.glassfish.jersey.internal.util.PropertiesHelper;
@@ -103,6 +106,7 @@
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.http.impl.io.ChunkedOutputStream;
 import org.apache.http.io.SessionOutputBuffer;
+import org.apache.http.protocol.HttpContext;
 import org.apache.http.util.TextUtils;
 import org.apache.http.util.VersionInfo;
 
@@ -180,6 +184,7 @@
 class ApacheConnector implements Connector {
 
     private static final Logger LOGGER = Logger.getLogger(ApacheConnector.class.getName());
+    private static final String JERSEY_REQUEST_ATTR_NAME = "JerseyRequestAttribute";
     private static final VersionInfo vi;
     private static final String release;
 
@@ -381,15 +386,15 @@
 
         final LayeredConnectionSocketFactory sslSocketFactory;
         if (sslContext != null) {
-            sslSocketFactory = new SSLConnectionSocketFactory(
+            sslSocketFactory = new SniSSLConnectionSocketFactory(
                     sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
         } else {
             if (useSystemProperties) {
-                sslSocketFactory = new SSLConnectionSocketFactory(
+                sslSocketFactory = new SniSSLConnectionSocketFactory(
                         (SSLSocketFactory) SSLSocketFactory.getDefault(),
                         supportedProtocols, supportedCipherSuites, hostnameVerifier);
             } else {
-                sslSocketFactory = new SSLConnectionSocketFactory(
+                sslSocketFactory = new SniSSLConnectionSocketFactory(
                         SSLContexts.createDefault(),
                         hostnameVerifier);
             }
@@ -450,14 +455,16 @@
     public ClientResponse apply(final ClientRequest clientRequest) throws ProcessingException {
         final HttpUriRequest request = getUriHttpRequest(clientRequest);
         final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(clientRequest, request);
+        final HttpHost httpHost = getHost(request);
 
         try {
             final CloseableHttpResponse response;
             final HttpClientContext context = HttpClientContext.create();
+
             if (preemptiveBasicAuth) {
                 final AuthCache authCache = new BasicAuthCache();
                 final BasicScheme basicScheme = new BasicScheme();
-                authCache.put(getHost(request), basicScheme);
+                authCache.put(httpHost, basicScheme);
                 context.setAuthCache(authCache);
             }
 
@@ -468,7 +475,8 @@
                 context.setCredentialsProvider(credentialsProvider);
             }
 
-            response = client.execute(getHost(request), request, context);
+            context.setAttribute(JERSEY_REQUEST_ATTR_NAME, clientRequest);
+            response = client.execute(httpHost, request, context);
             HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(),
                     this.getClass().getName(), clientRequest.getConfiguration());
 
@@ -821,4 +829,56 @@
             return super.createOutputStream(len, outbuffer);
         }
     }
+
+    private static final class SniSSLConnectionSocketFactory extends SSLConnectionSocketFactory {
+
+        private final ThreadLocal<HttpContext> httpContexts = new ThreadLocal<>();
+
+        public SniSSLConnectionSocketFactory(final SSLContext sslContext,
+                                             final String[] supportedProtocols,
+                                             final String[] supportedCipherSuites,
+                                             final HostnameVerifier hostnameVerifier) {
+            super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+        }
+
+        public SniSSLConnectionSocketFactory(final javax.net.ssl.SSLSocketFactory socketFactory,
+                                             final String[] supportedProtocols,
+                                             final String[] supportedCipherSuites,
+                                             final HostnameVerifier hostnameVerifier) {
+            super(socketFactory, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+        }
+
+        public SniSSLConnectionSocketFactory(
+                final SSLContext sslContext, final HostnameVerifier hostnameVerifier) {
+            super(sslContext, hostnameVerifier);
+        }
+
+        @Override
+        public Socket createLayeredSocket(
+                final Socket socket,
+                final String target,
+                final int port,
+                final HttpContext context) throws IOException {
+            httpContexts.set(context);
+            try {
+                return super.createLayeredSocket(socket, target, port, context);
+            } finally {
+                httpContexts.remove();
+            }
+        }
+
+        @Override
+        protected void prepareSocket(SSLSocket socket) throws IOException {
+            HttpContext context = httpContexts.get();
+
+            if (context != null) {
+                Object objectRequest = context.getAttribute(JERSEY_REQUEST_ATTR_NAME);
+                if (objectRequest != null) {
+                    ClientRequest clientRequest = (ClientRequest) objectRequest;
+                    SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(clientRequest).build();
+                    sniConfig.setSNIServerName(socket);
+                }
+            }
+        }
+    }
 }
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
index da7ccdd..eb36bdf 100644
--- a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -24,6 +24,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.Socket;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
@@ -37,6 +38,7 @@
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
+import javax.net.ssl.SSLSocket;
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.client.Client;
 import javax.ws.rs.core.Configuration;
@@ -48,6 +50,7 @@
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
 
+import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
 import org.apache.hc.client5.http.HttpRequestRetryStrategy;
 import org.apache.hc.client5.http.auth.AuthCache;
@@ -65,6 +68,7 @@
 import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
+import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -86,6 +90,7 @@
 import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
 import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
 import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
+import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.ssl.SSLContexts;
 import org.apache.hc.core5.util.TextUtils;
 import org.apache.hc.core5.util.Timeout;
@@ -95,6 +100,7 @@
 import org.glassfish.jersey.client.ClientResponse;
 import org.glassfish.jersey.client.RequestEntityProcessing;
 import org.glassfish.jersey.client.innate.ClientProxy;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
 import org.glassfish.jersey.client.spi.Connector;
 import org.glassfish.jersey.internal.util.PropertiesHelper;
@@ -176,6 +182,7 @@
 class Apache5Connector implements Connector {
 
     private static final Logger LOGGER = Logger.getLogger(Apache5Connector.class.getName());
+    private static final String JERSEY_REQUEST_ATTR_NAME = "JerseyRequestAttribute";
     private static final VersionInfo vi;
     private static final String release;
 
@@ -385,15 +392,15 @@
 
         final LayeredConnectionSocketFactory sslSocketFactory;
         if (sslContext != null) {
-            sslSocketFactory = new SSLConnectionSocketFactory(
+            sslSocketFactory = new SniSSLConnectionSocketFactory(
                     sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
         } else {
             if (useSystemProperties) {
-                sslSocketFactory = new SSLConnectionSocketFactory(
+                sslSocketFactory = new SniSSLConnectionSocketFactory(
                         (SSLSocketFactory) SSLSocketFactory.getDefault(),
                         supportedProtocols, supportedCipherSuites, hostnameVerifier);
             } else {
-                sslSocketFactory = new SSLConnectionSocketFactory(
+                sslSocketFactory = new SniSSLConnectionSocketFactory(
                         SSLContexts.createDefault(),
                         hostnameVerifier);
             }
@@ -458,12 +465,7 @@
         try {
             final CloseableHttpResponse response;
             final HttpClientContext context = HttpClientContext.create();
-            if (preemptiveBasicAuth) {
-                final AuthCache authCache = new BasicAuthCache();
-                final BasicScheme basicScheme = new BasicScheme();
-                authCache.put(getHost(request), basicScheme);
-                context.setAuthCache(authCache);
-            }
+            final HttpHost httpHost = getHost(request);
 
             // If a request-specific CredentialsProvider exists, use it instead of the default one
             CredentialsProvider credentialsProvider =
@@ -472,7 +474,18 @@
                 context.setCredentialsProvider(credentialsProvider);
             }
 
-            response = client.execute(getHost(request), request, context);
+            if (preemptiveBasicAuth) {
+                final AuthCache authCache = new BasicAuthCache();
+                final BasicScheme basicScheme = new BasicScheme();
+                final AuthScope authScope = new AuthScope(httpHost);
+                basicScheme.initPreemptive(credentialsProvider.getCredentials(authScope, context));
+                context.resetAuthExchange(httpHost, basicScheme);
+                authCache.put(httpHost, basicScheme); // must be after initPreemptive
+                context.setAuthCache(authCache);
+            }
+
+            context.setAttribute(JERSEY_REQUEST_ATTR_NAME, clientRequest);
+            response = client.execute(httpHost, request, context);
             HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(),
                     this.getClass().getName(), clientRequest.getConfiguration());
 
@@ -798,4 +811,77 @@
             );
         }
     }
+
+    private static final class SniSSLConnectionSocketFactory extends SSLConnectionSocketFactory {
+
+        private final ThreadLocal<HttpContext> httpContexts = new ThreadLocal<>();
+
+        public SniSSLConnectionSocketFactory(final SSLContext sslContext,
+                                             final String[] supportedProtocols,
+                                             final String[] supportedCipherSuites,
+                                             final HostnameVerifier hostnameVerifier) {
+            super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+        }
+
+        public SniSSLConnectionSocketFactory(final javax.net.ssl.SSLSocketFactory socketFactory,
+                                             final String[] supportedProtocols,
+                                             final String[] supportedCipherSuites,
+                                             final HostnameVerifier hostnameVerifier) {
+            super(socketFactory, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+        }
+
+        public SniSSLConnectionSocketFactory(
+                final SSLContext sslContext, final HostnameVerifier hostnameVerifier) {
+            super(sslContext, hostnameVerifier);
+        }
+
+        /* Pre 5.2 */
+        @Override
+        public Socket createLayeredSocket(
+                final Socket socket,
+                final String target,
+                final int port,
+                final HttpContext context) throws IOException {
+            httpContexts.set(context);
+            try {
+                return super.createLayeredSocket(socket, target, port, context);
+            } finally {
+                httpContexts.remove();
+            }
+        }
+
+        /* Post 5.2 */
+        public Socket createLayeredSocket(
+                final Socket socket,
+                final String target,
+                final int port,
+                final Object attachment,
+                final HttpContext context) throws IOException {
+            httpContexts.set(context);
+            try {
+                return super.createLayeredSocket(socket, target, port, attachment, context);
+            } finally {
+                httpContexts.remove();
+            }
+        }
+
+        @Override
+        protected void prepareSocket(SSLSocket socket) throws IOException {
+            HttpContext context =  httpContexts.get();
+
+            if (context != null) {
+                Object objectRequest = context.getAttribute(JERSEY_REQUEST_ATTR_NAME);
+                if (objectRequest != null) {
+                    ClientRequest clientRequest = (ClientRequest) objectRequest;
+                    SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(clientRequest).build();
+                    sniConfig.setSNIServerName(socket);
+
+                    final int socketTimeout = ((ClientRequest) objectRequest).resolveProperty(ClientProperties.READ_TIMEOUT, -1);
+                    if (socketTimeout >= 0) {
+                        socket.setSoTimeout(socketTimeout);
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java
index 612d540..afb0c6c 100644
--- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java
+++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -19,6 +19,7 @@
 import java.net.CookiePolicy;
 import java.net.URI;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -30,6 +31,7 @@
 
 import org.glassfish.jersey.SslConfigurator;
 import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.glassfish.jersey.jdk.connector.JdkConnectorProperties;
 
 /**
@@ -57,6 +59,7 @@
     private final int responseTimeout;
     private final int connectTimeout;
     private final ProxyConfiguration proxyConfiguration;
+    private final AtomicReference<SSLParamConfigurator> sniConfigs = new AtomicReference<>(null);
 
     ConnectorConfiguration(Client client, Configuration config) {
         final Map<String, Object> properties = config.getProperties();
@@ -170,6 +173,14 @@
         return proxyConfiguration;
     }
 
+    void setSniConfig(SSLParamConfigurator sniConfig) {
+        this.sniConfigs.compareAndSet(null, sniConfig);
+    }
+
+    SSLParamConfigurator getSniConfig() {
+        return sniConfigs.get();
+    }
+
     @Override
     public String toString() {
         return "ConnectorConfiguration{"
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java
index 2330dd4..d9ea76e 100644
--- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java
+++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -199,7 +199,8 @@
 
             }
 
-            socket = new SslFilter(transportFilter, sslContext, uri.getHost(), configuration.getHostnameVerifier());
+            socket = new SslFilter(transportFilter, sslContext, uri.getHost(),
+                            configuration.getHostnameVerifier(), configuration.getSniConfig());
         } else {
             socket = new TransportFilter(INPUT_BUFFER_SIZE, configuration.getThreadPoolConfig(),
                     configuration.getContainerIdleTimeout());
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java
index 28c7500..b20f00c 100644
--- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java
+++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,7 +16,11 @@
 
 package org.glassfish.jersey.jdk.connector.internal;
 
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
+
 import java.net.CookieManager;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
@@ -45,10 +49,16 @@
     }
 
     void send(HttpRequest httpRequest, CompletionHandler<HttpResponse> completionHandler) {
+        final Map<String, List<Object>> headers = new HashMap<>();
+        httpRequest.getHeaders().forEach((k, v) -> headers.put(k, (List) v));
+        final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().uri(httpRequest.getUri()).headers(headers).build();
+        connectorConfiguration.setSniConfig(sniConfig);
+
         final DestinationConnectionPool.DestinationKey destinationKey = new DestinationConnectionPool.DestinationKey(
-                httpRequest.getUri());
+                sniConfig.getSNIUri());
         DestinationConnectionPool destinationConnectionPool = destinationPools.get(destinationKey);
 
+
         if (destinationConnectionPool == null) {
             synchronized (this) {
                 // check again while holding the lock
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java
index 85f4541..979a9ce 100644
--- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java
+++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,6 +16,8 @@
 
 package org.glassfish.jersey.jdk.connector.internal;
 
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
+
 import java.nio.ByteBuffer;
 import java.nio.Buffer;
 import java.util.LinkedList;
@@ -26,7 +28,6 @@
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
 import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLParameters;
 
 
 /**
@@ -87,7 +88,8 @@
     SslFilter(Filter<ByteBuffer, ByteBuffer, ?, ?> downstreamFilter,
               SSLContext sslContext,
               String serverHost,
-              HostnameVerifier customHostnameVerifier) {
+              HostnameVerifier customHostnameVerifier,
+              SSLParamConfigurator sniConfig) {
         super(downstreamFilter);
         this.serverHost = serverHost;
         sslEngine = sslContext.createSSLEngine(serverHost, -1);
@@ -100,11 +102,11 @@
          * when {@link SslEngineConfigurator} supports Java 7.
          */
         if (customHostnameVerifier == null) {
-            SSLParameters sslParameters = sslEngine.getSSLParameters();
-            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
-            sslEngine.setSSLParameters(sslParameters);
+            sniConfig.setEndpointIdentificationAlgorithm(sslEngine);
         }
 
+        sniConfig.setSNIServerName(sslEngine);
+
         applicationInputBuffer = ByteBuffer.allocate(sslEngine.getSession().getApplicationBufferSize());
         networkOutputBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize());
     }
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java
index 5d205ce..ef13997 100644
--- a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java
+++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,10 +23,12 @@
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.SocketException;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -41,7 +43,9 @@
 import javax.net.ssl.SSLServerSocketFactory;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocket;
+
 import org.glassfish.jersey.SslConfigurator;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ConditionEvaluationResult;
 import org.junit.jupiter.api.extension.ExecutionCondition;
@@ -391,7 +395,10 @@
                 .keyStorePassword("asdfgh");
 
         TransportFilter transportFilter = new TransportFilter(17_000, ThreadPoolConfig.defaultConfig(), 100_000);
-        final SslFilter sslFilter = new SslFilter(transportFilter, sslConfig.createSSLContext(), host, customHostnameVerifier);
+        final SSLParamConfigurator sslParamConfigurator = SSLParamConfigurator.builder()
+                .uri(URI.create("Https://" + host)).headers(Collections.emptyMap()).build();
+        final SslFilter sslFilter = new SslFilter(
+                transportFilter, sslConfig.createSSLContext(), host, customHostnameVerifier, sslParamConfigurator);
 
         // exceptions errors that occur before SSL handshake has finished are thrown from this method
         final AtomicReference<Throwable> exception = new AtomicReference<>();
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
index 8bb117a..9406b51 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -36,9 +36,6 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
-
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.client.Client;
 import javax.ws.rs.core.Configuration;
@@ -83,6 +80,7 @@
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.client.ClientResponse;
 import org.glassfish.jersey.client.innate.ClientProxy;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
 import org.glassfish.jersey.client.spi.Connector;
 import org.glassfish.jersey.message.internal.OutboundMessageContext;
@@ -278,17 +276,20 @@
                                  (String[]) null, /* enable default protocols */
                                  false /* true if the first write request shouldn't be encrypted */
                          );
-                         int port = requestUri.getPort();
-                         SslHandler sslHandler = jdkSslContext.newHandler(ch.alloc(), requestUri.getHost(),
-                                                                          port <= 0 ? 443 : port, executorService);
+
+                         final int port = requestUri.getPort();
+                         final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
+                                 .request(jerseyRequest).setSNIAlways(true).build();
+                         final SslHandler sslHandler = jdkSslContext.newHandler(
+                                 ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService
+                         );
                          if (ClientProperties.getValue(config.getProperties(),
                                                        NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) {
-                            SSLEngine sslEngine = sslHandler.engine();
-                            SSLParameters sslParameters = sslEngine.getSSLParameters();
-                            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
-                            sslEngine.setSSLParameters(sslParameters);
+                             sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine());
                          }
 
+                         sslConfig.setSNIServerName(sslHandler.engine());
+
                          p.addLast(sslHandler);
                      }
 
@@ -374,7 +375,9 @@
             setHeaders(jerseyRequest, nettyRequest.headers());
 
             // host header - http 1.1
-            nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+            if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
+                nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+            }
 
             if (jerseyRequest.hasEntity()) {
                 // guard against prematurely closed channel
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
new file mode 100644
index 0000000..edfe29b
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.client.innate.http;
+
+import org.glassfish.jersey.client.ClientRequest;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.ws.rs.core.UriBuilder;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A unified routines to configure {@link SSLParameters}.
+ * To be reused in connectors.
+ */
+public final class SSLParamConfigurator {
+    private final URI uri;
+    private final Map<String, List<Object>> httpHeaders;
+    private final Optional<SniConfigurator> sniConfigurator;
+
+    /**
+     * Builder of the {@link SSLParamConfigurator} instance.
+     */
+    public static final class Builder {
+        private ClientRequest clientRequest;
+        private URI uri;
+        private Map<String, List<Object>> httpHeaders;
+        private boolean setAlways = false;
+
+        /**
+         * Sets the {@link ClientRequest} instance.
+         * @param clientRequest the {@link ClientRequest}
+         * @return the builder instance
+         */
+        public Builder request(ClientRequest clientRequest) {
+            this.clientRequest = clientRequest;
+            this.httpHeaders = null;
+            this.uri = null;
+            return this;
+        }
+
+        /**
+         * Sets the HTTP request {@link URI} instance.
+         * @param uri The request uri
+         * @return the builder instance
+         */
+        public Builder uri(URI uri) {
+            this.clientRequest = null;
+            this.uri = uri;
+            return this;
+        }
+
+        /**
+         * Sets the HTTP request headers
+         * @param httpHeaders the http request headers
+         * @return the builder instance
+         */
+        public Builder headers(Map<String, List<Object>> httpHeaders) {
+            this.clientRequest = null;
+            this.httpHeaders = httpHeaders;
+            return this;
+        }
+
+        /**
+         * Sets SNI only when {@link javax.ws.rs.core.HttpHeaders#HOST} differs from the request host name if set to
+         * {@code false}. Default is {@code false}.
+         * @param setAlways set SNI always (default)
+         * @return the builder instance
+         */
+        public Builder setSNIAlways(boolean setAlways) {
+            this.setAlways = setAlways;
+            return this;
+        }
+
+        /**
+         * Builds the {@link SSLParamConfigurator} instance.
+         * @return the configured {@link SSLParamConfigurator} instance.
+         */
+        public SSLParamConfigurator build() {
+            return new SSLParamConfigurator(this);
+        }
+    }
+
+    private SSLParamConfigurator(SSLParamConfigurator.Builder builder) {
+        this.uri = builder.clientRequest != null ? builder.clientRequest.getUri() : builder.uri;
+        this.httpHeaders = builder.clientRequest != null ? builder.clientRequest.getHeaders() : builder.httpHeaders;
+        sniConfigurator = SniConfigurator.createWhenHostHeader(uri, httpHeaders, builder.setAlways);
+    }
+
+    /**
+     * Create a new instance of TlsSupport class
+     **/
+    public static SSLParamConfigurator.Builder builder() {
+        return new SSLParamConfigurator.Builder();
+    }
+
+    /**
+     * Get the host name either set by the request URI or by
+     * {@link javax.ws.rs.core.HttpHeaders#HOST} header if it differs from HTTP request host name.
+     * @return the hostName the {@link SSLEngine} is to use.
+     */
+    public String getSNIHostName() {
+        return sniConfigurator.isPresent() ? sniConfigurator.get().getHostName() : uri.getHost();
+    }
+
+    /**
+     * Replaces hostname within the {@link ClientRequest} uri with a resolved IP address. Should the hostname be not known,
+     * the original request URI is returned. The purpose of this method is to replace the host with the IP so that
+     * {code HttpUrlConnection} does not replace user defined {@link javax.net.ssl.SNIHostName} with the host from the request
+     * uri.
+     * @return the request uri with ip address of the resolved host.
+     */
+    public URI toIPRequestUri() {
+        String host = uri.getHost();
+        try {
+            InetAddress ip = InetAddress.getByName(host);
+            return UriBuilder.fromUri(uri).host(ip.getHostAddress()).build();
+        } catch (UnknownHostException e) {
+            return uri;
+        }
+    }
+
+    /**
+     * Return true iff SNI is to be set, i.e.
+     * {@link javax.ws.rs.core.HttpHeaders#HOST} header if it differs from HTTP request host name.
+     * @return Return {@code true} when {@link javax.net.ssl.SNIHostName} is to be set.
+     */
+    public boolean isSNIRequired() {
+        return sniConfigurator.isPresent();
+    }
+
+    /**
+     * Get the request URI or altered by {@link javax.ws.rs.core.HttpHeaders#HOST} header.
+     * @return The possibly altered request URI.
+     * @see #getSNIHostName()
+     */
+    public URI getSNIUri() {
+        return sniConfigurator.isPresent() ? UriBuilder.fromUri(uri).host(getSNIHostName()).build() : uri;
+    }
+
+    /**
+     * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used
+     * (i.e. {@link javax.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name)
+     * @param sslEngine the {@link SSLEngine} the {@link SSLParameters} are set for.
+     */
+    public void setSNIServerName(SSLEngine sslEngine) {
+        sniConfigurator.ifPresent(sni -> sni.setServerNames(sslEngine));
+    }
+
+
+    /**
+     * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used
+     * (i.e. {@link javax.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name)
+     * @param sslSocket the {@link SSLSocket} the {@link SSLParameters} are set for.
+     */
+    public void setSNIServerName(SSLSocket sslSocket) {
+        sniConfigurator.ifPresent(sni -> sni.setServerNames(sslSocket));
+    }
+
+    /**
+     * Set setEndpointIdentificationAlgorithm to HTTPS. This is to prevent man-in-the-middle attacks.
+     * @param sslEngine the {@link SSLEngine} the algorithm is set for.
+     * @see SSLParameters#setEndpointIdentificationAlgorithm(String)
+     */
+    public void setEndpointIdentificationAlgorithm(SSLEngine sslEngine) {
+        SSLParameters sslParameters = sslEngine.getSSLParameters();
+        sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+        sslEngine.setSSLParameters(sslParameters);
+    }
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
new file mode 100644
index 0000000..fc5d057
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.client.innate.http;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.ws.rs.core.HttpHeaders;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A unified routines to set {@link SNIHostName} for the {@link javax.net.ssl.SSLContext}.
+ * To be reused in connectors.
+ */
+final class SniConfigurator {
+    private final String hostName;
+    private SniConfigurator(String hostName) {
+        this.hostName = hostName;
+    }
+
+    /**
+     * Get the hostName from the {@link HttpHeaders#HOST} header.
+     * @return
+     */
+    String getHostName() {
+        return hostName;
+    }
+
+    /**
+     * Create ClientSNI when {@link HttpHeaders#HOST} is set different from the request URI host (or {@code whenDiffer}.is false).
+     * @param hostUri the Uri of the HTTP request
+     * @param headers the HttpHeaders
+     * @param whenDiffer create {@SniConfigurator only when different from the request URI host}
+     * @return ClientSNI or empty when {@link HttpHeaders#HOST}
+     */
+    static Optional<SniConfigurator> createWhenHostHeader(URI hostUri, Map<String, List<Object>> headers, boolean whenDiffer) {
+        List<Object> hostHeaders = headers.get(HttpHeaders.HOST);
+        if (hostHeaders == null || hostHeaders.get(0) == null) {
+            return Optional.empty();
+        }
+
+        final String hostHeader = hostHeaders.get(0).toString();
+        final String trimmedHeader;
+        if (hostHeader != null) {
+            int index = hostHeader.indexOf(':'); // RFC 7230  Host = uri-host [ ":" port ] ;
+            final String trimmedHeader0 = index != -1 ? hostHeader.substring(0, index).trim() : hostHeader.trim();
+            trimmedHeader = trimmedHeader0.isEmpty() ? hostHeader : trimmedHeader0;
+        } else {
+            return Optional.empty();
+        }
+
+        final String hostUriString = hostUri.getHost();
+        if (!whenDiffer && hostUriString.equals(trimmedHeader)) {
+            return Optional.empty();
+        }
+
+        return Optional.of(new SniConfigurator(trimmedHeader));
+    }
+
+    /**
+     * Set {@link SNIServerName} for the given {@link SSLEngine} SSLParameters.
+     * @param sslEngine
+     */
+    void setServerNames(SSLEngine sslEngine) {
+        SSLParameters sslParameters = sslEngine.getSSLParameters();
+        updateSSLParameters(sslParameters);
+        sslEngine.setSSLParameters(sslParameters);
+    }
+
+    /**
+     * Set {@link SNIServerName} for the given {@link SSLSocket} SSLParameters.
+     * @param sslSocket
+     */
+    void setServerNames(SSLSocket sslSocket) {
+        SSLParameters sslParameters = sslSocket.getSSLParameters();
+        updateSSLParameters(sslParameters);
+        sslSocket.setSSLParameters(sslParameters);
+    }
+
+    private SSLParameters updateSSLParameters(SSLParameters sslParameters) {
+        SNIHostName serverName = new SNIHostName(hostName);
+        List<SNIServerName> serverNames = new LinkedList<>();
+        serverNames.add(serverName);
+
+        sslParameters.setServerNames(serverNames);
+
+        return sslParameters;
+    }
+
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java
new file mode 100644
index 0000000..bbedce0
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey client MOST INTERNAL http related classes/interfaces.
+ * Shall not be used outside of Jersey. The module shall not be exported to outside of Jersey.
+ */
+package org.glassfish.jersey.client.innate.http;
\ No newline at end of file
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
index dc8af25..212dac0 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -21,10 +21,13 @@
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.net.HttpURLConnection;
+import java.net.InetAddress;
 import java.net.ProtocolException;
+import java.net.Socket;
 import java.net.SocketTimeoutException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.UnknownHostException;
 import java.security.AccessController;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
@@ -42,6 +45,7 @@
 
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.client.Client;
@@ -55,6 +59,7 @@
 import org.glassfish.jersey.client.JerseyClient;
 import org.glassfish.jersey.client.RequestEntityProcessing;
 import org.glassfish.jersey.client.innate.ClientProxy;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
 import org.glassfish.jersey.client.spi.Connector;
 import org.glassfish.jersey.internal.util.PropertiesHelper;
@@ -315,11 +320,35 @@
         }
     }
 
+    /**
+     * Secure connection if necessary.
+     * <p/>
+     * Provided implementation sets {@link HostnameVerifier} and {@link SSLSocketFactory} to give connection, if that
+     * is an instance of {@link HttpsURLConnection}.
+     *
+     * @param clientRequest the actual client request.
+     * @param uc     http connection to be secured.
+     */
+    private void secureConnection(
+            final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) {
+        secureConnection(clientRequest.getClient(), uc); // keep this for compatibility
+
+        if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI
+            HttpsURLConnection suc = (HttpsURLConnection) uc;
+            SniSSLSocketFactory socketFactory = new SniSSLSocketFactory(suc.getSSLSocketFactory());
+            socketFactory.setSniConfig(sniConfig);
+            suc.setSSLSocketFactory(socketFactory);
+        }
+    }
+
     private ClientResponse _apply(final ClientRequest request) throws IOException {
         final HttpURLConnection uc;
-        Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(request);
+        final Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(request);
+        final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(request).build();
+        final URI sniUri = sniConfig.isSNIRequired() ? sniConfig.toIPRequestUri() : request.getUri();
+
         proxy.ifPresent(clientProxy -> ClientProxy.setBasicAuthorizationHeader(request.getHeaders(), proxy.get()));
-        uc = this.connectionFactory.getConnection(request.getUri().toURL(), proxy.isPresent() ? proxy.get().proxy() : null);
+        uc = this.connectionFactory.getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null);
         uc.setDoInput(true);
 
         final String httpMethod = request.getMethod();
@@ -335,7 +364,7 @@
 
         uc.setReadTimeout(request.resolveProperty(ClientProperties.READ_TIMEOUT, uc.getReadTimeout()));
 
-        secureConnection(request.getClient(), uc);
+        secureConnection(request, uc, sniConfig);
 
         final Object entity = request.getEntity();
         Exception storedException = null;
@@ -559,4 +588,84 @@
     public String getName() {
         return "HttpUrlConnection " + AccessController.doPrivileged(PropertiesHelper.getSystemProperty("java.version"));
     }
+
+    private static class SniSSLSocketFactory extends SSLSocketFactory {
+        private final SSLSocketFactory socketFactory;
+        private ThreadLocal<SSLParamConfigurator> sniConfigs = new ThreadLocal<>();
+
+        public void setSniConfig(SSLParamConfigurator sniConfigs) {
+            this.sniConfigs.set(sniConfigs);
+        }
+
+        private SniSSLSocketFactory(SSLSocketFactory socketFactory) {
+            this.socketFactory = socketFactory;
+        }
+
+        @Override
+        public String[] getDefaultCipherSuites() {
+            return socketFactory.getDefaultCipherSuites();
+        }
+
+        @Override
+        public String[] getSupportedCipherSuites() {
+            return socketFactory.getSupportedCipherSuites();
+        }
+
+        @Override
+        public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException {
+            Socket superSocket = socketFactory.createSocket(socket, s, i, b);
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        @Override
+        public Socket createSocket(String s, int i) throws IOException, UnknownHostException {
+            Socket superSocket = socketFactory.createSocket(s, i);
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        @Override
+        public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException {
+            Socket superSocket = socketFactory.createSocket(s, i, inetAddress, i1);
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        @Override
+        public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
+            Socket superSocket = socketFactory.createSocket(inetAddress, i);
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        @Override
+        public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException {
+            Socket superSocket = socketFactory.createSocket(inetAddress, i, inetAddress1, i1);
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        @Override
+        public Socket createSocket(Socket s, InputStream consumed, boolean autoClose) throws IOException {
+            Socket superSocket = socketFactory.createSocket(s, consumed, autoClose);
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        @Override
+        public Socket createSocket() throws IOException {
+            Socket superSocket = socketFactory.createSocket();
+            setSNIServerName(superSocket);
+            return superSocket;
+        }
+
+        private void setSNIServerName(Socket superSocket) {
+            SSLParamConfigurator sniConfig = this.sniConfigs.get();
+            if (null != sniConfig && SSLSocket.class.isInstance(superSocket)) {
+                sniConfig.setSNIServerName((SSLSocket) superSocket);
+            }
+            this.sniConfigs.remove();
+        }
+    }
 }
diff --git a/docs/src/main/docbook/client.xml b/docs/src/main/docbook/client.xml
index a4e40d7..d9719b6 100644
--- a/docs/src/main/docbook/client.xml
+++ b/docs/src/main/docbook/client.xml
@@ -1088,8 +1088,22 @@
             <para>
                 See javadoc of the &jersey.client.HttpAuthenticationFeature; for more details.
             </para>
-    </section>
+        </section>
 
+        <section>
+            <title><anchor xml:id="SNI"/>Server Name Indication (SNI) Support</title>
+            <para>
+                When using SSL/TLS protocols for the connection, <literal>SNIHostName</literal> is set automatically
+                based on the host name in the HTTPS request.
+            </para>
+            <para>
+                There might be use-cases where the <literal>SNIHostName</literal> is required to be set for other host
+                than the host specified in the HTTPS request. For those cases, when the HTTP header <literal>Host</literal>
+                is set, the <literal>SNIHostName</literal> is set for the host specified in the <literal>Host</literal> header.
+                Note that only <literal>Apache Connector, JDK Connector, Netty connector</literal>, and the default
+                <literal>HttpUrlConnector</literal> do support this feature.
+            </para>
+        </section>
 
     </section>
 
diff --git a/etc/config/copyright-exclude b/etc/config/copyright-exclude
index 987175b..9577477 100644
--- a/etc/config/copyright-exclude
+++ b/etc/config/copyright-exclude
@@ -69,6 +69,7 @@
 /tests/e2e/server/mvc/MvcEncodingTest/MustacheResource.mustache
 /test/resources/org/glassfish/jersey/server/config/jaxrs-components
 /tests/e2e-entity/src/test/resources/org/glassfish/jersey/tests/e2e/entity/xxe.txt
+/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer
 /core-server/src/main/java/com/sun/research/ws/wadl
 /core-common/src/main/java/org/glassfish/jersey/internal/jsr166
 /core-common/src/main/jsr166/org/glassfish/jersey/internal/jsr166
diff --git a/pom.xml b/pom.xml
index 3df2d34..95c5f35 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2213,7 +2213,7 @@
         <helidon.version>1.4.9</helidon.version>
         <xmlunit.version>2.9.0</xmlunit.version>
         <httpclient.version>4.5.13</httpclient.version>
-        <httpclient5.version>5.1.3</httpclient5.version>
+        <httpclient5.version>5.2.1</httpclient5.version>
         <jackson.version>2.14.1</jackson.version>
         <jackson1.version>1.9.13</jackson1.version>
         <javassist.version>3.29.0-GA</javassist.version>
diff --git a/tests/e2e-tls/pom.xml b/tests/e2e-tls/pom.xml
new file mode 100644
index 0000000..2f26262
--- /dev/null
+++ b/tests/e2e-tls/pom.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.tests</groupId>
+        <artifactId>project</artifactId>
+        <version>2.39-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>e2e-tls</artifactId>
+    <name>jersey-tests-e2e-tls</name>
+    <packaging>jar</packaging>
+
+    <description>Jersey E2E SSL/TLS tests</description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <forkCount>1</forkCount>
+                    <reuseForks>false</reuseForks>
+                    <enableAssertions>false</enableAssertions>
+                    <skipTests>${skip.e2e}</skipTests>
+                    <systemPropertyVariables>
+                        <sun.net.http.allowRestrictedHeaders>true</sun.net.http.allowRestrictedHeaders>
+                        <property>
+                            <name>ssl.debug</name>
+                            <value>true</value>
+                        </property>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-bundle</artifactId>
+            <type>pom</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-util</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-suite</artifactId>
+            <version>${junit-platform-suite.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.specto</groupId>
+            <artifactId>hoverfly-java-junit5</artifactId>
+            <version>0.14.0</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-apache-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-apache5-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-grizzly-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-jetty-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-jdk-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.security</groupId>
+            <artifactId>oauth1-signature</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>jdk11+</id>
+            <activation>
+                <jdk>[11,)</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>com.sun.xml.bind</groupId>
+                    <artifactId>jaxb-osgi</artifactId>
+                    <scope>test</scope>
+                </dependency>
+            </dependencies>
+            <properties>
+                <!-- https://bugs.openjdk.java.net/browse/JDK-8211426 -->
+                <surefire.security.argline>-Djdk.tls.server.protocols=TLSv1.2</surefire.security.argline>
+            </properties>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java
new file mode 100644
index 0000000..703d1c8
--- /dev/null
+++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.tls;
+
+import org.glassfish.jersey.tests.e2e.tls.explorer.SSLCapabilities;
+import org.glassfish.jersey.tests.e2e.tls.explorer.SSLExplorer;
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIMatcher;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class ClientHelloTestServer {
+    private ServerSocket serverSocket;
+    private Thread serverThread;
+    private volatile State state = State.NONE;
+
+    private enum State {
+        NONE,
+        INIT,
+        STARTED,
+        STOPPED
+    }
+
+    protected ServerSocketFactory getServerSocketFactory() {
+        return ServerSocketFactory.getDefault();
+    }
+
+    protected void afterHandshake(Socket socket, SSLCapabilities capabilities) {
+
+    }
+
+    public void init(int port) {
+        ServerSocketFactory factory = getServerSocketFactory();
+        try {
+            serverSocket = factory.createServerSocket(port);
+
+            state = State.INIT;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void start() {
+        if (state != State.INIT) {
+            System.out.println("Server has not been initialized");
+        }
+        Thread thread = new Thread(() -> {
+            while (state == State.INIT) {
+                Socket socket = null;
+                try {
+                    socket = serverSocket.accept();
+
+                    inspect(socket);
+                } catch (SocketException e) {
+                    if (!e.getMessage().equals("Interrupted function call: accept failed")) {
+                        e.printStackTrace();
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+        serverThread = thread;
+        thread.start();
+    }
+
+    public void stop() {
+        try {
+            state = State.STOPPED;
+            serverSocket.close();
+            serverThread.join();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void inspect(Socket socket) throws IOException {
+        InputStream ins = socket.getInputStream();
+
+        byte[] buffer = new byte[0xFF];
+        int position = 0;
+        SSLCapabilities capabilities = null;
+
+// Read the header of TLS record
+        while (position < SSLExplorer.RECORD_HEADER_SIZE) {
+            int count = SSLExplorer.RECORD_HEADER_SIZE - position;
+            int n = ins.read(buffer, position, count);
+            if (n < 0) {
+                throw new IOException("unexpected end of stream!");
+            }
+            position += n;
+        }
+
+// Get the required size to explore the SSL capabilities
+        int recordLength = SSLExplorer.getRequiredSize(buffer, 0, position);
+        if (buffer.length < recordLength) {
+            buffer = Arrays.copyOf(buffer, recordLength);
+        }
+
+        while (position < recordLength) {
+            int count = recordLength - position;
+            int n = ins.read(buffer, position, count);
+            if (n < 0) {
+                throw new IOException("unexpected end of stream!");
+            }
+            position += n;
+        }
+
+// Explore
+        capabilities = SSLExplorer.explore(buffer, 0, recordLength);
+        if (capabilities != null) {
+            System.out.println("Record version: " + capabilities.getRecordVersion());
+            System.out.println("Hello version: " + capabilities.getHelloVersion());
+        }
+
+        afterHandshake(socket, capabilities);
+    }
+}
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java
new file mode 100644
index 0000000..1504d44
--- /dev/null
+++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.tls;
+
+import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
+import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.HttpUrlConnectorProvider;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.jdk.connector.JdkConnectorProvider;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+import org.glassfish.jersey.tests.e2e.tls.explorer.SSLCapabilities;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import javax.net.ssl.SNIServerName;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class SniTest {
+    private static final int PORT = 8443;
+    private static final String LOCALHOST = "127.0.0.1";
+
+
+    static {
+        // JDK specific settings
+        System.setProperty("jdk.net.hosts.file", SniTest.class.getResource("/hosts").getPath());
+    }
+
+    public static ConnectorProvider[] getConnectors() {
+        return new ConnectorProvider[] {
+                new NettyConnectorProvider(),
+                new ApacheConnectorProvider(),
+                new Apache5ConnectorProvider(),
+                new JdkConnectorProvider(),
+                new HttpUrlConnectorProvider()
+        };
+    }
+
+    @ParameterizedTest
+    @MethodSource("getConnectors")
+    public void server1Test(ConnectorProvider provider) {
+        ClientConfig clientConfig = new ClientConfig();
+        clientConfig.connectorProvider(provider);
+        serverTest(clientConfig, "www.host1.com");
+    }
+
+    public void serverTest(ClientConfig clientConfig, String hostName) {
+        String newHostName = replaceWhenHostNotKnown(hostName);
+        final List<SNIServerName> serverNames = new LinkedList<>();
+        final String[] requestHostName = new String[1];
+        ClientHelloTestServer server = new ClientHelloTestServer() {
+            @Override
+            protected void afterHandshake(Socket socket, SSLCapabilities capabilities) {
+                serverNames.addAll(capabilities.getServerNames());
+            }
+        };
+        server.init(PORT);
+        server.start();
+
+        clientConfig.property(ClientProperties.READ_TIMEOUT, 2000);
+        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 2000);
+        try (Response r = ClientBuilder.newClient(clientConfig)
+                .register(new ClientRequestFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext) throws IOException {
+                        requestHostName[0] = requestContext.getUri().getHost();
+                    }
+                })
+                .target("https://" + (newHostName.equals(LOCALHOST) ? LOCALHOST : "www.host0.com") + ":" + PORT)
+                .path("host")
+                .request()
+                .header(HttpHeaders.HOST, hostName + ":8080")
+                .get()) {
+            // empty
+        } catch (Exception e) {
+            Throwable cause = e;
+            while (cause != null
+                    && !SocketTimeoutException.class.isInstance(cause)
+                    && TimeoutException.class.isInstance(cause)) {
+                cause = cause.getCause();
+            }
+            if (cause == null && /*IOE*/ !e.getMessage().contains("Stream closed")) {
+                throw e;
+            }
+        }
+
+        server.stop();
+
+        if (serverNames.isEmpty()) {
+            throw new IllegalStateException("ServerNames are empty");
+        }
+
+        String clientSniName = new String(serverNames.get(0).getEncoded());
+        if (!hostName.equals(clientSniName)) {
+            throw new IllegalStateException("Unexpected client SNI name " + clientSniName);
+        }
+
+        if (!LOCALHOST.equals(newHostName) && requestHostName[0].equals(hostName)) {
+            throw new IllegalStateException("The HTTP Request is made with the same");
+        }
+
+        System.out.append("Found expected Client SNI ").println(serverNames.get(0));
+    }
+
+    /*
+     * The method checks whether the JDK-dependent property "jdk.net.hosts.file" works.
+     * If it does, the request is made with the hostname, so that the 3rd party client has
+     * the request with the hostname. If a real address is returned or UnknownHostException
+     * is thrown, the property did not work and the request needs to use 127.0.0.1.
+     */
+    private static String replaceWhenHostNotKnown(String hostName) {
+        try {
+            InetAddress inetAddress = InetAddress.getByName(hostName);
+            return LOCALHOST.equals(inetAddress.getHostAddress()) ? hostName : LOCALHOST;
+        } catch (UnknownHostException e) {
+            return LOCALHOST;
+        }
+    }
+}
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java
new file mode 100644
index 0000000..e21b86a
--- /dev/null
+++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2013, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ *   - Neither the name of Oracle or the names of its
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.glassfish.jersey.tests.e2e.tls.explorer;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import javax.net.ssl.SNIServerName;
+
+/**
+ * Encapsulates the security capabilities of an SSL/TLS connection.
+ * <P>
+ * The security capabilities are the list of ciphersuites to be accepted in
+ * an SSL/TLS handshake, the record version, the hello version, and server
+ * name indication, etc., of an SSL/TLS connection.
+ * <P>
+ * <code>SSLCapabilities</code> can be retrieved by exploring the network
+ * data of an SSL/TLS connection via {@link SSLExplorer#explore(ByteBuffer)}
+ * or {@link SSLExplorer#explore(byte[], int, int)}.
+ *
+ * @see SSLExplorer
+ */
+public abstract class SSLCapabilities {
+
+    /**
+     * Returns the record version of an SSL/TLS connection
+     *
+     * @return a non-null record version
+     */
+    public abstract String getRecordVersion();
+
+    /**
+     * Returns the hello version of an SSL/TLS connection
+     *
+     * @return a non-null hello version
+     */
+    public abstract String getHelloVersion();
+
+    /**
+     * Returns a <code>List</code> containing all {@link SNIServerName}s
+     * of the server name indication.
+     *
+     * @return a non-null immutable list of {@link SNIServerName}s
+     *         of the server name indication parameter, may be empty
+     *         if no server name indication.
+     *
+     * @see SNIServerName
+     */
+    public abstract List<SNIServerName> getServerNames();
+}
+
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java
new file mode 100644
index 0000000..71c1f4e
--- /dev/null
+++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java
@@ -0,0 +1,627 @@
+package org.glassfish.jersey.tests.e2e.tls.explorer;
+
+/*
+ * Copyright (c) 2013, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ *   - Neither the name of Oracle or the names of its
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLProtocolException;
+import javax.net.ssl.StandardConstants;
+import java.nio.ByteBuffer;
+import java.nio.BufferUnderflowException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Instances of this class acts as an explorer of the network data of an
+ * SSL/TLS connection.
+ */
+public final class SSLExplorer {
+
+    // Private constructor prevents construction outside this class.
+    private SSLExplorer() {
+    }
+
+    /**
+     * The header size of TLS/SSL records.
+     * <P>
+     * The value of this constant is {@value}.
+     */
+    public static final int RECORD_HEADER_SIZE = 0x05;
+
+    /**
+     * Returns the required number of bytes in the {@code source}
+     * {@link ByteBuffer} necessary to explore SSL/TLS connection.
+     * <P>
+     * This method tries to parse as few bytes as possible from
+     * {@code source} byte buffer to get the length of an
+     * SSL/TLS record.
+     * <P>
+     * This method accesses the {@code source} parameter in read-only
+     * mode, and does not update the buffer's properties such as capacity,
+     * limit, position, and mark values.
+     *
+     * @param  source
+     *         a {@link ByteBuffer} containing
+     *         inbound or outbound network data for an SSL/TLS connection.
+     * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE}
+     *         bytes remaining in {@code source}
+     * @return the required size in byte to explore an SSL/TLS connection
+     */
+    public static int getRequiredSize(ByteBuffer source) {
+
+        ByteBuffer input = source.duplicate();
+
+        // Do we have a complete header?
+        if (input.remaining() < RECORD_HEADER_SIZE) {
+            throw new BufferUnderflowException();
+        }
+
+        // Is it a handshake message?
+        byte firstByte = input.get();
+        byte secondByte = input.get();
+        byte thirdByte = input.get();
+        if ((firstByte & 0x80) != 0 && thirdByte == 0x01) {
+            // looks like a V2ClientHello
+            // return (((firstByte & 0x7F) << 8) | (secondByte & 0xFF)) + 2;
+            return RECORD_HEADER_SIZE;   // Only need the header fields
+        } else {
+            return (((input.get() & 0xFF) << 8) | (input.get() & 0xFF)) + 5;
+        }
+    }
+
+    /**
+     * Returns the required number of bytes in the {@code source} byte array
+     * necessary to explore SSL/TLS connection.
+     * <P>
+     * This method tries to parse as few bytes as possible from
+     * {@code source} byte array to get the length of an
+     * SSL/TLS record.
+     *
+     * @param  source
+     *         a byte array containing inbound or outbound network data for
+     *         an SSL/TLS connection.
+     * @param  offset
+     *         the start offset in array {@code source} at which the
+     *         network data is read from.
+     * @param  length
+     *         the maximum number of bytes to read.
+     *
+     * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE}
+     *         bytes remaining in {@code source}
+     * @return the required size in byte to explore an SSL/TLS connection
+     */
+    public static int getRequiredSize(byte[] source,
+                                            int offset, int length) throws IOException {
+
+        ByteBuffer byteBuffer =
+                ByteBuffer.wrap(source, offset, length).asReadOnlyBuffer();
+        return getRequiredSize(byteBuffer);
+    }
+
+    /**
+     * Launch and explore the security capabilities from byte buffer.
+     * <P>
+     * This method tries to parse as few records as possible from
+     * {@code source} byte buffer to get the {@link SSLCapabilities}
+     * of an SSL/TLS connection.
+     * <P>
+     * Please NOTE that this method must be called before any handshaking
+     * occurs.  The behavior of this method is not defined in this release
+     * if the handshake has begun, or has completed.
+     * <P>
+     * This method accesses the {@code source} parameter in read-only
+     * mode, and does not update the buffer's properties such as capacity,
+     * limit, position, and mark values.
+     *
+     * @param  source
+     *         a {@link ByteBuffer} containing
+     *         inbound or outbound network data for an SSL/TLS connection.
+     *
+     * @throws IOException on network data error
+     * @throws BufferUnderflowException if not enough source bytes available
+     *         to make a complete exploration.
+     *
+     * @return the explored {@link SSLCapabilities} of the SSL/TLS
+     *         connection
+     */
+    public static SSLCapabilities explore(ByteBuffer source)
+            throws IOException {
+
+        ByteBuffer input = source.duplicate();
+
+        // Do we have a complete header?
+        if (input.remaining() < RECORD_HEADER_SIZE) {
+            throw new BufferUnderflowException();
+        }
+
+        // Is it a handshake message?
+        byte firstByte = input.get();
+        byte secondByte = input.get();
+        byte thirdByte = input.get();
+        if ((firstByte & 0x80) != 0 && thirdByte == 0x01) {
+            // looks like a V2ClientHello
+            return exploreV2HelloRecord(input,
+                    firstByte, secondByte, thirdByte);
+        } else if (firstByte == 22) {   // 22: handshake record
+            return exploreTLSRecord(input,
+                    firstByte, secondByte, thirdByte);
+        } else {
+            throw new SSLException("Not handshake record");
+        }
+    }
+
+    /**
+     * Launch and explore the security capabilities from byte array.
+     * <P>
+     * Please NOTE that this method must be called before any handshaking
+     * occurs.  The behavior of this method is not defined in this release
+     * if the handshake has begun, or has completed.  Once handshake has
+     * begun, or has completed, the security capabilities can not and
+     * should not be launched with this method.
+     *
+     * @param  source
+     *         a byte array containing inbound or outbound network data for
+     *         an SSL/TLS connection.
+     * @param  offset
+     *         the start offset in array {@code source} at which the
+     *         network data is read from.
+     * @param  length
+     *         the maximum number of bytes to read.
+     *
+     * @throws IOException on network data error
+     * @throws BufferUnderflowException if not enough source bytes available
+     *         to make a complete exploration.
+     * @return the explored {@link SSLCapabilities} of the SSL/TLS
+     *         connection
+     *
+     * @see #explore(ByteBuffer)
+     */
+    public static SSLCapabilities explore(byte[] source,
+                                                int offset, int length) throws IOException {
+        ByteBuffer byteBuffer =
+                ByteBuffer.wrap(source, offset, length).asReadOnlyBuffer();
+        return explore(byteBuffer);
+    }
+
+    /*
+     * uint8 V2CipherSpec[3];
+     * struct {
+     *     uint16 msg_length;         // The highest bit MUST be 1;
+     *                                // the remaining bits contain the length
+     *                                // of the following data in bytes.
+     *     uint8 msg_type;            // MUST be 1
+     *     Version version;
+     *     uint16 cipher_spec_length; // It cannot be zero and MUST be a
+     *                                // multiple of the V2CipherSpec length.
+     *     uint16 session_id_length;  // This field MUST be empty.
+     *     uint16 challenge_length;   // SHOULD use a 32-byte challenge
+     *     V2CipherSpec cipher_specs[V2ClientHello.cipher_spec_length];
+     *     opaque session_id[V2ClientHello.session_id_length];
+     *     opaque challenge[V2ClientHello.challenge_length;
+     * } V2ClientHello;
+     */
+    private static SSLCapabilities exploreV2HelloRecord(
+            ByteBuffer input, byte firstByte, byte secondByte,
+            byte thirdByte) throws IOException {
+
+        // We only need the header. We have already had enough source bytes.
+        // int recordLength = (firstByte & 0x7F) << 8) | (secondByte & 0xFF);
+        try {
+            // Is it a V2ClientHello?
+            if (thirdByte != 0x01) {
+                throw new SSLException(
+                        "Unsupported or Unrecognized SSL record");
+            }
+
+            // What's the hello version?
+            byte helloVersionMajor = input.get();
+            byte helloVersionMinor = input.get();
+
+            // 0x00: major version of SSLv20
+            // 0x02: minor version of SSLv20
+            //
+            // SNIServerName is an extension, SSLv20 doesn't support extension.
+            return new SSLCapabilitiesImpl((byte) 0x00, (byte) 0x02,
+                    helloVersionMajor, helloVersionMinor,
+                    Collections.<SNIServerName>emptyList());
+        } catch (BufferUnderflowException bufe) {
+            throw new SSLProtocolException(
+                    "Invalid handshake record");
+        }
+    }
+
+    /*
+     * struct {
+     *     uint8 major;
+     *     uint8 minor;
+     * } ProtocolVersion;
+     *
+     * enum {
+     *     change_cipher_spec(20), alert(21), handshake(22),
+     *     application_data(23), (255)
+     * } ContentType;
+     *
+     * struct {
+     *     ContentType type;
+     *     ProtocolVersion version;
+     *     uint16 length;
+     *     opaque fragment[TLSPlaintext.length];
+     * } TLSPlaintext;
+     */
+    private static SSLCapabilities exploreTLSRecord(
+            ByteBuffer input, byte firstByte, byte secondByte,
+            byte thirdByte) throws IOException {
+
+        // Is it a handshake message?
+        if (firstByte != 22) {        // 22: handshake record
+            throw new SSLException("Not handshake record");
+        }
+
+        // We need the record version to construct SSLCapabilities.
+        byte recordMajorVersion = secondByte;
+        byte recordMinorVersion = thirdByte;
+
+        // Is there enough data for a full record?
+        int recordLength = getInt16(input);
+        if (recordLength > input.remaining()) {
+            throw new BufferUnderflowException();
+        }
+
+        // We have already had enough source bytes.
+        try {
+            return exploreHandshake(input,
+                    recordMajorVersion, recordMinorVersion, recordLength);
+        } catch (BufferUnderflowException bufe) {
+            throw new SSLProtocolException(
+                    "Invalid handshake record");
+        }
+    }
+
+    /*
+     * enum {
+     *     hello_request(0), client_hello(1), server_hello(2),
+     *     certificate(11), server_key_exchange (12),
+     *     certificate_request(13), server_hello_done(14),
+     *     certificate_verify(15), client_key_exchange(16),
+     *     finished(20)
+     *     (255)
+     * } HandshakeType;
+     *
+     * struct {
+     *     HandshakeType msg_type;
+     *     uint24 length;
+     *     select (HandshakeType) {
+     *         case hello_request:       HelloRequest;
+     *         case client_hello:        ClientHello;
+     *         case server_hello:        ServerHello;
+     *         case certificate:         Certificate;
+     *         case server_key_exchange: ServerKeyExchange;
+     *         case certificate_request: CertificateRequest;
+     *         case server_hello_done:   ServerHelloDone;
+     *         case certificate_verify:  CertificateVerify;
+     *         case client_key_exchange: ClientKeyExchange;
+     *         case finished:            Finished;
+     *     } body;
+     * } Handshake;
+     */
+    private static SSLCapabilities exploreHandshake(
+            ByteBuffer input, byte recordMajorVersion,
+            byte recordMinorVersion, int recordLength) throws IOException {
+
+        // What is the handshake type?
+        byte handshakeType = input.get();
+        if (handshakeType != 0x01) {   // 0x01: client_hello message
+            throw new IllegalStateException("Not initial handshaking");
+        }
+
+        // What is the handshake body length?
+        int handshakeLength = getInt24(input);
+
+        // Theoretically, a single handshake message might span multiple
+        // records, but in practice this does not occur.
+        if (handshakeLength > (recordLength - 4)) { // 4: handshake header size
+            throw new SSLException("Handshake message spans multiple records");
+        }
+
+        input = input.duplicate();
+        input.limit(handshakeLength + input.position());
+        return exploreClientHello(input,
+                recordMajorVersion, recordMinorVersion);
+    }
+
+    /*
+     * struct {
+     *     uint32 gmt_unix_time;
+     *     opaque random_bytes[28];
+     * } Random;
+     *
+     * opaque SessionID<0..32>;
+     *
+     * uint8 CipherSuite[2];
+     *
+     * enum { null(0), (255) } CompressionMethod;
+     *
+     * struct {
+     *     ProtocolVersion client_version;
+     *     Random random;
+     *     SessionID session_id;
+     *     CipherSuite cipher_suites<2..2^16-2>;
+     *     CompressionMethod compression_methods<1..2^8-1>;
+     *     select (extensions_present) {
+     *         case false:
+     *             struct {};
+     *         case true:
+     *             Extension extensions<0..2^16-1>;
+     *     };
+     * } ClientHello;
+     */
+    private static SSLCapabilities exploreClientHello(
+            ByteBuffer input,
+            byte recordMajorVersion,
+            byte recordMinorVersion) throws IOException {
+
+        List<SNIServerName> snList = Collections.<SNIServerName>emptyList();
+
+        // client version
+        byte helloMajorVersion = input.get();
+        byte helloMinorVersion = input.get();
+
+        // ignore random
+        int position = input.position();
+        input.position(position + 32);  // 32: the length of Random
+
+        // ignore session id
+        ignoreByteVector8(input);
+
+        // ignore cipher_suites
+        ignoreByteVector16(input);
+
+        // ignore compression methods
+        ignoreByteVector8(input);
+
+        if (input.remaining() > 0) {
+            snList = exploreExtensions(input);
+        }
+
+        return new SSLCapabilitiesImpl(
+                recordMajorVersion, recordMinorVersion,
+                helloMajorVersion, helloMinorVersion, snList);
+    }
+
+    /*
+     * struct {
+     *     ExtensionType extension_type;
+     *     opaque extension_data<0..2^16-1>;
+     * } Extension;
+     *
+     * enum {
+     *     server_name(0), max_fragment_length(1),
+     *     client_certificate_url(2), trusted_ca_keys(3),
+     *     truncated_hmac(4), status_request(5), (65535)
+     * } ExtensionType;
+     */
+    private static List<SNIServerName> exploreExtensions(ByteBuffer input)
+            throws IOException {
+
+        int length = getInt16(input);           // length of extensions
+        while (length > 0) {
+            int extType = getInt16(input);      // extenson type
+            int extLen = getInt16(input);       // length of extension data
+
+            if (extType == 0x00) {      // 0x00: type of server name indication
+                return exploreSNIExt(input, extLen);
+            } else {                    // ignore other extensions
+                ignoreByteVector(input, extLen);
+            }
+
+            length -= extLen + 4;
+        }
+
+        return Collections.<SNIServerName>emptyList();
+    }
+
+    /*
+     * struct {
+     *     NameType name_type;
+     *     select (name_type) {
+     *         case host_name: HostName;
+     *     } name;
+     * } ServerName;
+     *
+     * enum {
+     *     host_name(0), (255)
+     * } NameType;
+     *
+     * opaque HostName<1..2^16-1>;
+     *
+     * struct {
+     *     ServerName server_name_list<1..2^16-1>
+     * } ServerNameList;
+     */
+    private static List<SNIServerName> exploreSNIExt(ByteBuffer input,
+                                                     int extLen) throws IOException {
+
+        Map<Integer, SNIServerName> sniMap = new LinkedHashMap<>();
+
+        int remains = extLen;
+        if (extLen >= 2) {     // "server_name" extension in ClientHello
+            int listLen = getInt16(input);     // length of server_name_list
+            if (listLen == 0 || listLen + 2 != extLen) {
+                throw new SSLProtocolException(
+                        "Invalid server name indication extension");
+            }
+
+            remains -= 2;     // 0x02: the length field of server_name_list
+            while (remains > 0) {
+                int code = getInt8(input);      // name_type
+                int snLen = getInt16(input);    // length field of server name
+                if (snLen > remains) {
+                    throw new SSLProtocolException(
+                            "Not enough data to fill declared vector size");
+                }
+                byte[] encoded = new byte[snLen];
+                input.get(encoded);
+
+                SNIServerName serverName;
+                switch (code) {
+                    case StandardConstants.SNI_HOST_NAME:
+                        if (encoded.length == 0) {
+                            throw new SSLProtocolException(
+                                    "Empty HostName in server name indication");
+                        }
+                        serverName = new SNIHostName(encoded);
+                        break;
+                    default:
+                        serverName = new UnknownServerName(code, encoded);
+                }
+                // check for duplicated server name type
+                if (sniMap.put(serverName.getType(), serverName) != null) {
+                    throw new SSLProtocolException("Duplicated server name of type " + serverName.getType());
+                }
+
+                remains -= encoded.length + 3;  // NameType: 1 byte
+                // HostName length: 2 bytes
+            }
+        } else if (extLen == 0) {     // "server_name" extension in ServerHello
+            throw new SSLProtocolException(
+                    "Not server name indication extension in client");
+        }
+
+        if (remains != 0) {
+            throw new SSLProtocolException(
+                    "Invalid server name indication extension");
+        }
+
+        return Collections.<SNIServerName>unmodifiableList(
+                new ArrayList<>(sniMap.values()));
+    }
+
+    private static int getInt8(ByteBuffer input) {
+        return input.get();
+    }
+
+    private static int getInt16(ByteBuffer input) {
+        return ((input.get() & 0xFF) << 8) | (input.get() & 0xFF);
+    }
+
+    private static int getInt24(ByteBuffer input) {
+        return ((input.get() & 0xFF) << 16) | ((input.get() & 0xFF) << 8) | (input.get() & 0xFF);
+    }
+
+    private static void ignoreByteVector8(ByteBuffer input) {
+        ignoreByteVector(input, getInt8(input));
+    }
+
+    private static void ignoreByteVector16(ByteBuffer input) {
+        ignoreByteVector(input, getInt16(input));
+    }
+
+    private static void ignoreByteVector24(ByteBuffer input) {
+        ignoreByteVector(input, getInt24(input));
+    }
+
+    private static void ignoreByteVector(ByteBuffer input, int length) {
+        if (length != 0) {
+            int position = input.position();
+            input.position(position + length);
+        }
+    }
+
+    private static class UnknownServerName extends SNIServerName {
+        UnknownServerName(int code, byte[] encoded) {
+            super(code, encoded);
+        }
+    }
+
+    private static final class SSLCapabilitiesImpl extends SSLCapabilities {
+        private static final Map<Integer, String> versionMap = new HashMap<>(5);
+
+        private final String recordVersion;
+        private final String helloVersion;
+        List<SNIServerName> sniNames;
+
+        static {
+            versionMap.put(0x0002, "SSLv2Hello");
+            versionMap.put(0x0300, "SSLv3");
+            versionMap.put(0x0301, "TLSv1");
+            versionMap.put(0x0302, "TLSv1.1");
+            versionMap.put(0x0303, "TLSv1.2");
+        }
+
+        SSLCapabilitiesImpl(byte recordMajorVersion, byte recordMinorVersion,
+                            byte helloMajorVersion, byte helloMinorVersion,
+                            List<SNIServerName> sniNames) {
+
+            int version = (recordMajorVersion << 8) | recordMinorVersion;
+            this.recordVersion = versionMap.get(version) != null
+                    ? versionMap.get(version)
+                    : unknownVersion(recordMajorVersion, recordMinorVersion);
+
+            version = (helloMajorVersion << 8) | helloMinorVersion;
+            this.helloVersion = versionMap.get(version) != null
+                    ? versionMap.get(version)
+                    : unknownVersion(helloMajorVersion, helloMinorVersion);
+
+            this.sniNames = sniNames;
+        }
+
+        @Override
+        public String getRecordVersion() {
+            return recordVersion;
+        }
+
+        @Override
+        public String getHelloVersion() {
+            return helloVersion;
+        }
+
+        @Override
+        public List<SNIServerName> getServerNames() {
+            if (!sniNames.isEmpty()) {
+                return Collections.<SNIServerName>unmodifiableList(sniNames);
+            }
+
+            return sniNames;
+        }
+
+        private static String unknownVersion(byte major, byte minor) {
+            return "Unknown-" + ((int) major) + "." + ((int) minor);
+        }
+    }
+}
+
diff --git a/tests/e2e-tls/src/test/resources/hosts b/tests/e2e-tls/src/test/resources/hosts
new file mode 100644
index 0000000..438ebb6
--- /dev/null
+++ b/tests/e2e-tls/src/test/resources/hosts
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Eclipse Public License v. 2.0, which is available at
+# http://www.eclipse.org/legal/epl-2.0.
+#
+# This Source Code may also be made available under the following Secondary
+# Licenses when the conditions for such availability set forth in the
+# Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+# version 2 with the GNU Classpath Exception, which is available at
+# https://www.gnu.org/software/classpath/license.html.
+#
+# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+127.0.0.1   www.host0.com
+127.0.0.1	www.host1.com
+127.0.0.1	www.host2.com
+127.0.0.1   www.host3.com
\ No newline at end of file
diff --git a/tests/pom.xml b/tests/pom.xml
index 3966d7d..a140de6 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-    Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2011, 2023 Oracle and/or its affiliates. All rights reserved.
 
     This program and the accompanying materials are made available under the
     terms of the Eclipse Public License v. 2.0, which is available at
@@ -44,6 +44,7 @@
         <module>e2e-inject</module>
         <module>e2e-server</module>
         <module>e2e-testng</module>
+        <module>e2e-tls</module>
         <module>integration</module>
         <module>jmockit</module>
         <module>mem-leaks</module>