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.jetty;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.InetSocketAddress;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.nio.channels.ReadableByteChannel;
026import java.time.Duration;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.List;
031
032import org.eclipse.jetty.http.MimeTypes;
033import org.eclipse.jetty.server.AbstractConnector;
034import org.eclipse.jetty.server.ConnectionFactory;
035import org.eclipse.jetty.server.Connector;
036import org.eclipse.jetty.server.Handler;
037import org.eclipse.jetty.server.HttpConfiguration;
038import org.eclipse.jetty.server.Server;
039import org.eclipse.jetty.server.ServerConnector;
040import org.eclipse.jetty.server.handler.ErrorHandler;
041import org.eclipse.jetty.server.handler.HandlerWrapper;
042import org.eclipse.jetty.server.session.DefaultSessionCache;
043import org.eclipse.jetty.server.session.FileSessionDataStore;
044import org.eclipse.jetty.server.session.SessionHandler;
045import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
046import org.eclipse.jetty.servlet.ServletHolder;
047import org.eclipse.jetty.servlet.ServletMapping;
048import org.eclipse.jetty.util.resource.JarResource;
049import org.eclipse.jetty.util.resource.Resource;
050import org.eclipse.jetty.util.resource.ResourceCollection;
051import org.eclipse.jetty.util.thread.ThreadPool;
052import org.eclipse.jetty.webapp.AbstractConfiguration;
053import org.eclipse.jetty.webapp.Configuration;
054import org.eclipse.jetty.webapp.WebAppContext;
055
056import org.springframework.boot.web.server.ErrorPage;
057import org.springframework.boot.web.server.MimeMappings;
058import org.springframework.boot.web.server.WebServer;
059import org.springframework.boot.web.servlet.ServletContextInitializer;
060import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
061import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
062import org.springframework.context.ResourceLoaderAware;
063import org.springframework.core.io.ResourceLoader;
064import org.springframework.util.Assert;
065import org.springframework.util.StringUtils;
066
067/**
068 * {@link ServletWebServerFactory} that can be used to create a {@link JettyWebServer}.
069 * Can be initialized using Spring's {@link ServletContextInitializer}s or Jetty
070 * {@link Configuration}s.
071 * <p>
072 * Unless explicitly configured otherwise this factory will create servers that listen for
073 * HTTP requests on port 8080.
074 *
075 * @author Phillip Webb
076 * @author Dave Syer
077 * @author Andrey Hihlovskiy
078 * @author Andy Wilkinson
079 * @author EddĂș MelĂ©ndez
080 * @author Venil Noronha
081 * @author Henri Kerola
082 * @since 2.0.0
083 * @see #setPort(int)
084 * @see #setConfigurations(Collection)
085 * @see JettyWebServer
086 */
087public class JettyServletWebServerFactory extends AbstractServletWebServerFactory
088                implements ConfigurableJettyWebServerFactory, ResourceLoaderAware {
089
090        private List<Configuration> configurations = new ArrayList<>();
091
092        private boolean useForwardHeaders;
093
094        /**
095         * The number of acceptor threads to use.
096         */
097        private int acceptors = -1;
098
099        /**
100         * The number of selector threads to use.
101         */
102        private int selectors = -1;
103
104        private List<JettyServerCustomizer> jettyServerCustomizers = new ArrayList<>();
105
106        private ResourceLoader resourceLoader;
107
108        private ThreadPool threadPool;
109
110        /**
111         * Create a new {@link JettyServletWebServerFactory} instance.
112         */
113        public JettyServletWebServerFactory() {
114        }
115
116        /**
117         * Create a new {@link JettyServletWebServerFactory} that listens for requests using
118         * the specified port.
119         * @param port the port to listen on
120         */
121        public JettyServletWebServerFactory(int port) {
122                super(port);
123        }
124
125        /**
126         * Create a new {@link JettyServletWebServerFactory} with the specified context path
127         * and port.
128         * @param contextPath the root context path
129         * @param port the port to listen on
130         */
131        public JettyServletWebServerFactory(String contextPath, int port) {
132                super(contextPath, port);
133        }
134
135        @Override
136        public WebServer getWebServer(ServletContextInitializer... initializers) {
137                JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext();
138                int port = (getPort() >= 0) ? getPort() : 0;
139                InetSocketAddress address = new InetSocketAddress(getAddress(), port);
140                Server server = createServer(address);
141                configureWebAppContext(context, initializers);
142                server.setHandler(addHandlerWrappers(context));
143                this.logger.info("Server initialized with port: " + port);
144                if (getSsl() != null && getSsl().isEnabled()) {
145                        customizeSsl(server, address);
146                }
147                for (JettyServerCustomizer customizer : getServerCustomizers()) {
148                        customizer.customize(server);
149                }
150                if (this.useForwardHeaders) {
151                        new ForwardHeadersCustomizer().customize(server);
152                }
153                return getJettyWebServer(server);
154        }
155
156        private Server createServer(InetSocketAddress address) {
157                Server server = new Server(getThreadPool());
158                server.setConnectors(new Connector[] { createConnector(address, server) });
159                return server;
160        }
161
162        private AbstractConnector createConnector(InetSocketAddress address, Server server) {
163                ServerConnector connector = new ServerConnector(server, this.acceptors,
164                                this.selectors);
165                connector.setHost(address.getHostString());
166                connector.setPort(address.getPort());
167                for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
168                        if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) {
169                                ((HttpConfiguration.ConnectionFactory) connectionFactory)
170                                                .getHttpConfiguration().setSendServerVersion(false);
171                        }
172                }
173                return connector;
174        }
175
176        private Handler addHandlerWrappers(Handler handler) {
177                if (getCompression() != null && getCompression().getEnabled()) {
178                        handler = applyWrapper(handler,
179                                        JettyHandlerWrappers.createGzipHandlerWrapper(getCompression()));
180                }
181                if (StringUtils.hasText(getServerHeader())) {
182                        handler = applyWrapper(handler, JettyHandlerWrappers
183                                        .createServerHeaderHandlerWrapper(getServerHeader()));
184                }
185                return handler;
186        }
187
188        private Handler applyWrapper(Handler handler, HandlerWrapper wrapper) {
189                wrapper.setHandler(handler);
190                return wrapper;
191        }
192
193        private void customizeSsl(Server server, InetSocketAddress address) {
194                new SslServerCustomizer(address, getSsl(), getSslStoreProvider(), getHttp2())
195                                .customize(server);
196        }
197
198        /**
199         * Configure the given Jetty {@link WebAppContext} for use.
200         * @param context the context to configure
201         * @param initializers the set of initializers to apply
202         */
203        protected final void configureWebAppContext(WebAppContext context,
204                        ServletContextInitializer... initializers) {
205                Assert.notNull(context, "Context must not be null");
206                context.setTempDirectory(getTempDirectory());
207                if (this.resourceLoader != null) {
208                        context.setClassLoader(this.resourceLoader.getClassLoader());
209                }
210                String contextPath = getContextPath();
211                context.setContextPath(StringUtils.hasLength(contextPath) ? contextPath : "/");
212                context.setDisplayName(getDisplayName());
213                configureDocumentRoot(context);
214                if (isRegisterDefaultServlet()) {
215                        addDefaultServlet(context);
216                }
217                if (shouldRegisterJspServlet()) {
218                        addJspServlet(context);
219                        context.addBean(new JasperInitializer(context), true);
220                }
221                addLocaleMappings(context);
222                ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
223                Configuration[] configurations = getWebAppContextConfigurations(context,
224                                initializersToUse);
225                context.setConfigurations(configurations);
226                context.setThrowUnavailableOnStartupException(true);
227                configureSession(context);
228                postProcessWebAppContext(context);
229        }
230
231        private void configureSession(WebAppContext context) {
232                SessionHandler handler = context.getSessionHandler();
233                Duration sessionTimeout = getSession().getTimeout();
234                handler.setMaxInactiveInterval(
235                                isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds());
236                if (getSession().isPersistent()) {
237                        DefaultSessionCache cache = new DefaultSessionCache(handler);
238                        FileSessionDataStore store = new FileSessionDataStore();
239                        store.setStoreDir(getValidSessionStoreDir());
240                        cache.setSessionDataStore(store);
241                        handler.setSessionCache(cache);
242                }
243        }
244
245        private boolean isNegative(Duration sessionTimeout) {
246                return sessionTimeout == null || sessionTimeout.isNegative();
247        }
248
249        private void addLocaleMappings(WebAppContext context) {
250                getLocaleCharsetMappings().forEach((locale, charset) -> context
251                                .addLocaleEncoding(locale.toString(), charset.toString()));
252        }
253
254        private File getTempDirectory() {
255                String temp = System.getProperty("java.io.tmpdir");
256                return (temp != null) ? new File(temp) : null;
257        }
258
259        private void configureDocumentRoot(WebAppContext handler) {
260                File root = getValidDocumentRoot();
261                File docBase = (root != null) ? root : createTempDir("jetty-docbase");
262                try {
263                        List<Resource> resources = new ArrayList<>();
264                        Resource rootResource = (docBase.isDirectory()
265                                        ? Resource.newResource(docBase.getCanonicalFile())
266                                        : JarResource.newJarResource(Resource.newResource(docBase)));
267                        resources.add((root != null) ? new LoaderHidingResource(rootResource)
268                                        : rootResource);
269                        for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) {
270                                Resource resource = createResource(resourceJarUrl);
271                                if (resource.exists() && resource.isDirectory()) {
272                                        resources.add(resource);
273                                }
274                        }
275                        handler.setBaseResource(
276                                        new ResourceCollection(resources.toArray(new Resource[0])));
277                }
278                catch (Exception ex) {
279                        throw new IllegalStateException(ex);
280                }
281        }
282
283        private Resource createResource(URL url) throws Exception {
284                if ("file".equals(url.getProtocol())) {
285                        File file = new File(url.toURI());
286                        if (file.isFile()) {
287                                return Resource.newResource("jar:" + url + "!/META-INF/resources");
288                        }
289                }
290                return Resource.newResource(url + "META-INF/resources");
291        }
292
293        /**
294         * Add Jetty's {@code DefaultServlet} to the given {@link WebAppContext}.
295         * @param context the jetty {@link WebAppContext}
296         */
297        protected final void addDefaultServlet(WebAppContext context) {
298                Assert.notNull(context, "Context must not be null");
299                ServletHolder holder = new ServletHolder();
300                holder.setName("default");
301                holder.setClassName("org.eclipse.jetty.servlet.DefaultServlet");
302                holder.setInitParameter("dirAllowed", "false");
303                holder.setInitOrder(1);
304                context.getServletHandler().addServletWithMapping(holder, "/");
305                context.getServletHandler().getServletMapping("/").setDefault(true);
306        }
307
308        /**
309         * Add Jetty's {@code JspServlet} to the given {@link WebAppContext}.
310         * @param context the jetty {@link WebAppContext}
311         */
312        protected final void addJspServlet(WebAppContext context) {
313                Assert.notNull(context, "Context must not be null");
314                ServletHolder holder = new ServletHolder();
315                holder.setName("jsp");
316                holder.setClassName(getJsp().getClassName());
317                holder.setInitParameter("fork", "false");
318                holder.setInitParameters(getJsp().getInitParameters());
319                holder.setInitOrder(3);
320                context.getServletHandler().addServlet(holder);
321                ServletMapping mapping = new ServletMapping();
322                mapping.setServletName("jsp");
323                mapping.setPathSpecs(new String[] { "*.jsp", "*.jspx" });
324                context.getServletHandler().addServletMapping(mapping);
325        }
326
327        /**
328         * Return the Jetty {@link Configuration}s that should be applied to the server.
329         * @param webAppContext the Jetty {@link WebAppContext}
330         * @param initializers the {@link ServletContextInitializer}s to apply
331         * @return configurations to apply
332         */
333        protected Configuration[] getWebAppContextConfigurations(WebAppContext webAppContext,
334                        ServletContextInitializer... initializers) {
335                List<Configuration> configurations = new ArrayList<>();
336                configurations.add(
337                                getServletContextInitializerConfiguration(webAppContext, initializers));
338                configurations.addAll(getConfigurations());
339                configurations.add(getErrorPageConfiguration());
340                configurations.add(getMimeTypeConfiguration());
341                return configurations.toArray(new Configuration[0]);
342        }
343
344        /**
345         * Create a configuration object that adds error handlers.
346         * @return a configuration object for adding error pages
347         */
348        private Configuration getErrorPageConfiguration() {
349                return new AbstractConfiguration() {
350
351                        @Override
352                        public void configure(WebAppContext context) throws Exception {
353                                ErrorHandler errorHandler = context.getErrorHandler();
354                                context.setErrorHandler(new JettyEmbeddedErrorHandler(errorHandler));
355                                addJettyErrorPages(errorHandler, getErrorPages());
356                        }
357
358                };
359        }
360
361        /**
362         * Create a configuration object that adds mime type mappings.
363         * @return a configuration object for adding mime type mappings
364         */
365        private Configuration getMimeTypeConfiguration() {
366                return new AbstractConfiguration() {
367
368                        @Override
369                        public void configure(WebAppContext context) throws Exception {
370                                MimeTypes mimeTypes = context.getMimeTypes();
371                                for (MimeMappings.Mapping mapping : getMimeMappings()) {
372                                        mimeTypes.addMimeMapping(mapping.getExtension(),
373                                                        mapping.getMimeType());
374                                }
375                        }
376
377                };
378        }
379
380        /**
381         * Return a Jetty {@link Configuration} that will invoke the specified
382         * {@link ServletContextInitializer}s. By default this method will return a
383         * {@link ServletContextInitializerConfiguration}.
384         * @param webAppContext the Jetty {@link WebAppContext}
385         * @param initializers the {@link ServletContextInitializer}s to apply
386         * @return the {@link Configuration} instance
387         */
388        protected Configuration getServletContextInitializerConfiguration(
389                        WebAppContext webAppContext, ServletContextInitializer... initializers) {
390                return new ServletContextInitializerConfiguration(initializers);
391        }
392
393        /**
394         * Post process the Jetty {@link WebAppContext} before it's used with the Jetty
395         * Server. Subclasses can override this method to apply additional processing to the
396         * {@link WebAppContext}.
397         * @param webAppContext the Jetty {@link WebAppContext}
398         */
399        protected void postProcessWebAppContext(WebAppContext webAppContext) {
400        }
401
402        /**
403         * Factory method called to create the {@link JettyWebServer}. Subclasses can override
404         * this method to return a different {@link JettyWebServer} or apply additional
405         * processing to the Jetty server.
406         * @param server the Jetty server.
407         * @return a new {@link JettyWebServer} instance
408         */
409        protected JettyWebServer getJettyWebServer(Server server) {
410                return new JettyWebServer(server, getPort() >= 0);
411        }
412
413        @Override
414        public void setResourceLoader(ResourceLoader resourceLoader) {
415                this.resourceLoader = resourceLoader;
416        }
417
418        @Override
419        public void setUseForwardHeaders(boolean useForwardHeaders) {
420                this.useForwardHeaders = useForwardHeaders;
421        }
422
423        @Override
424        public void setAcceptors(int acceptors) {
425                this.acceptors = acceptors;
426        }
427
428        @Override
429        public void setSelectors(int selectors) {
430                this.selectors = selectors;
431        }
432
433        /**
434         * Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server}
435         * before it is started. Calling this method will replace any existing customizers.
436         * @param customizers the Jetty customizers to apply
437         */
438        public void setServerCustomizers(
439                        Collection<? extends JettyServerCustomizer> customizers) {
440                Assert.notNull(customizers, "Customizers must not be null");
441                this.jettyServerCustomizers = new ArrayList<>(customizers);
442        }
443
444        /**
445         * Returns a mutable collection of Jetty {@link JettyServerCustomizer}s that will be
446         * applied to the {@link Server} before the it is created.
447         * @return the {@link JettyServerCustomizer}s
448         */
449        public Collection<JettyServerCustomizer> getServerCustomizers() {
450                return this.jettyServerCustomizers;
451        }
452
453        @Override
454        public void addServerCustomizers(JettyServerCustomizer... customizers) {
455                Assert.notNull(customizers, "Customizers must not be null");
456                this.jettyServerCustomizers.addAll(Arrays.asList(customizers));
457        }
458
459        /**
460         * Sets Jetty {@link Configuration}s that will be applied to the {@link WebAppContext}
461         * before the server is created. Calling this method will replace any existing
462         * configurations.
463         * @param configurations the Jetty configurations to apply
464         */
465        public void setConfigurations(Collection<? extends Configuration> configurations) {
466                Assert.notNull(configurations, "Configurations must not be null");
467                this.configurations = new ArrayList<>(configurations);
468        }
469
470        /**
471         * Returns a mutable collection of Jetty {@link Configuration}s that will be applied
472         * to the {@link WebAppContext} before the server is created.
473         * @return the Jetty {@link Configuration}s
474         */
475        public Collection<Configuration> getConfigurations() {
476                return this.configurations;
477        }
478
479        /**
480         * Add {@link Configuration}s that will be applied to the {@link WebAppContext} before
481         * the server is started.
482         * @param configurations the configurations to add
483         */
484        public void addConfigurations(Configuration... configurations) {
485                Assert.notNull(configurations, "Configurations must not be null");
486                this.configurations.addAll(Arrays.asList(configurations));
487        }
488
489        /**
490         * Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}.
491         * @return a Jetty {@link ThreadPool} or {@code null}
492         */
493        public ThreadPool getThreadPool() {
494                return this.threadPool;
495        }
496
497        /**
498         * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to
499         * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly.
500         * @param threadPool a Jetty ThreadPool to be used
501         */
502        public void setThreadPool(ThreadPool threadPool) {
503                this.threadPool = threadPool;
504        }
505
506        private void addJettyErrorPages(ErrorHandler errorHandler,
507                        Collection<ErrorPage> errorPages) {
508                if (errorHandler instanceof ErrorPageErrorHandler) {
509                        ErrorPageErrorHandler handler = (ErrorPageErrorHandler) errorHandler;
510                        for (ErrorPage errorPage : errorPages) {
511                                if (errorPage.isGlobal()) {
512                                        handler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE,
513                                                        errorPage.getPath());
514                                }
515                                else {
516                                        if (errorPage.getExceptionName() != null) {
517                                                handler.addErrorPage(errorPage.getExceptionName(),
518                                                                errorPage.getPath());
519                                        }
520                                        else {
521                                                handler.addErrorPage(errorPage.getStatusCode(),
522                                                                errorPage.getPath());
523                                        }
524                                }
525                        }
526                }
527        }
528
529        private static final class LoaderHidingResource extends Resource {
530
531                private final Resource delegate;
532
533                private LoaderHidingResource(Resource delegate) {
534                        this.delegate = delegate;
535                }
536
537                @Override
538                public Resource addPath(String path) throws IOException, MalformedURLException {
539                        if (path.startsWith("/org/springframework/boot")) {
540                                return null;
541                        }
542                        return this.delegate.addPath(path);
543                }
544
545                @Override
546                public boolean isContainedIn(Resource resource) throws MalformedURLException {
547                        return this.delegate.isContainedIn(resource);
548                }
549
550                @Override
551                public void close() {
552                        this.delegate.close();
553                }
554
555                @Override
556                public boolean exists() {
557                        return this.delegate.exists();
558                }
559
560                @Override
561                public boolean isDirectory() {
562                        return this.delegate.isDirectory();
563                }
564
565                @Override
566                public long lastModified() {
567                        return this.delegate.lastModified();
568                }
569
570                @Override
571                public long length() {
572                        return this.delegate.length();
573                }
574
575                @Override
576                @Deprecated
577                public URL getURL() {
578                        return this.delegate.getURL();
579                }
580
581                @Override
582                public File getFile() throws IOException {
583                        return this.delegate.getFile();
584                }
585
586                @Override
587                public String getName() {
588                        return this.delegate.getName();
589                }
590
591                @Override
592                public InputStream getInputStream() throws IOException {
593                        return this.delegate.getInputStream();
594                }
595
596                @Override
597                public ReadableByteChannel getReadableByteChannel() throws IOException {
598                        return this.delegate.getReadableByteChannel();
599                }
600
601                @Override
602                public boolean delete() throws SecurityException {
603                        return this.delegate.delete();
604                }
605
606                @Override
607                public boolean renameTo(Resource dest) throws SecurityException {
608                        return this.delegate.renameTo(dest);
609                }
610
611                @Override
612                public String[] list() {
613                        return this.delegate.list();
614                }
615
616        }
617
618}