/* * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.boot.web.embedded.undertow; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.Socket; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedKeyManager; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; import io.undertow.Undertow; import io.undertow.Undertow.Builder; import io.undertow.server.HandlerWrapper; import io.undertow.server.HttpHandler; import io.undertow.server.handlers.accesslog.AccessLogHandler; import io.undertow.server.handlers.accesslog.AccessLogReceiver; import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; import io.undertow.server.handlers.resource.FileResourceManager; import io.undertow.server.handlers.resource.Resource; import io.undertow.server.handlers.resource.ResourceChangeListener; import io.undertow.server.handlers.resource.ResourceManager; import io.undertow.server.handlers.resource.URLResource; import io.undertow.server.session.SessionManager; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.MimeMapping; import io.undertow.servlet.api.ServletContainerInitializerInfo; import io.undertow.servlet.api.ServletStackTraces; import io.undertow.servlet.handlers.DefaultServlet; import io.undertow.servlet.util.ImmediateInstanceFactory; import org.xnio.OptionMap; import org.xnio.Options; import org.xnio.Sequence; import org.xnio.SslClientAuthMode; import org.xnio.Xnio; import org.xnio.XnioWorker; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings.Mapping; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl.ClientAuth; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; /** * {@link ServletWebServerFactory} that can be used to create * {@link UndertowServletWebServer}s. * <p> * Unless explicitly configured otherwise, the factory will create servers that listen for * HTTP requests on port 8080. * * @author Ivan Sopov * @author Andy Wilkinson * @author Marcos Barbero * @author EddĂș MelĂ©ndez * @since 2.0.0 * @see UndertowServletWebServer */ public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory implements ResourceLoaderAware { private static final Set<Class<?>> NO_CLASSES = Collections.emptySet(); private List<UndertowBuilderCustomizer> builderCustomizers = new ArrayList<>(); private List<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers = new ArrayList<>(); private ResourceLoader resourceLoader; private Integer bufferSize; private Integer ioThreads; private Integer workerThreads; private Boolean directBuffers; private File accessLogDirectory; private String accessLogPattern; private String accessLogPrefix; private String accessLogSuffix; private boolean accessLogEnabled = false; private boolean accessLogRotate = true; private boolean useForwardHeaders; /** * Create a new {@link UndertowServletWebServerFactory} instance. */ public UndertowServletWebServerFactory() { super(); getJsp().setRegistered(false); } /** * Create a new {@link UndertowServletWebServerFactory} that listens for requests * using the specified port. * @param port the port to listen on */ public UndertowServletWebServerFactory(int port) { super(port); getJsp().setRegistered(false); } /** * Create a new {@link UndertowServletWebServerFactory} with the specified context * path and port. * @param contextPath the root context path * @param port the port to listen on */ public UndertowServletWebServerFactory(String contextPath, int port) { super(contextPath, port); getJsp().setRegistered(false); } /** * Set {@link UndertowBuilderCustomizer}s that should be applied to the Undertow * {@link Builder}. Calling this method will replace any existing customizers. * @param customizers the customizers to set */ public void setBuilderCustomizers( Collection<? extends UndertowBuilderCustomizer> customizers) { Assert.notNull(customizers, "Customizers must not be null"); this.builderCustomizers = new ArrayList<>(customizers); } /** * Returns a mutable collection of the {@link UndertowBuilderCustomizer}s that will be * applied to the Undertow {@link Builder} . * @return the customizers that will be applied */ public Collection<UndertowBuilderCustomizer> getBuilderCustomizers() { return this.builderCustomizers; } /** * Add {@link UndertowBuilderCustomizer}s that should be used to customize the * Undertow {@link Builder}. * @param customizers the customizers to add */ public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) { Assert.notNull(customizers, "Customizers must not be null"); this.builderCustomizers.addAll(Arrays.asList(customizers)); } /** * Set {@link UndertowDeploymentInfoCustomizer}s that should be applied to the * Undertow {@link DeploymentInfo}. Calling this method will replace any existing * customizers. * @param customizers the customizers to set */ public void setDeploymentInfoCustomizers( Collection<? extends UndertowDeploymentInfoCustomizer> customizers) { Assert.notNull(customizers, "Customizers must not be null"); this.deploymentInfoCustomizers = new ArrayList<>(customizers); } /** * Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that * will be applied to the Undertow {@link DeploymentInfo} . * @return the customizers that will be applied */ public Collection<UndertowDeploymentInfoCustomizer> getDeploymentInfoCustomizers() { return this.deploymentInfoCustomizers; } /** * Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the * Undertow {@link DeploymentInfo}. * @param customizers the customizers to add */ public void addDeploymentInfoCustomizers( UndertowDeploymentInfoCustomizer... customizers) { Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null"); this.deploymentInfoCustomizers.addAll(Arrays.asList(customizers)); } @Override public WebServer getWebServer(ServletContextInitializer... initializers) { DeploymentManager manager = createDeploymentManager(initializers); int port = getPort(); Builder builder = createBuilder(port); return getUndertowWebServer(builder, manager, port); } private Builder createBuilder(int port) { Builder builder = Undertow.builder(); if (this.bufferSize != null) { builder.setBufferSize(this.bufferSize); } if (this.ioThreads != null) { builder.setIoThreads(this.ioThreads); } if (this.workerThreads != null) { builder.setWorkerThreads(this.workerThreads); } if (this.directBuffers != null) { builder.setDirectBuffers(this.directBuffers); } if (getSsl() != null && getSsl().isEnabled()) { configureSsl(getSsl(), port, builder); } else { builder.addHttpListener(port, getListenAddress()); } for (UndertowBuilderCustomizer customizer : this.builderCustomizers) { customizer.customize(builder); } return builder; } private void configureSsl(Ssl ssl, int port, Builder builder) { try { SSLContext sslContext = SSLContext.getInstance(ssl.getProtocol()); sslContext.init(getKeyManagers(), getTrustManagers(), null); builder.addHttpsListener(port, getListenAddress(), sslContext); builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, getSslClientAuthMode(ssl)); if (ssl.getEnabledProtocols() != null) { builder.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, Sequence.of(ssl.getEnabledProtocols())); } if (ssl.getCiphers() != null) { builder.setSocketOption(Options.SSL_ENABLED_CIPHER_SUITES, Sequence.of(ssl.getCiphers())); } } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException(ex); } catch (KeyManagementException ex) { throw new IllegalStateException(ex); } } private String getListenAddress() { if (getAddress() == null) { return "0.0.0.0"; } return getAddress().getHostAddress(); } private SslClientAuthMode getSslClientAuthMode(Ssl ssl) { if (ssl.getClientAuth() == ClientAuth.NEED) { return SslClientAuthMode.REQUIRED; } if (ssl.getClientAuth() == ClientAuth.WANT) { return SslClientAuthMode.REQUESTED; } return SslClientAuthMode.NOT_REQUESTED; } private KeyManager[] getKeyManagers() { try { KeyStore keyStore = getKeyStore(); KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); Ssl ssl = getSsl(); char[] keyPassword = (ssl.getKeyPassword() != null ? ssl.getKeyPassword().toCharArray() : null); if (keyPassword == null && ssl.getKeyStorePassword() != null) { keyPassword = ssl.getKeyStorePassword().toCharArray(); } keyManagerFactory.init(keyStore, keyPassword); return getConfigurableAliasKeyManagers(ssl, keyManagerFactory.getKeyManagers()); } catch (Exception ex) { throw new IllegalStateException(ex); } } private KeyManager[] getConfigurableAliasKeyManagers(Ssl ssl, KeyManager[] keyManagers) { for (int i = 0; i < keyManagers.length; i++) { if (keyManagers[i] instanceof X509ExtendedKeyManager) { keyManagers[i] = new ConfigurableAliasKeyManager( (X509ExtendedKeyManager) keyManagers[i], ssl.getKeyAlias()); } } return keyManagers; } private KeyStore getKeyStore() throws Exception { if (getSslStoreProvider() != null) { return getSslStoreProvider().getKeyStore(); } Ssl ssl = getSsl(); return loadKeyStore(ssl.getKeyStoreType(), ssl.getKeyStore(), ssl.getKeyStorePassword()); } private TrustManager[] getTrustManagers() { try { KeyStore store = getTrustStore(); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(store); return trustManagerFactory.getTrustManagers(); } catch (Exception ex) { throw new IllegalStateException(ex); } } private KeyStore getTrustStore() throws Exception { if (getSslStoreProvider() != null) { return getSslStoreProvider().getTrustStore(); } Ssl ssl = getSsl(); return loadKeyStore(ssl.getTrustStoreType(), ssl.getTrustStore(), ssl.getTrustStorePassword()); } private KeyStore loadKeyStore(String type, String resource, String password) throws Exception { type = (type == null ? "JKS" : type); if (resource == null) { return null; } KeyStore store = KeyStore.getInstance(type); URL url = ResourceUtils.getURL(resource); store.load(url.openStream(), password == null ? null : password.toCharArray()); return store; } private DeploymentManager createDeploymentManager( ServletContextInitializer... initializers) { DeploymentInfo deployment = Servlets.deployment(); registerServletContainerInitializerToDriveServletContextInitializers(deployment, initializers); deployment.setClassLoader(getServletClassLoader()); deployment.setContextPath(getContextPath()); deployment.setDisplayName(getDisplayName()); deployment.setDeploymentName("spring-boot"); if (isRegisterDefaultServlet()) { deployment.addServlet(Servlets.servlet("default", DefaultServlet.class)); } configureErrorPages(deployment); deployment.setServletStackTraces(ServletStackTraces.NONE); deployment.setResourceManager(getDocumentRootResourceManager()); configureMimeMappings(deployment); for (UndertowDeploymentInfoCustomizer customizer : this.deploymentInfoCustomizers) { customizer.customize(deployment); } if (isAccessLogEnabled()) { configureAccessLog(deployment); } if (isPersistSession()) { File dir = getValidSessionStoreDir(); deployment.setSessionPersistenceManager(new FileSessionPersistence(dir)); } addLocaleMappings(deployment); DeploymentManager manager = Servlets.newContainer().addDeployment(deployment); manager.deploy(); SessionManager sessionManager = manager.getDeployment().getSessionManager(); int sessionTimeout = (getSessionTimeout() > 0 ? getSessionTimeout() : -1); sessionManager.setDefaultSessionTimeout(sessionTimeout); return manager; } private void configureAccessLog(DeploymentInfo deploymentInfo) { deploymentInfo.addInitialHandlerChainWrapper(new HandlerWrapper() { @Override public HttpHandler wrap(HttpHandler handler) { return createAccessLogHandler(handler); } }); } private AccessLogHandler createAccessLogHandler(HttpHandler handler) { try { createAccessLogDirectoryIfNecessary(); String prefix = (this.accessLogPrefix != null ? this.accessLogPrefix : "access_log."); AccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver( createWorker(), this.accessLogDirectory, prefix, this.accessLogSuffix, this.accessLogRotate); String formatString = (this.accessLogPattern != null) ? this.accessLogPattern : "common"; return new AccessLogHandler(handler, accessLogReceiver, formatString, Undertow.class.getClassLoader()); } catch (IOException ex) { throw new IllegalStateException("Failed to create AccessLogHandler", ex); } } private void createAccessLogDirectoryIfNecessary() { Assert.state(this.accessLogDirectory != null, "Access log directory is not set"); if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) { throw new IllegalStateException("Failed to create access log directory '" + this.accessLogDirectory + "'"); } } private XnioWorker createWorker() throws IOException { Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader()); return xnio.createWorker( OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap()); } private void addLocaleMappings(DeploymentInfo deployment) { for (Map.Entry<Locale, Charset> entry : getLocaleCharsetMappings().entrySet()) { Locale locale = entry.getKey(); Charset charset = entry.getValue(); deployment.addLocaleCharsetMapping(locale.toString(), charset.toString()); } } private void registerServletContainerInitializerToDriveServletContextInitializers( DeploymentInfo deployment, ServletContextInitializer... initializers) { ServletContextInitializer[] mergedInitializers = mergeInitializers(initializers); Initializer initializer = new Initializer(mergedInitializers); deployment.addServletContainerInitalizer(new ServletContainerInitializerInfo( Initializer.class, new ImmediateInstanceFactory<ServletContainerInitializer>(initializer), NO_CLASSES)); } private ClassLoader getServletClassLoader() { if (this.resourceLoader != null) { return this.resourceLoader.getClassLoader(); } return getClass().getClassLoader(); } private ResourceManager getDocumentRootResourceManager() { File root = getCanonicalDocumentRoot(); List<URL> metaInfResourceUrls = getUrlsOfJarsWithMetaInfResources(); List<URL> resourceJarUrls = new ArrayList<URL>(); List<ResourceManager> resourceManagers = new ArrayList<ResourceManager>(); ResourceManager rootResourceManager = root.isDirectory() ? new FileResourceManager(root, 0) : new JarResourceManager(root); resourceManagers.add(rootResourceManager); for (URL url : metaInfResourceUrls) { if ("file".equals(url.getProtocol())) { File file = new File(url.getFile()); if (file.isFile()) { try { resourceJarUrls.add(new URL("jar:" + url + "!/")); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } } else { resourceManagers.add(new FileResourceManager( new File(file, "META-INF/resources"), 0)); } } else { resourceJarUrls.add(url); } } resourceManagers.add(new MetaInfResourcesResourceManager(resourceJarUrls)); return new CompositeResourceManager( resourceManagers.toArray(new ResourceManager[resourceManagers.size()])); } /** * Return the document root in canonical form. Undertow uses File#getCanonicalFile() * to determine whether a resource has been requested using the proper case but on * Windows {@code java.io.tmpdir} may be set as a tilde-compressed pathname. * @return the canonical document root */ private File getCanonicalDocumentRoot() { try { File root = getValidDocumentRoot(); root = (root != null ? root : createTempDir("undertow-docbase")); return root.getCanonicalFile(); } catch (IOException e) { throw new IllegalStateException("Cannot get canonical document root", e); } } private void configureErrorPages(DeploymentInfo servletBuilder) { for (ErrorPage errorPage : getErrorPages()) { servletBuilder.addErrorPage(getUndertowErrorPage(errorPage)); } } private io.undertow.servlet.api.ErrorPage getUndertowErrorPage(ErrorPage errorPage) { if (errorPage.getStatus() != null) { return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(), errorPage.getStatusCode()); } if (errorPage.getException() != null) { return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(), errorPage.getException()); } return new io.undertow.servlet.api.ErrorPage(errorPage.getPath()); } private void configureMimeMappings(DeploymentInfo servletBuilder) { for (Mapping mimeMapping : getMimeMappings()) { servletBuilder.addMimeMapping(new MimeMapping(mimeMapping.getExtension(), mimeMapping.getMimeType())); } } /** * Factory method called to create the {@link UndertowServletWebServer}. Subclasses * can override this method to return a different {@link UndertowServletWebServer} or * apply additional processing to the {@link Builder} and {@link DeploymentManager} * used to bootstrap Undertow * @param builder the builder * @param manager the deployment manager * @param port the port that Undertow should listen on * @return a new {@link UndertowServletWebServer} instance */ protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) { return new UndertowServletWebServer(builder, manager, getContextPath(), isUseForwardHeaders(), port >= 0, getCompression(), getServerHeader()); } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } public void setBufferSize(Integer bufferSize) { this.bufferSize = bufferSize; } public void setIoThreads(Integer ioThreads) { this.ioThreads = ioThreads; } public void setWorkerThreads(Integer workerThreads) { this.workerThreads = workerThreads; } public void setDirectBuffers(Boolean directBuffers) { this.directBuffers = directBuffers; } public void setAccessLogDirectory(File accessLogDirectory) { this.accessLogDirectory = accessLogDirectory; } public void setAccessLogPattern(String accessLogPattern) { this.accessLogPattern = accessLogPattern; } public String getAccessLogPrefix() { return this.accessLogPrefix; } public void setAccessLogPrefix(String accessLogPrefix) { this.accessLogPrefix = accessLogPrefix; } public void setAccessLogSuffix(String accessLogSuffix) { this.accessLogSuffix = accessLogSuffix; } public void setAccessLogEnabled(boolean accessLogEnabled) { this.accessLogEnabled = accessLogEnabled; } public boolean isAccessLogEnabled() { return this.accessLogEnabled; } public void setAccessLogRotate(boolean accessLogRotate) { this.accessLogRotate = accessLogRotate; } protected final boolean isUseForwardHeaders() { return this.useForwardHeaders; } /** * Set if x-forward-* headers should be processed. * @param useForwardHeaders if x-forward headers should be used * @since 1.3.0 */ public void setUseForwardHeaders(boolean useForwardHeaders) { this.useForwardHeaders = useForwardHeaders; } /** * {@link ResourceManager} that exposes resource in {@code META-INF/resources} * directory of nested (in {@code BOOT-INF/lib} or {@code WEB-INF/lib}) jars. */ private static final class MetaInfResourcesResourceManager implements ResourceManager { private final List<URL> metaInfResourceJarUrls; private MetaInfResourcesResourceManager(List<URL> metaInfResourceJarUrls) { this.metaInfResourceJarUrls = metaInfResourceJarUrls; } @Override public void close() throws IOException { } @Override public Resource getResource(String path) { for (URL url : this.metaInfResourceJarUrls) { try { URL resourceUrl = new URL(url + "META-INF/resources" + path); URLConnection connection = resourceUrl.openConnection(); if (connection.getContentLength() >= 0) { return new URLResource(resourceUrl, connection, path); } } catch (IOException ex) { // Continue } } return null; } @Override public boolean isResourceChangeListenerSupported() { return false; } @Override public void registerResourceChangeListener(ResourceChangeListener listener) { } @Override public void removeResourceChangeListener(ResourceChangeListener listener) { } } /** * {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer * ServletContextInitializers}. */ private static class Initializer implements ServletContainerInitializer { private final ServletContextInitializer[] initializers; Initializer(ServletContextInitializer[] initializers) { this.initializers = initializers; } @Override public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } } /** * {@link X509ExtendedKeyManager} that supports custom alias configuration. */ private static class ConfigurableAliasKeyManager extends X509ExtendedKeyManager { private final X509ExtendedKeyManager keyManager; private final String alias; ConfigurableAliasKeyManager(X509ExtendedKeyManager keyManager, String alias) { this.keyManager = keyManager; this.alias = alias; } @Override public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) { return this.keyManager.chooseEngineClientAlias(strings, principals, sslEngine); } @Override public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) { if (this.alias == null) { return this.keyManager.chooseEngineServerAlias(s, principals, sslEngine); } return this.alias; } @Override public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { return this.keyManager.chooseClientAlias(keyType, issuers, socket); } @Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { return this.keyManager.chooseServerAlias(keyType, issuers, socket); } @Override public X509Certificate[] getCertificateChain(String alias) { return this.keyManager.getCertificateChain(alias); } @Override public String[] getClientAliases(String keyType, Principal[] issuers) { return this.keyManager.getClientAliases(keyType, issuers); } @Override public PrivateKey getPrivateKey(String alias) { return this.keyManager.getPrivateKey(alias); } @Override public String[] getServerAliases(String keyType, Principal[] issuers) { return this.keyManager.getServerAliases(keyType, issuers); } } }