001/*
002 * Copyright 2002-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 *      https://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.web.socket.config.annotation;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022
023import org.springframework.lang.Nullable;
024import org.springframework.scheduling.TaskScheduler;
025import org.springframework.util.Assert;
026import org.springframework.util.ObjectUtils;
027import org.springframework.web.socket.server.HandshakeInterceptor;
028import org.springframework.web.socket.sockjs.SockJsService;
029import org.springframework.web.socket.sockjs.frame.SockJsMessageCodec;
030import org.springframework.web.socket.sockjs.transport.TransportHandler;
031import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService;
032import org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService;
033
034/**
035 * A helper class for configuring SockJS fallback options for use with an
036 * {@link org.springframework.web.socket.config.annotation.EnableWebSocket} and
037 * {@link WebSocketConfigurer} setup.
038 *
039 * @author Rossen Stoyanchev
040 * @since 4.0
041 */
042public class SockJsServiceRegistration {
043
044        @Nullable
045        private TaskScheduler scheduler;
046
047        @Nullable
048        private String clientLibraryUrl;
049
050        @Nullable
051        private Integer streamBytesLimit;
052
053        @Nullable
054        private Boolean sessionCookieNeeded;
055
056        @Nullable
057        private Long heartbeatTime;
058
059        @Nullable
060        private Long disconnectDelay;
061
062        @Nullable
063        private Integer httpMessageCacheSize;
064
065        @Nullable
066        private Boolean webSocketEnabled;
067
068        private final List<TransportHandler> transportHandlers = new ArrayList<>();
069
070        private final List<TransportHandler> transportHandlerOverrides = new ArrayList<>();
071
072        private final List<HandshakeInterceptor> interceptors = new ArrayList<>();
073
074        private final List<String> allowedOrigins = new ArrayList<>();
075
076        @Nullable
077        private Boolean suppressCors;
078
079        @Nullable
080        private SockJsMessageCodec messageCodec;
081
082
083        public SockJsServiceRegistration() {
084        }
085
086
087        /**
088         * A scheduler instance to use for scheduling SockJS heart-beats.
089         */
090        public SockJsServiceRegistration setTaskScheduler(TaskScheduler scheduler) {
091                Assert.notNull(scheduler, "TaskScheduler is required");
092                this.scheduler = scheduler;
093                return this;
094        }
095
096        /**
097         * Transports with no native cross-domain communication (e.g. "eventsource",
098         * "htmlfile") must get a simple page from the "foreign" domain in an invisible
099         * iframe so that code in the iframe can run from  a domain local to the SockJS
100         * server. Since the iframe needs to load the SockJS javascript client library,
101         * this property allows specifying where to load it from.
102         * <p>By default this is set to point to
103         * "https://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js". However it can
104         * also be set to point to a URL served by the application.
105         * <p>Note that it's possible to specify a relative URL in which case the URL
106         * must be relative to the iframe URL. For example assuming a SockJS endpoint
107         * mapped to "/sockjs", and resulting iframe URL "/sockjs/iframe.html", then the
108         * the relative URL must start with "../../" to traverse up to the location
109         * above the SockJS mapping. In case of a prefix-based Servlet mapping one more
110         * traversal may be needed.
111         */
112        public SockJsServiceRegistration setClientLibraryUrl(String clientLibraryUrl) {
113                this.clientLibraryUrl = clientLibraryUrl;
114                return this;
115        }
116
117        /**
118         * Streaming transports save responses on the client side and don't free
119         * memory used by delivered messages. Such transports need to recycle the
120         * connection once in a while. This property sets a minimum number of bytes
121         * that can be send over a single HTTP streaming request before it will be
122         * closed. After that client will open a new request. Setting this value to
123         * one effectively disables streaming and will make streaming transports to
124         * behave like polling transports.
125         * <p>The default value is 128K (i.e. 128 * 1024).
126         */
127        public SockJsServiceRegistration setStreamBytesLimit(int streamBytesLimit) {
128                this.streamBytesLimit = streamBytesLimit;
129                return this;
130        }
131
132        /**
133         * The SockJS protocol requires a server to respond to the initial "/info" request
134         * from clients with a "cookie_needed" boolean property that indicates whether the use
135         * of a JSESSIONID cookie is required for the application to function correctly, e.g.
136         * for load balancing or in Java Servlet containers for the use of an HTTP session.
137         *
138         * <p>This is especially important for IE 8,9 that support XDomainRequest -- a modified
139         * AJAX/XHR -- that can do requests across domains but does not send any cookies. In
140         * those cases, the SockJS client prefers the "iframe-htmlfile" transport over
141         * "xdr-streaming" in order to be able to send cookies.
142         *
143         * <p>The default value is "true" to maximize the chance for applications to work
144         * correctly in IE 8,9 with support for cookies (and the JSESSIONID cookie in
145         * particular). However, an application can choose to set this to "false" if the use
146         * of cookies (and HTTP session) is not required.
147         */
148        public SockJsServiceRegistration setSessionCookieNeeded(boolean sessionCookieNeeded) {
149                this.sessionCookieNeeded = sessionCookieNeeded;
150                return this;
151        }
152
153        /**
154         * The amount of time in milliseconds when the server has not sent any
155         * messages and after which the server should send a heartbeat frame to the
156         * client in order to keep the connection from breaking.
157         * <p>The default value is 25,000 (25 seconds).
158         */
159        public SockJsServiceRegistration setHeartbeatTime(long heartbeatTime) {
160                this.heartbeatTime = heartbeatTime;
161                return this;
162        }
163
164        /**
165         * The amount of time in milliseconds before a client is considered
166         * disconnected after not having a receiving connection, i.e. an active
167         * connection over which the server can send data to the client.
168         * <p>The default value is 5000.
169         */
170        public SockJsServiceRegistration setDisconnectDelay(long disconnectDelay) {
171                this.disconnectDelay = disconnectDelay;
172                return this;
173        }
174
175        /**
176         * The number of server-to-client messages that a session can cache while waiting for
177         * the next HTTP polling request from the client. All HTTP transports use this
178         * property since even streaming transports recycle HTTP requests periodically.
179         * <p>The amount of time between HTTP requests should be relatively brief and will not
180         * exceed the allows disconnect delay (see
181         * {@link #setDisconnectDelay(long)}), 5 seconds by default.
182         * <p>The default size is 100.
183         */
184        public SockJsServiceRegistration setHttpMessageCacheSize(int httpMessageCacheSize) {
185                this.httpMessageCacheSize = httpMessageCacheSize;
186                return this;
187        }
188
189        /**
190         * Some load balancers don't support WebSocket. This option can be used to
191         * disable the WebSocket transport on the server side.
192         * <p>The default value is "true".
193         */
194        public SockJsServiceRegistration setWebSocketEnabled(boolean webSocketEnabled) {
195                this.webSocketEnabled = webSocketEnabled;
196                return this;
197        }
198
199        public SockJsServiceRegistration setTransportHandlers(TransportHandler... handlers) {
200                this.transportHandlers.clear();
201                if (!ObjectUtils.isEmpty(handlers)) {
202                        this.transportHandlers.addAll(Arrays.asList(handlers));
203                }
204                return this;
205        }
206
207        public SockJsServiceRegistration setTransportHandlerOverrides(TransportHandler... handlers) {
208                this.transportHandlerOverrides.clear();
209                if (!ObjectUtils.isEmpty(handlers)) {
210                        this.transportHandlerOverrides.addAll(Arrays.asList(handlers));
211                }
212                return this;
213        }
214
215        public SockJsServiceRegistration setInterceptors(HandshakeInterceptor... interceptors) {
216                this.interceptors.clear();
217                if (!ObjectUtils.isEmpty(interceptors)) {
218                        this.interceptors.addAll(Arrays.asList(interceptors));
219                }
220                return this;
221        }
222
223        /**
224         * Configure allowed {@code Origin} header values.
225         * @since 4.1.2
226         */
227        protected SockJsServiceRegistration setAllowedOrigins(String... allowedOrigins) {
228                this.allowedOrigins.clear();
229                if (!ObjectUtils.isEmpty(allowedOrigins)) {
230                        this.allowedOrigins.addAll(Arrays.asList(allowedOrigins));
231                }
232                return this;
233        }
234
235        /**
236         * This option can be used to disable automatic addition of CORS headers for
237         * SockJS requests.
238         * <p>The default value is "false".
239         * @since 4.1.2
240         */
241        public SockJsServiceRegistration setSupressCors(boolean suppressCors) {
242                this.suppressCors = suppressCors;
243                return this;
244        }
245
246        /**
247         * The codec to use for encoding and decoding SockJS messages.
248         * <p>By default {@code Jackson2SockJsMessageCodec} is used requiring the
249         * Jackson library to be present on the classpath.
250         * @param codec the codec to use.
251         * @since 4.1
252         */
253        public SockJsServiceRegistration setMessageCodec(SockJsMessageCodec codec) {
254                this.messageCodec = codec;
255                return this;
256        }
257
258        protected SockJsService getSockJsService() {
259                TransportHandlingSockJsService service = createSockJsService();
260                service.setHandshakeInterceptors(this.interceptors);
261
262                if (this.clientLibraryUrl != null) {
263                        service.setSockJsClientLibraryUrl(this.clientLibraryUrl);
264                }
265                if (this.streamBytesLimit != null) {
266                        service.setStreamBytesLimit(this.streamBytesLimit);
267                }
268                if (this.sessionCookieNeeded != null) {
269                        service.setSessionCookieNeeded(this.sessionCookieNeeded);
270                }
271                if (this.heartbeatTime != null) {
272                        service.setHeartbeatTime(this.heartbeatTime);
273                }
274                if (this.disconnectDelay != null) {
275                        service.setDisconnectDelay(this.disconnectDelay);
276                }
277                if (this.httpMessageCacheSize != null) {
278                        service.setHttpMessageCacheSize(this.httpMessageCacheSize);
279                }
280                if (this.webSocketEnabled != null) {
281                        service.setWebSocketEnabled(this.webSocketEnabled);
282                }
283                if (this.suppressCors != null) {
284                        service.setSuppressCors(this.suppressCors);
285                }
286                service.setAllowedOrigins(this.allowedOrigins);
287
288                if (this.messageCodec != null) {
289                        service.setMessageCodec(this.messageCodec);
290                }
291                return service;
292        }
293
294        /**
295         * Return the TaskScheduler, if configured.
296         */
297        @Nullable
298        protected TaskScheduler getTaskScheduler() {
299                return this.scheduler;
300        }
301
302        private TransportHandlingSockJsService createSockJsService() {
303                Assert.state(this.scheduler != null, "No TaskScheduler available");
304                Assert.state(this.transportHandlers.isEmpty() || this.transportHandlerOverrides.isEmpty(),
305                                "Specify either TransportHandlers or TransportHandler overrides, not both");
306                return (!this.transportHandlers.isEmpty() ?
307                                new TransportHandlingSockJsService(this.scheduler, this.transportHandlers) :
308                                new DefaultSockJsService(this.scheduler, this.transportHandlerOverrides));
309        }
310
311}