/**
* Copyright 2016 Yahoo Inc.
*
* 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.yahoo.pulsar.broker.web;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.apache.bookkeeper.test.PortManager;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
import com.google.common.io.CharStreams;
import com.google.common.io.Closeables;
import com.yahoo.pulsar.broker.PulsarService;
import com.yahoo.pulsar.broker.ServiceConfiguration;
import com.yahoo.pulsar.client.admin.PulsarAdmin;
import com.yahoo.pulsar.client.admin.PulsarAdminException.ConflictException;
import com.yahoo.pulsar.client.api.Authentication;
import com.yahoo.pulsar.client.api.ClientConfiguration;
import com.yahoo.pulsar.client.impl.auth.AuthenticationTls;
import com.yahoo.pulsar.common.policies.data.ClusterData;
import com.yahoo.pulsar.common.util.SecurityUtility;
import com.yahoo.pulsar.zookeeper.MockedZooKeeperClientFactoryImpl;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
/**
* Tests for the {@code WebService} class. Note that this test only covers the newly added ApiVersionFilter related
* tests for now as this test class was added quite a bit after the class was written.
*
*/
public class WebServiceTest {
private PulsarService pulsar;
private final static int BROKER_WEBSERVICE_PORT = PortManager.nextFreePort();
private final static int BROKER_WEBSERVICE_PORT_TLS = PortManager.nextFreePort();
private static final String BROKER_URL_BASE = "http://localhost:" + BROKER_WEBSERVICE_PORT;
private static final String BROKER_URL_BASE_TLS = "https://localhost:" + BROKER_WEBSERVICE_PORT_TLS;
private static final String BROKER_LOOKUP_URL = BROKER_URL_BASE
+ "/lookup/v2/destination/persistent/my-property/local/my-namespace/my-topic";
private static final String BROKER_LOOKUP_URL_TLS = BROKER_URL_BASE_TLS
+ "/lookup/v2/destination/persistent/my-property/local/my-namespace/my-topic";
private static final String TLS_SERVER_CERT_FILE_PATH = "./src/test/resources/certificate/server.crt";
private static final String TLS_SERVER_KEY_FILE_PATH = "./src/test/resources/certificate/server.key";
private static final String TLS_CLIENT_CERT_FILE_PATH = "./src/test/resources/certificate/client.crt";
private static final String TLS_CLIENT_KEY_FILE_PATH = "./src/test/resources/certificate/client.key";
/**
* Test that if the enableClientVersionCheck option is enabled, the {@code ApiVersionFilter} is added to the filter
* chain. We test this indirectly by creating live PulsarService and making an http call to it.
*/
@Test
public void testFilterEnabled() throws Exception {
setupEnv(true, "1.0", false, false, false, false);
// Make an HTTP request to lookup a namespace. The request should fail
// with a 400 error.
try {
makeHttpRequest(false, false);
Assert.fail("Request should have failed."); // We should have gotten an exception on the previous
// line.
} catch (IOException ex) {
Assert.assertTrue(ex.getMessage().contains("HTTP response code: 400"));
}
}
/**
* Test that if the enableClientVersionCheck option is disabled, the {@code ApiVersionFilter} is not added to the
* filter chain. We test this indirectly by creating live PulsarService and making an http call to it.
*
*/
@Test
public void testFilterDisabled() throws Exception {
setupEnv(false, "1.0", false, false, false, false);
try {
// Make an HTTP request to lookup a namespace. The request should
// succeed
makeHttpRequest(false, false);
} catch (Exception e) {
Assert.fail("HTTP request to lookup a namespace shouldn't fail ", e);
}
}
/**
* Test that the {@WebService} class properly passes the allowUnversionedClients value. We do this by setting
* allowUnversionedClients to true, then making a request with no version, which should go through.
*
*/
@Test
public void testDefaultClientVersion() throws Exception {
setupEnv(true, "1.0", true, false, false, false);
try {
// Make an HTTP request to lookup a namespace. The request should
// succeed
makeHttpRequest(false, false);
} catch (Exception e) {
Assert.fail("HTTP request to lookup a namespace shouldn't fail ", e);
}
}
/**
* Test that if enableTls option is enabled, WebServcie is available both on HTTP and HTTPS.
*
* @throws Exception
*/
@Test
public void testTlsEnabled() throws Exception {
setupEnv(false, "1.0", false, true, false, false);
// Make requests both HTTP and HTTPS. The requests should succeed
try {
makeHttpRequest(false, false);
} catch (Exception e) {
Assert.fail("HTTP request shouldn't fail ", e);
}
try {
makeHttpRequest(true, false);
} catch (Exception e) {
Assert.fail("HTTPS request shouldn't fail ", e);
}
}
/**
* Test that if enableTls option is disabled, WebServcie is available only on HTTP.
*
* @throws Exception
*/
@Test
public void testTlsDisabled() throws Exception {
setupEnv(false, "1.0", false, false, false, false);
// Make requests both HTTP and HTTPS. Only the HTTP request should succeed
try {
makeHttpRequest(false, false);
} catch (Exception e) {
Assert.fail("HTTP request shouldn't fail ", e);
}
try {
makeHttpRequest(true, false);
Assert.fail("HTTPS request should fail ");
} catch (Exception e) {
Assert.assertTrue(e.getMessage().contains("Connection refused"));
}
}
/**
* Test that if enableAuth option and allowInsecure option are enabled, WebServcie requires trusted/untrusted client
* certificate.
*
* @throws Exception
*/
@Test
public void testTlsAuthAllowInsecure() throws Exception {
setupEnv(false, "1.0", false, true, true, true);
// Only the request with client certificate should succeed
try {
makeHttpRequest(true, false);
Assert.fail("Request without client certficate should fail");
} catch (Exception e) {
Assert.assertTrue(e.getMessage().contains("HTTP response code: 401"));
}
try {
makeHttpRequest(true, true);
} catch (Exception e) {
Assert.fail("Request with client certificate shouldn't fail", e);
}
}
/**
* Test that if enableAuth option is enabled, WebServcie requires trusted client certificate.
*
* @throws Exception
*/
@Test
public void testTlsAuthDisallowInsecure() throws Exception {
setupEnv(false, "1.0", false, true, true, false);
// Only the request with trusted client certificate should succeed
try {
makeHttpRequest(true, false);
Assert.fail("Request without client certficate should fail");
} catch (Exception e) {
Assert.assertTrue(e.getMessage().contains("HTTP response code: 401"));
}
try {
makeHttpRequest(true, true);
} catch (Exception e) {
Assert.fail("Request with client certificate shouldn't fail", e);
}
}
@Test
public void testSplitPath() {
String result = PulsarWebResource.splitPath("prop/cluster/ns/topic1", 4);
Assert.assertEquals(result, "topic1");
}
private String makeHttpRequest(boolean useTls, boolean useAuth) throws Exception {
InputStream response = null;
try {
if (useTls) {
KeyManager[] keyManagers = null;
if (useAuth) {
Certificate[] tlsCert = SecurityUtility.loadCertificatesFromPemFile(TLS_CLIENT_CERT_FILE_PATH);
PrivateKey tlsKey = SecurityUtility.loadPrivateKeyFromPemFile(TLS_CLIENT_KEY_FILE_PATH);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setKeyEntry("private", tlsKey, "".toCharArray(), tlsCert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "".toCharArray());
keyManagers = kmf.getKeyManagers();
}
TrustManager[] trustManagers = InsecureTrustManagerFactory.INSTANCE.getTrustManagers();
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(keyManagers, trustManagers, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslCtx.getSocketFactory());
response = new URL(BROKER_LOOKUP_URL_TLS).openStream();
} else {
response = new URL(BROKER_LOOKUP_URL).openStream();
}
String resp = CharStreams.toString(new InputStreamReader(response));
log.info("Response: {}", resp);
return resp;
} finally {
Closeables.close(response, false);
}
}
private void setupEnv(boolean enableFilter, String minApiVersion, boolean allowUnversionedClients,
boolean enableTls, boolean enableAuth, boolean allowInsecure) throws Exception {
Set<String> providers = new HashSet<>();
providers.add("com.yahoo.pulsar.broker.authentication.AuthenticationProviderTls");
Set<String> roles = new HashSet<>();
roles.add("client");
ServiceConfiguration config = new ServiceConfiguration();
config.setWebServicePort(BROKER_WEBSERVICE_PORT);
config.setWebServicePortTls(BROKER_WEBSERVICE_PORT_TLS);
config.setClientLibraryVersionCheckEnabled(enableFilter);
config.setAuthenticationEnabled(enableAuth);
config.setAuthenticationProviders(providers);
config.setAuthorizationEnabled(false);
config.setClientLibraryVersionCheckAllowUnversioned(allowUnversionedClients);
config.setSuperUserRoles(roles);
config.setTlsEnabled(enableTls);
config.setTlsCertificateFilePath(TLS_SERVER_CERT_FILE_PATH);
config.setTlsKeyFilePath(TLS_SERVER_KEY_FILE_PATH);
config.setTlsAllowInsecureConnection(allowInsecure);
config.setTlsTrustCertsFilePath(allowInsecure ? "" : TLS_CLIENT_CERT_FILE_PATH);
config.setClusterName("local");
config.setAdvertisedAddress("localhost"); // TLS certificate expects localhost
pulsar = spy(new PulsarService(config));
doReturn(new MockedZooKeeperClientFactoryImpl()).when(pulsar).getZooKeeperClientFactory();
pulsar.start();
try {
pulsar.getZkClient().delete("/minApiVersion", -1);
} catch (Exception ex) {
}
pulsar.getZkClient().create("/minApiVersion", minApiVersion.getBytes(), null, CreateMode.PERSISTENT);
String serviceUrl = BROKER_URL_BASE;
ClientConfiguration clientConfig = new ClientConfiguration();
if (enableTls && enableAuth) {
serviceUrl = BROKER_URL_BASE_TLS;
Map<String, String> authParams = new HashMap<>();
authParams.put("tlsCertFile", TLS_CLIENT_CERT_FILE_PATH);
authParams.put("tlsKeyFile", TLS_CLIENT_KEY_FILE_PATH);
Authentication auth = new AuthenticationTls();
auth.configure(authParams);
clientConfig.setAuthentication(auth);
clientConfig.setUseTls(true);
clientConfig.setTlsAllowInsecureConnection(true);
}
PulsarAdmin pulsarAdmin = new PulsarAdmin(new URL(serviceUrl), clientConfig);
try {
pulsarAdmin.clusters().createCluster(config.getClusterName(),
new ClusterData(pulsar.getWebServiceAddress()));
} catch (ConflictException ce) {
// This is OK.
} finally {
pulsarAdmin.close();
}
}
@AfterMethod(alwaysRun = true)
void teardown() throws Exception {
try {
pulsar.close();
} catch (Exception e) {
Assert.fail("Got exception while closing the pulsar instance ", e);
}
}
private static final Logger log = LoggerFactory.getLogger(WebServiceTest.class);
}