// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.server.ssl; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.eclipse.jetty.util.thread.Scheduler; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.Test; public class SslContextFactoryReloadTest { public static final String KEYSTORE_1 = "src/test/resources/reload_keystore_1.jks"; public static final String KEYSTORE_2 = "src/test/resources/reload_keystore_2.jks"; private Server server; private SslContextFactory sslContextFactory; private ServerConnector connector; private void start(Handler handler) throws Exception { server = new Server(); sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(KEYSTORE_1); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyStoreType("JKS"); sslContextFactory.setKeyStoreProvider(null); HttpConfiguration httpsConfig = new HttpConfiguration(); httpsConfig.addCustomizer(new SecureRequestCustomizer()); connector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig)); server.addConnector(connector); server.setHandler(handler); server.start(); } @After public void dispose() throws Exception { if (server != null) server.stop(); } @Test public void testReload() throws Exception { start(new EchoHandler()); SSLContext ctx = SSLContext.getInstance("TLSv1.2"); ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, null); SSLSocketFactory socketFactory = ctx.getSocketFactory(); try (SSLSocket client1 = (SSLSocket)socketFactory.createSocket("localhost", connector.getLocalPort())) { String serverDN1 = client1.getSession().getPeerPrincipal().getName(); Assert.assertThat(serverDN1, Matchers.startsWith("CN=localhost1")); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n"; OutputStream output1 = client1.getOutputStream(); output1.write(request.getBytes(StandardCharsets.UTF_8)); output1.flush(); HttpTester.Response response1 = HttpTester.parseResponse(HttpTester.from(client1.getInputStream())); Assert.assertNotNull(response1); Assert.assertThat(response1.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); // Reconfigure SslContextFactory. sslContextFactory.reload(sslContextFactory -> { sslContextFactory.setKeyStorePath(KEYSTORE_2); sslContextFactory.setKeyStorePassword("storepwd"); }); // New connection should use the new keystore. try (SSLSocket client2 = (SSLSocket)socketFactory.createSocket("localhost", connector.getLocalPort())) { String serverDN2 = client2.getSession().getPeerPrincipal().getName(); Assert.assertThat(serverDN2, Matchers.startsWith("CN=localhost2")); OutputStream output2 = client1.getOutputStream(); output2.write(request.getBytes(StandardCharsets.UTF_8)); output2.flush(); HttpTester.Response response2 = HttpTester.parseResponse(HttpTester.from(client1.getInputStream())); Assert.assertNotNull(response2); Assert.assertThat(response2.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); } // Must still be possible to make requests with the first connection. output1.write(request.getBytes(StandardCharsets.UTF_8)); output1.flush(); response1 = HttpTester.parseResponse(HttpTester.from(client1.getInputStream())); Assert.assertNotNull(response1); Assert.assertThat(response1.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); } } @Test public void testReloadWhileServing() throws Exception { start(new EchoHandler()); Scheduler scheduler = new ScheduledExecutorScheduler(); scheduler.start(); try { SSLContext ctx = SSLContext.getInstance("TLSv1.2"); ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, null); SSLSocketFactory socketFactory = ctx.getSocketFactory(); // Perform 4 reloads while connections are being served. AtomicInteger reloads = new AtomicInteger(4); long reloadPeriod = 500; AtomicBoolean running = new AtomicBoolean(true); scheduler.schedule(new Runnable() { @Override public void run() { if (reloads.decrementAndGet() == 0) { running.set(false); } else { try { sslContextFactory.reload(sslContextFactory -> { if (sslContextFactory.getKeyStorePath().endsWith(KEYSTORE_1)) sslContextFactory.setKeyStorePath(KEYSTORE_2); else sslContextFactory.setKeyStorePath(KEYSTORE_1); }); scheduler.schedule(this, reloadPeriod, TimeUnit.MILLISECONDS); } catch (Exception x) { running.set(false); reloads.set(-1); } } } }, reloadPeriod, TimeUnit.MILLISECONDS); byte[] content = new byte[16 * 1024]; while (running.get()) { try (SSLSocket client = (SSLSocket)socketFactory.createSocket("localhost", connector.getLocalPort())) { // We need to invalidate the session every time we open a new SSLSocket. // This is because when the client uses session resumption, it caches // the server certificates and then checks that it is the same during // a new TLS handshake. If the SslContextFactory is reloaded during the // TLS handshake, the client will see the new certificate and blow up. // Note that browsers can handle this case better: they will just not // use session resumption and fallback to the normal TLS handshake. client.getSession().invalidate(); String request1 = "" + "POST / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: " + content.length + "\r\n" + "\r\n"; OutputStream outputStream = client.getOutputStream(); outputStream.write(request1.getBytes(StandardCharsets.UTF_8)); outputStream.write(content); outputStream.flush(); InputStream inputStream = client.getInputStream(); HttpTester.Response response1 = HttpTester.parseResponse(HttpTester.from(inputStream)); Assert.assertNotNull(response1); Assert.assertThat(response1.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); String request2 = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n"; outputStream.write(request2.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); HttpTester.Response response2 = HttpTester.parseResponse(HttpTester.from(inputStream)); Assert.assertNotNull(response2); Assert.assertThat(response2.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); } } Assert.assertEquals(0, reloads.get()); } finally { scheduler.stop(); } } private static class EchoHandler extends AbstractHandler { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { baseRequest.setHandled(true); if (HttpMethod.POST.is(request.getMethod())) IO.copy(request.getInputStream(), response.getOutputStream()); else response.setContentLength(0); } } }