blob: 38fd926a56f97a2b6e6520d3e20f9bfee2b8c269 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.hadoop.http;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.net.NetUtils;
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
import org.apache.hadoop.security.ssl.SSLFactory;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.util.StringUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This testcase issues SSL certificates configures the HttpServer to serve
* HTTPS using the created certificates and calls an echo servlet using the
* corresponding HTTPS URL.
*/
public class TestSSLHttpServer extends HttpServerFunctionalTest {
private static final String BASEDIR =
GenericTestUtils.getTempPath(TestSSLHttpServer.class.getSimpleName());
private static final Logger LOG =
LoggerFactory.getLogger(TestSSLHttpServer.class);
private static final String HTTPS_CIPHER_SUITES_KEY = "https.cipherSuites";
private static final String JAVAX_NET_DEBUG_KEY = "javax.net.debug";
private static Configuration conf;
private static HttpServer2 server;
private static String keystoresDir;
private static String sslConfDir;
private static SSLFactory clientSslFactory;
private static String cipherSuitesPropertyValue;
private static String sslDebugPropertyValue;
private static final String excludeCiphers = "TLS_ECDHE_RSA_WITH_RC4_128_SHA,"
+ "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, \n"
+ "SSL_RSA_WITH_DES_CBC_SHA,"
+ "SSL_DHE_RSA_WITH_DES_CBC_SHA, "
+ "SSL_RSA_EXPORT_WITH_RC4_40_MD5,\t \n"
+ "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA,"
+ "SSL_RSA_WITH_RC4_128_MD5 \t";
private static final String oneEnabledCiphers = excludeCiphers
+ ",TLS_RSA_WITH_AES_128_CBC_SHA";
private static final String exclusiveEnabledCiphers
= "\tTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, \n"
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,"
+ "TLS_RSA_WITH_AES_128_CBC_SHA,"
+ "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, "
+ "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,"
+ "TLS_DHE_RSA_WITH_AES_128_CBC_SHA,\t\n "
+ "TLS_DHE_DSS_WITH_AES_128_CBC_SHA";
@BeforeClass
public static void setup() throws Exception {
turnOnSSLDebugLogging();
storeHttpsCipherSuites();
conf = new Configuration();
conf.setInt(HttpServer2.HTTP_MAX_THREADS, 10);
File base = new File(BASEDIR);
FileUtil.fullyDelete(base);
base.mkdirs();
keystoresDir = new File(BASEDIR).getAbsolutePath();
sslConfDir = KeyStoreTestUtil.getClasspathDir(TestSSLHttpServer.class);
KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, conf, false, true,
excludeCiphers);
Configuration sslConf = KeyStoreTestUtil.getSslConfig();
clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, sslConf);
clientSslFactory.init();
server = new HttpServer2.Builder()
.setName("test")
.addEndpoint(new URI("https://localhost"))
.setConf(conf)
.keyPassword(sslConf.get("ssl.server.keystore.keypassword"))
.keyStore(sslConf.get("ssl.server.keystore.location"),
sslConf.get("ssl.server.keystore.password"),
sslConf.get("ssl.server.keystore.type", "jks"))
.trustStore(sslConf.get("ssl.server.truststore.location"),
sslConf.get("ssl.server.truststore.password"),
sslConf.get("ssl.server.truststore.type", "jks"))
.excludeCiphers(
sslConf.get("ssl.server.exclude.cipher.list")).build();
server.addServlet("echo", "/echo", TestHttpServer.EchoServlet.class);
server.addServlet("longheader", "/longheader", LongHeaderServlet.class);
server.start();
baseUrl = new URL("https://"
+ NetUtils.getHostPortString(server.getConnectorAddress(0)));
LOG.info("HTTP server started: " + baseUrl);
}
@AfterClass
public static void cleanup() throws Exception {
server.stop();
FileUtil.fullyDelete(new File(BASEDIR));
KeyStoreTestUtil.cleanupSSLConfig(keystoresDir, sslConfDir);
clientSslFactory.destroy();
restoreHttpsCipherSuites();
restoreSSLDebugLogging();
}
/**
* Stores the JVM property value of https.cipherSuites and sets its
* value to an empty string.
* This ensures that the value https.cipherSuites does
* not affect the result of tests.
*/
private static void storeHttpsCipherSuites() {
String cipherSuites = System.getProperty(HTTPS_CIPHER_SUITES_KEY);
if (cipherSuites != null) {
LOG.info(
"Found value for property {}: {}", HTTPS_CIPHER_SUITES_KEY,
cipherSuites);
cipherSuitesPropertyValue = cipherSuites;
}
System.clearProperty(HTTPS_CIPHER_SUITES_KEY);
}
private static void restoreHttpsCipherSuites() {
if (cipherSuitesPropertyValue != null) {
LOG.info("Restoring property {} to value: {}", HTTPS_CIPHER_SUITES_KEY,
cipherSuitesPropertyValue);
System.setProperty(HTTPS_CIPHER_SUITES_KEY, cipherSuitesPropertyValue);
cipherSuitesPropertyValue = null;
}
}
private static void turnOnSSLDebugLogging() {
String sslDebug = System.getProperty(JAVAX_NET_DEBUG_KEY);
if (sslDebug != null) {
sslDebugPropertyValue = sslDebug;
}
System.setProperty(JAVAX_NET_DEBUG_KEY, "all");
}
private static void restoreSSLDebugLogging() {
if (sslDebugPropertyValue != null) {
System.setProperty(JAVAX_NET_DEBUG_KEY, sslDebugPropertyValue);
sslDebugPropertyValue = null;
} else {
System.clearProperty(JAVAX_NET_DEBUG_KEY);
}
}
@Test
public void testEcho() throws Exception {
assertEquals("a:b\nc:d\n", readOut(new URL(baseUrl, "/echo?a=b&c=d")));
assertEquals("a:b\nc<:d\ne:>\n", readOut(new URL(baseUrl,
"/echo?a=b&c<=d&e=>")));
}
/**
* Test that verifies headers can be up to 64K long. The test adds a 63K
* header leaving 1K for other headers. This is because the header buffer
* setting is for ALL headers, names and values included.
*/
@Test
public void testLongHeader() throws Exception {
URL url = new URL(baseUrl, "/longheader");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(clientSslFactory.createSSLSocketFactory());
testLongHeader(conn);
}
private static String readOut(URL url) throws Exception {
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(clientSslFactory.createSSLSocketFactory());
InputStream in = conn.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copyBytes(in, out, 1024);
return out.toString();
}
/**
* Test that verifies that excluded ciphers (SSL_RSA_WITH_RC4_128_SHA,
* TLS_ECDH_ECDSA_WITH_RC4_128_SHA,TLS_ECDH_RSA_WITH_RC4_128_SHA,
* TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,TLS_ECDHE_RSA_WITH_RC4_128_SHA) are not
* available for negotiation during SSL connection.
*/
@Test
public void testExcludedCiphers() throws Exception {
URL url = new URL(baseUrl, "/echo?a=b&c=d");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
SSLSocketFactory sslSocketF = clientSslFactory.createSSLSocketFactory();
PrefferedCipherSSLSocketFactory testPreferredCipherSSLSocketF
= new PrefferedCipherSSLSocketFactory(sslSocketF,
StringUtils.getTrimmedStrings(excludeCiphers));
conn.setSSLSocketFactory(testPreferredCipherSSLSocketF);
assertFalse("excludedCipher list is empty", excludeCiphers.isEmpty());
try {
InputStream in = conn.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copyBytes(in, out, 1024);
fail("No Ciphers in common, SSLHandshake must fail.");
} catch (SSLHandshakeException ex) {
LOG.info("No Ciphers in common, expected succesful test result.", ex);
}
}
/** Test that verified that additionally included cipher
* TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA is only available cipher for working
* TLS connection from client to server disabled for all other common ciphers.
*/
@Test
public void testOneEnabledCiphers() throws Exception {
URL url = new URL(baseUrl, "/echo?a=b&c=d");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
SSLSocketFactory sslSocketF = clientSslFactory.createSSLSocketFactory();
PrefferedCipherSSLSocketFactory testPreferredCipherSSLSocketF
= new PrefferedCipherSSLSocketFactory(sslSocketF,
StringUtils.getTrimmedStrings(oneEnabledCiphers));
conn.setSSLSocketFactory(testPreferredCipherSSLSocketF);
assertFalse("excludedCipher list is empty", oneEnabledCiphers.isEmpty());
try {
InputStream in = conn.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copyBytes(in, out, 1024);
assertEquals(out.toString(), "a:b\nc:d\n");
LOG.info("Atleast one additional enabled cipher than excluded ciphers,"
+ " expected successful test result.");
} catch (SSLHandshakeException ex) {
fail("Atleast one additional cipher available for successful handshake."
+ " Unexpected test failure: " + ex);
}
}
/** Test verifies that mutually exclusive server's disabled cipher suites and
* client's enabled cipher suites can successfully establish TLS connection.
*/
@Test
public void testExclusiveEnabledCiphers() throws Exception {
URL url = new URL(baseUrl, "/echo?a=b&c=d");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
SSLSocketFactory sslSocketF = clientSslFactory.createSSLSocketFactory();
PrefferedCipherSSLSocketFactory testPreferredCipherSSLSocketF
= new PrefferedCipherSSLSocketFactory(sslSocketF,
StringUtils.getTrimmedStrings(exclusiveEnabledCiphers));
conn.setSSLSocketFactory(testPreferredCipherSSLSocketF);
assertFalse("excludedCipher list is empty",
exclusiveEnabledCiphers.isEmpty());
try {
InputStream in = conn.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copyBytes(in, out, 1024);
assertEquals(out.toString(), "a:b\nc:d\n");
LOG.info("Atleast one additional enabled cipher than excluded ciphers,"
+ " expected successful test result.");
} catch (SSLHandshakeException ex) {
fail("Atleast one additional cipher available for successful handshake."
+ " Unexpected test failure: " + ex);
}
}
private class PrefferedCipherSSLSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory delegateSocketFactory;
private final String[] enabledCipherSuites;
public PrefferedCipherSSLSocketFactory(SSLSocketFactory sslSocketFactory,
String[] pEnabledCipherSuites) {
delegateSocketFactory = sslSocketFactory;
if (null != pEnabledCipherSuites && pEnabledCipherSuites.length > 0) {
enabledCipherSuites = pEnabledCipherSuites;
} else {
enabledCipherSuites = null;
}
}
@Override
public String[] getDefaultCipherSuites() {
return delegateSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return delegateSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String string, int i, boolean bln)
throws IOException {
SSLSocket sslSocket = (SSLSocket) delegateSocketFactory.createSocket(
socket, string, i, bln);
if (null != enabledCipherSuites) {
sslSocket.setEnabledCipherSuites(enabledCipherSuites);
}
return sslSocket;
}
@Override
public Socket createSocket(String string, int i) throws IOException,
UnknownHostException {
SSLSocket sslSocket = (SSLSocket) delegateSocketFactory.createSocket(
string, i);
if (null != enabledCipherSuites) {
sslSocket.setEnabledCipherSuites(enabledCipherSuites);
}
return sslSocket;
}
@Override
public Socket createSocket(String string, int i, InetAddress ia, int i1)
throws IOException, UnknownHostException {
SSLSocket sslSocket = (SSLSocket) delegateSocketFactory.createSocket(
string, i, ia, i1);
if (null != enabledCipherSuites) {
sslSocket.setEnabledCipherSuites(enabledCipherSuites);
}
return sslSocket;
}
@Override
public Socket createSocket(InetAddress ia, int i) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegateSocketFactory.createSocket(ia,
i);
if (null != enabledCipherSuites) {
sslSocket.setEnabledCipherSuites(enabledCipherSuites);
}
return sslSocket;
}
@Override
public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1)
throws IOException {
SSLSocket sslSocket = (SSLSocket) delegateSocketFactory.createSocket(ia,
i, ia1, i1);
if (null != enabledCipherSuites) {
sslSocket.setEnabledCipherSuites(enabledCipherSuites);
}
return sslSocket;
}
}
}