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.io.File;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.List;
026
027import io.undertow.Handlers;
028import io.undertow.Undertow;
029import io.undertow.UndertowOptions;
030import io.undertow.server.HttpHandler;
031import io.undertow.server.handlers.accesslog.AccessLogHandler;
032import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
033import io.undertow.servlet.api.DeploymentInfo;
034import org.xnio.OptionMap;
035import org.xnio.Options;
036import org.xnio.Xnio;
037import org.xnio.XnioWorker;
038
039import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
040import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
041import org.springframework.boot.web.server.WebServer;
042import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter;
043import org.springframework.util.Assert;
044
045/**
046 * {@link ReactiveWebServerFactory} that can be used to create {@link UndertowWebServer}s.
047 *
048 * @author Brian Clozel
049 * @since 2.0.0
050 */
051public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory
052                implements ConfigurableUndertowWebServerFactory {
053
054        private List<UndertowBuilderCustomizer> builderCustomizers = new ArrayList<>();
055
056        private List<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers = new ArrayList<>();
057
058        private Integer bufferSize;
059
060        private Integer ioThreads;
061
062        private Integer workerThreads;
063
064        private Boolean directBuffers;
065
066        private File accessLogDirectory;
067
068        private String accessLogPattern;
069
070        private String accessLogPrefix;
071
072        private String accessLogSuffix;
073
074        private boolean accessLogEnabled = false;
075
076        private boolean accessLogRotate = true;
077
078        private boolean useForwardHeaders;
079
080        /**
081         * Create a new {@link UndertowReactiveWebServerFactory} instance.
082         */
083        public UndertowReactiveWebServerFactory() {
084        }
085
086        /**
087         * Create a new {@link UndertowReactiveWebServerFactory} that listens for requests
088         * using the specified port.
089         * @param port the port to listen on
090         */
091        public UndertowReactiveWebServerFactory(int port) {
092                super(port);
093        }
094
095        @Override
096        public WebServer getWebServer(
097                        org.springframework.http.server.reactive.HttpHandler httpHandler) {
098                Undertow.Builder builder = createBuilder(getPort());
099                Closeable closeable = configureHandler(builder, httpHandler);
100                return new UndertowWebServer(builder, getPort() >= 0, closeable);
101        }
102
103        private Undertow.Builder createBuilder(int port) {
104                Undertow.Builder builder = Undertow.builder();
105                if (this.bufferSize != null) {
106                        builder.setBufferSize(this.bufferSize);
107                }
108                if (this.ioThreads != null) {
109                        builder.setIoThreads(this.ioThreads);
110                }
111                if (this.workerThreads != null) {
112                        builder.setWorkerThreads(this.workerThreads);
113                }
114                if (this.directBuffers != null) {
115                        builder.setDirectBuffers(this.directBuffers);
116                }
117                if (getSsl() != null && getSsl().isEnabled()) {
118                        customizeSsl(builder);
119                }
120                else {
121                        builder.addHttpListener(port, getListenAddress());
122                }
123                for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
124                        customizer.customize(builder);
125                }
126                return builder;
127        }
128
129        private Closeable configureHandler(Undertow.Builder builder,
130                        org.springframework.http.server.reactive.HttpHandler httpHandler) {
131                HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
132                if (this.useForwardHeaders) {
133                        handler = Handlers.proxyPeerAddress(handler);
134                }
135                handler = UndertowCompressionConfigurer.configureCompression(getCompression(),
136                                handler);
137                Closeable closeable = null;
138                if (isAccessLogEnabled()) {
139                        closeable = configureAccessLogHandler(builder, handler);
140                }
141                else {
142                        builder.setHandler(handler);
143                }
144                return closeable;
145        }
146
147        private Closeable configureAccessLogHandler(Undertow.Builder builder,
148                        HttpHandler handler) {
149                try {
150                        createAccessLogDirectoryIfNecessary();
151                        XnioWorker worker = createWorker();
152                        String prefix = (this.accessLogPrefix != null) ? this.accessLogPrefix
153                                        : "access_log.";
154                        DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(
155                                        worker, this.accessLogDirectory, prefix, this.accessLogSuffix,
156                                        this.accessLogRotate);
157                        String formatString = ((this.accessLogPattern != null) ? this.accessLogPattern
158                                        : "common");
159                        builder.setHandler(new AccessLogHandler(handler, accessLogReceiver,
160                                        formatString, Undertow.class.getClassLoader()));
161                        return () -> {
162                                try {
163                                        accessLogReceiver.close();
164                                        worker.shutdown();
165                                }
166                                catch (IOException ex) {
167                                        throw new IllegalStateException(ex);
168                                }
169                        };
170                }
171                catch (IOException ex) {
172                        throw new IllegalStateException("Failed to create AccessLogHandler", ex);
173                }
174        }
175
176        private void createAccessLogDirectoryIfNecessary() {
177                Assert.state(this.accessLogDirectory != null, "Access log directory is not set");
178                if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) {
179                        throw new IllegalStateException("Failed to create access log directory '"
180                                        + this.accessLogDirectory + "'");
181                }
182        }
183
184        private XnioWorker createWorker() throws IOException {
185                Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());
186                return xnio.createWorker(
187                                OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap());
188        }
189
190        private void customizeSsl(Undertow.Builder builder) {
191                new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider())
192                                .customize(builder);
193                if (getHttp2() != null) {
194                        builder.setServerOption(UndertowOptions.ENABLE_HTTP2, getHttp2().isEnabled());
195                }
196        }
197
198        private String getListenAddress() {
199                if (getAddress() == null) {
200                        return "0.0.0.0";
201                }
202                return getAddress().getHostAddress();
203        }
204
205        /**
206         * Set {@link UndertowDeploymentInfoCustomizer}s that should be applied to the
207         * Undertow {@link DeploymentInfo}. Calling this method will replace any existing
208         * customizers.
209         * @param customizers the customizers to set
210         */
211        public void setDeploymentInfoCustomizers(
212                        Collection<? extends UndertowDeploymentInfoCustomizer> customizers) {
213                Assert.notNull(customizers, "Customizers must not be null");
214                this.deploymentInfoCustomizers = new ArrayList<>(customizers);
215        }
216
217        /**
218         * Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that
219         * will be applied to the Undertow {@link DeploymentInfo}.
220         * @return the customizers that will be applied
221         */
222        public Collection<UndertowDeploymentInfoCustomizer> getDeploymentInfoCustomizers() {
223                return this.deploymentInfoCustomizers;
224        }
225
226        @Override
227        public void addDeploymentInfoCustomizers(
228                        UndertowDeploymentInfoCustomizer... customizers) {
229                Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null");
230                this.deploymentInfoCustomizers.addAll(Arrays.asList(customizers));
231        }
232
233        @Override
234        public void setAccessLogDirectory(File accessLogDirectory) {
235                this.accessLogDirectory = accessLogDirectory;
236        }
237
238        @Override
239        public void setAccessLogPattern(String accessLogPattern) {
240                this.accessLogPattern = accessLogPattern;
241        }
242
243        @Override
244        public void setAccessLogPrefix(String accessLogPrefix) {
245                this.accessLogPrefix = accessLogPrefix;
246        }
247
248        @Override
249        public void setAccessLogSuffix(String accessLogSuffix) {
250                this.accessLogSuffix = accessLogSuffix;
251        }
252
253        public boolean isAccessLogEnabled() {
254                return this.accessLogEnabled;
255        }
256
257        @Override
258        public void setAccessLogEnabled(boolean accessLogEnabled) {
259                this.accessLogEnabled = accessLogEnabled;
260        }
261
262        @Override
263        public void setAccessLogRotate(boolean accessLogRotate) {
264                this.accessLogRotate = accessLogRotate;
265        }
266
267        protected final boolean isUseForwardHeaders() {
268                return this.useForwardHeaders;
269        }
270
271        @Override
272        public void setUseForwardHeaders(boolean useForwardHeaders) {
273                this.useForwardHeaders = useForwardHeaders;
274        }
275
276        @Override
277        public void setBufferSize(Integer bufferSize) {
278                this.bufferSize = bufferSize;
279        }
280
281        @Override
282        public void setIoThreads(Integer ioThreads) {
283                this.ioThreads = ioThreads;
284        }
285
286        @Override
287        public void setWorkerThreads(Integer workerThreads) {
288                this.workerThreads = workerThreads;
289        }
290
291        @Override
292        public void setUseDirectBuffers(Boolean directBuffers) {
293                this.directBuffers = directBuffers;
294        }
295
296        /**
297         * Set {@link UndertowBuilderCustomizer}s that should be applied to the Undertow
298         * {@link io.undertow.Undertow.Builder Builder}. Calling this method will replace any
299         * existing customizers.
300         * @param customizers the customizers to set
301         */
302        public void setBuilderCustomizers(
303                        Collection<? extends UndertowBuilderCustomizer> customizers) {
304                Assert.notNull(customizers, "Customizers must not be null");
305                this.builderCustomizers = new ArrayList<>(customizers);
306        }
307
308        /**
309         * Returns a mutable collection of the {@link UndertowBuilderCustomizer}s that will be
310         * applied to the Undertow {@link io.undertow.Undertow.Builder Builder}.
311         * @return the customizers that will be applied
312         */
313        public Collection<UndertowBuilderCustomizer> getBuilderCustomizers() {
314                return this.builderCustomizers;
315        }
316
317        /**
318         * Add {@link UndertowBuilderCustomizer}s that should be used to customize the
319         * Undertow {@link io.undertow.Undertow.Builder Builder}.
320         * @param customizers the customizers to add
321         */
322        @Override
323        public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
324                Assert.notNull(customizers, "Customizers must not be null");
325                this.builderCustomizers.addAll(Arrays.asList(customizers));
326        }
327
328}