/** * Copyright 2015 StreamSets Inc. * * Licensed under 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 com.streamsets.datacollector.http; import com.codahale.metrics.MetricRegistry; import com.google.common.collect.ImmutableSet; import com.streamsets.datacollector.main.DataCollectorBuildInfo; import com.streamsets.datacollector.main.FileUserGroupManager; import com.streamsets.datacollector.main.RuntimeInfo; import com.streamsets.datacollector.main.RuntimeModule; import com.streamsets.datacollector.main.StandaloneRuntimeInfo; import com.streamsets.datacollector.util.Configuration; import com.streamsets.lib.security.http.RemoteSSOService; import com.streamsets.lib.security.http.SSOPrincipal; import com.streamsets.lib.security.http.SSOService; import com.streamsets.testing.NetworkUtils; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.client.filter.CsrfProtectionFilter; import org.junit.Assert; import org.junit.Test; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermission; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import static com.streamsets.datacollector.util.AwaitConditionUtil.waitForStart; public class TestWebServerTaskHttpHttps { private RuntimeInfo runtimeInfo; static class PingServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(HttpServletResponse.SC_OK); resp.getWriter().write("ping"); } } @SuppressWarnings("unchecked") private WebServerTask createWebServerTask( final String confDir, final Configuration conf, final Set<WebAppProvider> webAppProviders, boolean isDPMEnabled ) throws Exception { runtimeInfo = new StandaloneRuntimeInfo( RuntimeModule.SDC_PROPERTY_PREFIX, new MetricRegistry(), Collections.<ClassLoader>emptyList() ) { @Override public String getConfigDir() { return confDir; } @Override public String getId() { return "sdcId"; } @Override public String getAppAuthToken() { return "appAuthToken"; } }; runtimeInfo.setDPMEnabled(isDPMEnabled); Set<ContextConfigurator> configurators = new HashSet<>(); configurators.add(new ContextConfigurator() { @Override public void init(ServletContextHandler context) { context.addServlet(new ServletHolder(new PingServlet()), "/ping"); context.addServlet(new ServletHolder(new PingServlet()), "/rest/v1/ping"); context.addServlet(new ServletHolder(new PingServlet()), "/public-rest/v1/ping"); } }); return new DataCollectorWebServerTask(new DataCollectorBuildInfo(), runtimeInfo, conf, configurators, webAppProviders, new FileUserGroupManager() ); } @SuppressWarnings("unchecked") private WebServerTask createWebServerTask(final String confDir, final Configuration conf) throws Exception { return createWebServerTask(confDir, conf, Collections.<WebAppProvider>emptySet(), false); } private String createTestDir() { File dir = new File("target", UUID.randomUUID().toString()); Assert.assertTrue(dir.mkdirs()); return dir.getAbsolutePath(); } private Callable<Boolean> isWebServerTaskRunning(final WebServerTask ws) { return new Callable<Boolean>() { @Override public Boolean call() throws Exception { ws.getServerURI(); return true; } }; } @Test public void testInvalidPorts() throws Exception { Configuration conf = new Configuration(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, 0); conf.set(WebServerTask.HTTPS_PORT_KEY, 0); WebServerTask ws = createWebServerTask(createTestDir(), conf); try { ws.initTask(); } catch (IllegalArgumentException iae) { // } conf.set(WebServerTask.HTTP_PORT_KEY, 0); conf.set(WebServerTask.HTTPS_PORT_KEY, 10000); ws = createWebServerTask(createTestDir(), conf); try { ws.initTask(); } catch (IllegalArgumentException iae) { // } conf.set(WebServerTask.HTTP_PORT_KEY, 10000); conf.set(WebServerTask.HTTPS_PORT_KEY, 0); try { ws.initTask(); } catch (IllegalArgumentException iae) { // } } @Test public void testGetServerURI() throws Exception { Configuration conf = new Configuration(); int httpPort = NetworkUtils.getRandomPort(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, httpPort); final WebServerTask ws = createWebServerTask(createTestDir(), conf); ws.initTask(); // Server hasn't yet started try { ws.getServerURI(); Assert.fail("Expected ServerNotYetRunningException but didn't get any"); } catch (ServerNotYetRunningException se) { // Expected } catch (Exception e) { Assert.fail("Expected ServerNotYetRunningException but got " + e); } // Now start the server try { new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); Assert.assertEquals(httpPort, ws.getServerURI().getPort()); } finally { ws.stopTask(); } } @Test public void testHttp() throws Exception { Configuration conf = new Configuration(); int httpPort = NetworkUtils.getRandomPort(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, httpPort); final WebServerTask ws = createWebServerTask(createTestDir(), conf); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + httpPort + "/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); Assert.assertNull(runtimeInfo.getSSLContext()); } finally { ws.stopTask(); } } @Test public void testHttpRandomPort() throws Exception { Configuration conf = new Configuration(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, 0); final WebServerTask ws = createWebServerTask(createTestDir(), conf); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + ws.getServerURI().getPort() + "/ping") .openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); } finally { ws.stopTask(); } } //making the url connection to trust a self signed cert on localhost static void configureHttpsUrlConnection(HttpsURLConnection conn) throws Exception { SSLContext sc = SSLContext.getInstance("SSL"); TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } @Override public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } } }; sc.init(null, trustAllCerts, new java.security.SecureRandom()); conn.setSSLSocketFactory(sc.getSocketFactory()); conn.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String s, SSLSession sslSession) { return true; } }); } @Test public void testHttps() throws Exception { Configuration conf = new Configuration(); int httpsPort = NetworkUtils.getRandomPort(); String confDir = createTestDir(); String keyStore = new File(confDir, "sdc-keystore.jks").getAbsolutePath(); OutputStream os = new FileOutputStream(keyStore); IOUtils.copy(getClass().getClassLoader().getResourceAsStream("sdc-keystore.jks"),os); os.close(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, -1); conf.set(WebServerTask.HTTPS_PORT_KEY, httpsPort); conf.set(WebServerTask.HTTPS_KEYSTORE_PATH_KEY, "sdc-keystore.jks"); conf.set(WebServerTask.HTTPS_KEYSTORE_PASSWORD_KEY, "password"); conf.set(WebServerTask.HTTPS_TRUSTSTORE_PATH_KEY, ""); final WebServerTask ws = createWebServerTask(confDir, conf); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); HttpsURLConnection conn = (HttpsURLConnection) new URL("https://127.0.0.1:" + httpsPort + "/ping") .openConnection(); configureHttpsUrlConnection(conn); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); Assert.assertNotNull(runtimeInfo.getSSLContext()); } finally { ws.stopTask(); } } @Test public void testHttpsRandomPort() throws Exception { Configuration conf = new Configuration(); String confDir = createTestDir(); String keyStore = new File(confDir, "sdc-keystore.jks").getAbsolutePath(); OutputStream os = new FileOutputStream(keyStore); IOUtils.copy(getClass().getClassLoader().getResourceAsStream("sdc-keystore.jks"), os); os.close(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, -1); conf.set(WebServerTask.HTTPS_PORT_KEY, 0); conf.set(WebServerTask.HTTPS_KEYSTORE_PATH_KEY, "sdc-keystore.jks"); conf.set(WebServerTask.HTTPS_KEYSTORE_PASSWORD_KEY, "password"); final WebServerTask ws = createWebServerTask(confDir, conf); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); HttpsURLConnection conn = (HttpsURLConnection) new URL("https://127.0.0.1:" + ws.getServerURI().getPort() + "/ping") .openConnection(); configureHttpsUrlConnection(conn); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); Assert.assertNotNull(runtimeInfo.getSSLContext()); } finally { ws.stopTask(); } } @Test public void testHttpRedirectToHttpss() throws Exception { Configuration conf = new Configuration(); int httpPort = NetworkUtils.getRandomPort(); int httpsPort = NetworkUtils.getRandomPort(); String confDir = createTestDir(); String keyStore = new File(confDir, "sdc-keystore.jks").getAbsolutePath(); OutputStream os = new FileOutputStream(keyStore); IOUtils.copy(getClass().getClassLoader().getResourceAsStream("sdc-keystore.jks"),os); os.close(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, httpPort); conf.set(WebServerTask.HTTPS_PORT_KEY, httpsPort); conf.set(WebServerTask.HTTPS_KEYSTORE_PATH_KEY, "sdc-keystore.jks"); conf.set(WebServerTask.HTTPS_KEYSTORE_PASSWORD_KEY, "password"); final WebServerTask ws = createWebServerTask(confDir, conf); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + httpPort + "/ping").openConnection(); conn.setInstanceFollowRedirects(false); Assert.assertTrue(conn.getResponseCode() >= 300 && conn.getResponseCode() < 400); Assert.assertTrue(conn.getHeaderField("Location").startsWith("https://")); HttpsURLConnection conns = (HttpsURLConnection) new URL(conn.getHeaderField("Location")).openConnection(); configureHttpsUrlConnection(conns); Assert.assertEquals(HttpURLConnection.HTTP_OK, conns.getResponseCode()); Assert.assertNotNull(runtimeInfo.getSSLContext()); } finally { ws.stopTask(); } } @Test public void testWebApp() throws Exception { WebAppProvider webAppProvider = new WebAppProvider() { @Override public ServletContextHandler get() { ServletContextHandler handler = new ServletContextHandler(); handler.setContextPath("/webapp"); handler.addServlet(new ServletHolder(new PingServlet()), "/ping"); return handler; } @Override public Configuration getAppConfiguration() { return new Configuration(); } @Override public void postStart() { } }; Configuration conf = new Configuration(); int httpPort = NetworkUtils.getRandomPort(); conf.set(WebServerTask.AUTHENTICATION_KEY, "none"); conf.set(WebServerTask.HTTP_PORT_KEY, httpPort); final WebServerTask ws = createWebServerTask(createTestDir(), conf, ImmutableSet.of(webAppProvider), false); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + httpPort + "/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) new URL("http://127.0.0.1:" + httpPort + "/webapp/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); } finally { ws.stopTask(); } } private static class DummySSOService implements SSOService { boolean delegated; @Override public void setDelegateTo(SSOService ssoService) { } @Override public SSOService getDelegateTo() { return null; } @Override public void setConfiguration(Configuration configuration) { } @Override public void register(Map<String, String> attributes) { } @Override public String createRedirectToLoginUrl(String requestUrl, boolean duplicateRedirect) { return null; } @Override public String getLogoutUrl() { return null; } @Override public SSOPrincipal validateUserToken(String authToken) { return null; } @Override public boolean invalidateUserToken(String authToken) { return false; } @Override public SSOPrincipal validateAppToken( String authToken, String componentId ) { return null; } @Override public boolean invalidateAppToken(String authToken) { return false; } @Override public void clearCaches() { } } @Test public void testWebAppSSOServiceDelegation() throws Exception { final DummySSOService delegatedTo = new DummySSOService(); final Configuration conf = new Configuration(); conf.set(RemoteSSOService.SECURITY_SERVICE_APP_AUTH_TOKEN_CONFIG, "authToken"); WebAppProvider webAppProvider = new WebAppProvider() { @Override public ServletContextHandler get() { ServletContextHandler handler = new ServletContextHandler(); handler.setContextPath("/webapp"); handler.addEventListener(new ServletContextListener() { @Override public void contextInitialized(ServletContextEvent sce) { SSOService ssoService = (SSOService) sce.getServletContext().getAttribute(SSOService.SSO_SERVICE_KEY); ssoService.setDelegateTo(delegatedTo); delegatedTo.delegated = true; } @Override public void contextDestroyed(ServletContextEvent sce) { } }); handler.addServlet(new ServletHolder(new PingServlet()), "/ping"); return handler; } @Override public Configuration getAppConfiguration() { return conf; } @Override public void postStart() { } }; int httpPort = NetworkUtils.getRandomPort(); conf.set(WebServerTask.HTTP_PORT_KEY, httpPort); final WebServerTask ws = createWebServerTask(createTestDir(), conf, ImmutableSet.of(webAppProvider), true); try { ws.initTask(); new Thread() { @Override public void run() { try { ws.runTask(); } catch (Exception ex) { // Ignore Registration failure exception } } }.start(); waitForStart(ws); Assert.assertTrue(delegatedTo.delegated); } finally { ws.stopTask(); } } @Test public void testAuthorizationConstraints() throws Exception { WebAppProvider webAppProvider = new WebAppProvider() { @Override public ServletContextHandler get() { ServletContextHandler handler = new ServletContextHandler(); handler.setContextPath("/webapp"); handler.addServlet(new ServletHolder(new PingServlet()), "/ping"); handler.addServlet(new ServletHolder(new PingServlet()), "/rest/v1/ping"); handler.addServlet(new ServletHolder(new PingServlet()), "/public-rest/v1/ping"); return handler; } @Override public Configuration getAppConfiguration() { return new Configuration(); } @Override public void postStart() { } }; Configuration conf = new Configuration(); int httpPort = NetworkUtils.getRandomPort(); conf.set(WebServerTask.AUTHENTICATION_KEY, "basic"); conf.set(WebServerTask.HTTP_PORT_KEY, httpPort); String confDir = createTestDir(); File realmFile = new File(confDir, "basic-realm.properties"); try ( InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("basic-realm.properties"); OutputStream os = new FileOutputStream(realmFile) ) { IOUtils.copy(is, os); } Set<PosixFilePermission> set = new HashSet<>(); set.add(PosixFilePermission.OWNER_EXECUTE); set.add(PosixFilePermission.OWNER_READ); set.add(PosixFilePermission.OWNER_WRITE); Files.setPosixFilePermissions(realmFile.toPath(), set); final WebServerTask ws = createWebServerTask(confDir, conf, ImmutableSet.of(webAppProvider), false); try { ws.initTask(); new Thread() { @Override public void run() { ws.runTask(); } }.start(); waitForStart(ws); String baseUrl = "http://127.0.0.1:" + httpPort; // root app HttpURLConnection conn = (HttpURLConnection) new URL(baseUrl + "/ping").openConnection(); conn.setRequestProperty(CsrfProtectionFilter.HEADER_NAME, "CSRF"); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) openWithBasicAuth(new URL(baseUrl + "/ping")); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) new URL(baseUrl + "/rest/v1/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); conn = (HttpURLConnection) openWithBasicAuth(new URL(baseUrl + "/rest/v1/ping")); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) new URL(baseUrl + "/public-rest/v1/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) openWithBasicAuth(new URL(baseUrl + "/public-rest/v1/ping")); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); // web app conn = (HttpURLConnection) new URL(baseUrl + "/webapp/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) openWithBasicAuth(new URL(baseUrl + "/webapp/ping")); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) new URL(baseUrl + "/webapp/rest/v1/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); conn = (HttpURLConnection) openWithBasicAuth(new URL(baseUrl + "/webapp/rest/v1/ping")); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) new URL(baseUrl + "/webapp/public-rest/v1/ping").openConnection(); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); conn = (HttpURLConnection) openWithBasicAuth(new URL(baseUrl + "/webapp/public-rest/v1/ping")); Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); } finally { ws.stopTask(); } } @Test public void testKeystoreAbsolutePath() { Configuration conf = new Configuration(); conf.set(WebServerTask.HTTPS_KEYSTORE_PATH_KEY, "/tmp/absolute/sdc-keystore.jks"); File keystore = WebServerTask.getHttpsKeystore(conf, "/tmp/config"); String absolutePath = keystore.getAbsolutePath(); Assert.assertEquals("/tmp/absolute/sdc-keystore.jks", absolutePath); } @Test public void testKeystoreRelativePath() { Configuration conf = new Configuration(); conf.set(WebServerTask.HTTPS_KEYSTORE_PATH_KEY, "sdc-keystore.jks"); File keystore = WebServerTask.getHttpsKeystore(conf, "/tmp/config"); String absolutePath = keystore.getAbsolutePath(); Assert.assertEquals("/tmp/config/sdc-keystore.jks", absolutePath); } private URLConnection openWithBasicAuth(URL url) throws Exception { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); byte[] authEncBytes = Base64.encodeBase64("admin:admin".getBytes()); String authStringEnc = new String(authEncBytes); conn.setRequestProperty("Authorization", "Basic " + authStringEnc); return conn; } }