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}