diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 1ee00538be0..b6b11cd9e2c 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -453,6 +453,44 @@ that sets up the connector to be secure: } ---- +[[howto-enable-multiple-connectors-in-tomcat]] +=== Enable Multiple Connectors Tomcat +Add a `org.apache.catalina.connector.Connector` to the +`TomcatEmbeddedServletContainerFactory` which can allow multiple connectors eg a HTTP and +HTTPS connector: + +[source,java,indent=0,subs="verbatim,quotes,attributes"] +---- + @Bean + public EmbeddedServletContainerFactory servletContainer() { + TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory(); + tomcat.addAdditionalTomcatConnectors(createSslConnector()); + return tomcat; + } + + private Connector createSslConnector() { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); + try { + File keystore = new ClassPathResource("keystore").getFile(); + File truststore = new ClassPathResource("keystore").getFile(); + connector.setScheme("https"); + connector.setSecure(true); + connector.setPort(8443); + protocol.setSSLEnabled(true); + protocol.setKeystoreFile(keystore.getAbsolutePath()); + protocol.setKeystorePass("changeit"); + protocol.setTruststoreFile(truststore.getAbsolutePath()); + protocol.setTruststorePass("changeit"); + protocol.setKeyAlias("apitester"); + return connector; + } + catch (IOException ex) { + throw new IllegalStateException("can't access keystore: [" + "keystore" + + "] or truststore: [" + "keystore" + "]", ex); + } + } +---- [[howto-use-jetty-instead-of-tomcat]] diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index cbcfef522b7..1aa4beb05e5 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -31,6 +31,7 @@ spring-boot-sample-servlet spring-boot-sample-simple spring-boot-sample-tomcat + spring-boot-sample-tomcat-multi-connectors spring-boot-sample-traditional spring-boot-sample-web-method-security spring-boot-sample-web-secure diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml new file mode 100644 index 00000000000..9341cf5bd79 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.0.0.BUILD-SNAPSHOT + + spring-boot-sample-tomcat-multi-connectors + jar + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.springframework.boot + spring-boot-starter + + + org.springframework + spring-webmvc + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java new file mode 100644 index 00000000000..66e9dfc5cac --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2014 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 sample.tomcat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.Http11NioProtocol; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.FileCopyUtils; + +/** + * Sample Application to show Tomcat running 2 connectors + * + * @author Brock Mills + */ +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class SampleTomcatTwoConnectorsApplication { + + @Bean + public EmbeddedServletContainerFactory servletContainer() { + TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory(); + tomcat.addAdditionalTomcatConnectors(createSslConnector()); + return tomcat; + } + + private Connector createSslConnector() { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); + try { + File keystore = getKeyStoreFile(); + File truststore = keystore; + connector.setScheme("https"); + connector.setSecure(true); + connector.setPort(8443); + protocol.setSSLEnabled(true); + protocol.setKeystoreFile(keystore.getAbsolutePath()); + protocol.setKeystorePass("changeit"); + protocol.setTruststoreFile(truststore.getAbsolutePath()); + protocol.setTruststorePass("changeit"); + protocol.setKeyAlias("apitester"); + return connector; + } + catch (IOException ex) { + throw new IllegalStateException("cant access keystore: [" + "keystore" + + "] or truststore: [" + "keystore" + "]", ex); + } + } + + private File getKeyStoreFile() throws IOException { + ClassPathResource resource = new ClassPathResource("keystore"); + try { + return resource.getFile(); + } + catch (Exception ex) { + File temp = File.createTempFile("keystore", ".tmp"); + FileCopyUtils.copy(resource.getInputStream(), new FileOutputStream(temp)); + return temp; + } + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleTomcatTwoConnectorsApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java new file mode 100644 index 00000000000..1af3656bb29 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2014 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 sample.tomcat.web; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SampleController { + + @RequestMapping("/hello") + public String helloWorld() { + return "hello"; + } +} diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore new file mode 100644 index 00000000000..6547e5b5194 Binary files /dev/null and b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore differ diff --git a/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java new file mode 100644 index 00000000000..b72b20835d6 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2014 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 sample.tomcat; + +import java.io.IOException; +import java.net.HttpURLConnection; + +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 org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertEquals; + +/** + * Basic integration tests for 2 connector demo application. + * + * @author Brock Mills + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleTomcatTwoConnectorsApplication.class) +@WebAppConfiguration +@IntegrationTest +@DirtiesContext +public class SampleTomcatTwoConnectorsApplicationTests { + + @BeforeClass + public static void setUp() { + + try { + // setup ssl context to ignore certificate errors + SSLContext ctx = SSLContext.getInstance("TLS"); + X509TrustManager tm = new X509TrustManager() { + + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] chain, String authType) + throws java.security.cert.CertificateException { + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] chain, String authType) + throws java.security.cert.CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + ctx.init(null, new TrustManager[] { tm }, null); + SSLContext.setDefault(ctx); + } + catch (Exception ex) { + ex.printStackTrace(); + } + + } + + @Test + public void testHello() throws Exception { + RestTemplate template = new RestTemplate(); + final MySimpleClientHttpRequestFactory factory = new MySimpleClientHttpRequestFactory( + new HostnameVerifier() { + + @Override + public boolean verify(final String hostname, final SSLSession session) { + return true; // these guys are alright by me... + } + }); + template.setRequestFactory(factory); + + ResponseEntity entity = template.getForEntity( + "http://localhost:8080/hello", String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals("hello", entity.getBody()); + + ResponseEntity httpsEntity = template.getForEntity( + "https://localhost:8443/hello", String.class); + assertEquals(HttpStatus.OK, httpsEntity.getStatusCode()); + assertEquals("hello", httpsEntity.getBody()); + + } + + /** + * Http Request Factory for ignoring SSL hostname errors. Not for production use! + */ + class MySimpleClientHttpRequestFactory extends SimpleClientHttpRequestFactory { + + private final HostnameVerifier verifier; + + public MySimpleClientHttpRequestFactory(final HostnameVerifier verifier) { + this.verifier = verifier; + } + + @Override + protected void prepareConnection(final HttpURLConnection connection, + final String httpMethod) throws IOException { + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setHostnameVerifier(this.verifier); + } + super.prepareConnection(connection, httpMethod); + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java index 6d2c73f902d..f12ea68b769 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java @@ -119,7 +119,7 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer } } connector.getProtocolHandler().start(); - this.logger.info("Tomcat started on port: " + connector.getLocalPort()); + logPorts(); } catch (Exception ex) { this.logger.error("Cannot start connector: ", ex); @@ -129,6 +129,16 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer } } + private void logPorts() { + StringBuilder ports = new StringBuilder(); + for (Connector additionalConnector : this.tomcat.getService().findConnectors()) { + ports.append(ports.length() == 0 ? "" : " "); + ports.append(additionalConnector.getLocalPort() + "/" + + additionalConnector.getScheme()); + } + this.logger.info("Tomcat started on port(s): " + ports.toString()); + } + @Override public synchronized void stop() throws EmbeddedServletContainerException { try { diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index e9385f20a45..e4d28442f1e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -65,6 +65,7 @@ import org.springframework.util.StreamUtils; * * @author Phillip Webb * @author Dave Syer + * @author Brock Mills * @see #setPort(int) * @see #setContextLifecycleListeners(Collection) * @see TomcatEmbeddedServletContainer @@ -84,6 +85,8 @@ public class TomcatEmbeddedServletContainerFactory extends private List tomcatConnectorCustomizers = new ArrayList(); + private List additionalTomcatConnectors = new ArrayList(); + private ResourceLoader resourceLoader; private String protocol = DEFAULT_PROTOCOL; @@ -130,6 +133,10 @@ public class TomcatEmbeddedServletContainerFactory extends tomcat.getHost().setAutoDeploy(false); tomcat.getEngine().setBackgroundProcessorDelay(-1); + for (Connector additionalConnector : this.additionalTomcatConnectors) { + tomcat.getService().addConnector(additionalConnector); + } + prepareContext(tomcat.getHost(), initializers); this.logger.info("Server initialized with port: " + getPort()); return getTomcatEmbeddedServletContainer(tomcat); @@ -430,6 +437,24 @@ public class TomcatEmbeddedServletContainerFactory extends return this.tomcatConnectorCustomizers; } + /** + * Add {@link Connector}s in addition to the default connector, e.g. for SSL or AJP + * @param connectors the connectors to add + */ + public void addAdditionalTomcatConnectors(Connector... connectors) { + Assert.notNull(connectors, "Connectors must not be null"); + this.additionalTomcatConnectors.addAll(Arrays.asList(connectors)); + } + + /** + * Returns a mutable collection of the {@link Connector}s that will be added to the + * Tomcat + * @return the additionalTomcatConnectors + */ + public List getAdditionalTomcatConnectors() { + return this.additionalTomcatConnectors; + } + private static class TomcatErrorPage { private final String location; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java index 221badea974..bb1ab962d2a 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java @@ -115,6 +115,26 @@ public class TomcatEmbeddedServletContainerFactoryTests extends } } + @Test + public void tomcatAdditionalConnectors() throws Exception { + TomcatEmbeddedServletContainerFactory factory = getFactory(); + Connector[] listeners = new Connector[4]; + for (int i = 0; i < listeners.length; i++) { + listeners[i] = mock(Connector.class); + } + factory.addAdditionalTomcatConnectors(listeners); + this.container = factory.getEmbeddedServletContainer(); + assertEquals(listeners.length, factory.getAdditionalTomcatConnectors().size()); + } + + @Test + public void addNullAdditionalConnectorThrows() { + TomcatEmbeddedServletContainerFactory 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 { TomcatEmbeddedServletContainerFactory factory = getFactory();