ConcurrentHttpsUrlConnectionTest.java

/*
 * Copyright (c) 2024 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.connector;

import org.junit.jupiter.api.Test;

import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;

import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManagerFactory;

/**
 * Jersey client seems to be not thread-safe:
 * When the first GET request is in progress,
 * all parallel requests from other Jersey client instances fail
 * with SSLHandshakeException: PKIX path building failed.
 * <p>
 * Once the first GET request is completed,
 * all subsequent requests work without error.
 * <p>
 * BUG 5749
 */
public class ConcurrentHttpsUrlConnectionTest {
    private static int THREAD_NUMBER = 5;

    private static volatile int responseCounter = 0;

    private static SSLContext createContext() throws Exception {
        URL url = ConcurrentHttpsUrlConnectionTest.class.getResource("keystore.jks");
        KeyStore keyStore = KeyStore.getInstance("JKS");
        try (InputStream is = url.openStream()) {
            keyStore.load(is, "password".toCharArray());
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keyStore, "password".toCharArray());
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
        tmf.init(keyStore);
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
        return context;
    }

    @Test
    public void testSSLConnections() throws Exception {
        if (THREAD_NUMBER == 1) {
            System.out.println("\nThis is the working case (THREAD_NUMBER==1). Set THREAD_NUMBER > 1 to reproduce the error! \n");
        }

        final HttpsServer server = new HttpsServer(createContext());
        Executors.newFixedThreadPool(1).submit(server);

        // set THREAD_NUMBER > 1 to reproduce an issue
        ExecutorService executorService2clients = Executors.newFixedThreadPool(THREAD_NUMBER);

        final ClientBuilder builder = ClientBuilder.newBuilder().sslContext(createContext())
                .hostnameVerifier(new HostnameVerifier() {
                    public boolean verify(String arg0, SSLSession arg1) {
                        return true;
                    }
                });

        AtomicInteger counter = new AtomicInteger(0);

        for (int i = 0; i < THREAD_NUMBER; i++) {
            executorService2clients.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Client client = builder.build();
                        String ret = client.target("https://127.0.0.1:" + server.getPort() + "/" + new Random().nextInt())
                                .request(MediaType.TEXT_HTML)
                                .get(new GenericType<String>() {
                                });
                        System.out.print(++responseCounter + ". Server returned: " + ret);
                    } catch (Exception e) {
                        //get an exception here, if jersey lib is buggy and THREAD_NUMBER > 1:
                        //jakarta.ws.rs.ProcessingException: javax.net.ssl.SSLHandshakeException: PKIX path building failed:
                        e.printStackTrace();
                    } finally {
                        System.out.println(counter.incrementAndGet());
                    }
                }
            });
        }

        while (counter.get() != THREAD_NUMBER) {
            Thread.sleep(100L);
        }
        server.stop();
    }
}