diff --git a/src/main/java/com/corundumstudio/socketio/SocketIOPipelineFactory.java b/src/main/java/com/corundumstudio/socketio/SocketIOPipelineFactory.java index eab9936..85be842 100644 --- a/src/main/java/com/corundumstudio/socketio/SocketIOPipelineFactory.java +++ b/src/main/java/com/corundumstudio/socketio/SocketIOPipelineFactory.java @@ -34,6 +34,9 @@ import org.jboss.netty.handler.ssl.SslHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.corundumstudio.socketio.handler.AuthorizeHandler; +import com.corundumstudio.socketio.handler.PacketHandler; +import com.corundumstudio.socketio.handler.ResourceHandler; import com.corundumstudio.socketio.namespace.NamespacesHub; import com.corundumstudio.socketio.parser.Decoder; import com.corundumstudio.socketio.parser.Encoder; @@ -58,6 +61,7 @@ public class SocketIOPipelineFactory implements ChannelPipelineFactory, Disconne public static final String HTTP_REQUEST_DECODER = "decoder"; public static final String SSL_HANDLER = "ssl"; public static final String FLASH_POLICY_HANDLER = "flashPolicyHandler"; + public static final String RESOURCE_HANDLER = "resourceHandler"; private final Logger log = LoggerFactory.getLogger(getClass()); @@ -70,6 +74,7 @@ public class SocketIOPipelineFactory implements ChannelPipelineFactory, Disconne private WebSocketTransport webSocketTransport; private FlashSocketTransport flashSocketTransport; private final FlashPolicyHandler flashPolicyHandler = new FlashPolicyHandler(); + private ResourceHandler resourceHandler; private SocketIOEncoder socketIOEncoder; private CancelableScheduler scheduler; @@ -77,8 +82,10 @@ public class SocketIOPipelineFactory implements ChannelPipelineFactory, Disconne private PacketHandler packetHandler; private HeartbeatHandler heartbeatHandler; private SSLContext sslContext; + private Configuration configuration; public void start(Configuration configuration, NamespacesHub namespacesHub) { + this.configuration = configuration; scheduler = new CancelableScheduler(configuration.getHeartbeatThreadPoolSize()); ackManager = new AckManager(scheduler); @@ -106,6 +113,7 @@ public class SocketIOPipelineFactory implements ChannelPipelineFactory, Disconne xhrPollingTransport = new XHRPollingTransport(connectPath, ackManager, this, scheduler, authorizeHandler, configuration); webSocketTransport = new WebSocketTransport(connectPath, isSsl, ackManager, this, authorizeHandler, heartbeatHandler); flashSocketTransport = new FlashSocketTransport(connectPath, isSsl, ackManager, this, authorizeHandler, heartbeatHandler); + resourceHandler = new ResourceHandler(configuration.getContext()); socketIOEncoder = new SocketIOEncoder(encoder); } @@ -120,7 +128,10 @@ public class SocketIOPipelineFactory implements ChannelPipelineFactory, Disconne public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeline = pipeline(); - pipeline.addLast(FLASH_POLICY_HANDLER, flashPolicyHandler); + boolean isFlashTransport = configuration.getTransports().contains(FlashSocketTransport.NAME); + if (isFlashTransport) { + pipeline.addLast(FLASH_POLICY_HANDLER, flashPolicyHandler); + } if (sslContext != null) { SSLEngine engine = sslContext.createSSLEngine(); @@ -132,6 +143,9 @@ public class SocketIOPipelineFactory implements ChannelPipelineFactory, Disconne pipeline.addLast(HTTP_AGGREGATOR, new HttpChunkAggregator(65536)); pipeline.addLast(HTTP_ENCODER, new HttpResponseEncoder()); + if (isFlashTransport) { + pipeline.addLast(RESOURCE_HANDLER, resourceHandler); + } pipeline.addLast(PACKET_HANDLER, packetHandler); pipeline.addLast(AUTHORIZE_HANDLER, authorizeHandler); diff --git a/src/main/java/com/corundumstudio/socketio/AuthorizeHandler.java b/src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java similarity index 97% rename from src/main/java/com/corundumstudio/socketio/AuthorizeHandler.java rename to src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java index ce398aa..6ac4c1d 100644 --- a/src/main/java/com/corundumstudio/socketio/AuthorizeHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.corundumstudio.socketio; +package com.corundumstudio.socketio.handler; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; @@ -42,6 +42,9 @@ import org.jboss.netty.handler.codec.http.QueryStringDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.corundumstudio.socketio.Configuration; +import com.corundumstudio.socketio.Disconnectable; +import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.messages.AuthorizeMessage; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; diff --git a/src/main/java/com/corundumstudio/socketio/PacketHandler.java b/src/main/java/com/corundumstudio/socketio/handler/PacketHandler.java similarity index 93% rename from src/main/java/com/corundumstudio/socketio/PacketHandler.java rename to src/main/java/com/corundumstudio/socketio/handler/PacketHandler.java index 7767c77..031a170 100644 --- a/src/main/java/com/corundumstudio/socketio/PacketHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/PacketHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.corundumstudio.socketio; +package com.corundumstudio.socketio.handler; import java.util.Collections; @@ -26,6 +26,9 @@ import org.jboss.netty.util.CharsetUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.corundumstudio.socketio.AckRequest; +import com.corundumstudio.socketio.PacketListener; +import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.messages.PacketsMessage; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; diff --git a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java new file mode 100644 index 0000000..01734e3 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java @@ -0,0 +1,227 @@ +/** + * Copyright 2012 Nikita Koksharov + * + * Licensed 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 com.corundumstudio.socketio.handler; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; +import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import javax.activation.MimetypesFileTypeMap; + +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelFutureProgressListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.DefaultFileRegion; +import org.jboss.netty.channel.FileRegion; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.channel.ChannelHandler.Sharable; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.QueryStringDecoder; +import org.jboss.netty.handler.ssl.SslHandler; +import org.jboss.netty.handler.stream.ChunkedFile; +import org.jboss.netty.util.CharsetUtil; + +@Sharable +public class ResourceHandler extends SimpleChannelUpstreamHandler { + + public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; + public static final int HTTP_CACHE_SECONDS = 60; + + private final Map resources = new HashMap(); + + public ResourceHandler(String context) { + addResource(context + "/static/flashsocket/WebSocketMain.swf", + "/static/flashsocket/WebSocketMain.swf"); + addResource(context + "/static/flashsocket/WebSocketMainInsecure.swf", + "/static/flashsocket/WebSocketMainInsecure.swf"); + } + + private void addResource(String pathPart, String resourcePath) { + URL resource = getClass().getResource(resourcePath); + File file = new File(resource.getFile()); + resources.put(pathPart, file); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + HttpRequest req = (HttpRequest) msg; + QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); + File resource = resources.get(queryDecoder.getPath()); + if (resource != null) { + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); + + if (isNotModified(req, resource)) { + sendNotModified(ctx); + return; + } + + RandomAccessFile raf; + try { + raf = new RandomAccessFile(resource, "r"); + } catch (FileNotFoundException fnfe) { + sendError(ctx, NOT_FOUND); + return; + } + long fileLength = raf.length(); + + setContentLength(res, fileLength); + setContentTypeHeader(res, resource); + setDateAndCacheHeaders(res, resource); + writeContent(raf, fileLength, e.getChannel()); + return; + } + } + ctx.sendUpstream(e); + } + + private boolean isNotModified(HttpRequest request, File file) throws ParseException { + String ifModifiedSince = request.getHeader(HttpHeaders.Names.IF_MODIFIED_SINCE); + if (ifModifiedSince != null && !ifModifiedSince.equals("")) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); + + // Only compare up to the second because the datetime format we send to the client does + // not have milliseconds + long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; + long fileLastModifiedSeconds = file.lastModified() / 1000; + return ifModifiedSinceDateSeconds == fileLastModifiedSeconds; + } + return false; + } + + private void sendNotModified(ChannelHandlerContext ctx) { + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED); + setDateHeader(response); + + // Close the connection as soon as the error message is sent. + ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Sets the Date header for the HTTP response + * + * @param response + * HTTP response + */ + private void setDateHeader(HttpResponse response) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); + + Calendar time = new GregorianCalendar(); + response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); + } + + private void writeContent(RandomAccessFile raf, long fileLength, Channel ch) throws IOException { + ChannelFuture writeFuture; + if (ch.getPipeline().get(SslHandler.class) != null) { + // Cannot use zero-copy with HTTPS. + writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192)); + } else { + // No encryption - use zero-copy. + final FileRegion region = + new DefaultFileRegion(raf.getChannel(), 0, fileLength); + writeFuture = ch.write(region); + writeFuture.addListener(new ChannelFutureProgressListener() { + public void operationComplete(ChannelFuture future) { + region.releaseExternalResources(); + } + + @Override + public void operationProgressed(ChannelFuture future, long amount, long current, long total) + throws Exception { + } + + }); + } + + writeFuture.addListener(ChannelFutureListener.CLOSE); + } + + private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); + response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.setContent(ChannelBuffers.copiedBuffer( + "Failure: " + status.toString() + "\r\n", + CharsetUtil.UTF_8)); + + // Close the connection as soon as the error message is sent. + ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Sets the Date and Cache headers for the HTTP Response + * + * @param response + * HTTP response + * @param fileToCache + * file to extract content type + */ + private void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); + + // Date header + Calendar time = new GregorianCalendar(); + response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); + + // Add cache headers + time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); + response.setHeader(HttpHeaders.Names.EXPIRES, dateFormatter.format(time.getTime())); + response.setHeader(HttpHeaders.Names.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); + response.setHeader( + HttpHeaders.Names.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); + } + + /** + * Sets the content type header for the HTTP Response + * + * @param response + * HTTP response + * @param file + * file to extract content type + */ + private void setContentTypeHeader(HttpResponse response, File file) { + MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); + } +} diff --git a/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java b/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java index ed4a4ba..0ce36aa 100644 --- a/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java +++ b/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java @@ -21,8 +21,8 @@ public class SchedulerKey { public enum Type {POLLING, HEARBEAT_TIMEOUT, CLOSE_TIMEOUT, AUTHORIZE, ACK_TIMEOUT}; - private Type type; - private UUID sessionId; + private final Type type; + private final UUID sessionId; public SchedulerKey(Type type, UUID sessionId) { this.type = type; diff --git a/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java b/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java index 8a2653f..610a134 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java +++ b/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java @@ -25,6 +25,8 @@ import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.util.CharsetUtil; +import com.corundumstudio.socketio.SocketIOPipelineFactory; + @Sharable public class FlashPolicyHandler extends SimpleChannelUpstreamHandler { @@ -47,6 +49,7 @@ public class FlashPolicyHandler extends SimpleChannelUpstreamHandler { f.addListener(ChannelFutureListener.CLOSE); return; } + ctx.getPipeline().remove(SocketIOPipelineFactory.FLASH_POLICY_HANDLER); super.messageReceived(ctx, e); } } diff --git a/src/main/java/com/corundumstudio/socketio/transport/FlashSocketTransport.java b/src/main/java/com/corundumstudio/socketio/transport/FlashSocketTransport.java index fa67a8e..c11f0af 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/FlashSocketTransport.java +++ b/src/main/java/com/corundumstudio/socketio/transport/FlashSocketTransport.java @@ -19,10 +19,10 @@ import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelHandler.Sharable; import com.corundumstudio.socketio.AckManager; -import com.corundumstudio.socketio.AuthorizeHandler; import com.corundumstudio.socketio.DisconnectableHub; import com.corundumstudio.socketio.HeartbeatHandler; import com.corundumstudio.socketio.SocketIOPipelineFactory; +import com.corundumstudio.socketio.handler.AuthorizeHandler; @Sharable public class FlashSocketTransport extends WebSocketTransport { diff --git a/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java b/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java index a82f000..ac6cfc7 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java +++ b/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java @@ -43,13 +43,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.corundumstudio.socketio.AckManager; -import com.corundumstudio.socketio.AuthorizeHandler; import com.corundumstudio.socketio.CompositeIterable; import com.corundumstudio.socketio.Disconnectable; import com.corundumstudio.socketio.DisconnectableHub; import com.corundumstudio.socketio.HeartbeatHandler; import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.SocketIOPipelineFactory; +import com.corundumstudio.socketio.handler.AuthorizeHandler; import com.corundumstudio.socketio.messages.PacketsMessage; @Sharable @@ -156,7 +156,6 @@ public class WebSocketTransport extends SimpleChannelUpstreamHandler implements authorizeHandler.connect(client); heartbeatHandler.onHeartbeat(client); - channel.getPipeline().remove(SocketIOPipelineFactory.FLASH_POLICY_HANDLER); removeHandler(channel.getPipeline()); } diff --git a/src/main/java/com/corundumstudio/socketio/transport/XHRPollingTransport.java b/src/main/java/com/corundumstudio/socketio/transport/XHRPollingTransport.java index 1ed9902..6d31d98 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/XHRPollingTransport.java +++ b/src/main/java/com/corundumstudio/socketio/transport/XHRPollingTransport.java @@ -41,13 +41,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.corundumstudio.socketio.AckManager; -import com.corundumstudio.socketio.AuthorizeHandler; import com.corundumstudio.socketio.CompositeIterable; import com.corundumstudio.socketio.Configuration; import com.corundumstudio.socketio.Disconnectable; import com.corundumstudio.socketio.DisconnectableHub; import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.SocketIOPipelineFactory; +import com.corundumstudio.socketio.handler.AuthorizeHandler; import com.corundumstudio.socketio.messages.PacketsMessage; import com.corundumstudio.socketio.messages.XHRErrorMessage; import com.corundumstudio.socketio.messages.XHRPostMessage; @@ -102,8 +102,6 @@ public class XHRPollingTransport extends SimpleChannelUpstreamHandler implements private void handleMessage(HttpRequest req, QueryStringDecoder queryDecoder, Channel channel) throws IOException { - channel.getPipeline().remove(SocketIOPipelineFactory.FLASH_POLICY_HANDLER); - String[] parts = queryDecoder.getPath().split("/"); if (parts.length > 3) { UUID sessionId = UUID.fromString(parts[4]); diff --git a/src/main/resources/static/flashsocket/WebSocketMain.swf b/src/main/resources/static/flashsocket/WebSocketMain.swf new file mode 100644 index 0000000..20a451f Binary files /dev/null and b/src/main/resources/static/flashsocket/WebSocketMain.swf differ diff --git a/src/main/resources/static/flashsocket/WebSocketMainInsecure.swf b/src/main/resources/static/flashsocket/WebSocketMainInsecure.swf new file mode 100644 index 0000000..5949ff3 Binary files /dev/null and b/src/main/resources/static/flashsocket/WebSocketMainInsecure.swf differ diff --git a/src/test/java/com/corundumstudio/socketio/PacketHandlerTest.java b/src/test/java/com/corundumstudio/socketio/PacketHandlerTest.java index b06e7eb..ad8a7b8 100644 --- a/src/test/java/com/corundumstudio/socketio/PacketHandlerTest.java +++ b/src/test/java/com/corundumstudio/socketio/PacketHandlerTest.java @@ -32,6 +32,7 @@ import org.jboss.netty.channel.UpstreamMessageEvent; import org.junit.Before; import org.junit.Test; +import com.corundumstudio.socketio.handler.PacketHandler; import com.corundumstudio.socketio.messages.PacketsMessage; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub;