001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.web.embedded.undertow;
018
019import java.io.File;
020import java.io.IOException;
021import java.net.MalformedURLException;
022import java.net.URL;
023import java.time.Duration;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.EventListener;
029import java.util.List;
030import java.util.Set;
031
032import javax.servlet.ServletContainerInitializer;
033import javax.servlet.ServletContext;
034import javax.servlet.ServletContextEvent;
035import javax.servlet.ServletContextListener;
036import javax.servlet.ServletException;
037
038import io.undertow.Undertow;
039import io.undertow.Undertow.Builder;
040import io.undertow.UndertowOptions;
041import io.undertow.server.HttpHandler;
042import io.undertow.server.handlers.accesslog.AccessLogHandler;
043import io.undertow.server.handlers.accesslog.AccessLogReceiver;
044import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
045import io.undertow.server.handlers.resource.FileResourceManager;
046import io.undertow.server.handlers.resource.Resource;
047import io.undertow.server.handlers.resource.ResourceChangeListener;
048import io.undertow.server.handlers.resource.ResourceManager;
049import io.undertow.server.handlers.resource.URLResource;
050import io.undertow.server.session.SessionManager;
051import io.undertow.servlet.Servlets;
052import io.undertow.servlet.api.DeploymentInfo;
053import io.undertow.servlet.api.DeploymentManager;
054import io.undertow.servlet.api.ListenerInfo;
055import io.undertow.servlet.api.MimeMapping;
056import io.undertow.servlet.api.ServletContainerInitializerInfo;
057import io.undertow.servlet.api.ServletStackTraces;
058import io.undertow.servlet.handlers.DefaultServlet;
059import io.undertow.servlet.util.ImmediateInstanceFactory;
060import org.xnio.OptionMap;
061import org.xnio.Options;
062import org.xnio.Xnio;
063import org.xnio.XnioWorker;
064
065import org.springframework.boot.web.server.ErrorPage;
066import org.springframework.boot.web.server.MimeMappings.Mapping;
067import org.springframework.boot.web.server.WebServer;
068import org.springframework.boot.web.servlet.ServletContextInitializer;
069import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
070import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
071import org.springframework.context.ResourceLoaderAware;
072import org.springframework.core.io.ResourceLoader;
073import org.springframework.util.Assert;
074
075/**
076 * {@link ServletWebServerFactory} that can be used to create
077 * {@link UndertowServletWebServer}s.
078 * <p>
079 * Unless explicitly configured otherwise, the factory will create servers that listen for
080 * HTTP requests on port 8080.
081 *
082 * @author Ivan Sopov
083 * @author Andy Wilkinson
084 * @author Marcos Barbero
085 * @author EddĂș MelĂ©ndez
086 * @since 2.0.0
087 * @see UndertowServletWebServer
088 */
089public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory
090                implements ConfigurableUndertowWebServerFactory, ResourceLoaderAware {
091
092        private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
093
094        private List<UndertowBuilderCustomizer> builderCustomizers = new ArrayList<>();
095
096        private List<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers = new ArrayList<>();
097
098        private ResourceLoader resourceLoader;
099
100        private Integer bufferSize;
101
102        private Integer ioThreads;
103
104        private Integer workerThreads;
105
106        private Boolean directBuffers;
107
108        private File accessLogDirectory;
109
110        private String accessLogPattern;
111
112        private String accessLogPrefix;
113
114        private String accessLogSuffix;
115
116        private boolean accessLogEnabled = false;
117
118        private boolean accessLogRotate = true;
119
120        private boolean useForwardHeaders;
121
122        private boolean eagerInitFilters = true;
123
124        /**
125         * Create a new {@link UndertowServletWebServerFactory} instance.
126         */
127        public UndertowServletWebServerFactory() {
128                getJsp().setRegistered(false);
129        }
130
131        /**
132         * Create a new {@link UndertowServletWebServerFactory} that listens for requests
133         * using the specified port.
134         * @param port the port to listen on
135         */
136        public UndertowServletWebServerFactory(int port) {
137                super(port);
138                getJsp().setRegistered(false);
139        }
140
141        /**
142         * Create a new {@link UndertowServletWebServerFactory} with the specified context
143         * path and port.
144         * @param contextPath the root context path
145         * @param port the port to listen on
146         */
147        public UndertowServletWebServerFactory(String contextPath, int port) {
148                super(contextPath, port);
149                getJsp().setRegistered(false);
150        }
151
152        /**
153         * Set {@link UndertowBuilderCustomizer}s that should be applied to the Undertow
154         * {@link Builder}. Calling this method will replace any existing customizers.
155         * @param customizers the customizers to set
156         */
157        public void setBuilderCustomizers(
158                        Collection<? extends UndertowBuilderCustomizer> customizers) {
159                Assert.notNull(customizers, "Customizers must not be null");
160                this.builderCustomizers = new ArrayList<>(customizers);
161        }
162
163        /**
164         * Returns a mutable collection of the {@link UndertowBuilderCustomizer}s that will be
165         * applied to the Undertow {@link Builder}.
166         * @return the customizers that will be applied
167         */
168        public Collection<UndertowBuilderCustomizer> getBuilderCustomizers() {
169                return this.builderCustomizers;
170        }
171
172        @Override
173        public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
174                Assert.notNull(customizers, "Customizers must not be null");
175                this.builderCustomizers.addAll(Arrays.asList(customizers));
176        }
177
178        /**
179         * Set {@link UndertowDeploymentInfoCustomizer}s that should be applied to the
180         * Undertow {@link DeploymentInfo}. Calling this method will replace any existing
181         * customizers.
182         * @param customizers the customizers to set
183         */
184        public void setDeploymentInfoCustomizers(
185                        Collection<? extends UndertowDeploymentInfoCustomizer> customizers) {
186                Assert.notNull(customizers, "Customizers must not be null");
187                this.deploymentInfoCustomizers = new ArrayList<>(customizers);
188        }
189
190        /**
191         * Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that
192         * will be applied to the Undertow {@link DeploymentInfo}.
193         * @return the customizers that will be applied
194         */
195        public Collection<UndertowDeploymentInfoCustomizer> getDeploymentInfoCustomizers() {
196                return this.deploymentInfoCustomizers;
197        }
198
199        @Override
200        public void addDeploymentInfoCustomizers(
201                        UndertowDeploymentInfoCustomizer... customizers) {
202                Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null");
203                this.deploymentInfoCustomizers.addAll(Arrays.asList(customizers));
204        }
205
206        @Override
207        public WebServer getWebServer(ServletContextInitializer... initializers) {
208                DeploymentManager manager = createDeploymentManager(initializers);
209                int port = getPort();
210                Builder builder = createBuilder(port);
211                return getUndertowWebServer(builder, manager, port);
212        }
213
214        private Builder createBuilder(int port) {
215                Builder builder = Undertow.builder();
216                if (this.bufferSize != null) {
217                        builder.setBufferSize(this.bufferSize);
218                }
219                if (this.ioThreads != null) {
220                        builder.setIoThreads(this.ioThreads);
221                }
222                if (this.workerThreads != null) {
223                        builder.setWorkerThreads(this.workerThreads);
224                }
225                if (this.directBuffers != null) {
226                        builder.setDirectBuffers(this.directBuffers);
227                }
228                if (getSsl() != null && getSsl().isEnabled()) {
229                        customizeSsl(builder);
230                }
231                else {
232                        builder.addHttpListener(port, getListenAddress());
233                }
234                for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
235                        customizer.customize(builder);
236                }
237                return builder;
238        }
239
240        private void customizeSsl(Builder builder) {
241                new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider())
242                                .customize(builder);
243                if (getHttp2() != null) {
244                        builder.setServerOption(UndertowOptions.ENABLE_HTTP2, getHttp2().isEnabled());
245                }
246        }
247
248        private String getListenAddress() {
249                if (getAddress() == null) {
250                        return "0.0.0.0";
251                }
252                return getAddress().getHostAddress();
253        }
254
255        private DeploymentManager createDeploymentManager(
256                        ServletContextInitializer... initializers) {
257                DeploymentInfo deployment = Servlets.deployment();
258                registerServletContainerInitializerToDriveServletContextInitializers(deployment,
259                                initializers);
260                deployment.setClassLoader(getServletClassLoader());
261                deployment.setContextPath(getContextPath());
262                deployment.setDisplayName(getDisplayName());
263                deployment.setDeploymentName("spring-boot");
264                if (isRegisterDefaultServlet()) {
265                        deployment.addServlet(Servlets.servlet("default", DefaultServlet.class));
266                }
267                configureErrorPages(deployment);
268                deployment.setServletStackTraces(ServletStackTraces.NONE);
269                deployment.setResourceManager(getDocumentRootResourceManager());
270                deployment.setEagerFilterInit(this.eagerInitFilters);
271                configureMimeMappings(deployment);
272                for (UndertowDeploymentInfoCustomizer customizer : this.deploymentInfoCustomizers) {
273                        customizer.customize(deployment);
274                }
275                if (isAccessLogEnabled()) {
276                        configureAccessLog(deployment);
277                }
278                if (getSession().isPersistent()) {
279                        File dir = getValidSessionStoreDir();
280                        deployment.setSessionPersistenceManager(new FileSessionPersistence(dir));
281                }
282                addLocaleMappings(deployment);
283                DeploymentManager manager = Servlets.newContainer().addDeployment(deployment);
284                manager.deploy();
285                SessionManager sessionManager = manager.getDeployment().getSessionManager();
286                Duration timeoutDuration = getSession().getTimeout();
287                int sessionTimeout = (isZeroOrLess(timeoutDuration) ? -1
288                                : (int) timeoutDuration.getSeconds());
289                sessionManager.setDefaultSessionTimeout(sessionTimeout);
290                return manager;
291        }
292
293        private boolean isZeroOrLess(Duration timeoutDuration) {
294                return timeoutDuration == null || timeoutDuration.isZero()
295                                || timeoutDuration.isNegative();
296        }
297
298        private void configureAccessLog(DeploymentInfo deploymentInfo) {
299                try {
300                        createAccessLogDirectoryIfNecessary();
301                        XnioWorker worker = createWorker();
302                        String prefix = (this.accessLogPrefix != null) ? this.accessLogPrefix
303                                        : "access_log.";
304                        DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(
305                                        worker, this.accessLogDirectory, prefix, this.accessLogSuffix,
306                                        this.accessLogRotate);
307                        EventListener listener = new AccessLogShutdownListener(worker,
308                                        accessLogReceiver);
309                        deploymentInfo.addListener(new ListenerInfo(AccessLogShutdownListener.class,
310                                        new ImmediateInstanceFactory<>(listener)));
311                        deploymentInfo.addInitialHandlerChainWrapper(
312                                        (handler) -> createAccessLogHandler(handler, accessLogReceiver));
313                }
314                catch (IOException ex) {
315                        throw new IllegalStateException("Failed to create AccessLogHandler", ex);
316                }
317        }
318
319        private AccessLogHandler createAccessLogHandler(HttpHandler handler,
320                        AccessLogReceiver accessLogReceiver) {
321                createAccessLogDirectoryIfNecessary();
322                String formatString = (this.accessLogPattern != null) ? this.accessLogPattern
323                                : "common";
324                return new AccessLogHandler(handler, accessLogReceiver, formatString,
325                                Undertow.class.getClassLoader());
326        }
327
328        private void createAccessLogDirectoryIfNecessary() {
329                Assert.state(this.accessLogDirectory != null, "Access log directory is not set");
330                if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) {
331                        throw new IllegalStateException("Failed to create access log directory '"
332                                        + this.accessLogDirectory + "'");
333                }
334        }
335
336        private XnioWorker createWorker() throws IOException {
337                Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());
338                return xnio.createWorker(
339                                OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap());
340        }
341
342        private void addLocaleMappings(DeploymentInfo deployment) {
343                getLocaleCharsetMappings().forEach((locale, charset) -> deployment
344                                .addLocaleCharsetMapping(locale.toString(), charset.toString()));
345        }
346
347        private void registerServletContainerInitializerToDriveServletContextInitializers(
348                        DeploymentInfo deployment, ServletContextInitializer... initializers) {
349                ServletContextInitializer[] mergedInitializers = mergeInitializers(initializers);
350                Initializer initializer = new Initializer(mergedInitializers);
351                deployment.addServletContainerInitializer(new ServletContainerInitializerInfo(
352                                Initializer.class,
353                                new ImmediateInstanceFactory<ServletContainerInitializer>(initializer),
354                                NO_CLASSES));
355        }
356
357        private ClassLoader getServletClassLoader() {
358                if (this.resourceLoader != null) {
359                        return this.resourceLoader.getClassLoader();
360                }
361                return getClass().getClassLoader();
362        }
363
364        private ResourceManager getDocumentRootResourceManager() {
365                File root = getValidDocumentRoot();
366                File docBase = getCanonicalDocumentRoot(root);
367                List<URL> metaInfResourceUrls = getUrlsOfJarsWithMetaInfResources();
368                List<URL> resourceJarUrls = new ArrayList<>();
369                List<ResourceManager> managers = new ArrayList<>();
370                ResourceManager rootManager = (docBase.isDirectory()
371                                ? new FileResourceManager(docBase, 0) : new JarResourceManager(docBase));
372                if (root != null) {
373                        rootManager = new LoaderHidingResourceManager(rootManager);
374                }
375                managers.add(rootManager);
376                for (URL url : metaInfResourceUrls) {
377                        if ("file".equals(url.getProtocol())) {
378                                try {
379                                        File file = new File(url.toURI());
380                                        if (file.isFile()) {
381                                                resourceJarUrls.add(new URL("jar:" + url + "!/"));
382                                        }
383                                        else {
384                                                managers.add(new FileResourceManager(
385                                                                new File(file, "META-INF/resources"), 0));
386                                        }
387                                }
388                                catch (Exception ex) {
389                                        throw new RuntimeException(ex);
390                                }
391                        }
392                        else {
393                                resourceJarUrls.add(url);
394                        }
395                }
396                managers.add(new MetaInfResourcesResourceManager(resourceJarUrls));
397                return new CompositeResourceManager(managers.toArray(new ResourceManager[0]));
398        }
399
400        private File getCanonicalDocumentRoot(File docBase) {
401                try {
402                        File root = (docBase != null) ? docBase : createTempDir("undertow-docbase");
403                        return root.getCanonicalFile();
404                }
405                catch (IOException ex) {
406                        throw new IllegalStateException("Cannot get canonical document root", ex);
407                }
408        }
409
410        private void configureErrorPages(DeploymentInfo servletBuilder) {
411                for (ErrorPage errorPage : getErrorPages()) {
412                        servletBuilder.addErrorPage(getUndertowErrorPage(errorPage));
413                }
414        }
415
416        private io.undertow.servlet.api.ErrorPage getUndertowErrorPage(ErrorPage errorPage) {
417                if (errorPage.getStatus() != null) {
418                        return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(),
419                                        errorPage.getStatusCode());
420                }
421                if (errorPage.getException() != null) {
422                        return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(),
423                                        errorPage.getException());
424                }
425                return new io.undertow.servlet.api.ErrorPage(errorPage.getPath());
426        }
427
428        private void configureMimeMappings(DeploymentInfo servletBuilder) {
429                for (Mapping mimeMapping : getMimeMappings()) {
430                        servletBuilder.addMimeMapping(new MimeMapping(mimeMapping.getExtension(),
431                                        mimeMapping.getMimeType()));
432                }
433        }
434
435        /**
436         * Factory method called to create the {@link UndertowServletWebServer}. Subclasses
437         * can override this method to return a different {@link UndertowServletWebServer} or
438         * apply additional processing to the {@link Builder} and {@link DeploymentManager}
439         * used to bootstrap Undertow
440         * @param builder the builder
441         * @param manager the deployment manager
442         * @param port the port that Undertow should listen on
443         * @return a new {@link UndertowServletWebServer} instance
444         */
445        protected UndertowServletWebServer getUndertowWebServer(Builder builder,
446                        DeploymentManager manager, int port) {
447                return new UndertowServletWebServer(builder, manager, getContextPath(),
448                                isUseForwardHeaders(), port >= 0, getCompression(), getServerHeader());
449        }
450
451        @Override
452        public void setResourceLoader(ResourceLoader resourceLoader) {
453                this.resourceLoader = resourceLoader;
454        }
455
456        @Override
457        public void setBufferSize(Integer bufferSize) {
458                this.bufferSize = bufferSize;
459        }
460
461        @Override
462        public void setIoThreads(Integer ioThreads) {
463                this.ioThreads = ioThreads;
464        }
465
466        @Override
467        public void setWorkerThreads(Integer workerThreads) {
468                this.workerThreads = workerThreads;
469        }
470
471        @Override
472        public void setUseDirectBuffers(Boolean directBuffers) {
473                this.directBuffers = directBuffers;
474        }
475
476        @Override
477        public void setAccessLogDirectory(File accessLogDirectory) {
478                this.accessLogDirectory = accessLogDirectory;
479        }
480
481        @Override
482        public void setAccessLogPattern(String accessLogPattern) {
483                this.accessLogPattern = accessLogPattern;
484        }
485
486        public String getAccessLogPrefix() {
487                return this.accessLogPrefix;
488        }
489
490        @Override
491        public void setAccessLogPrefix(String accessLogPrefix) {
492                this.accessLogPrefix = accessLogPrefix;
493        }
494
495        @Override
496        public void setAccessLogSuffix(String accessLogSuffix) {
497                this.accessLogSuffix = accessLogSuffix;
498        }
499
500        @Override
501        public void setAccessLogEnabled(boolean accessLogEnabled) {
502                this.accessLogEnabled = accessLogEnabled;
503        }
504
505        public boolean isAccessLogEnabled() {
506                return this.accessLogEnabled;
507        }
508
509        @Override
510        public void setAccessLogRotate(boolean accessLogRotate) {
511                this.accessLogRotate = accessLogRotate;
512        }
513
514        protected final boolean isUseForwardHeaders() {
515                return this.useForwardHeaders;
516        }
517
518        @Override
519        public void setUseForwardHeaders(boolean useForwardHeaders) {
520                this.useForwardHeaders = useForwardHeaders;
521        }
522
523        /**
524         * Return if filters should be initialized eagerly.
525         * @return {@code true} if filters are initialized eagerly, otherwise {@code false}.
526         * @since 2.0.0
527         */
528        public boolean isEagerInitFilters() {
529                return this.eagerInitFilters;
530        }
531
532        /**
533         * Set whether filters should be initialized eagerly.
534         * @param eagerInitFilters {@code true} if filters are initialized eagerly, otherwise
535         * {@code false}.
536         * @since 2.0.0
537         */
538        public void setEagerInitFilters(boolean eagerInitFilters) {
539                this.eagerInitFilters = eagerInitFilters;
540        }
541
542        /**
543         * {@link ResourceManager} that exposes resource in {@code META-INF/resources}
544         * directory of nested (in {@code BOOT-INF/lib} or {@code WEB-INF/lib}) jars.
545         */
546        private static final class MetaInfResourcesResourceManager
547                        implements ResourceManager {
548
549                private final List<URL> metaInfResourceJarUrls;
550
551                private MetaInfResourcesResourceManager(List<URL> metaInfResourceJarUrls) {
552                        this.metaInfResourceJarUrls = metaInfResourceJarUrls;
553                }
554
555                @Override
556                public void close() throws IOException {
557                }
558
559                @Override
560                public Resource getResource(String path) {
561                        for (URL url : this.metaInfResourceJarUrls) {
562                                URLResource resource = getMetaInfResource(url, path);
563                                if (resource != null) {
564                                        return resource;
565                                }
566                        }
567                        return null;
568                }
569
570                @Override
571                public boolean isResourceChangeListenerSupported() {
572                        return false;
573                }
574
575                @Override
576                public void registerResourceChangeListener(ResourceChangeListener listener) {
577                }
578
579                @Override
580                public void removeResourceChangeListener(ResourceChangeListener listener) {
581
582                }
583
584                private URLResource getMetaInfResource(URL resourceJar, String path) {
585                        try {
586                                URL resourceUrl = new URL(resourceJar + "META-INF/resources" + path);
587                                URLResource resource = new URLResource(resourceUrl, path);
588                                if (resource.getContentLength() < 0) {
589                                        return null;
590                                }
591                                return resource;
592                        }
593                        catch (MalformedURLException ex) {
594                                return null;
595                        }
596                }
597
598        }
599
600        /**
601         * {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer
602         * ServletContextInitializers}.
603         */
604        private static class Initializer implements ServletContainerInitializer {
605
606                private final ServletContextInitializer[] initializers;
607
608                Initializer(ServletContextInitializer[] initializers) {
609                        this.initializers = initializers;
610                }
611
612                @Override
613                public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
614                                throws ServletException {
615                        for (ServletContextInitializer initializer : this.initializers) {
616                                initializer.onStartup(servletContext);
617                        }
618                }
619
620        }
621
622        private static final class LoaderHidingResourceManager implements ResourceManager {
623
624                private final ResourceManager delegate;
625
626                private LoaderHidingResourceManager(ResourceManager delegate) {
627                        this.delegate = delegate;
628                }
629
630                @Override
631                public Resource getResource(String path) throws IOException {
632                        if (path.startsWith("/org/springframework/boot")) {
633                                return null;
634                        }
635                        return this.delegate.getResource(path);
636                }
637
638                @Override
639                public boolean isResourceChangeListenerSupported() {
640                        return this.delegate.isResourceChangeListenerSupported();
641                }
642
643                @Override
644                public void registerResourceChangeListener(ResourceChangeListener listener) {
645                        this.delegate.registerResourceChangeListener(listener);
646                }
647
648                @Override
649                public void removeResourceChangeListener(ResourceChangeListener listener) {
650                        this.delegate.removeResourceChangeListener(listener);
651                }
652
653                @Override
654                public void close() throws IOException {
655                        this.delegate.close();
656                }
657
658        }
659
660        private static class AccessLogShutdownListener implements ServletContextListener {
661
662                private final XnioWorker worker;
663
664                private final DefaultAccessLogReceiver accessLogReceiver;
665
666                AccessLogShutdownListener(XnioWorker worker,
667                                DefaultAccessLogReceiver accessLogReceiver) {
668                        this.worker = worker;
669                        this.accessLogReceiver = accessLogReceiver;
670                }
671
672                @Override
673                public void contextInitialized(ServletContextEvent sce) {
674                }
675
676                @Override
677                public void contextDestroyed(ServletContextEvent sce) {
678                        try {
679                                this.accessLogReceiver.close();
680                                this.worker.shutdown();
681                        }
682                        catch (IOException ex) {
683                                throw new IllegalStateException(ex);
684                        }
685                }
686
687        }
688
689}