From 3c45435c6ac67758b1015c2c8185e6a4c9436179 Mon Sep 17 00:00:00 2001 From: Ivan Greguric Ortolan Date: Thu, 14 Nov 2013 11:36:01 +0100 Subject: [PATCH 1/3] Fixed ResourceHandler to correctly handle resource from classpath --- .../socketio/handler/ResourceHandler.java | 170 +++--------------- 1 file changed, 23 insertions(+), 147 deletions(-) diff --git a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java index e671b76..90f6787 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java @@ -15,56 +15,37 @@ */ package com.corundumstudio.socketio.handler; -import static io.netty.handler.codec.http.HttpHeaders.setContentLength; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import io.netty.buffer.ByteBuf; 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.Sharable; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.DefaultFileRegion; -import io.netty.channel.FileRegion; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.stream.ChunkedFile; +import io.netty.handler.stream.ChunkedStream; +import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.CharsetUtil; -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.io.InputStream; import java.util.HashMap; -import java.util.Locale; import java.util.Map; -import java.util.TimeZone; - -import javax.activation.MimetypesFileTypeMap; @Sharable -public class ResourceHandler extends ChannelInboundHandlerAdapter { +public class ResourceHandler extends ChunkedWriteHandler { 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(); + private final Map resources = new HashMap(); public ResourceHandler(String context) { addResource(context + "/static/flashsocket/WebSocketMain.swf", @@ -74,12 +55,7 @@ public class ResourceHandler extends ChannelInboundHandlerAdapter { } public void addResource(String pathPart, String resourcePath) { - URL resource = getClass().getResource(resourcePath); - // in case of usage exclude-swf-files profile - if (resource != null) { - File file = new File(resource.getFile()); - resources.put(pathPart, file); - } + resources.put(pathPart, resourcePath); } @Override @@ -88,139 +64,39 @@ public class ResourceHandler extends ChannelInboundHandlerAdapter { if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); - File resource = resources.get(queryDecoder.path()); + String resource = resources.get(queryDecoder.path()); if (resource != null) { + // create ok response HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); - - if (isNotModified(req, resource)) { - sendNotModified(ctx); - req.release(); - return; - } - - RandomAccessFile raf; - try { - raf = new RandomAccessFile(resource, "r"); - } catch (FileNotFoundException fnfe) { + // set content type + HttpHeaders.setHeader(res, HttpHeaders.Names.CONTENT_TYPE, "application/octet-stream"); + // write header + ctx.write(res); + // create resource inputstream and check + InputStream is = getClass().getResourceAsStream(resource); + if (is == null) { sendError(ctx, NOT_FOUND); return; } - long fileLength = raf.length(); - - setContentLength(res, fileLength); - setContentTypeHeader(res, resource); - setDateAndCacheHeaders(res, resource); - // write the response header - ctx.write(res); - // write the content to the channel - ChannelFuture writeFuture = writeContent(raf, fileLength, ctx.channel()); - // close the request channel + // write the stream + ChannelFuture writeFuture = ctx.channel().write(new ChunkedStream(is)); + // close the channel on finish writeFuture.addListener(ChannelFutureListener.CLOSE); return; } } - super.channelRead(ctx, msg); + ctx.fireChannelRead(msg); } - - private boolean isNotModified(HttpRequest request, File file) throws ParseException { - String ifModifiedSince = request.headers().get(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.channel().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(); - HttpHeaders.setHeader(response, HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); - } - - private ChannelFuture writeContent(RandomAccessFile raf, long fileLength, Channel ch) throws IOException { - ChannelFuture writeFuture; - if (ch.pipeline().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); - } - - return writeFuture; - } private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { - HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); + + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); HttpHeaders.setHeader(response, CONTENT_TYPE, "text/plain; charset=UTF-8"); - ByteBuf content = Unpooled.copiedBuffer( - "Failure: " + status.toString() + "\r\n", - CharsetUtil.UTF_8); - + ByteBuf content = Unpooled.copiedBuffer( "Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8); + // write response ctx.channel().write(response); - // Close the connection as soon as the error message is sent. ctx.channel().write(content).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(); - HttpHeaders.setHeader(response, HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); - - // Add cache headers - time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); - HttpHeaders.setHeader(response, HttpHeaders.Names.EXPIRES, dateFormatter.format(time.getTime())); - HttpHeaders.setHeader(response, HttpHeaders.Names.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); - HttpHeaders.setHeader(response, - 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(); - HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); - } } From 5b47df9cc0e7450644f35cfa49eb2aec15272ae7 Mon Sep 17 00:00:00 2001 From: Ivan Greguric Ortolan Date: Sat, 16 Nov 2013 15:59:50 +0100 Subject: [PATCH 2/3] modified the resource handler to reenable caching on resources, added javax.activation to the dependecies (required for mimetype check to work) --- pom.xml | 5 + .../socketio/handler/ResourceHandler.java | 178 +++++++++++++++--- 2 files changed, 159 insertions(+), 24 deletions(-) diff --git a/pom.xml b/pom.xml index f1b9536..5f7b271 100644 --- a/pom.xml +++ b/pom.xml @@ -126,6 +126,11 @@ jackson-databind 2.2.3 + + javax.activation + activation + 1.1-rev-1 + diff --git a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java index 90f6787..111b889 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java @@ -15,6 +15,7 @@ */ package com.corundumstudio.socketio.handler; +import static io.netty.handler.codec.http.HttpHeaders.setContentLength; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; @@ -27,76 +28,205 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.stream.ChunkedStream; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.CharsetUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; @Sharable public class ResourceHandler extends ChunkedWriteHandler { + + private final Logger log = LoggerFactory.getLogger(getClass()); 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(); + 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"); + addResource(context + "/static/flashsocket/WebSocketMain.swf", "/static/flashsocket/WebSocketMain.swf"); + addResource(context + "/static/flashsocket/WebSocketMainInsecure.swf", "/static/flashsocket/WebSocketMainInsecure.swf"); } public void addResource(String pathPart, String resourcePath) { - resources.put(pathPart, resourcePath); + + URL resUrl = getClass().getResource(resourcePath); + if (resUrl == null) { + log.error("The specified resource was not found: " + resourcePath); + return; + } + resources.put(pathPart, resUrl); } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); - String resource = resources.get(queryDecoder.path()); - if (resource != null) { - // create ok response - HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); - // set content type - HttpHeaders.setHeader(res, HttpHeaders.Names.CONTENT_TYPE, "application/octet-stream"); - // write header - ctx.write(res); - // create resource inputstream and check - InputStream is = getClass().getResourceAsStream(resource); + URL resUrl = resources.get(queryDecoder.path()); + if (resUrl != null) { + URLConnection fileUrl = resUrl.openConnection(); + long lastModified = fileUrl.getLastModified(); + // check if file has been modified since last request + if (isNotModified(req, lastModified)) { + sendNotModified(ctx); + req.release(); + return; + } + // create resource input-stream and check existence + final InputStream is = fileUrl.getInputStream(); if (is == null) { sendError(ctx, NOT_FOUND); return; } - // write the stream - ChannelFuture writeFuture = ctx.channel().write(new ChunkedStream(is)); - // close the channel on finish - writeFuture.addListener(ChannelFutureListener.CLOSE); + // create ok response + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); + // set Content-Length header + setContentLength(res, fileUrl.getContentLengthLong()); + // set Content-Type header + setContentTypeHeader(res, fileUrl); + // set Date, Expires, Cache-Control and Last-Modified headers + setDateAndCacheHeaders(res, lastModified); + // write initial response header + ctx.write(res); + // write the content stream + ChannelFuture writeFuture = ctx.channel().write(new ChunkedStream(is, fileUrl.getContentLength())); + // add operation complete listener so we can close the channel and the input stream + writeFuture.addListener(new GenericFutureListener>() { + @Override + public void operationComplete(Future future) throws Exception { + // close the channel and insput stream on finish + ctx.channel().close(); + is.close(); + } + }); return; } } ctx.fireChannelRead(msg); } + + /* + * Checks if the content has been modified sicne the date provided by the IF_MODIFIED_SINCE http header + * */ + private boolean isNotModified(HttpRequest request, long lastModified) throws ParseException { + String ifModifiedSince = request.headers().get(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 = lastModified / 1000; + return ifModifiedSinceDateSeconds == fileLastModifiedSeconds; + } + return false; + } + /* + * Sends a Not Modified response to the client + * + * */ + 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.channel().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(); + HttpHeaders.setHeader(response, HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); + } + + + /** + * Sends an Error response with status message + * + * @param ctx + * @param status + */ private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { - - HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); HttpHeaders.setHeader(response, CONTENT_TYPE, "text/plain; charset=UTF-8"); ByteBuf content = Unpooled.copiedBuffer( "Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8); - // write response + ctx.channel().write(response); // Close the connection as soon as the error message is sent. ctx.channel().write(content).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, long lastModified) { + SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); + dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); + + // Date header + Calendar time = new GregorianCalendar(); + HttpHeaders.setHeader(response, HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); + + // Add cache headers + time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); + HttpHeaders.setHeader(response, HttpHeaders.Names.EXPIRES, dateFormatter.format(time.getTime())); + HttpHeaders.setHeader(response, HttpHeaders.Names.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); + HttpHeaders.setHeader(response, HttpHeaders.Names.LAST_MODIFIED, dateFormatter.format(new Date(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, URLConnection resUrlConnection) { + MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); + String resName = resUrlConnection.getURL().getFile(); + HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, mimeTypesMap.getContentType(resName)); + } } From 7b20ab11a09fef2fd85533f977b61135e4273202 Mon Sep 17 00:00:00 2001 From: Ivan Greguric Ortolan Date: Sat, 16 Nov 2013 16:44:16 +0100 Subject: [PATCH 3/3] fixing flash policy handler, was not working after page reload --- .../socketio/transport/FlashPolicyHandler.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java b/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java index 15f1784..80a817e 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java +++ b/src/main/java/com/corundumstudio/socketio/transport/FlashPolicyHandler.java @@ -27,16 +27,15 @@ import io.netty.util.CharsetUtil; @Sharable public class FlashPolicyHandler extends ChannelInboundHandlerAdapter { - private final ByteBuf requestBuffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer( - "", CharsetUtil.UTF_8)); + private final ByteBuf requestBuffer = Unpooled.copiedBuffer( "", CharsetUtil.UTF_8); - private final ByteBuf responseBuffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer( + private final ByteBuf responseBuffer = Unpooled.copiedBuffer( "" + "" + " " + " " + " " - + "", CharsetUtil.UTF_8)); + + "", CharsetUtil.UTF_8); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { @@ -45,7 +44,7 @@ public class FlashPolicyHandler extends ChannelInboundHandlerAdapter { ByteBuf data = message.slice(0, requestBuffer.readableBytes()); if (data.equals(requestBuffer)) { message.release(); - ChannelFuture f = ctx.writeAndFlush(responseBuffer); + ChannelFuture f = ctx.writeAndFlush(Unpooled.copiedBuffer(responseBuffer)); f.addListener(ChannelFutureListener.CLOSE); return; }