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}