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.lang.reflect.Field; 020import java.net.BindException; 021import java.net.InetSocketAddress; 022import java.net.SocketAddress; 023import java.util.ArrayList; 024import java.util.List; 025 026import javax.servlet.ServletException; 027 028import io.undertow.Handlers; 029import io.undertow.Undertow; 030import io.undertow.Undertow.Builder; 031import io.undertow.server.HttpHandler; 032import io.undertow.servlet.api.DeploymentManager; 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.xnio.channels.BoundChannel; 036 037import org.springframework.boot.web.server.Compression; 038import org.springframework.boot.web.server.PortInUseException; 039import org.springframework.boot.web.server.WebServer; 040import org.springframework.boot.web.server.WebServerException; 041import org.springframework.util.ReflectionUtils; 042import org.springframework.util.StringUtils; 043 044/** 045 * {@link WebServer} that can be used to control an embedded Undertow server. Typically 046 * this class should be created using {@link UndertowServletWebServerFactory} and not 047 * directly. 048 * 049 * @author Ivan Sopov 050 * @author Andy Wilkinson 051 * @author EddĂș MelĂ©ndez 052 * @author Christoph Dreis 053 * @author Kristine Jetzke 054 * @since 2.0.0 055 * @see UndertowServletWebServerFactory 056 */ 057public class UndertowServletWebServer implements WebServer { 058 059 private static final Log logger = LogFactory.getLog(UndertowServletWebServer.class); 060 061 private final Object monitor = new Object(); 062 063 private final Builder builder; 064 065 private final DeploymentManager manager; 066 067 private final String contextPath; 068 069 private final boolean useForwardHeaders; 070 071 private final boolean autoStart; 072 073 private final Compression compression; 074 075 private final String serverHeader; 076 077 private Undertow undertow; 078 079 private volatile boolean started = false; 080 081 /** 082 * Create a new {@link UndertowServletWebServer} instance. 083 * @param builder the builder 084 * @param manager the deployment manager 085 * @param contextPath the root context path 086 * @param autoStart if the server should be started 087 * @param compression compression configuration 088 */ 089 public UndertowServletWebServer(Builder builder, DeploymentManager manager, 090 String contextPath, boolean autoStart, Compression compression) { 091 this(builder, manager, contextPath, false, autoStart, compression); 092 } 093 094 /** 095 * Create a new {@link UndertowServletWebServer} instance. 096 * @param builder the builder 097 * @param manager the deployment manager 098 * @param contextPath the root context path 099 * @param useForwardHeaders if x-forward headers should be used 100 * @param autoStart if the server should be started 101 * @param compression compression configuration 102 */ 103 public UndertowServletWebServer(Builder builder, DeploymentManager manager, 104 String contextPath, boolean useForwardHeaders, boolean autoStart, 105 Compression compression) { 106 this(builder, manager, contextPath, useForwardHeaders, autoStart, compression, 107 null); 108 } 109 110 /** 111 * Create a new {@link UndertowServletWebServer} instance. 112 * @param builder the builder 113 * @param manager the deployment manager 114 * @param contextPath the root context path 115 * @param useForwardHeaders if x-forward headers should be used 116 * @param autoStart if the server should be started 117 * @param compression compression configuration 118 * @param serverHeader string to be used in HTTP header 119 */ 120 public UndertowServletWebServer(Builder builder, DeploymentManager manager, 121 String contextPath, boolean useForwardHeaders, boolean autoStart, 122 Compression compression, String serverHeader) { 123 this.builder = builder; 124 this.manager = manager; 125 this.contextPath = contextPath; 126 this.useForwardHeaders = useForwardHeaders; 127 this.autoStart = autoStart; 128 this.compression = compression; 129 this.serverHeader = serverHeader; 130 } 131 132 @Override 133 public void start() throws WebServerException { 134 synchronized (this.monitor) { 135 if (this.started) { 136 return; 137 } 138 try { 139 if (!this.autoStart) { 140 return; 141 } 142 if (this.undertow == null) { 143 this.undertow = createUndertowServer(); 144 } 145 this.undertow.start(); 146 this.started = true; 147 UndertowServletWebServer.logger 148 .info("Undertow started on port(s) " + getPortsDescription() 149 + " with context path '" + this.contextPath + "'"); 150 } 151 catch (Exception ex) { 152 try { 153 if (findBindException(ex) != null) { 154 List<Port> failedPorts = getConfiguredPorts(); 155 List<Port> actualPorts = getActualPorts(); 156 failedPorts.removeAll(actualPorts); 157 if (failedPorts.size() == 1) { 158 throw new PortInUseException( 159 failedPorts.iterator().next().getNumber()); 160 } 161 } 162 throw new WebServerException("Unable to start embedded Undertow", ex); 163 } 164 finally { 165 stopSilently(); 166 } 167 } 168 } 169 } 170 171 public DeploymentManager getDeploymentManager() { 172 synchronized (this.monitor) { 173 return this.manager; 174 } 175 } 176 177 private void stopSilently() { 178 try { 179 if (this.undertow != null) { 180 this.undertow.stop(); 181 } 182 } 183 catch (Exception ex) { 184 // Ignore 185 } 186 } 187 188 private BindException findBindException(Exception ex) { 189 Throwable candidate = ex; 190 while (candidate != null) { 191 if (candidate instanceof BindException) { 192 return (BindException) candidate; 193 } 194 candidate = candidate.getCause(); 195 } 196 return null; 197 } 198 199 private Undertow createUndertowServer() throws ServletException { 200 HttpHandler httpHandler = this.manager.start(); 201 httpHandler = getContextHandler(httpHandler); 202 if (this.useForwardHeaders) { 203 httpHandler = Handlers.proxyPeerAddress(httpHandler); 204 } 205 if (StringUtils.hasText(this.serverHeader)) { 206 httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader); 207 } 208 this.builder.setHandler(httpHandler); 209 return this.builder.build(); 210 } 211 212 private HttpHandler getContextHandler(HttpHandler httpHandler) { 213 HttpHandler contextHandler = UndertowCompressionConfigurer 214 .configureCompression(this.compression, httpHandler); 215 if (StringUtils.isEmpty(this.contextPath)) { 216 return contextHandler; 217 } 218 return Handlers.path().addPrefixPath(this.contextPath, contextHandler); 219 } 220 221 private String getPortsDescription() { 222 List<Port> ports = getActualPorts(); 223 if (!ports.isEmpty()) { 224 return StringUtils.collectionToDelimitedString(ports, " "); 225 } 226 return "unknown"; 227 } 228 229 private List<Port> getActualPorts() { 230 List<Port> ports = new ArrayList<>(); 231 try { 232 if (!this.autoStart) { 233 ports.add(new Port(-1, "unknown")); 234 } 235 else { 236 for (BoundChannel channel : extractChannels()) { 237 ports.add(getPortFromChannel(channel)); 238 } 239 } 240 } 241 catch (Exception ex) { 242 // Continue 243 } 244 return ports; 245 } 246 247 @SuppressWarnings("unchecked") 248 private List<BoundChannel> extractChannels() { 249 Field channelsField = ReflectionUtils.findField(Undertow.class, "channels"); 250 ReflectionUtils.makeAccessible(channelsField); 251 return (List<BoundChannel>) ReflectionUtils.getField(channelsField, 252 this.undertow); 253 } 254 255 private Port getPortFromChannel(BoundChannel channel) { 256 SocketAddress socketAddress = channel.getLocalAddress(); 257 if (socketAddress instanceof InetSocketAddress) { 258 String protocol = (ReflectionUtils.findField(channel.getClass(), 259 "ssl") != null) ? "https" : "http"; 260 return new Port(((InetSocketAddress) socketAddress).getPort(), protocol); 261 } 262 return null; 263 } 264 265 private List<Port> getConfiguredPorts() { 266 List<Port> ports = new ArrayList<>(); 267 for (Object listener : extractListeners()) { 268 try { 269 Port port = getPortFromListener(listener); 270 if (port.getNumber() != 0) { 271 ports.add(port); 272 } 273 } 274 catch (Exception ex) { 275 // Continue 276 } 277 } 278 return ports; 279 } 280 281 @SuppressWarnings("unchecked") 282 private List<Object> extractListeners() { 283 Field listenersField = ReflectionUtils.findField(Undertow.class, "listeners"); 284 ReflectionUtils.makeAccessible(listenersField); 285 return (List<Object>) ReflectionUtils.getField(listenersField, this.undertow); 286 } 287 288 private Port getPortFromListener(Object listener) { 289 Field typeField = ReflectionUtils.findField(listener.getClass(), "type"); 290 ReflectionUtils.makeAccessible(typeField); 291 String protocol = ReflectionUtils.getField(typeField, listener).toString(); 292 Field portField = ReflectionUtils.findField(listener.getClass(), "port"); 293 ReflectionUtils.makeAccessible(portField); 294 int port = (Integer) ReflectionUtils.getField(portField, listener); 295 return new Port(port, protocol); 296 } 297 298 @Override 299 public void stop() throws WebServerException { 300 synchronized (this.monitor) { 301 if (!this.started) { 302 return; 303 } 304 this.started = false; 305 try { 306 this.manager.stop(); 307 this.manager.undeploy(); 308 this.undertow.stop(); 309 } 310 catch (Exception ex) { 311 throw new WebServerException("Unable to stop undertow", ex); 312 } 313 } 314 } 315 316 @Override 317 public int getPort() { 318 List<Port> ports = getActualPorts(); 319 if (ports.isEmpty()) { 320 return 0; 321 } 322 return ports.get(0).getNumber(); 323 } 324 325 /** 326 * An active Undertow port. 327 */ 328 private static final class Port { 329 330 private final int number; 331 332 private final String protocol; 333 334 private Port(int number, String protocol) { 335 this.number = number; 336 this.protocol = protocol; 337 } 338 339 public int getNumber() { 340 return this.number; 341 } 342 343 @Override 344 public boolean equals(Object obj) { 345 if (this == obj) { 346 return true; 347 } 348 if (obj == null) { 349 return false; 350 } 351 if (getClass() != obj.getClass()) { 352 return false; 353 } 354 Port other = (Port) obj; 355 if (this.number != other.number) { 356 return false; 357 } 358 return true; 359 } 360 361 @Override 362 public int hashCode() { 363 return this.number; 364 } 365 366 @Override 367 public String toString() { 368 return this.number + " (" + this.protocol + ")"; 369 } 370 371 } 372 373}