jetty / jetty.project

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

Dynamically update SNI details for jetty client when host is IP #11532

Open MohammadNC opened 6 months ago

MohammadNC commented 6 months ago

Jetty version(s) 11.0.11 Java version/vendor (use: java -version) 17.0.1 Spring boot version(s) 2.7.4 OS type/version Linux

Hi Team,

In the given code snippet, the SNI details are correctly set when the host is specified as a fully qualified domain name (FQDN). When the host is specified as an IP address, SNI details are missing, we have added IP to the host to avoid the DNS resolution. Our requirement dictates that even when the host is an IP address, we should dynamically set the SNI details for each request.

We need to modify the code to ensure that SNI details are included in the handshake even when the host is an IP address. This entails dynamically setting the SNI details based on each request.

public org.eclipse.jetty.client.HttpClient getHttpClient() throws IOException {
        SslContextFactory sslContextFactory = new SslContextFactory.Client(true);
        ClientConnector clientConnector = new ClientConnector() {
            protected void configure(SelectableChannel selectable) throws IOException {
                super.configure(selectable);
                if (selectable instanceof NetworkChannel) {
                    NetworkChannel channel = (NetworkChannel)selectable;
                    channel.setOption(java.net.StandardSocketOptions.SO_KEEPALIVE,
                            tcpConfigOptionProvider.getTcpKeepalive().getEnable());
                    // Set keepalive parameters only if it is enabled
                    if(tcpConfigOptionProvider.getTcpKeepalive().getEnable()) {
                        channel.setOption(jdk.net.ExtendedSocketOptions.TCP_KEEPIDLE,
                                Integer.parseInt(StringUtils.chop(tcpConfigOptionProvider.getTcpKeepalive().getTime())));
                        channel.setOption(jdk.net.ExtendedSocketOptions.TCP_KEEPINTERVAL,
                                Integer.parseInt(StringUtils.chop(tcpConfigOptionProvider.getTcpKeepalive().getInterval())));
                        channel.setOption(jdk.net.ExtendedSocketOptions.TCP_KEEPCOUNT,
                                tcpConfigOptionProvider.getTcpKeepalive().getProbes());
                    }

                    tcpKeepaliveChannelCofigDetails(channel);

                }
            }
        };
        clientConnector.setSslContextFactory((SslContextFactory.Client) sslContextFactory);
        sslContextFactory.setIncludeCipherSuites(config.getCiphers());
        sslContextFactory.setEndpointIdentificationAlgorithm(null);
        try {
            sslContextFactory.setSslContext(getSSLContext());
        } catch (Exception e) {
            logger.error("Exception occurred while setting SSL context");
        }
        HTTP2Client http2Client = new HTTP2Client(clientConnector);
        // HTTP2Client http2Client = new HTTP2Client();

        org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2 transport = new org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2(
                http2Client);
        transport.setUseALPN(true);
        org.eclipse.jetty.client.HttpClient httpClient = new org.eclipse.jetty.client.HttpClient(
                transport) {
            @Override
            protected void doStart() throws Exception {
                super.doStart();
            }
            @Override
            public Origin createOrigin(HttpRequest request, Origin.Protocol protocol)
            {
                String scheme = request.getScheme();
                if (!HttpScheme.HTTP.is(scheme) && !HttpScheme.HTTPS.is(scheme) &&
                        !HttpScheme.WS.is(scheme) && !HttpScheme.WSS.is(scheme))
                    throw new IllegalArgumentException("Invalid protocol " + scheme);
                scheme = scheme.toLowerCase(Locale.ENGLISH);
                String host = request.getHost();
                host = host.toLowerCase(Locale.ENGLISH);
                /**
                 * overriding the implementation from jetty client- start
                 * host will be overriden with ip from cookies if available
                 * 
                 */
                List<HttpCookie> cookies = request.getCookies();
                logger.debug("cookies found in request: {}",cookies);
                String ip = getIpFromCookies(cookies);
                if(StringUtils.isNotBlank(ip)) {
                    host = ip;
                }
                /**
                 * Overriding the implementation from jetty client- end
                 */
                int port = request.getPort();
                port = normalizePort(scheme, port);
                logger.debug("SSL Client: Origin formed on host: {} port: {}", host, port);
                return new Origin(scheme, host, port, request.getTag(), protocol);
            }

            private String getIpFromCookies(List<HttpCookie> cookies) {
                String ip = "";
                if(!cookies.isEmpty()) {
                    Iterator<HttpCookie> itr = cookies.iterator();
                    while(itr.hasNext()) {
                        HttpCookie httpCookie = itr.next();
                        if(httpCookie.getName().equals(CommonConstants.CUSTOM_SOURCE)) {
                            ip = httpCookie.getValue();
                            logger.info("SSL Client: cookie found: {}", ip);
                            itr.remove();
                            break;
                        }
                    }
                }
                return ip;
            }
        };

        // Adding listener for jetty client metrics
        httpClient.getRequestListeners().add(new JettySSLClientRequestMetrics());
        httpClient.setIdleTimeout(JCidleTimeout);
        httpClient.setMaxRequestsQueuedPerDestination(JCmaxRequestsQueuedPerDestination);
        httpClient.setMaxConnectionsPerDestination(JCmaxConnectionsPerDestination);
        httpClient.setUserAgentField(null);
        httpClient.setRemoveIdleDestinations(true);
        setHttpsclient(httpClient);
        return httpClient;
    }

Following API is used to send the message

        Mono<ResponseEntity<byte[]>> MonoRsp = destWebClient
            .method(HttpMethod.resolve(contextData.getRequestEntity().getMethod().name())).uri(uri)
            .headers(hdrs -> hdrs.addAll(httpHeaaders))
            .body(BodyInserters.fromValue(body)).retrieve().toEntity(byte[].class)
            .timeout(Duration.ofMillis(perTryTimeout.getTimeMillis()))
            .onErrorResume(Throwable.class, throwable -> {
                Mono<ResponseEntity<byte[]>> rsp = Mono.just(handleError(throwable, contextData));
                return rsp;
            }).doOnCancel(() -> logger.info("Dest Request transaction has been Canceled"))
            .publishOn(Schedulers.fromExecutor(testExecutor));
Note: Tried below for approach to update the SNI details, but unable to get the requested server details..
    SslContextFactory sslContextFactory = new SslContextFactory.Client(true) { 
                 @Override
            public void customize(SSLEngine sslEngine) {
                super.customize(sslEngine);
                SniProvider sniProvider = getSNIProvider();
                if (sniProvider != null) {
                    List<SNIServerName> sniServerNames = new ArrayList<>();
                    sslEngine.getPeerHost();
                    sniServerNames.add(new SNIHostName("???????"));  //Here need to update the server fqdn ?
                    sniProvider.apply(sslEngine, sniServerNames);
                }
            }
    };

Kindly provide some suggestions to update the SNI details dynamically, without DNS resolutions. Thanks in advance.

sbordet commented 6 months ago

I'm afraid you cannot do this due to the TLS OpenJDK implementation.

SNI only supports, per specification, host names; it states:

Literal IPv4 and IPv6 addresses are not permitted in "HostName".

OpenJDK up to version 16 included allowed to skeak in IP addresses, but since OpenJDK 17 this is not possible anymore.

We have a test for this: https://github.com/jetty/jetty.project/blob/jetty-12.0.7/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java#L1334

This is not really a Jetty issue, there is not much that we can do as it is the JDK implementation that eventually adds the SNI extension to the ClientHello sent to the server and we have no control over this step.

joakime commented 6 months ago

Also, Jetty 11 is now at End of Community Support.

See:

Please upgrade to Jetty 12.

joakime commented 6 months ago

Also of note, that the Specification (and Java) puts an additional requirement on "HostName" in that it must be a Fully Qualified DNS Hostname. The spec (and java) require that the "HostName" contain at least 1 dot . (eg: example.org) and the "HostName" also may not end with a dot . (eg: foo. is illegal).