/* * Copyright 2013 The Netty Project * * The Netty Project licenses this file to you under the Apache License, version * 2.0 (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.jboss.aerogear.io.netty.handler.codec.sockjs.handler; import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpHeaders.Values.KEEP_ALIVE; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static java.util.UUID.randomUUID; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsServiceFactory; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.EventSourceTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.HtmlFileTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.JsonpPollingTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.JsonpSendTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.RawWebSocketTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.Transports; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.WebSocketTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.XhrPollingTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.XhrSendTransport; import org.jboss.aerogear.io.netty.handler.codec.sockjs.transport.XhrStreamingTransport; import io.netty.util.CharsetUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This handler is the main entry point for SockJS HTTP Request. * * It is responsible for inspecting the request uri and adding ChannelHandlers for * different transport protocols that SockJS support. Once this has been done this * handler will be removed from the channel pipeline. */ public class SockJsHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final InternalLogger logger = InternalLoggerFactory.getInstance(SockJsHandler.class); private final Map<String, SockJsServiceFactory> factories = new LinkedHashMap<String, SockJsServiceFactory>(); private static final ConcurrentMap<String, SockJsSession> sessions = new ConcurrentHashMap<String, SockJsSession>(); private static final PathParams NON_SUPPORTED_PATH = new NonSupportedPath(); private static final Pattern SERVER_SESSION_PATTERN = Pattern.compile("^/([^/.]+)/([^/.]+)/([^/.]+)"); /** * Sole constructor which takes one or more {@code SockJSServiceFactory}. These factories will * later be used by the server to create the SockJS services that will be exposed by this server * * @param factories one or more {@link SockJsServiceFactory}s. */ public SockJsHandler(final SockJsServiceFactory... factories) { for (SockJsServiceFactory factory : factories) { this.factories.put(factory.config().prefix(), factory); } } @Override public void messageReceived(final ChannelHandlerContext ctx, final FullHttpRequest request) throws Exception { final String path = new QueryStringDecoder(request.getUri()).path(); for (SockJsServiceFactory factory : factories.values()) { if (path.startsWith(factory.config().prefix())) { handleService(factory, request, ctx); return; } } writeNotFoundResponse(request, ctx); } private static void handleService(final SockJsServiceFactory factory, final FullHttpRequest request, final ChannelHandlerContext ctx) throws Exception { if (logger.isDebugEnabled()) { logger.debug("RequestUri : [{}]", request.getUri()); } final String pathWithoutPrefix = request.getUri().replaceFirst(factory.config().prefix(), ""); final String path = new QueryStringDecoder(pathWithoutPrefix).path(); if (Greeting.matches(path)) { writeResponse(ctx.channel(), request, Greeting.response(request)); } else if (Info.matches(path)) { writeResponse(ctx.channel(), request, Info.response(factory.config(), request)); } else if (Iframe.matches(path)) { writeResponse(ctx.channel(), request, Iframe.response(factory.config(), request)); } else if (Transports.Type.WEBSOCKET.path().equals(path)) { addTransportHandler(new RawWebSocketTransport(factory.config(), factory.create()), ctx); ctx.fireChannelRead(request.retain()); } else { final PathParams sessionPath = matches(path); if (sessionPath.matches()) { handleSession(factory, request, ctx, sessionPath); } else { writeNotFoundResponse(request, ctx); } } } private static void handleSession(final SockJsServiceFactory factory, final FullHttpRequest request, final ChannelHandlerContext ctx, final PathParams pathParams) throws Exception { switch (pathParams.transport()) { case XHR: addTransportHandler(new XhrPollingTransport(factory.config(), request), ctx); addSessionHandler(new PollingSessionState(sessions, getSession(factory, pathParams.sessionId())), ctx); break; case JSONP: addTransportHandler(new JsonpPollingTransport(factory.config(), request), ctx); addSessionHandler(new PollingSessionState(sessions, getSession(factory, pathParams.sessionId())), ctx); break; case XHR_SEND: checkSessionExists(pathParams.sessionId(), request); addTransportHandler(new XhrSendTransport(factory.config()), ctx); addSessionHandler(new SendingSessionState(sessions, sessions.get(pathParams.sessionId())), ctx); break; case XHR_STREAMING: addTransportHandler(new XhrStreamingTransport(factory.config(), request), ctx); addSessionHandler(new StreamingSessionState(sessions, getSession(factory, pathParams.sessionId())), ctx); break; case EVENTSOURCE: addTransportHandler(new EventSourceTransport(factory.config(), request), ctx); addSessionHandler(new StreamingSessionState(sessions, getSession(factory, pathParams.sessionId())), ctx); break; case HTMLFILE: addTransportHandler(new HtmlFileTransport(factory.config(), request), ctx); addSessionHandler(new StreamingSessionState(sessions, getSession(factory, pathParams.sessionId())), ctx); break; case JSONP_SEND: checkSessionExists(pathParams.sessionId(), request); addTransportHandler(new JsonpSendTransport(factory.config()), ctx); addSessionHandler(new SendingSessionState(sessions, sessions.get(pathParams.sessionId())), ctx); break; case WEBSOCKET: addTransportHandler(new WebSocketTransport(factory.config()), ctx); addSessionHandler(new WebSocketSessionState(new SockJsSession(randomUUID().toString(), factory.create())), ctx); break; } ctx.fireChannelRead(request.retain()); } private static void addTransportHandler(final ChannelHandler transportHandler, final ChannelHandlerContext ctx) { ctx.pipeline().addLast(transportHandler); } private static void addSessionHandler(final SessionState sessionState, final ChannelHandlerContext ctx) { ctx.pipeline().addLast(new SessionHandler(sessionState)); } private static void checkSessionExists(final String sessionId, final HttpRequest request) throws SessionNotFoundException { if (!sessions.containsKey(sessionId)) { throw new SessionNotFoundException(sessionId, request); } } private static SockJsSession getSession(final SockJsServiceFactory factory, final String sessionId) { SockJsSession session = sessions.get(sessionId); if (session == null) { final SockJsSession newSession = new SockJsSession(sessionId, factory.create()); session = sessions.putIfAbsent(sessionId, newSession); if (session == null) { session = newSession; } logger.debug("Created new session [{}]", sessionId); } else { logger.debug("Using existing session [{}]", sessionId); } return session; } private static void writeNotFoundResponse(final HttpRequest request, final ChannelHandlerContext ctx) { final FullHttpResponse response = new DefaultFullHttpResponse(request.getProtocolVersion(), NOT_FOUND, Unpooled.copiedBuffer("Not found", CharsetUtil.UTF_8)); writeResponse(ctx.channel(), request, response); } private static void writeResponse(final Channel channel, final HttpRequest request, final FullHttpResponse response) { response.headers().set(CONTENT_LENGTH, response.content().readableBytes()); boolean hasKeepAliveHeader = HttpHeaders.equalsIgnoreCase(KEEP_ALIVE, request.headers().get(CONNECTION)); if (!request.getProtocolVersion().isKeepAliveDefault() && hasKeepAliveHeader) { response.headers().set(CONNECTION, KEEP_ALIVE); } final ChannelFuture wf = channel.writeAndFlush(response); if (!HttpHeaders.isKeepAlive(request)) { wf.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof SessionNotFoundException) { final SessionNotFoundException se = (SessionNotFoundException) cause; logger.debug("Could not find session [{}]", se.sessionId()); writeNotFoundResponse(se.httpRequest(), ctx); } else { logger.error("exception caught:", cause); ctx.fireExceptionCaught(cause); } } static PathParams matches(final String path) { final Matcher matcher = SERVER_SESSION_PATTERN.matcher(path); if (matcher.find()) { final String serverId = matcher.group(1); final String sessionId = matcher.group(2); final String transport = matcher.group(3); return new MatchingSessionPath(serverId, sessionId, transport); } else { return NON_SUPPORTED_PATH; } } private static final class SessionNotFoundException extends Exception { private static final long serialVersionUID = 1101611486620901143L; private final String sessionId; private final HttpRequest request; private SessionNotFoundException(final String sessionId, final HttpRequest request) { this.sessionId = sessionId; this.request = request; } public String sessionId() { return sessionId; } public HttpRequest httpRequest() { return request; } } /** * Represents HTTP path parameters in SockJS. * * The path consists of the following parts: * http://server:port/prefix/serverId/sessionId/transport * */ public interface PathParams { boolean matches(); /** * The serverId is chosen by the client and exists to make it easier to configure * load balancers to enable sticky sessions. * * @return String the server id for this path. */ String serverId(); /** * The sessionId is a unique random number which identifies the session. * * @return String the session identifier for this path. */ String sessionId(); /** * The type of transport. * * @return Transports.Type the type of the transport. */ Transports.Type transport(); } public static class MatchingSessionPath implements PathParams { private final String serverId; private final String sessionId; private final Transports.Type transport; public MatchingSessionPath(final String serverId, final String sessionId, final String transport) { this.serverId = serverId; this.sessionId = sessionId; this.transport = Transports.Type.valueOf(transport.toUpperCase()); } @Override public boolean matches() { return true; } @Override public String serverId() { return serverId; } @Override public String sessionId() { return sessionId; } @Override public Transports.Type transport() { return transport; } } public static class NonSupportedPath implements PathParams { @Override public boolean matches() { return false; } @Override public String serverId() { throw new UnsupportedOperationException("serverId is not available in path"); } @Override public String sessionId() { throw new UnsupportedOperationException("sessionId is not available in path"); } @Override public Transports.Type transport() { throw new UnsupportedOperationException("transport is not available in path"); } } }