blob: 647a54d8f9232dc3664df89e24672a00b91353c8 [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.brooklyn.launcher;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.File;
import java.io.FileInputStream;
import java.net.SocketException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.rest.BrooklynWebConfig;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.http.HttpTool;
import org.apache.brooklyn.util.http.HttpToolResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
public class BrooklynWebServerTest {
public static final Logger log = LoggerFactory.getLogger(BrooklynWebServer.class);
private BrooklynProperties brooklynProperties;
private BrooklynWebServer webServer;
private List<LocalManagementContext> managementContexts = Lists.newCopyOnWriteArrayList();
@BeforeMethod(alwaysRun=true)
public void setUp(){
brooklynProperties = BrooklynProperties.Factory.newEmpty();
}
@AfterMethod(alwaysRun=true)
public void tearDown() throws Exception {
for (LocalManagementContext managementContext : managementContexts) {
Entities.destroyAll(managementContext);
}
managementContexts.clear();
if (webServer != null) webServer.stop();
}
private LocalManagementContext newManagementContext(BrooklynProperties brooklynProperties) {
LocalManagementContext result = new LocalManagementContextForTests(brooklynProperties);
managementContexts.add(result);
return result;
}
@Test
public void verifyHttp() throws Exception {
webServer = new BrooklynWebServer(newManagementContext(brooklynProperties));
webServer.skipSecurity();
try {
webServer.start();
HttpToolResponse response = HttpTool.execAndConsume(HttpTool.httpClientBuilder().build(), new HttpGet(webServer.getRootUrl()));
assertEquals(response.getResponseCode(), 200);
} finally {
webServer.stop();
}
}
@Test
public void verifySecurityInitialized() throws Exception {
webServer = new BrooklynWebServer(newManagementContext(brooklynProperties));
webServer.start();
try {
HttpToolResponse response = HttpTool.execAndConsume(HttpTool.httpClientBuilder().build(), new HttpGet(webServer.getRootUrl()));
assertEquals(response.getResponseCode(), 401);
} finally {
webServer.stop();
}
}
@Test
public void verifySecurityInitializedExplicitUser() throws Exception {
webServer = new BrooklynWebServer(newManagementContext(brooklynProperties));
webServer.start();
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("myuser", "somepass"));
HttpClient client = HttpTool.httpClientBuilder()
.credentials(new UsernamePasswordCredentials("myuser", "somepass"))
.uri(webServer.getRootUrl())
.build();
try {
HttpToolResponse response = HttpTool.execAndConsume(client, new HttpGet(webServer.getRootUrl()));
assertEquals(response.getResponseCode(), 401);
} finally {
webServer.stop();
}
}
@DataProvider(name="keystorePaths")
public Object[][] getKeystorePaths() {
return new Object[][] {
{getFile("server.ks")},
{new File(getFile("server.ks")).toURI().toString()},
{"classpath://server.ks"}};
}
@Test(dataProvider="keystorePaths")
public void verifyHttps(String keystoreUrl) throws Exception {
Map<String,?> flags = ImmutableMap.<String,Object>builder()
.put("httpsEnabled", true)
.put("keystoreUrl", keystoreUrl)
.put("keystorePassword", "password")
.build();
webServer = new BrooklynWebServer(flags, newManagementContext(brooklynProperties));
webServer.skipSecurity().start();
try {
KeyStore keyStore = load("client.ks", "password");
KeyStore trustStore = load("client.ts", "password");
SSLSocketFactory socketFactory = new SSLSocketFactory(SSLSocketFactory.TLS, keyStore, "password", trustStore, (SecureRandom)null, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpToolResponse response = HttpTool.execAndConsume(
HttpTool.httpClientBuilder()
.port(webServer.getActualPort())
.https(true)
.socketFactory(socketFactory)
.build(),
new HttpGet(webServer.getRootUrl()));
assertEquals(response.getResponseCode(), 200);
} finally {
webServer.stop();
}
}
@Test
public void verifyHttpsFromConfig() throws Exception {
brooklynProperties.put(BrooklynWebConfig.HTTPS_REQUIRED, true);
brooklynProperties.put(BrooklynWebConfig.KEYSTORE_URL, getFile("server.ks"));
brooklynProperties.put(BrooklynWebConfig.KEYSTORE_PASSWORD, "password");
verifyHttpsFromConfig(brooklynProperties);
}
@Test
public void verifyHttpsCiphers() throws Exception {
brooklynProperties.put(BrooklynWebConfig.HTTPS_REQUIRED, true);
brooklynProperties.put(BrooklynWebConfig.TRANSPORT_PROTOCOLS, "IGNORED");
brooklynProperties.put(BrooklynWebConfig.TRANSPORT_CIPHERS, "IGNORED");
try {
verifyHttpsFromConfig(brooklynProperties);
fail("Expected to fail due to unsupported ciphers during connection negotiation");
} catch (Exception e) {
// if the server manages to process the error in the protocol ciphers above
// before the client enters the SSL handshake, then the server closes the socket
// abruptly and the client throws java.net.SocketException
// (see org.eclipse.jetty.io.ssl.SslConnection.onOpen)
//
// however, if the client manages to enter SSL negotiations, then the same behavior above
// causes the client to reports javax.net.ssl.SSLHandshakeException
// (see org.apache.http.conn.ssl.SSLSocketFactory.connectSocket)
//
// this race happens because org.apache.http.conn.ssl.SSLSockerFactory.connectSocket(...)
// calls java.net.Socket.connect(), and next javax.net.ssl.SSLSocket.startHandshake(),
// which provides the opportunity for the server to interweave between those and close the
// socket before the client calls startHandshake(), or maybe miss it
assertTrue((Exceptions.getFirstThrowableOfType(e, SocketException.class) != null)
|| (Exceptions.getFirstThrowableOfType(e, SSLException.class) != null)
|| (Exceptions.getFirstThrowableOfType(e, SSLHandshakeException.class) != null),
"Expected to fail due to inability to negotiate");
}
}
private void verifyHttpsFromConfig(BrooklynProperties brooklynProperties) throws Exception {
webServer = new BrooklynWebServer(MutableMap.of(), newManagementContext(brooklynProperties));
webServer.skipSecurity();
webServer.start();
try {
KeyStore keyStore = load("client.ks", "password");
KeyStore trustStore = load("client.ts", "password");
SSLSocketFactory socketFactory = new SSLSocketFactory(SSLSocketFactory.TLS, keyStore, "password", trustStore, (SecureRandom)null, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpToolResponse response = HttpTool.execAndConsume(
HttpTool.httpClientBuilder()
.port(webServer.getActualPort())
.https(true)
.socketFactory(socketFactory)
.build(),
new HttpGet(webServer.getRootUrl()));
assertEquals(response.getResponseCode(), 200);
} finally {
webServer.stop();
}
}
private KeyStore load(String name, String password) throws Exception {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream instream = new FileInputStream(new File(getFile(name)));
keystore.load(instream, password.toCharArray());
return keystore;
}
@Test
public void testGetFileFromUrl() throws Exception {
// On Windows will treat as relative paths
String url = "file:///tmp/special%40file%20with%20spaces";
String file = "/tmp/special@file with spaces";
assertEquals(getFile(new URL(url)), new File(file).getAbsolutePath());
}
private String getFile(String classpathResource) {
// this works because both IDE and Maven run tests with classes/resources on the file system
return getFile(getClass().getResource("/" + classpathResource));
}
private String getFile(URL url) {
try {
return new File(url.toURI()).getAbsolutePath();
} catch (URISyntaxException e) {
throw Exceptions.propagate(e);
}
}
}