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.Closeable;
020import java.lang.reflect.Field;
021import java.net.BindException;
022import java.net.InetSocketAddress;
023import java.net.SocketAddress;
024import java.util.ArrayList;
025import java.util.List;
026
027import io.undertow.Undertow;
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.xnio.channels.BoundChannel;
031
032import org.springframework.boot.web.server.PortInUseException;
033import org.springframework.boot.web.server.WebServer;
034import org.springframework.boot.web.server.WebServerException;
035import org.springframework.util.ReflectionUtils;
036import org.springframework.util.StringUtils;
037
038/**
039 * {@link WebServer} that can be used to control an Undertow web server. Usually this
040 * class should be created using the {@link UndertowReactiveWebServerFactory} and not
041 * directly.
042 *
043 * @author Ivan Sopov
044 * @author Andy Wilkinson
045 * @author EddĂș MelĂ©ndez
046 * @author Christoph Dreis
047 * @author Brian Clozel
048 * @since 2.0.0
049 */
050public class UndertowWebServer implements WebServer {
051
052        private static final Log logger = LogFactory.getLog(UndertowServletWebServer.class);
053
054        private final Object monitor = new Object();
055
056        private final Undertow.Builder builder;
057
058        private final boolean autoStart;
059
060        private final Closeable closeable;
061
062        private Undertow undertow;
063
064        private volatile boolean started = false;
065
066        /**
067         * Create a new {@link UndertowWebServer} instance.
068         * @param builder the builder
069         * @param autoStart if the server should be started
070         */
071        public UndertowWebServer(Undertow.Builder builder, boolean autoStart) {
072                this(builder, autoStart, null);
073        }
074
075        /**
076         * Create a new {@link UndertowWebServer} instance.
077         * @param builder the builder
078         * @param autoStart if the server should be started
079         * @param closeable called when the server is stopped
080         * @since 2.0.4
081         */
082        public UndertowWebServer(Undertow.Builder builder, boolean autoStart,
083                        Closeable closeable) {
084                this.builder = builder;
085                this.autoStart = autoStart;
086                this.closeable = closeable;
087        }
088
089        @Override
090        public void start() throws WebServerException {
091                synchronized (this.monitor) {
092                        if (this.started) {
093                                return;
094                        }
095                        try {
096                                if (!this.autoStart) {
097                                        return;
098                                }
099                                if (this.undertow == null) {
100                                        this.undertow = this.builder.build();
101                                }
102                                this.undertow.start();
103                                this.started = true;
104                                logger.info("Undertow started on port(s) " + getPortsDescription());
105                        }
106                        catch (Exception ex) {
107                                try {
108                                        if (findBindException(ex) != null) {
109                                                List<UndertowWebServer.Port> failedPorts = getConfiguredPorts();
110                                                List<UndertowWebServer.Port> actualPorts = getActualPorts();
111                                                failedPorts.removeAll(actualPorts);
112                                                if (failedPorts.size() == 1) {
113                                                        throw new PortInUseException(
114                                                                        failedPorts.iterator().next().getNumber());
115                                                }
116                                        }
117                                        throw new WebServerException("Unable to start embedded Undertow", ex);
118                                }
119                                finally {
120                                        stopSilently();
121                                }
122                        }
123                }
124        }
125
126        private void stopSilently() {
127                try {
128                        if (this.undertow != null) {
129                                this.undertow.stop();
130                                this.closeable.close();
131                        }
132                }
133                catch (Exception ex) {
134                        // Ignore
135                }
136        }
137
138        private BindException findBindException(Exception ex) {
139                Throwable candidate = ex;
140                while (candidate != null) {
141                        if (candidate instanceof BindException) {
142                                return (BindException) candidate;
143                        }
144                        candidate = candidate.getCause();
145                }
146                return null;
147        }
148
149        private String getPortsDescription() {
150                List<UndertowWebServer.Port> ports = getActualPorts();
151                if (!ports.isEmpty()) {
152                        return StringUtils.collectionToDelimitedString(ports, " ");
153                }
154                return "unknown";
155        }
156
157        private List<UndertowWebServer.Port> getActualPorts() {
158                List<UndertowWebServer.Port> ports = new ArrayList<>();
159                try {
160                        if (!this.autoStart) {
161                                ports.add(new UndertowWebServer.Port(-1, "unknown"));
162                        }
163                        else {
164                                for (BoundChannel channel : extractChannels()) {
165                                        ports.add(getPortFromChannel(channel));
166                                }
167                        }
168                }
169                catch (Exception ex) {
170                        // Continue
171                }
172                return ports;
173        }
174
175        @SuppressWarnings("unchecked")
176        private List<BoundChannel> extractChannels() {
177                Field channelsField = ReflectionUtils.findField(Undertow.class, "channels");
178                ReflectionUtils.makeAccessible(channelsField);
179                return (List<BoundChannel>) ReflectionUtils.getField(channelsField,
180                                this.undertow);
181        }
182
183        private UndertowWebServer.Port getPortFromChannel(BoundChannel channel) {
184                SocketAddress socketAddress = channel.getLocalAddress();
185                if (socketAddress instanceof InetSocketAddress) {
186                        Field sslField = ReflectionUtils.findField(channel.getClass(), "ssl");
187                        String protocol = (sslField != null) ? "https" : "http";
188                        return new UndertowWebServer.Port(
189                                        ((InetSocketAddress) socketAddress).getPort(), protocol);
190                }
191                return null;
192        }
193
194        private List<UndertowWebServer.Port> getConfiguredPorts() {
195                List<UndertowWebServer.Port> ports = new ArrayList<>();
196                for (Object listener : extractListeners()) {
197                        try {
198                                ports.add(getPortFromListener(listener));
199                        }
200                        catch (Exception ex) {
201                                // Continue
202                        }
203                }
204                return ports;
205        }
206
207        @SuppressWarnings("unchecked")
208        private List<Object> extractListeners() {
209                Field listenersField = ReflectionUtils.findField(Undertow.class, "listeners");
210                ReflectionUtils.makeAccessible(listenersField);
211                return (List<Object>) ReflectionUtils.getField(listenersField, this.undertow);
212        }
213
214        private UndertowWebServer.Port getPortFromListener(Object listener) {
215                Field typeField = ReflectionUtils.findField(listener.getClass(), "type");
216                ReflectionUtils.makeAccessible(typeField);
217                String protocol = ReflectionUtils.getField(typeField, listener).toString();
218                Field portField = ReflectionUtils.findField(listener.getClass(), "port");
219                ReflectionUtils.makeAccessible(portField);
220                int port = (Integer) ReflectionUtils.getField(portField, listener);
221                return new UndertowWebServer.Port(port, protocol);
222        }
223
224        @Override
225        public void stop() throws WebServerException {
226                synchronized (this.monitor) {
227                        if (!this.started) {
228                                return;
229                        }
230                        this.started = false;
231                        try {
232                                this.undertow.stop();
233                                if (this.closeable != null) {
234                                        this.closeable.close();
235                                }
236                        }
237                        catch (Exception ex) {
238                                throw new WebServerException("Unable to stop undertow", ex);
239                        }
240                }
241        }
242
243        @Override
244        public int getPort() {
245                List<UndertowWebServer.Port> ports = getActualPorts();
246                if (ports.isEmpty()) {
247                        return 0;
248                }
249                return ports.get(0).getNumber();
250        }
251
252        /**
253         * An active Undertow port.
254         */
255        private static final class Port {
256
257                private final int number;
258
259                private final String protocol;
260
261                private Port(int number, String protocol) {
262                        this.number = number;
263                        this.protocol = protocol;
264                }
265
266                public int getNumber() {
267                        return this.number;
268                }
269
270                @Override
271                public boolean equals(Object obj) {
272                        if (this == obj) {
273                                return true;
274                        }
275                        if (obj == null) {
276                                return false;
277                        }
278                        if (getClass() != obj.getClass()) {
279                                return false;
280                        }
281                        UndertowWebServer.Port other = (UndertowWebServer.Port) obj;
282                        if (this.number != other.number) {
283                                return false;
284                        }
285                        return true;
286                }
287
288                @Override
289                public int hashCode() {
290                        return this.number;
291                }
292
293                @Override
294                public String toString() {
295                        return this.number + " (" + this.protocol + ")";
296                }
297
298        }
299
300}