/** * Copyright 2012 Ronen Hamias, Anton Kharenko * * 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 io.scalecube.socketio.pipeline; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; 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.HttpUtil; import io.netty.handler.codec.http.HttpVersion; 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.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.activation.MimetypesFileTypeMap; 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; @ChannelHandler.Sharable public class ResourceHandler extends ChannelInboundHandlerAdapter { 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<String, URL> resources = new HashMap<>(); public void addResource(String pathPart, String 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 { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; QueryStringDecoder queryDecoder = new QueryStringDecoder(req.uri()); String requestPath = queryDecoder.path(); URL resUrl = resources.get(requestPath); if (resUrl != null) { if (log.isDebugEnabled()) log.debug("Received HTTP resource request: {} {} from channel: {}", req.method(), requestPath, ctx.channel()); URLConnection fileUrl = resUrl.openConnection(); long lastModified = fileUrl.getLastModified(); // check if file has been modified since last request if (isNotModified(req, lastModified)) { sendNotModified(ctx); return; } // create resource input-stream and check existence final InputStream is = fileUrl.getInputStream(); if (is == null) { sendError(ctx, HttpResponseStatus.NOT_FOUND); return; } // create ok response HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); // set Content-Length header HttpUtil.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 ctx.pipeline().addBefore(ctx.name(), "chunked-writer-handler", new ChunkedWriteHandler()); ChannelFuture writeFuture = ctx.writeAndFlush(new ChunkedStream(is, fileUrl.getContentLength())); // add operation complete listener so we can close the channel and the input stream writeFuture.addListener(ChannelFutureListener.CLOSE); ReferenceCountUtil.release(msg); return; } } super.channelRead(ctx, 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(HttpHeaderNames.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(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_MODIFIED); setDateHeader(response); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(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.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); } /** * Sends an Error response with status message * * @param ctx channel context * @param status status */ private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); ByteBuf content = Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8); ctx.write(response); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(content).addListener(ChannelFutureListener.CLOSE); } /** * Sets the Date and Cache headers for the HTTP Response * * @param response * HTTP response */ 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(); response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); // Add cache headers time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime())); response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); response.headers().set(HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(lastModified))); } /** * Sets the content type header for the HTTP Response * * @param response * HTTP response */ private void setContentTypeHeader(HttpResponse response, URLConnection resUrlConnection) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); String resName = resUrlConnection.getURL().getFile(); response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(resName)); } }