From 8b77a0298f7871c2ceb548b986b8d957249b88f1 Mon Sep 17 00:00:00 2001 From: brockwmills Date: Thu, 20 Mar 2014 14:11:46 +1100 Subject: [PATCH] Allow multiple connectors with Tomcat Update TomcatEmbeddedServletContainerFactory to allow for additional containers (e.g. SSL or AJP in addition to HTTP). Fixes gh-528 --- spring-boot-docs/src/main/asciidoc/howto.adoc | 38 +++++ spring-boot-samples/pom.xml | 1 + .../pom.xml | 43 ++++++ .../SampleTomcatTwoConnectorsApplication.java | 91 ++++++++++++ .../sample/tomcat/web/SampleController.java | 29 ++++ .../src/main/resources/keystore | Bin 0 -> 3602 bytes ...leTomcatTwoConnectorsApplicationTests.java | 136 ++++++++++++++++++ .../TomcatEmbeddedServletContainer.java | 12 +- ...TomcatEmbeddedServletContainerFactory.java | 25 ++++ ...tEmbeddedServletContainerFactoryTests.java | 20 +++ 10 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/SampleTomcatTwoConnectorsApplication.java create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/java/sample/tomcat/web/SampleController.java create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/main/resources/keystore create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-multi-connectors/src/test/java/sample/tomcat/SampleTomcatTwoConnectorsApplicationTests.java diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 1ee00538be..b6b11cd9e2 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 cbcfef522b..1aa4beb05e 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 0000000000..9341cf5bd7 --- /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 0000000000..66e9dfc5ca --- /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 0000000000..1af3656bb2 --- /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 0000000000000000000000000000000000000000..6547e5b51944ab7e0456d7ae86768967d01fc45a GIT binary patch literal 3602 zcmd6pc{mjO7RP6^Z({~E)=VVZm@&v!7-Y#dM95GWvNJ<55;G+GT9maI%OnyaLL_Tc zQbv{{=`CvrAzZ!peed(W_x^wH{o_2p=X-w7d7kr}-#MS(*KMV-qI3MU0 zOe6;rNdN%Q;6;K`D*(U(K+_;U(Y&mj@~l7zP!7Zo1hN1iG{~0#eJuOYo4TYc4#o@j zf(AvN_cDEG{9@E#rI|8@eFQ;%Iq%L?9*#cW2H0WMXz+-p)em z!&%?l*bRkK)Org9Y3soQ=8x=SZ!ZN%^A|h74xF^<3CHz6PEvAbBWI=htvG($XuXDu zbvh4HzV2L&LSx-)$igR{8Ha|>RK@Y>bjMtH>LmWOT9N#EaZL^dqu%XAt0E^1vu5LmeCFD9WGG8JwJL4LkdTRfsac^s_JWSisN1^cR^^ck!4b{&$^`goJv|AhCUwKxMe01h`qK`}6)<{; zsOZQ=5@)pvBh|T+`#uP!c%4jxjh(A>j`*=S$pwBKp{u)RT_^W8LdGimsbT-A#Nseb zvC}V+d`%(T2OIUjC5q!93`^Zyt!>(x$>&!gWX=RwgCehv&AU2Qcu^fO69si-?h+^B z{pzB2OUO@DiOrbq1Wn(o;S|M!Tyf)ww_InRi_9D6mz8V$2)?R(!B-qKNsB>@3za7? z)ggMWIdx^eaUM7>Xses)sbcod4c-Czu&E%-`fS3@ULJXRURZjK{Y^aAcWQ!A^{q9_ zpWXEt5!D5_@`O+_=EMN}q-8o~-pqWbC`v{z!anh3;`h+%({QG{%YGM@aW^ zSEs*Kx(Qvh9g+ZqcHX@tsXX1<@r@h{$JbE^VWM)b#^=qjRMzU@BrV0>^b84(uG6M! zA1pR2^Ofw_b~+i5!q{C8X97=TvgOf)WSMc1RKeR5(tgEtj&as&wj31#hh<9?6lY0a zDU>j5(Z^R~*VJymT7+FWk|3GU%h~&-mPX$_71=kGUpO|akg?eyr93bvkeZUh%E2q@ zlX1h=Ik7`ANm(n9o+FyCe$vGhXEqM;Ms8VJEOU?PhhnV;WCSj*=lFT=OcUXjn?XB; zrjgpVJv~{Vbo(lA9p<;UuDMbZ30$Osn$*&udN*p5@ zo_v4Vd981EXzTS2`n~RO8m#I?$A{vd8RUflYRexsVdmx*8%A~Q#SrO0|?Q}+J z9D2867g*+kS|8~|?tw;Aq%MwY76e!%G>`i2QH5a$LjKUKETi&)Lzi+akZT{9)FccSnu8T=3t@$W z^lUNwXue;99nNm&9qOWT}2+5y_L7)%(@^QkU=3c(Oyzokn9BL7;@{oe*m1M=;ELqJXt4G09}!Vi{@;2??} zrtXld?azrprOb}A33jX|o%((lY;P#@_V%4-W_joG@!tlA;dcS_uOZie+~nVe123;L zi236SRPmE6M{RL|F6-~+5!};&maNN9MiaBXX*)zbQeDCP6z_(K_WJNnUYjE&b7Hpd`UVUxmdb~Cb%wlu8#CTFYvb~jzZO;S zJxDCiKSid&ynaw2sIn{a@%F}U@^=p0@U66JM9H<+R>mE!aTbAQlTy<@S>BtE7CmCp z*wApC8&WgR!>@(4b9`0&?o47?J38)Nh`0GHd&jQD%d$)I&E{==cR*ku09Y)EmOzW` zzrGkRLt2r z=h@};g{#Wl>yia3!+N#84lO0Hnz@?2pL2Gnl{s_U{6v~am609p*DH-Ee9H0ug=u7= zk&FG@udjro9^i~HQ5%N;i5@`7mE6}SP@Y11#`6J7h2340dU9i6M!QC3V{ow4 zk+r8C8`5r=GLbMjUda!kFAa1yGQGW5zUFtr$_KN0p!16v2QnALquBCjF@(7IA*Dxw ze6>`p@w=$#45KYXT%QveTE>~Kf)DK4Qvc8(Ia)iHBXXxYI(ukc7e(e(h}M?Xsh{Ah zx8?Sgpfi0%+e?e=f>xL0CoSd1AXbMT^x%9ctnsRf|D0vNFdt&r?<-DR%Fh0oy5C!g z|GDA-FZmy~DRNyEOyXQqp-#=UIxOd0#2A+v2sHofRex4h+@;(&w=I=ms3gcx{vHIX z*ofC%O?V1B!I#OlXYy>6(R?Xdk5Bt(FUftwIHvT)O$!9sa_oE1`k;V+wAR{em(~kC zs(DHveF~{daezDP9#wsQIejcgREGPrC+0dN{%1r<3?8dEiAlUVB|+#gIE&kCYf~`6 z9$dIyABjUa7?w2Gx+nBkddnVjYQ2X%&2pwslxJ#qCQ7m$E8;NHEq~z5llvlr13CgW z$)=-hLJr7#s-y*g%USj&iPwGaCzQWW7qH1-nRJbrO{l9d3Qd={t$i{PL=7jpbu?TZ z#eaqk-L-6SP4hB(dF*6wmh8yHqfsN5ul?aJU2!XHz~Y6UF@+554otH(pb|glacvqL z=hYRmBfWfZZyMwM({wYsmzpUtg3q1`pc8~S0aEi{4YGsw4z%pd6o03d4A6>Y@}%>mB_N{bd!r+R}3)xFZuZZj17ozBklUamoJ!yKfnJyDC^*2h1sF*2a} k^AkPD55?ih(kn5nVX2{>6?{U0daLEFdSS3< 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 6d2c73f902..f12ea68b76 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 e9385f20a4..e4d28442f1 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 221badea97..bb1ab962d2 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();