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}