/* * Copyright 2012-2017 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 org.springframework.boot.web.embedded.tomcat; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.servlet.ServletException; import org.apache.catalina.Container; import org.apache.catalina.Context; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.LifecycleState; import org.apache.catalina.Service; import org.apache.catalina.SessionIdGenerator; import org.apache.catalina.Valve; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardWrapper; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.util.CharsetMapper; import org.apache.catalina.valves.RemoteIpValve; import org.apache.jasper.servlet.JspServlet; import org.apache.tomcat.util.net.SSLHostConfig; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.mockito.InOrder; import org.springframework.boot.testutil.InternalOutputCapture; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactoryTests; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.SocketUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; /** * Tests for {@link TomcatServletWebServerFactory}. * * @author Phillip Webb * @author Dave Syer * @author Stephane Nicoll */ public class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests { @Rule public InternalOutputCapture outputCapture = new InternalOutputCapture(); @Override protected TomcatServletWebServerFactory getFactory() { return new TomcatServletWebServerFactory(0); } @After public void restoreTccl() { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } // JMX MBean names clash if you get more than one Engine with the same name... @Test public void tomcatEngineNames() throws Exception { TomcatServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(); factory.setPort(SocketUtils.findAvailableTcpPort(40000)); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); // Make sure that the names are different String firstName = ((TomcatWebServer) this.webServer).getTomcat().getEngine() .getName(); String secondName = tomcatWebServer.getTomcat().getEngine().getName(); assertThat(firstName).as("Tomcat engines must have different names") .isNotEqualTo(secondName); tomcatWebServer.stop(); } @Test public void tomcatListeners() throws Exception { TomcatServletWebServerFactory factory = getFactory(); LifecycleListener[] listeners = new LifecycleListener[4]; for (int i = 0; i < listeners.length; i++) { listeners[i] = mock(LifecycleListener.class); } factory.setContextLifecycleListeners(Arrays.asList(listeners[0], listeners[1])); factory.addContextLifecycleListeners(listeners[2], listeners[3]); this.webServer = factory.getWebServer(); InOrder ordered = inOrder((Object[]) listeners); for (LifecycleListener listener : listeners) { ordered.verify(listener).lifecycleEvent(any(LifecycleEvent.class)); } } @Test public void tomcatCustomizers() throws Exception { TomcatServletWebServerFactory factory = getFactory(); TomcatContextCustomizer[] listeners = new TomcatContextCustomizer[4]; for (int i = 0; i < listeners.length; i++) { listeners[i] = mock(TomcatContextCustomizer.class); } factory.setTomcatContextCustomizers(Arrays.asList(listeners[0], listeners[1])); factory.addContextCustomizers(listeners[2], listeners[3]); this.webServer = factory.getWebServer(); InOrder ordered = inOrder((Object[]) listeners); for (TomcatContextCustomizer listener : listeners) { ordered.verify(listener).customize(any(Context.class)); } } @Test public void tomcatConnectorCustomizers() throws Exception { TomcatServletWebServerFactory factory = getFactory(); TomcatConnectorCustomizer[] listeners = new TomcatConnectorCustomizer[4]; for (int i = 0; i < listeners.length; i++) { listeners[i] = mock(TomcatConnectorCustomizer.class); } factory.setTomcatConnectorCustomizers(Arrays.asList(listeners[0], listeners[1])); factory.addConnectorCustomizers(listeners[2], listeners[3]); this.webServer = factory.getWebServer(); InOrder ordered = inOrder((Object[]) listeners); for (TomcatConnectorCustomizer listener : listeners) { ordered.verify(listener).customize(any(Connector.class)); } } @Test public void tomcatAdditionalConnectors() throws Exception { TomcatServletWebServerFactory factory = getFactory(); Connector[] listeners = new Connector[4]; for (int i = 0; i < listeners.length; i++) { Connector connector = mock(Connector.class); given(connector.getState()).willReturn(LifecycleState.STOPPED); listeners[i] = connector; } factory.addAdditionalTomcatConnectors(listeners); this.webServer = factory.getWebServer(); Map<Service, Connector[]> connectors = ((TomcatWebServer) this.webServer) .getServiceConnectors(); assertThat(connectors.values().iterator().next().length) .isEqualTo(listeners.length + 1); } @Test public void addNullAdditionalConnectorThrows() { TomcatServletWebServerFactory factory = getFactory(); this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Connectors must not be null"); factory.addAdditionalTomcatConnectors((Connector[]) null); } @Test public void sessionTimeout() throws Exception { TomcatServletWebServerFactory factory = getFactory(); factory.setSessionTimeout(10); assertTimeout(factory, 1); } @Test public void sessionTimeoutInMins() throws Exception { TomcatServletWebServerFactory factory = getFactory(); factory.setSessionTimeout(1, TimeUnit.MINUTES); assertTimeout(factory, 1); } @Test public void noSessionTimeout() throws Exception { TomcatServletWebServerFactory factory = getFactory(); factory.setSessionTimeout(0); assertTimeout(factory, -1); } @Test public void valve() throws Exception { TomcatServletWebServerFactory factory = getFactory(); Valve valve = mock(Valve.class); factory.addContextValves(valve); this.webServer = factory.getWebServer(); verify(valve).setNext(any(Valve.class)); } @Test public void setNullTomcatContextCustomizersThrows() { TomcatServletWebServerFactory factory = getFactory(); this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("TomcatContextCustomizers must not be null"); factory.setTomcatContextCustomizers(null); } @Test public void addNullContextCustomizersThrows() { TomcatServletWebServerFactory factory = getFactory(); this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("TomcatContextCustomizers must not be null"); factory.addContextCustomizers((TomcatContextCustomizer[]) null); } @Test public void setNullTomcatConnectorCustomizersThrows() { TomcatServletWebServerFactory factory = getFactory(); this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("TomcatConnectorCustomizers must not be null"); factory.setTomcatConnectorCustomizers(null); } @Test public void addNullConnectorCustomizersThrows() { TomcatServletWebServerFactory factory = getFactory(); this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("TomcatConnectorCustomizers must not be null"); factory.addConnectorCustomizers((TomcatConnectorCustomizer[]) null); } @Test public void uriEncoding() throws Exception { TomcatServletWebServerFactory factory = getFactory(); factory.setUriEncoding(Charset.forName("US-ASCII")); Tomcat tomcat = getTomcat(factory); Connector connector = ((TomcatWebServer) this.webServer).getServiceConnectors() .get(tomcat.getService())[0]; assertThat(connector.getURIEncoding()).isEqualTo("US-ASCII"); } @Test public void defaultUriEncoding() throws Exception { TomcatServletWebServerFactory factory = getFactory(); Tomcat tomcat = getTomcat(factory); Connector connector = ((TomcatWebServer) this.webServer).getServiceConnectors() .get(tomcat.getService())[0]; assertThat(connector.getURIEncoding()).isEqualTo("UTF-8"); } @Test public void sslCiphersConfiguration() throws Exception { Ssl ssl = new Ssl(); ssl.setKeyStore("test.jks"); ssl.setKeyStorePassword("secret"); ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); TomcatServletWebServerFactory factory = getFactory(); factory.setSsl(ssl); Tomcat tomcat = getTomcat(factory); Connector connector = ((TomcatWebServer) this.webServer).getServiceConnectors() .get(tomcat.getService())[0]; SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler() .findSslHostConfigs(); assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("ALPHA:BRAVO:CHARLIE"); } @Test public void sslEnabledMultipleProtocolsConfiguration() throws Exception { Ssl ssl = getSsl(null, "password", "src/test/resources/test.jks"); ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); TomcatServletWebServerFactory factory = getFactory(); factory.setSsl(ssl); this.webServer = factory.getWebServer(sessionServletRegistration()); this.webServer.start(); Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); Connector connector = tomcat.getConnector(); SSLHostConfig sslHostConfig = connector.getProtocolHandler() .findSslHostConfigs()[0]; assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS"); assertThat(sslHostConfig.getEnabledProtocols()) .containsExactlyInAnyOrder("TLSv1.1", "TLSv1.2"); } @Test public void sslEnabledProtocolsConfiguration() throws Exception { Ssl ssl = getSsl(null, "password", "src/test/resources/test.jks"); ssl.setEnabledProtocols(new String[] { "TLSv1.2" }); ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); TomcatServletWebServerFactory factory = getFactory(); factory.setSsl(ssl); this.webServer = factory.getWebServer(sessionServletRegistration()); Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); this.webServer.start(); Connector connector = tomcat.getConnector(); SSLHostConfig sslHostConfig = connector.getProtocolHandler() .findSslHostConfigs()[0]; assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS"); assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.2"); } @Test public void primaryConnectorPortClashThrowsIllegalStateException() throws InterruptedException, IOException { doWithBlockedPort(new BlockedPortAction() { @Override public void run(int port) { TomcatServletWebServerFactory factory = getFactory(); factory.setPort(port); try { TomcatServletWebServerFactoryTests.this.webServer = factory .getWebServer(); TomcatServletWebServerFactoryTests.this.webServer.start(); fail(); } catch (WebServerException ex) { // Ignore } } }); } @Test public void startupFailureDoesNotResultInUnstoppedThreadsBeingReported() throws IOException { super.portClashOfPrimaryConnectorResultsInPortInUseException(); String string = this.outputCapture.toString(); assertThat(string) .doesNotContain("appears to have started a thread named [main]"); } @Test public void stopCalledWithoutStart() throws Exception { TomcatServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(exampleServletRegistration()); this.webServer.stop(); Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); assertThat(tomcat.getServer().getState()).isSameAs(LifecycleState.DESTROYED); } @Override protected void addConnector(int port, AbstractServletWebServerFactory factory) { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setPort(port); ((TomcatServletWebServerFactory) factory) .addAdditionalTomcatConnectors(connector); } @Test public void useForwardHeaders() throws Exception { TomcatServletWebServerFactory factory = getFactory(); factory.addContextValves(new RemoteIpValve()); assertForwardHeaderIsUsed(factory); } @Test public void disableDoesNotSaveSessionFiles() throws Exception { File baseDir = this.temporaryFolder.newFolder(); TomcatServletWebServerFactory factory = getFactory(); // If baseDir is not set SESSIONS.ser is written to a different temp directory // each time. By setting it we can really ensure that data isn't saved factory.setBaseDirectory(baseDir); this.webServer = factory.getWebServer(sessionServletRegistration()); this.webServer.start(); String s1 = getResponse(getLocalUrl("/session")); String s2 = getResponse(getLocalUrl("/session")); this.webServer.stop(); this.webServer = factory.getWebServer(sessionServletRegistration()); this.webServer.start(); String s3 = getResponse(getLocalUrl("/session")); String message = "Session error s1=" + s1 + " s2=" + s2 + " s3=" + s3; assertThat(s2.split(":")[0]).as(message).isEqualTo(s1.split(":")[1]); assertThat(s3.split(":")[0]).as(message).isNotEqualTo(s2.split(":")[1]); } @Test public void jndiLookupsCanBePerformedDuringApplicationContextRefresh() throws NamingException { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0) { @Override protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { tomcat.enableNaming(); return super.getTomcatWebServer(tomcat); } }; // Server is created in onRefresh this.webServer = factory.getWebServer(); // Lookups should now be possible new InitialContext().lookup("java:comp/env"); // Called in finishRefresh, giving us an opportunity to remove the context binding // and avoid a leak this.webServer.start(); // Lookups should no longer be possible this.thrown.expect(NamingException.class); new InitialContext().lookup("java:comp/env"); } @Test public void defaultLocaleCharsetMappingsAreOverriden() throws Exception { TomcatServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(); // override defaults, see org.apache.catalina.util.CharsetMapperDefault.properties assertThat(getCharset(Locale.ENGLISH).toString()).isEqualTo("UTF-8"); assertThat(getCharset(Locale.FRENCH).toString()).isEqualTo("UTF-8"); } @Test public void sessionIdGeneratorIsConfiguredWithAttributesFromTheManager() { System.setProperty("jvmRoute", "test"); try { TomcatServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(); this.webServer.start(); } finally { System.clearProperty("jvmRoute"); } Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); Context context = (Context) tomcat.getHost().findChildren()[0]; SessionIdGenerator sessionIdGenerator = context.getManager() .getSessionIdGenerator(); assertThat(sessionIdGenerator).isInstanceOf(LazySessionIdGenerator.class); assertThat(sessionIdGenerator.getJvmRoute()).isEqualTo("test"); } @Override protected JspServlet getJspServlet() throws ServletException { Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); Container container = tomcat.getHost().findChildren()[0]; StandardWrapper standardWrapper = (StandardWrapper) container.findChild("jsp"); if (standardWrapper == null) { return null; } standardWrapper.load(); return (JspServlet) standardWrapper.getServlet(); } @SuppressWarnings("unchecked") @Override protected Map<String, String> getActualMimeMappings() { Context context = (Context) ((TomcatWebServer) this.webServer).getTomcat() .getHost().findChildren()[0]; return (Map<String, String>) ReflectionTestUtils.getField(context, "mimeMappings"); } @Override protected Charset getCharset(Locale locale) { Context context = (Context) ((TomcatWebServer) this.webServer).getTomcat() .getHost().findChildren()[0]; CharsetMapper mapper = ((TomcatEmbeddedContext) context).getCharsetMapper(); String charsetName = mapper.getCharset(locale); return (charsetName != null) ? Charset.forName(charsetName) : null; } private void assertTimeout(TomcatServletWebServerFactory factory, int expected) { Tomcat tomcat = getTomcat(factory); Context context = (Context) tomcat.getHost().findChildren()[0]; assertThat(context.getSessionTimeout()).isEqualTo(expected); } private Tomcat getTomcat(TomcatServletWebServerFactory factory) { this.webServer = factory.getWebServer(); return ((TomcatWebServer) this.webServer).getTomcat(); } @Override protected void handleExceptionCausedByBlockedPort(RuntimeException ex, int blockedPort) { assertThat(ex).isInstanceOf(ConnectorStartFailedException.class); assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort); } }