From 62a5ce52d039d281243c7f97f499e803dc5bfe4b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 17 Sep 2014 09:30:37 -0700 Subject: [PATCH] Backport Jetty/Tomcat SSL support Fixes gh-1570 Cherry-picked from 0960908 and 258c6f1 --- .../autoconfigure/web/ServerProperties.java | 67 +++-- .../appendix-application-properties.adoc | 12 + spring-boot-docs/src/main/asciidoc/howto.adoc | 71 ++--- spring-boot-samples/pom.xml | 1 + .../spring-boot-sample-tomcat-ssl/pom.xml | 56 ++++ .../spring-boot-sample-tomcat-ssl/sample.jks | Bin 0 -> 2264 bytes .../tomcat/SampleTomcatSslApplication.java | 35 +++ .../sample/tomcat/web/SampleController.java | 32 +++ .../src/main/resources/application.properties | 4 + .../SampleTomcatSslApplicationTests.java | 67 +++++ ...tConfigurableEmbeddedServletContainer.java | 11 + .../ConfigurableEmbeddedServletContainer.java | 7 + .../boot/context/embedded/Ssl.java | 160 +++++++++++ .../JettyEmbeddedServletContainerFactory.java | 94 +++++++ ...TomcatEmbeddedServletContainerFactory.java | 82 ++++++ ...tEmbeddedServletContainerFactoryTests.java | 257 +++++++++++++++++- ...yEmbeddedServletContainerFactoryTests.java | 24 ++ ...tEmbeddedServletContainerFactoryTests.java | 20 ++ spring-boot/src/test/resources/test.jks | Bin 0 -> 2248 bytes spring-boot/src/test/resources/test.p12 | Bin 0 -> 2336 bytes 20 files changed, 920 insertions(+), 80 deletions(-) create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-ssl/sample.jks create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties create mode 100644 spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java create mode 100644 spring-boot/src/test/resources/test.jks create mode 100644 spring-boot/src/test/resources/test.p12 diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index d1af4329b76..40004ab7ec6 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -33,6 +33,7 @@ import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletCont import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; @@ -59,6 +60,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { private String contextPath; + private Ssl ssl; + @NotNull private String servletPath = "/"; @@ -132,6 +135,14 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { this.sessionTimeout = sessionTimeout; } + public Ssl getSsl() { + return this.ssl; + } + + public void setSsl(Ssl ssl) { + this.ssl = ssl; + } + public void setLoader(String value) { // no op to support Tomcat running as a traditional container (not embedded) } @@ -150,12 +161,41 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { if (getSessionTimeout() != null) { container.setSessionTimeout(getSessionTimeout()); } + if (getSsl() != null) { + container.setSsl(getSsl()); + } if (container instanceof TomcatEmbeddedServletContainerFactory) { getTomcat() .customizeTomcat((TomcatEmbeddedServletContainerFactory) container); } } + public String[] getPathsArray(Collection paths) { + String[] result = new String[paths.size()]; + int i = 0; + for (String path : paths) { + result[i++] = getPath(path); + } + return result; + } + + public String[] getPathsArray(String[] paths) { + String[] result = new String[paths.length]; + int i = 0; + for (String path : paths) { + result[i++] = getPath(path); + } + return result; + } + + public String getPath(String path) { + String prefix = getServletPrefix(); + if (!path.startsWith("/")) { + path = "/" + path; + } + return prefix + path; + } + public static class Tomcat { private String accessLogPattern; @@ -330,31 +370,4 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { } } - - public String[] getPathsArray(Collection paths) { - String[] result = new String[paths.size()]; - int i = 0; - for (String path : paths) { - result[i++] = getPath(path); - } - return result; - } - - public String[] getPathsArray(String[] paths) { - String[] result = new String[paths.length]; - int i = 0; - for (String path : paths) { - result[i++] = getPath(path); - } - return result; - } - - public String getPath(String path) { - String prefix = getServletPrefix(); - if (!path.startsWith("/")) { - path = "/" + path; - } - return prefix + path; - } - } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 0452cf7aeb6..a14029430b3 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -56,6 +56,18 @@ content into your application; rather pick only the properties that you need. server.session-timeout= # session timeout in seconds server.context-path= # the context path, defaults to '/' server.servlet-path= # the servlet path, defaults to '/' + server.ssl.client-auth= # want or need + server.ssl.key-alias= + server.ssl.key-password= + server.ssl.key-store= + server.ssl.key-store-password= + server.ssl.key-store-provider= + server.ssl.key-store-type= + server.ssl.protocol=TLS + server.ssl.trust-store= + server.ssl.trust-store-password= + server.ssl.trust-store-provider= + server.ssl.trust-store-type= server.tomcat.access-log-pattern= # log pattern of the access log server.tomcat.access-log-enabled=false # is access logging enabled server.tomcat.internal-proxies=10\.\d{1,3}\.\d{1,3}\.\d{1,3}|\ diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index ca815236b3d..71aac600dc3 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -389,6 +389,27 @@ and then inject the actual (``local'') port as a `@Value`. For example: +[[howto-configure-ssl]] +=== Configure SSL +SSL can be configured declaratively by setting the various `server.ssl.*` properties, +typically in `application.properties` or `application.yml`. For example: + +[source,properties,indent=0,subs="verbatim,quotes,attributes"] +---- + server.port = 8443 + server.ssl.key-store = classpath:keystore.jks + server.ssl.key-store-password = secret + server.ssl.key-password = another-secret +---- + +See {sc-spring-boot}/context/embedded/Ssl.{sc-ext}[`Ssl`] for details of all of the +supported properties. + +NOTE: Tomcat requires the key store (and trust store if you're using one) to be directly +accessible on the filesystem, i.e. it cannot be read from within a jar file. + + + [[howto-configure-tomcat]] === Configure Tomcat Generally you can follow the advice from @@ -401,56 +422,6 @@ nuclear option is to add your own `TomcatEmbeddedServletContainerFactory`. -[[howto-terminate-ssl-in-tomcat]] -=== Terminate SSL in Tomcat -Use an `EmbeddedServletContainerCustomizer` and in that add a `TomcatConnectorCustomizer` -that sets up the connector to be secure: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public EmbeddedServletContainerCustomizer containerCustomizer(){ - return new MyCustomizer(); - } - - // ... - - private static class MyCustomizer implements EmbeddedServletContainerCustomizer { - - @Override - public void customize(ConfigurableEmbeddedServletContainer factory) { - if(factory instanceof TomcatEmbeddedServletContainerFactory) { - customizeTomcat((TomcatEmbeddedServletContainerFactory) factory); - } - } - - public void customizeTomcat(TomcatEmbeddedServletContainerFactory factory) { - factory.addConnectorCustomizers(new TomcatConnectorCustomizer() { - @Override - public void customize(Connector connector) { - connector.setPort(serverPort); - connector.setSecure(true); - connector.setScheme("https"); - connector.setAttribute("keyAlias", "tomcat"); - connector.setAttribute("keystorePass", "password"); - try { - connector.setAttribute("keystoreFile", - ResourceUtils.getFile("src/ssl/tomcat.keystore").getAbsolutePath()); - } catch (FileNotFoundException e) { - throw new IllegalStateException("Cannot load keystore", e); - } - connector.setAttribute("clientAuth", "false"); - connector.setAttribute("sslProtocol", "TLS"); - connector.setAttribute("SSLEnabled", true); - } - }); - } - - } ----- - - - [[howto-enable-multiple-connectors-in-tomcat]] === Enable Multiple Connectors Tomcat Add a `org.apache.catalina.connector.Connector` to the diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 062bfd6f371..2f6bdaee696 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -44,6 +44,7 @@ spring-boot-sample-secure spring-boot-sample-servlet spring-boot-sample-simple + spring-boot-sample-tomcat-ssl spring-boot-sample-tomcat spring-boot-sample-tomcat-multi-connectors spring-boot-sample-tomcat8-jsp diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml b/spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml new file mode 100644 index 00000000000..2497c82769b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.1.7.BUILD-SNAPSHOT + + spring-boot-sample-tomcat-ssl + Spring Boot Tomcat Sample + Spring Boot Tomcat SSL Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.springframework + spring-webmvc + + + org.apache.httpcomponents + httpclient + + + org.springframework.boot + spring-boot-starter-test + test + + + org.yaml + snakeyaml + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/sample.jks b/spring-boot-samples/spring-boot-sample-tomcat-ssl/sample.jks new file mode 100644 index 0000000000000000000000000000000000000000..6aa9a28053a591e41453e665e5024e8a8cb78b3d GIT binary patch literal 2264 zcmchYX*3iJ7sqE|hQS!q5Mv)4GM2$i#uAFqC`%7x7baWA*i&dRX>3`uq(XS?3XSYp z%38`&ib7E$8j~$cF^}gt?|I+noW8#w?uYxk=iGD8|K9Vzd#pVc0002(2k@T|2@MMI zqxqr2AhQO*TVi`j@((S;e;g;l$#dAA{>vf0kX$R(Qn4oKgGEYjZ5zti2dw?Z6A zh%LuFCNI?9o+Z1duJL-++e#cjO`zlK?u9s030=k_*wD1#-$FbIDRDnA^vo@fm( zzjt(3VJrGOr0iHXSTM|rYN#>RZ@Dp`PwB2zrDQffLvuoR2~V3ReYa0&vU^dXd8isV zsAf*@!8s%xBvHLseXn6f?1kefe(8uAmAbaF$x{Ykzb6c6jdUwY1$y4tFzsj7 zIghr!T#ODfu@Po!a29@kXQ8kY#(LE<0o7?7PQ|eMeY@Equ?R-6*f@Na3o&stDQ=6( zQzDSQhCnS(9Bu9W_~giknP0vECqUsr4_9y_}nEU`cy z4}dApnAip92wMwgzciAFpc3i}+-#Zlq+iF7d1y}d4Qsp8=%l1N8NIs161I`HmkcpQ zY4*CUCFJJf(2!M{`&qQ}3($KeTQ=)mMrBs`DOb;%Of0tC)9he_p~w&CO#DfCgx(%s z{@|D(brX_Gb}ZDLmGej*JgEl0Et>q~kgTXuJg-PwvRjNx8sBbIShxD=xOySzw{;^X zAvrh5HTg>Xq@<{#^!Kg}B?qz@b<{ebD)yaSf&RChBIJQo-?Ahzw@qopSe^e&>^IuU zydM4Y1_C&>k7u|}=; z63R7$H6zat=hNExxEwXu1fQ*ytuEkP!{w{|#6TIEq1#*ck=6_NM*ILF65tmD-O5&R zMI!-MT<3U~t@}(CN4@RlZ~1I>C=!ywF)dNI{VvH;5Y3(Z4jY^%_c&fsm4Q`<1g|qX z&!h29jXjVE3nJnet*L)XL?-8<>qDbVGP%i^NwOZfwWO7?Mr!X7 zl}sG@9S_5}}td}$xrWIYY=e(VVBiv%A+M-{M z!3_^Tc=pV?niT!{D`!{e@W;MvrZ(OER{x7itVAtwE~spPtPtma|J=5dv&_oE!5H#` zdgXJ;+gJ4hI}*9QX9jpL`Gb)yCe%1}t!&O-^sihyZys%%5uF~WhsR_w(q7;vV5d4P zr%ZUA2}kO+L^2ePTgGT9Ua71w<+)poSyjTdLq&xbUn`<6&SpwFp(HRHUyU6J3WZ_! zfztko79+94Tq%mTYj53(RYcL&1~5`I#+w3`(Q|r+P(aT z%?r(^?IWw~19CB&uvXf(f7&BnEE{zwK4piVU`I4j1j?v5d4N<7VUJ8nM`$7S*mfKR z#9-JzPRZ?{M!@L+0N^V)IyeeP2T|^UK|m0QD+Ibs!wEoml^N!YO#vW~j~jraX(0A3 z6Kux?IRLez`O^X;{!4g%BhcRn>^H*qKZ3*|{_YGuz)KCJcu;)DSES5D2tDE`C02YR0R%Vy1T7k|RQ;3g<0icA$AuP0pOvc~jGl zz+NeKv_FT_;GWK&8XlDUv&hv9kxg?@c!bu?83i=YQ$S!K09Y)Glg3Hz?@|)ZCBlVz zP8i}#XZkMoje3I=h&I!!s_m?Qi@1MR`yv7X*yEs47qOs^t^?&=;*IQ!q&)gq_Sx5* z?fhU8Q*PSe*w7y)FH#P!9R^Xw!lTT+zI39L<&8cViaj$A(Z2Cg7!{V?uuyi#vlNCg z40i}2ivw&y&1-&Nh&WMG`&aIt>)(#tKTJ}^@696Kw1-{IzSOTnFF+0@k$o3%ZHS;Q#;t literal 0 HcmV?d00001 diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java new file mode 100644 index 00000000000..d6b6abce3e4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java @@ -0,0 +1,35 @@ +/* + * 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 org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@ComponentScan +@Configuration +@EnableAutoConfiguration +@EnableConfigurationProperties +public class SampleTomcatSslApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleTomcatSslApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java new file mode 100644 index 00000000000..ac00abeb98b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java @@ -0,0 +1,32 @@ +/* + * 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.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class SampleController { + + @RequestMapping("/") + @ResponseBody + public String helloWorld() { + return "Hello, world"; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties new file mode 100644 index 00000000000..c8897283947 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.port = 8443 +server.ssl.key-store = sample.jks +server.ssl.key-store-password = secret +server.ssl.key-password = password \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java new file mode 100644 index 00000000000..b6c5fa35455 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java @@ -0,0 +1,67 @@ +/* + * 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 org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.impl.client.HttpClients; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleTomcatSslApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port:0") +@DirtiesContext +public class SampleTomcatSslApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void testHome() throws Exception { + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + TestRestTemplate testRestTemplate = new TestRestTemplate(); + ((HttpComponentsClientHttpRequestFactory) testRestTemplate.getRequestFactory()) + .setHttpClient(httpClient); + ResponseEntity entity = testRestTemplate.getForEntity( + "https://localhost:" + this.port, String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals("Hello, world", entity.getBody()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java index 09c7a5335f6..f341ffd2c4e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java @@ -59,6 +59,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer implements private int sessionTimeout; + private Ssl ssl; + /** * Create a new {@link AbstractConfigurableEmbeddedServletContainer} instance. */ @@ -247,6 +249,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer implements this.jspServletClassName = jspServletClassName; } + @Override + public void setSsl(Ssl ssl) { + this.ssl = ssl; + } + + public Ssl getSsl() { + return this.ssl; + } + /** * @return the JSP servlet class name */ diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java index 021173c2010..ff068415792 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java @@ -139,4 +139,11 @@ public interface ConfigurableEmbeddedServletContainer { */ void addInitializers(ServletContextInitializer... initializers); + /** + * Sets the SSL configuration that will be applied to the container's default + * connector. + * @param ssl the SSL configuration + */ + void setSsl(Ssl ssl); + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java new file mode 100644 index 00000000000..1bed8222221 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java @@ -0,0 +1,160 @@ +/* + * 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 org.springframework.boot.context.embedded; + +/** + * Simple container-independent abstraction for SSL configuration. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class Ssl { + + private ClientAuth clientAuth; + + private String[] ciphers; + + private String keyAlias; + + private String keyPassword; + + private String keyStore; + + private String keyStorePassword; + + private String keyStoreType; + + private String keyStoreProvider; + + private String trustStore; + + private String trustStorePassword; + + private String trustStoreType; + + private String trustStoreProvider; + + private String protocol = "TLS"; + + public ClientAuth getClientAuth() { + return this.clientAuth; + } + + public void setClientAuth(ClientAuth clientAuth) { + this.clientAuth = clientAuth; + } + + public String[] getCiphers() { + return this.ciphers; + } + + public void setCiphers(String[] ciphers) { + this.ciphers = ciphers; + } + + public String getKeyAlias() { + return this.keyAlias; + } + + public void setKeyAlias(String keyAlias) { + this.keyAlias = keyAlias; + } + + public String getKeyPassword() { + return this.keyPassword; + } + + public void setKeyPassword(String keyPassword) { + this.keyPassword = keyPassword; + } + + public String getKeyStore() { + return this.keyStore; + } + + public void setKeyStore(String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStorePassword() { + return this.keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyStoreType() { + return this.keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public String getKeyStoreProvider() { + return this.keyStoreProvider; + } + + public void setKeyStoreProvider(String keyStoreProvider) { + this.keyStoreProvider = keyStoreProvider; + } + + public String getTrustStore() { + return this.trustStore; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStorePassword() { + return this.trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getTrustStoreType() { + return this.trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public String getTrustStoreProvider() { + return this.trustStoreProvider; + } + + public void setTrustStoreProvider(String trustStoreProvider) { + this.trustStoreProvider = trustStoreProvider; + } + + public String getProtocol() { + return this.protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public enum ClientAuth { + WANT, NEED; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index b733f69df14..ae96d191860 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -17,32 +17,41 @@ package org.springframework.boot.context.embedded.jetty; import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.ssl.SslSocketConnector; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; +import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.boot.context.embedded.MimeMappings; import org.springframework.boot.context.embedded.ServletContextInitializer; +import org.springframework.boot.context.embedded.Ssl; +import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -104,6 +113,16 @@ public class JettyEmbeddedServletContainerFactory extends configureWebAppContext(context, initializers); server.setHandler(context); this.logger.info("Server initialized with port: " + port); + + if (getSsl() != null) { + SslContextFactory sslContextFactory = new SslContextFactory(); + configureSsl(sslContextFactory, getSsl()); + + SslSocketConnector sslConnector = new SslSocketConnector(sslContextFactory); + sslConnector.setPort(port); + server.setConnectors(new Connector[] { sslConnector }); + } + for (JettyServerCustomizer customizer : getServerCustomizers()) { customizer.customize(server); } @@ -111,6 +130,81 @@ public class JettyEmbeddedServletContainerFactory extends return getJettyEmbeddedServletContainer(server); } + /** + * Configure the SSL connection. + * @param factory the Jetty {@link SslContextFactory}. + * @param ssl the ssl details. + */ + protected void configureSsl(SslContextFactory factory, Ssl ssl) { + factory.setProtocol(ssl.getProtocol()); + configureSslClientAuth(factory, ssl); + configureSslPasswords(factory, ssl); + factory.setCertAlias(ssl.getKeyAlias()); + configureSslKeyStore(factory, ssl); + if (ssl.getCiphers() != null) { + factory.setIncludeCipherSuites(ssl.getCiphers()); + } + configureSslTrustStore(factory, ssl); + } + + private void configureSslClientAuth(SslContextFactory factory, Ssl ssl) { + if (ssl.getClientAuth() == ClientAuth.NEED) { + factory.setNeedClientAuth(true); + factory.setWantClientAuth(true); + } + else if (ssl.getClientAuth() == ClientAuth.WANT) { + factory.setWantClientAuth(true); + } + } + + private void configureSslPasswords(SslContextFactory factory, Ssl ssl) { + if (ssl.getKeyStorePassword() != null) { + factory.setKeyStorePassword(ssl.getKeyStorePassword()); + } + if (ssl.getKeyPassword() != null) { + factory.setKeyManagerPassword(ssl.getKeyPassword()); + } + } + + private void configureSslKeyStore(SslContextFactory factory, Ssl ssl) { + try { + URL url = ResourceUtils.getURL(ssl.getKeyStore()); + factory.setKeyStoreResource(Resource.newResource(url)); + } + catch (IOException ex) { + throw new EmbeddedServletContainerException("Could not find key store '" + + ssl.getKeyStore() + "'", ex); + } + if (ssl.getKeyStoreType() != null) { + factory.setKeyStoreType(ssl.getKeyStoreType()); + } + if (ssl.getKeyStoreProvider() != null) { + factory.setKeyStoreProvider(ssl.getKeyStoreProvider()); + } + } + + private void configureSslTrustStore(SslContextFactory factory, Ssl ssl) { + if (ssl.getTrustStorePassword() != null) { + factory.setTrustStorePassword(ssl.getTrustStorePassword()); + } + if (ssl.getTrustStore() != null) { + try { + URL url = ResourceUtils.getURL(ssl.getTrustStore()); + factory.setTrustStoreResource(Resource.newResource(url)); + } + catch (IOException ex) { + throw new EmbeddedServletContainerException( + "Could not find trust store '" + ssl.getTrustStore() + "'", ex); + } + } + if (ssl.getTrustStoreType() != null) { + factory.setTrustStoreType(ssl.getTrustStoreType()); + } + if (ssl.getTrustStoreProvider() != null) { + factory.setTrustStoreProvider(ssl.getTrustStoreProvider()); + } + } + /** * Configure the given Jetty {@link WebAppContext} for use. * @param context the context to configure 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 f7d8d6a9439..51fa948a82e 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 @@ -17,6 +17,7 @@ package org.springframework.boot.context.embedded.tomcat; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; @@ -42,6 +43,7 @@ import org.apache.catalina.loader.WebappLoader; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat.FixContextListener; import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.springframework.beans.BeanUtils; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -50,12 +52,16 @@ import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.boot.context.embedded.MimeMappings; import org.springframework.boot.context.embedded.ServletContextInitializer; +import org.springframework.boot.context.embedded.Ssl; +import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * {@link EmbeddedServletContainerFactory} that can be used to create @@ -231,11 +237,87 @@ public class TomcatEmbeddedServletContainerFactory extends // If ApplicationContext is slow to start we want Tomcat not to bind to the socket // prematurely... connector.setProperty("bindOnInit", "false"); + + if (getSsl() != null) { + Assert.state( + connector.getProtocolHandler() instanceof AbstractHttp11JsseProtocol, + "To use SSL, the connector's protocol handler must be an " + + "AbstractHttp11JsseProtocol subclass"); + configureSsl((AbstractHttp11JsseProtocol) connector.getProtocolHandler(), + getSsl()); + connector.setScheme("https"); + connector.setSecure(true); + } + for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) { customizer.customize(connector); } } + /** + * Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. + * @param protocol the protocol + * @param ssl the ssl details + */ + protected void configureSsl(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + protocol.setSSLEnabled(true); + protocol.setSslProtocol(ssl.getProtocol()); + configureSslClientAuth(protocol, ssl); + protocol.setKeystorePass(ssl.getKeyStorePassword()); + protocol.setKeyPass(ssl.getKeyPassword()); + protocol.setKeyAlias(ssl.getKeyAlias()); + configureSslKeyStore(protocol, ssl); + String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers()); + protocol.setCiphers(ciphers); + configureSslTrustStore(protocol, ssl); + } + + private void configureSslClientAuth(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + if (ssl.getClientAuth() == ClientAuth.NEED) { + protocol.setClientAuth(Boolean.TRUE.toString()); + } + else if (ssl.getClientAuth() == ClientAuth.WANT) { + protocol.setClientAuth("want"); + } + } + + private void configureSslKeyStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + try { + File file = ResourceUtils.getFile(ssl.getKeyStore()); + protocol.setKeystoreFile(file.getAbsolutePath()); + } + catch (FileNotFoundException ex) { + throw new EmbeddedServletContainerException("Could not find key store " + + ssl.getKeyStore(), ex); + } + if (ssl.getKeyStoreType() != null) { + protocol.setKeystoreType(ssl.getKeyStoreType()); + } + if (ssl.getKeyStoreProvider() != null) { + protocol.setKeystoreProvider(ssl.getKeyStoreProvider()); + } + } + + private void configureSslTrustStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + if (ssl.getTrustStore() != null) { + try { + File file = ResourceUtils.getFile(ssl.getTrustStore()); + protocol.setTruststoreFile(file.getAbsolutePath()); + } + catch (FileNotFoundException ex) { + throw new EmbeddedServletContainerException("Could not find trust store " + + ssl.getTrustStore(), ex); + } + } + protocol.setTruststorePass(ssl.getTrustStorePassword()); + if (ssl.getTrustStoreType() != null) { + protocol.setTruststoreType(ssl.getTrustStoreType()); + } + if (ssl.getTrustStoreProvider() != null) { + protocol.setTruststoreProvider(ssl.getTrustStoreProvider()); + } + } + /** * Configure the Tomcat {@link Context}. * @param context the Tomcat context diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index c73ee315c32..2850624d4dd 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -16,11 +16,14 @@ package org.springframework.boot.context.embedded; +import java.io.File; +import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.security.KeyStore; import java.util.Arrays; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -31,12 +34,18 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.impl.client.HttpClients; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.mockito.InOrder; +import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpRequest; @@ -62,6 +71,7 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Greg Turnquist + * @author Andy Wilkinson */ public abstract class AbstractEmbeddedServletContainerFactoryTests { @@ -300,8 +310,232 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { assertThat(getResponse(getLocalUrl("/bang")), equalTo("Hello World")); } + @Test + public void basicSsl() throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test + public void pkcs12KeyStoreAndTrustStore() throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.p12"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyStoreType("pkcs12"); + ssl.setTrustStore("src/test/resources/test.p12"); + ssl.setTrustStorePassword("secret"); + ssl.setTrustStoreType("pkcs12"); + ssl.setClientAuth(ClientAuth.NEED); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + KeyStore keyStore = KeyStore.getInstance("pkcs12"); + keyStore.load(new FileInputStream(new File("src/test/resources/test.p12")), + "secret".toCharArray()); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "secret".toCharArray()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test + public void sslNeedsClientAuthenticationSucceedsWithClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.NEED); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), + "secret".toCharArray()); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test(expected = IOException.class) + public void sslNeedsClientAuthenticationFailsWithoutClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.NEED); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + getResponse(getLocalUrl("https", "/test.txt"), requestFactory); + } + + @Test + public void sslWantsClientAuthenticationSucceedsWithClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.WANT); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), + "secret".toCharArray()); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test + public void sslWantsClientAuthenticationSucceedsWithoutClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.WANT); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + protected String getLocalUrl(String resourcePath) { - return "http://localhost:" + this.container.getPort() + resourcePath; + return getLocalUrl("http", resourcePath); + } + + protected String getLocalUrl(String scheme, String resourcePath) { + return scheme + "://localhost:" + this.container.getPort() + resourcePath; } protected String getLocalUrl(int port, String resourcePath) { @@ -318,10 +552,27 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { } } + protected String getResponse(String url, + HttpComponentsClientHttpRequestFactory requestFactory) throws IOException, + URISyntaxException { + ClientHttpResponse response = getClientResponse(url, requestFactory); + try { + return StreamUtils.copyToString(response.getBody(), Charset.forName("UTF-8")); + } + finally { + response.close(); + } + } + protected ClientHttpResponse getClientResponse(String url) throws IOException, URISyntaxException { - HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); - ClientHttpRequest request = clientHttpRequestFactory.createRequest(new URI(url), + return getClientResponse(url, new HttpComponentsClientHttpRequestFactory()); + } + + protected ClientHttpResponse getClientResponse(String url, + HttpComponentsClientHttpRequestFactory requestFactory) throws IOException, + URISyntaxException { + ClientHttpRequest request = requestFactory.createRequest(new URI(url), HttpMethod.GET); ClientHttpResponse response = request.execute(); return response; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index f9b9d8af31d..929908113a7 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -21,11 +21,13 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ssl.SslConnector; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.Test; import org.mockito.InOrder; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; +import org.springframework.boot.context.embedded.Ssl; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -39,6 +41,7 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Dave Syer + * @author Andy Wilkinson */ public class JettyEmbeddedServletContainerFactoryTests extends AbstractEmbeddedServletContainerFactoryTests { @@ -94,6 +97,27 @@ public class JettyEmbeddedServletContainerFactoryTests extends assertTimeout(factory, 60); } + @Test + public void sslCiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); + + JettyEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container; + SslConnector sslConnector = (SslConnector) jettyContainer.getServer() + .getConnectors()[0]; + assertThat(sslConnector.getSslContextFactory().getIncludeCipherSuites(), + equalTo(new String[] { "ALPHA", "BRAVO", "CHARLIE" })); + } + private void assertTimeout(JettyEmbeddedServletContainerFactory factory, int expected) { this.container = factory.getEmbeddedServletContainer(); JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container; 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 1d51a316c16..a5071c61f8f 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 @@ -28,9 +28,11 @@ import org.apache.catalina.Service; import org.apache.catalina.Valve; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.junit.Test; import org.mockito.InOrder; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; +import org.springframework.boot.context.embedded.Ssl; import org.springframework.util.SocketUtils; import static org.hamcrest.Matchers.equalTo; @@ -228,6 +230,24 @@ public class TomcatEmbeddedServletContainerFactoryTests extends assertEquals("UTF-8", tomcat.getConnector().getURIEncoding()); } + @Test + public void sslCiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); + + TomcatEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + Tomcat tomcat = getTomcat(factory); + Connector connector = tomcat.getConnector(); + + AbstractHttp11JsseProtocol jsseProtocol = (AbstractHttp11JsseProtocol) connector + .getProtocolHandler(); + assertThat(jsseProtocol.getCiphers(), equalTo("ALPHA,BRAVO,CHARLIE")); + } + private void assertTimeout(TomcatEmbeddedServletContainerFactory factory, int expected) { Tomcat tomcat = getTomcat(factory); Context context = (Context) tomcat.getHost().findChildren()[0]; diff --git a/spring-boot/src/test/resources/test.jks b/spring-boot/src/test/resources/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..cc0d7081c2e213f41f30aaba7611d55982968852 GIT binary patch literal 2248 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 upo%