/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.redblackit.web.server;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.apache.log4j.Logger;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ssl.SslConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import com.redblackit.web.KeyAndTrustStoreInfo;
/**
* @author djnorth
*
*/
@RunWith(Parameterized.class)
public class DefaultEmbeddedJettyServerTest {
private static final String CONTENT_TYPE = "text/plain";
private static final String HOSTNAME = HostNetUtils.getLocalHostname();
/**
* The tests will check what happens when the key-store is the trust-store
* by using the short constructor defaulting trust-store to key-store. Also,
* both server trust-stores are set up to work with both clients, and
* vice-versa.
*
* The tests will also artificially create invalid scenarios, so make the
* parameters below valid.
*
* @return parameters
*/
@Parameters
public static List<Object[]> getParameters() {
if (logger.isDebugEnabled()) {
System.setProperty("javax.net.debug", "ssl");
}
Object[][] parameters = {
{ 18280, 0, null, null, null, null, null, null, null, null },
{ 0, 8643, KeyAndTrustStoreInfo.getServer0Ks(),
KeyAndTrustStoreInfo.SERVER0_KS_PWD,
KeyAndTrustStoreInfo.getServer0Ts(),
KeyAndTrustStoreInfo.SERVER0_TS_PWD,
KeyAndTrustStoreInfo.getClient0Ks(),
KeyAndTrustStoreInfo.CLIENT0_KS_PWD,
KeyAndTrustStoreInfo.getClient0Ts(),
KeyAndTrustStoreInfo.CLIENT0_TS_PWD },
{ 18380, 8743, KeyAndTrustStoreInfo.getServer1Ks(),
KeyAndTrustStoreInfo.SERVER1_KS_PWD,
KeyAndTrustStoreInfo.getServer1Ts(),
KeyAndTrustStoreInfo.SERVER1_TS_PWD,
KeyAndTrustStoreInfo.getClient1Ks(),
KeyAndTrustStoreInfo.CLIENT1_KS_PWD,
KeyAndTrustStoreInfo.getClient1Ts(),
KeyAndTrustStoreInfo.CLIENT1_TS_PWD } };
return Arrays.asList(parameters);
}
private static Logger logger = Logger.getLogger("web.server");
/**
* Supplied end expected parms
*/
private int suppliedHttpPort;
private int suppliedHttpsPort;
private String suppliedKeyStore;
private String suppliedKeyStorePassword;
private String suppliedTrustStore;
private String suppliedTrustStorePassword;
private String clientKeyStore;
private String clientKeyStorePassword;
private String clientTrustStore;
private String clientTrustStorePassword;
/**
* Server under test
*/
private EmbeddedJettyServer embeddedJettyServer = null;
/**
* Constructor to build tests from parameters
*
* @param suppliedHttpPort
* @param suppliedHttpsPort
* @param suppliedKeyStore
* @param suppliedKeyStorePassword
* @param suppliedTrustStore
* @param suppliedTrustStorePassword
* @param clientKeyStore
* @param clientKeyStorePassword
* @param clientTrustStore
* @param clientTrustStorePassword
*/
public DefaultEmbeddedJettyServerTest(int suppliedHttpPort,
int suppliedHttpsPort, String suppliedKeyStore,
String suppliedKeyStorePassword, String suppliedTrustStore,
String suppliedTrustStorePassword, String clientKeyStore,
String clientKeyStorePassword, String clientTrustStore,
String clientTrustStorePassword) {
this.suppliedHttpPort = suppliedHttpPort;
this.suppliedHttpsPort = suppliedHttpsPort;
this.suppliedKeyStore = suppliedKeyStore;
this.suppliedKeyStorePassword = suppliedKeyStorePassword;
this.suppliedTrustStore = suppliedTrustStore;
this.suppliedTrustStorePassword = suppliedTrustStorePassword;
this.clientKeyStore = clientKeyStore;
this.clientKeyStorePassword = clientKeyStorePassword;
this.clientTrustStore = clientTrustStore;
this.clientTrustStorePassword = clientTrustStorePassword;
logger.info("test with this=" + this);
}
/**
* Test constructor with all arguments, assuming they are good
*/
@Test
public void testContructorAllArgsGood() throws Exception {
logger.debug("test:all args good:" + this);
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, suppliedKeyStore,
suppliedKeyStorePassword, suppliedTrustStore,
suppliedTrustStorePassword);
verifyConstructedServer(suppliedHttpPort, suppliedHttpsPort,
suppliedKeyStore, suppliedKeyStorePassword, suppliedTrustStore,
suppliedTrustStorePassword);
}
/**
* Test constructor with all arguments, with inconsistent passwords for
* matching key-store and trust-store.
*/
@Test
public void testContructorAllArgsHttpsKeyEqTrustStorePasswordsInconsistent()
throws Exception {
if (suppliedHttpsPort > 0 && suppliedKeyStore != null
&& suppliedKeyStore.equals(suppliedTrustStore)) {
logger.debug("test:httpsPort > 0 and keyStore eq trustStore:" + this);
try {
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, suppliedKeyStore,
suppliedKeyStorePassword, suppliedKeyStore,
suppliedKeyStorePassword + "!!!");
Assert.fail("expected an exception for mis-matched passwords");
} catch (IllegalStateException ise) {
logger.info("expected exception for mis-matched passwords:this=" + this, ise);
}
} else {
logger.debug("skipping test:httpsPort <=0 or keyStore != trustStore:" + this);
}
}
/**
* Test constructor with all arguments, with missing key-store
*/
@Test
public void testContructorAllArgsHttpsMissingKeyStore() throws Exception {
if (suppliedHttpsPort > 0) {
logger.debug("test:httpsPort > 0 missing keyStore:" + this);
try {
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, null,
suppliedKeyStorePassword, suppliedTrustStore,
suppliedTrustStorePassword);
Assert.fail("expected an exception for missing keyStore:" + this);
} catch (IllegalArgumentException iae) {
logger.debug("expected exception for missing keyStore:this=" + this, iae);
}
} else {
logger.info("skipping test for missing keyStore:httpsPort <=0");
}
}
/**
* Test constructor with all arguments, with missing key-store pwd
*/
@Test
public void testContructorAllArgsHttpsMissingKeyStorePassword()
throws Exception {
if (suppliedHttpsPort > 0) {
logger.debug("test:httpsPort > 0 missing keyStorePassword:" + this);
try {
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, suppliedKeyStore,
null, suppliedTrustStore, suppliedTrustStorePassword);
Assert.fail("expected an exception for missing keyStorePassword:" + this);
} catch (IllegalArgumentException iae) {
logger.info("expected exception for missing keyStorePassword:this=" + this, iae);
}
} else {
logger.info("skipping test for missing keyStorePassword:httpsPort <=0");
}
}
/**
* Test constructor with all arguments, with missing trust-store
*/
@Test
public void testContructorAllArgsHttpsMissingTrustStore() throws Exception {
if (suppliedHttpsPort > 0) {
logger.debug("test:httpsPort > 0 missing trustStore:" + this);
try {
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, suppliedKeyStore,
suppliedKeyStorePassword, null,
suppliedTrustStorePassword);
Assert.fail("expected an exception for missing trustStore:" + this);
} catch (IllegalArgumentException iae) {
logger.info("expected exception for missing trustStore:this=" + this, iae);
}
} else {
logger.info("skipping test for missing trustStore:httpsPort <=0");
}
}
/**
* Test constructor with all arguments, with missing trust-store password
*/
@Test
public void testContructorAllArgsHttpsMissingTrustStorePassword()
throws Exception {
if (suppliedHttpsPort > 0) {
logger.debug("test:httpsPort > 0 missing trustStorePassword:" + this);
try {
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, suppliedKeyStore,
suppliedKeyStorePassword, suppliedTrustStore, null);
Assert.fail("expected an exception for missing trustStorePassword:" + this);
} catch (IllegalArgumentException iae) {
logger.info("expected exception for missing trustStorePassword:this=" + this, iae);
}
} else {
logger.info("skipping test for missing trustStorePassword:httpsPort <=0");
}
}
/**
* Test short constructor with keyStore arguments (trustStore -> keyStore)
*/
@Test
public void testContructorKeyStoreArgs() throws Exception {
logger.debug("test:keyStore args only:" + this);
this.embeddedJettyServer = new DefaultEmbeddedJettyServer(
suppliedHttpPort, suppliedHttpsPort, suppliedKeyStore,
suppliedKeyStorePassword);
verifyConstructedServer(suppliedHttpPort, suppliedHttpsPort,
suppliedKeyStore, suppliedKeyStorePassword, suppliedKeyStore,
suppliedKeyStorePassword);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("DefaultEmbeddedJettyServerTest [suppliedHttpPort=");
builder.append(suppliedHttpPort);
builder.append(", suppliedHttpsPort=");
builder.append(suppliedHttpsPort);
builder.append(", suppliedKeyStore=");
builder.append(suppliedKeyStore);
builder.append(", suppliedKeyStorePassword=");
builder.append(suppliedKeyStorePassword);
builder.append(", suppliedTrustStore=");
builder.append(suppliedTrustStore);
builder.append(", suppliedTrustStorePassword=");
builder.append(suppliedTrustStorePassword);
builder.append(", clientKeyStore=");
builder.append(clientKeyStore);
builder.append(", clientKeyStorePassword=");
builder.append(clientKeyStorePassword);
builder.append(", clientTrustStore=");
builder.append(clientTrustStore);
builder.append(", clientTrustStorePassword=");
builder.append(clientTrustStorePassword);
builder.append(", embeddedJettyServer=");
builder.append(embeddedJettyServer);
builder.append("]");
return builder.toString();
}
/**
* Helper to verify constructed server.
*
* We've suppressed deprecation warnings given that getPort etc have been
* deprecated without corresponding update to Javadoc.
*
* @param useHttpPort
* @param useHttpsPort
* @param useKeyStore
* @param useKeyStorePassword
* @param useTrustStore
* @param useTrustStorePassword
*/
@SuppressWarnings("deprecation")
private void verifyConstructedServer(int useHttpPort, int useHttpsPort,
String useKeyStore, String useKeyStorePassword,
String useTrustStore, String useTrustStorePassword)
throws Exception {
final String msg = "useHttpPort=" + useHttpPort + ":useHttpsPort="
+ useHttpsPort + ":useKeyStore=" + useKeyStore
+ ":useKeyStorePassword=" + useKeyStorePassword
+ ":useTrustStore=" + useTrustStore + ":useTrustStorePassword="
+ useTrustStorePassword + ":this=" + this;
Assert.assertNotNull(msg, embeddedJettyServer);
Connector connector = embeddedJettyServer.getHttpConnector();
if (useHttpPort > 0) {
Assert.assertNotNull("should have httpConnector" + msg, connector);
Assert.assertEquals("httpPort" + msg, useHttpPort,
connector.getPort());
} else {
Assert.assertNull("should not have httpConnector" + msg, connector);
}
SslConnector sslConnector = embeddedJettyServer.getHttpsConnector();
if (useHttpsPort > 0) {
Assert.assertNotNull("should have httpsConnector" + msg,
sslConnector);
Assert.assertEquals("httpsPort" + msg, useHttpsPort,
sslConnector.getPort());
verifyStoreFile("keyStore", msg, useKeyStore, sslConnector.getKeystore());
verifyStoreFile("trustStore", msg, useTrustStore, sslConnector.getTruststore());
} else {
Assert.assertNull("should not have httpsConnector" + msg,
sslConnector);
}
if (useHttpPort > 0 || useHttpsPort > 0) {
ServletContextHandler context = new ServletContextHandler(
ServletContextHandler.SESSIONS);
context.setContextPath("/");
embeddedJettyServer.getServer().setHandler(context);
context.addServlet(new ServletHolder(new EchoServlet()), "/echo");
try {
Assert.assertTrue("Server not started in 120 secs:"
+ embeddedJettyServer,
embeddedJettyServer.startWait(120));
if (useHttpPort > 0) {
final String body = "testHttp";
URL url = new URL("http", HOSTNAME, useHttpPort, "/echo");
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
verifyRequestResponse(useHttpPort, body, connection);
}
if (useHttpsPort > 0) {
final String body = "testHttps";
SSLSocketFactory factory = null;
SSLContext ctx;
KeyManagerFactory kmf;
KeyStore ks;
char[] kspwd = clientKeyStorePassword.toCharArray();
TrustManagerFactory tmf;
KeyStore ts;
char[] tspwd = clientTrustStorePassword.toCharArray();
ctx = SSLContext.getInstance("TLS");
kmf = KeyManagerFactory.getInstance("SunX509");
ks = KeyStore.getInstance("JKS");
tmf = TrustManagerFactory.getInstance("SunX509");
ts = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(clientKeyStore), kspwd);
ts.load(new FileInputStream(clientTrustStore), tspwd);
kmf.init(ks, kspwd);
tmf.init(ts);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
factory = ctx.getSocketFactory();
URL url = new URL("https", HOSTNAME, useHttpsPort, "/echo");
HttpsURLConnection sslConnection = (HttpsURLConnection) url
.openConnection();
sslConnection.disconnect();
sslConnection.setSSLSocketFactory(factory);
verifyRequestResponse(useHttpsPort, body, sslConnection);
}
} finally {
if (embeddedJettyServer != null) {
embeddedJettyServer.stopAndJoin();
embeddedJettyServer = null;
}
}
}
}
/**
* Helper comparing store files, allowing for path expansion
*
* @param storeName
* @param msg
* @param expectedStoreFileName
* @param actualStoreFileName
*/
private void verifyStoreFile(String storeName, String msg, String expectedStoreFileName, String actualStoreFileName) {
final File expectedStoreFile = new File(expectedStoreFileName).getAbsoluteFile();
final File actualStoreFile = new File(actualStoreFileName).getAbsoluteFile();
Assert.assertEquals(storeName + ':' + msg, expectedStoreFile, actualStoreFile);
}
/**
* Call this to run common test using socket that's been set up
* appropriately
*
* @param useHttpPort
* @param body
* @param connection
* @throws ProtocolException
* @throws IOException
*/
private void verifyRequestResponse(int useHttpPort, final String body,
HttpURLConnection connection) throws ProtocolException, IOException {
final int bodyLen = body.length();
OutputStream reqOutputStream = null;
InputStreamReader respReader = null;
try {
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Host", HOSTNAME + ':' + useHttpPort);
connection.setRequestProperty("Content-Type", CONTENT_TYPE);
connection.setRequestProperty("Content-Length",
Integer.toString(bodyLen));
reqOutputStream = connection.getOutputStream();
PrintWriter reqWriter = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(reqOutputStream)));
reqWriter.print(body);
logger.debug("sent body +---\n" + body + "\nsent body:bodyLen="
+ bodyLen + " ----");
reqWriter.flush();
reqWriter.close();
reqOutputStream = null;
int responseCode = connection.getResponseCode();
String responseMsg = connection.getResponseMessage();
logger.debug("status:" + responseCode + " " + responseMsg);
Assert.assertEquals("Should get 200 OK:" + responseMsg,
HttpURLConnection.HTTP_OK, responseCode);
String contentType = connection.getContentType();
logger.debug("Content-Type:" + contentType);
int contentLen = connection.getContentLength();
logger.debug("Content-Length:" + contentLen);
Assert.assertEquals("Content-Type", CONTENT_TYPE, contentType);
Map<String, List<String>> headers = connection.getHeaderFields();
logger.debug("headers:" + headers);
respReader = new InputStreamReader(connection.getInputStream());
char[] respBodyBytes = new char[contentLen + 1];
int readLen = respReader.read(respBodyBytes, 0,
respBodyBytes.length);
Assert.assertTrue("resp body not empty", readLen >= 0);
String respBody = new String(respBodyBytes, 0, readLen);
logger.debug("resp body +---\n" + respBody + "\nresp body:readLen="
+ readLen + " ----");
Assert.assertEquals("Content-Length", bodyLen, contentLen);
Assert.assertEquals("Length read", contentLen, readLen);
Assert.assertEquals("body text", body, respBody);
} finally {
if (reqOutputStream != null) {
reqOutputStream.close();
}
if (respReader != null) {
respReader.close();
}
}
}
}