Commit 8b77a029 authored by brockwmills's avatar brockwmills Committed by Phillip Webb

Allow multiple connectors with Tomcat

Update TomcatEmbeddedServletContainerFactory to allow for additional
containers (e.g. SSL or AJP in addition to HTTP).

Fixes gh-528
parent 48636e3d
......@@ -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]]
......
......@@ -31,6 +31,7 @@
<module>spring-boot-sample-servlet</module>
<module>spring-boot-sample-simple</module>
<module>spring-boot-sample-tomcat</module>
<module>spring-boot-sample-tomcat-multi-connectors</module>
<module>spring-boot-sample-traditional</module>
<module>spring-boot-sample-web-method-security</module>
<module>spring-boot-sample-web-secure</module>
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<!-- Your own application should inherit from spring-boot-starter-parent -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-sample-tomcat-multi-connectors</artifactId>
<packaging>jar</packaging>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
/*
* 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);
}
}
/*
* 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";
}
}
/*
* 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<String> entity = template.getForEntity(
"http://localhost:8080/hello", String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertEquals("hello", entity.getBody());
ResponseEntity<String> 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);
}
}
}
......@@ -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 {
......
......@@ -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<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new ArrayList<TomcatConnectorCustomizer>();
private List<Connector> additionalTomcatConnectors = new ArrayList<Connector>();
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<Connector> getAdditionalTomcatConnectors() {
return this.additionalTomcatConnectors;
}
private static class TomcatErrorPage {
private final String location;
......
......@@ -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();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment