Skip to content

Commit

Permalink
Add SockJS path detection
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed May 6, 2013
1 parent 97d225b commit 7845ebc
Show file tree
Hide file tree
Showing 25 changed files with 654 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.springframework.http.server;



/**
* TODO..
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,14 @@ public void startAsync() {
}
}

public void dispatch() {
Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext");
this.asyncContext.dispatch();
}

public void completeAsync() {
Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext");
if (isAsyncStarted() && !isAsyncCompleted()) {
this.asyncContext.complete();
}
}


// ---------------------------------------------------------------------
// Implementation of AsyncListener methods
// ---------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,9 @@ public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String ur
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
final HttpHeaders httpHeaders, URI uri) throws WebSocketConnectFailureException {

return doHandshake(webSocketHandler, httpHeaders, UriComponentsBuilder.fromUri(uri).build());
}

public WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
final HttpHeaders httpHeaders, UriComponents uriComponents) throws WebSocketConnectFailureException {

URI uri = uriComponents.toUri();

StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter();
session.setUri(uri);
session.setRemoteHostName(uriComponents.getHost());
session.setRemoteHostName(uri.getHost());
Endpoint endpoint = new StandardEndpointAdapter(webSocketHandler, session);

ClientEndpointConfig.Builder configBuidler = ClientEndpointConfig.Builder.create();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,11 @@ public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String ur
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri)
throws WebSocketConnectFailureException {

return doHandshake(webSocketHandler, headers, UriComponentsBuilder.fromUri(uri).build());
}

public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, UriComponents uriComponents)
throws WebSocketConnectFailureException {

// TODO: populate headers

URI uri = uriComponents.toUri();

JettyWebSocketSessionAdapter session = new JettyWebSocketSessionAdapter();
session.setUri(uri);
session.setRemoteHostName(uriComponents.getHost());
session.setRemoteHostName(uri.getHost());

JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler, session);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public final boolean doHandshake(ServerHttpRequest request, ServerHttpResponse r
protected void handleInvalidUpgradeHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
logger.debug("Invalid Upgrade header " + request.getHeaders().getUpgrade());
response.setStatusCode(HttpStatus.BAD_REQUEST);
response.getBody().write("Can \"Upgrade\" only to \"websocket\".".getBytes("UTF-8"));
response.getBody().write("Can \"Upgrade\" only to \"WebSocket\".".getBytes("UTF-8"));
}

protected void handleInvalidConnectHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
Expand Down Expand Up @@ -227,13 +227,13 @@ private static class RequestUpgradeStrategyFactory {
private RequestUpgradeStrategy create() {
String className;
if (tomcatWebSocketPresent) {
className = "org.springframework.websocket.server.support.TomcatRequestUpgradeStrategy";
className = "org.springframework.web.socket.server.support.TomcatRequestUpgradeStrategy";
}
else if (glassFishWebSocketPresent) {
className = "org.springframework.websocket.server.support.GlassFishRequestUpgradeStrategy";
className = "org.springframework.web.socket.server.support.GlassFishRequestUpgradeStrategy";
}
else if (jettyWebSocketPresent) {
className = "org.springframework.websocket.server.support.JettyRequestUpgradeStrategy";
className = "org.springframework.web.socket.server.support.JettyRequestUpgradeStrategy";
}
else {
throw new IllegalStateException("No suitable " + RequestUpgradeStrategy.class.getSimpleName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
* {@link org.springframework.web.socket.server.endpoint.EndpointExporter} for
* registering type-based endpoints,
* {@link org.springframework.web.socket.server.endpoint.SpringConfigurator} for
* instantiating annotated endpoints through Spring, and
* {@link org.springframework.websocket.server.support.EndpointHandshakeHandler}
* for integrating endpoints into HTTP request processing.
* instantiating annotated endpoints through Spring.
*/
package org.springframework.web.socket.server.endpoint;

Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ public final synchronized void closeInternal(CloseStatus status) throws IOExcept
disconnect(status);
}

// TODO: close status/reason
protected abstract void disconnect(CloseStatus status) throws IOException;

/**
Expand All @@ -104,12 +103,14 @@ protected void writeFrame(SockJsFrame frame) throws IOException {
else {
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
}
close();
disconnect(CloseStatus.SERVER_ERROR);
close(CloseStatus.SERVER_ERROR);
throw ex;
}
catch (Throwable ex) {
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
close();
disconnect(CloseStatus.SERVER_ERROR);
close(CloseStatus.SERVER_ERROR);
throw new SockJsRuntimeException("Failed to write " + frame, ex);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -39,7 +44,18 @@
import org.springframework.web.socket.WebSocketHandler;

/**
* Provides support for SockJS configuration options and serves the static SockJS URLs.
* An abstract class for {@link SockJsService} implementations. Provides configuration
* support, SockJS path resolution, and processing for static SockJS requests (e.g.
* "/info", "/iframe.html", etc). Sub-classes are responsible for handling transport
* requests.
*
* <p>
* It is expected that this service is mapped correctly to one or more prefixes such as
* "/echo" including all sub-URLs (e.g. "/echo/**"). A SockJS service itself is generally
* unaware of request mapping details but nevertheless must be able to extract the SockJS
* path, which is the portion of the request path following the prefix. In most cases,
* this class can auto-detect the SockJS path but you can also explicitly configure the
* list of valid prefixes with {@link #setValidSockJsPrefixes(String...)}.
*
* @author Rossen Stoyanchev
* @since 4.0
Expand All @@ -51,7 +67,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
private static final int ONE_YEAR = 365 * 24 * 60 * 60;


private String name = "SockJS Service " + ObjectUtils.getIdentityHexString(this);
private String name = "SockJSService@" + ObjectUtils.getIdentityHexString(this);

private String clientLibraryUrl = "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js";

Expand All @@ -67,6 +83,9 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf

private final TaskScheduler taskScheduler;

private final List<String> sockJsPrefixes = new ArrayList<String>();

private final Set<String> sockJsPathCache = new CopyOnWriteArraySet<String>();


public AbstractSockJsService(TaskScheduler scheduler) {
Expand All @@ -85,6 +104,38 @@ public String getName() {
return this.name;
}

/**
* Use this property to configure one or more prefixes that this SockJS service is
* allowed to serve. The prefix (e.g. "/echo") is needed to extract the SockJS
* specific portion of the URL (e.g. "${prefix}/info", "${prefix}/iframe.html", etc).
* <p>
* This property is not strictly required. In most cases, the SockJS path can be
* auto-detected since the initial request from the SockJS client is of the form
* "{prefix}/info". Assuming the SockJS service is mapped correctly (e.g. using
* Ant-style pattern "/echo/**") this should work fine. This property can be used
* to configure explicitly the prefixes this service is allowed to service.
*
* @param prefixes the prefixes to use; prefixes do not need to include the portions
* of the path that represent Servlet container context or Servlet path.
*/
public void setValidSockJsPrefixes(String... prefixes) {

this.sockJsPrefixes.clear();
for (String prefix : prefixes) {
if (prefix.endsWith("/") && (prefix.length() > 1)) {
prefix = prefix.substring(0, prefix.length() - 1);
}
this.sockJsPrefixes.add(prefix);
}

// sort with longest prefix at the top
Collections.sort(this.sockJsPrefixes, Collections.reverseOrder(new Comparator<String>() {
public int compare(String o1, String o2) {
return new Integer(o1.length()).compareTo(new Integer(o2.length()));
}
}));
}

/**
* Transports which don't support cross-domain communication natively (e.g.
* "eventsource", "htmlfile") rely on serving a simple page (using the
Expand Down Expand Up @@ -198,10 +249,18 @@ public boolean isWebSocketEnabled() {
*
* @throws Exception
*/
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
String sockJsPath, WebSocketHandler webSocketHandler) throws IOException, TransportErrorException {
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler)
throws IOException, TransportErrorException {

String sockJsPath = getSockJsPath(request);
if (sockJsPath == null) {
logger.warn("Could not determine SockJS path for URL \"" + request.getURI().getPath() +
". Consider setting validSockJsPrefixes.");
response.setStatusCode(HttpStatus.NOT_FOUND);
return;
}

logger.debug(request.getMethod() + " [" + sockJsPath + "]");
logger.debug(request.getMethod() + " with SockJS path [" + sockJsPath + "]");

try {
request.getHeaders();
Expand All @@ -225,13 +284,13 @@ else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) {
return;
}
else if (sockJsPath.equals("/websocket")) {
handleRawWebSocketRequest(request, response, webSocketHandler);
handleRawWebSocketRequest(request, response, handler);
return;
}

String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/");
if (pathSegments.length != 3) {
logger.debug("Expected /{server}/{session}/{transport} but got " + sockJsPath);
logger.warn("Expected \"/{server}/{session}/{transport}\" but got \"" + sockJsPath + "\"");
response.setStatusCode(HttpStatus.NOT_FOUND);
return;
}
Expand All @@ -245,13 +304,62 @@ else if (sockJsPath.equals("/websocket")) {
return;
}

handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), webSocketHandler);
handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), handler);
}
finally {
response.flush();
}
}

/**
* Return the SockJS path or null if the path could not be determined.
*/
private String getSockJsPath(ServerHttpRequest request) {

String path = request.getURI().getPath();

// SockJS prefix hints?
if (!this.sockJsPrefixes.isEmpty()) {
for (String prefix : this.sockJsPrefixes) {
int index = path.indexOf(prefix);
if (index != -1) {
this.sockJsPathCache.add(path.substring(0, index + prefix.length()));
return path.substring(index + prefix.length());
}
}
}

// SockJS info request?
if (path.endsWith("/info")) {
this.sockJsPathCache.add(path.substring(0, path.length() - 6));
return "/info";
}

// Have we seen this prefix before (following the initial /info request)?
String match = null;
for (String sockJsPath : this.sockJsPathCache) {
if (path.startsWith(sockJsPath)) {
if ((match == null) || (match.length() < sockJsPath.length())) {
match = sockJsPath;
}
}
}
if (match != null) {
return path.substring(match.length());
}

// SockJS greeting?
String pathNoSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
String lastSegment = pathNoSlash.substring(pathNoSlash.lastIndexOf('/') + 1);

if ((TransportType.fromValue(lastSegment) == null) && !lastSegment.startsWith("iframe")) {
this.sockJsPathCache.add(path);
return "";
}

return null;
}

protected abstract void handleRawWebSocketRequest(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler webSocketHandler) throws IOException;

Expand All @@ -263,18 +371,18 @@ protected abstract void handleTransportRequest(ServerHttpRequest request, Server
protected boolean validateRequest(String serverId, String sessionId, String transport) {

if (!StringUtils.hasText(serverId) || !StringUtils.hasText(sessionId) || !StringUtils.hasText(transport)) {
logger.debug("Empty server, session, or transport value");
logger.warn("Empty server, session, or transport value");
return false;
}

// Server and session id's must not contain "."
if (serverId.contains(".") || sessionId.contains(".")) {
logger.debug("Server or session contain a \".\"");
logger.warn("Server or session contain a \".\"");
return false;
}

if (!isWebSocketEnabled() && transport.equals(TransportType.WEBSOCKET.value())) {
logger.debug("Websocket transport is disabled");
logger.warn("Websocket transport is disabled");
return false;
}

Expand Down Expand Up @@ -346,7 +454,7 @@ else if (HttpMethod.OPTIONS.equals(request.getMethod())) {

response.setStatusCode(HttpStatus.NO_CONTENT);

addCorsHeaders(request, response, HttpMethod.GET, HttpMethod.OPTIONS);
addCorsHeaders(request, response, HttpMethod.OPTIONS, HttpMethod.GET);
addCacheHeaders(response);
}
else {
Expand Down Expand Up @@ -404,4 +512,5 @@ public void handle(ServerHttpRequest request, ServerHttpResponse response) throw
}
};


}
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,15 @@ protected void connectionClosedInternal(CloseStatus status) {
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
*/
public final void close() throws IOException {
close(CloseStatus.NORMAL);
close(new CloseStatus(3000, "Go away!"));
}

/**
* {@inheritDoc}
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
*/
public final void close(CloseStatus status) throws IOException {
if (!isClosed()) {
if (isOpen()) {
if (logger.isDebugEnabled()) {
logger.debug("Closing " + this + ", " + status);
}
Expand Down
Loading

0 comments on commit 7845ebc

Please sign in to comment.