Java tutorial
/* * Copyright 2012 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 ro.ieugen.fileserver.http; import static com.google.common.base.Preconditions.checkNotNull; import org.apache.commons.io.Charsets; import org.apache.commons.io.FilenameUtils; import org.jboss.netty.buffer.ChannelBuffer; 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.ExceptionEvent; import org.jboss.netty.channel.FileRegion; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.handler.codec.frame.TooLongFrameException; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.DATE; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.EXPIRES; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.IF_MODIFIED_SINCE; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.LAST_MODIFIED; import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive; import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import org.jboss.netty.handler.ssl.SslHandler; import org.jboss.netty.handler.stream.ChunkedFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.ieugen.fileserver.DirectoryRenderer; import ro.ieugen.fileserver.MustacheTemplate; import ro.ieugen.fileserver.config.DefaultServerConfiguration; import static ro.ieugen.fileserver.http.HandlerUtils.sendError; import javax.activation.MimetypesFileTypeMap; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; /** * A simple handler that serves incoming HTTP requests to send their respective * HTTP responses. It also implements {@code 'If-Modified-Since'} header to * take advantage of browser cache, as described in * <a href="http://tools.ietf.org/html/rfc2616#section-14.25">RFC 2616</a>. * <p/> * <h3>How Browser Caching Works</h3> * <p/> * Web browser caching works with HTTP headers as illustrated by the following * sample: * <ol> * <li>Request #1 returns the content of {@code /file1.txt}.</li> * <li>Contents of {@code /file1.txt} is cached by the browser.</li> * <li>Request #2 for {@code /file1.txt} does return the contents of the * file again. Rather, a 304 Not Modified is returned. This tells the * browser to use the contents stored in its cache.</li> * <li>The server knows the file has not been modified because the * {@code If-Modified-Since} date is the same as the file's last * modified date.</li> * </ol> * <p/> * <pre> * Request #1 Headers * =================== * GET /file1.txt HTTP/1.1 * * Response #1 Headers * =================== * HTTP/1.1 200 OK * Date: Tue, 01 Mar 2011 22:44:26 GMT * Last-Modified: Wed, 30 Jun 2010 21:36:48 GMT * Expires: Tue, 01 Mar 2012 22:44:26 GMT * Cache-Control: private, max-age=31536000 * * Request #2 Headers * =================== * GET /file1.txt HTTP/1.1 * If-Modified-Since: Wed, 30 Jun 2010 21:36:48 GMT * * Response #2 Headers * =================== * HTTP/1.1 304 Not Modified * Date: Tue, 01 Mar 2011 22:44:28 GMT * * </pre> */ public class HttpStaticFileServerHandler 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 static final Logger LOG = LoggerFactory.getLogger(HttpStaticFileServer.class); private DirectoryRenderer directoryRenderer; private final URI rootUri; public HttpStaticFileServerHandler(DefaultServerConfiguration serverConfiguration) { checkNotNull(serverConfiguration, "Root directory is null"); this.rootUri = serverConfiguration.getCanonicalRoot(); directoryRenderer = new DirectoryRenderer(rootUri, new MustacheTemplate(serverConfiguration.getTemplate())); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { HttpRequest request = (HttpRequest) e.getMessage(); final String path = sanitizeUri(request.getUri()); LOG.info("Requested URI is {}, sanitised path is {}", request.getUri(), path); if (path == null) { sendError(ctx, FORBIDDEN); return; } File file = new File(path); if (file.isHidden() || !file.exists()) { sendError(ctx, NOT_FOUND); return; } if (!(file.isFile() || file.isDirectory())) { sendError(ctx, FORBIDDEN); return; } Channel ch = e.getChannel(); ChannelFuture writeFuture = null; if (file.isDirectory()) { ch.write(directoryListingResponse(file)); } else { // Cache Validation if (sendRetrieveFromCacheIfNotModified(ctx, request, file)) return; RandomAccessFile raf = openFileOrSendError(ctx, file); if (raf == null) return; // Write the initial line and the header. ch.write(fileDownloadResponse(file, raf)); // Write the content. if (ch.getPipeline().get(SslHandler.class) != null) { // Cannot use zero-copy with HTTPS. writeFuture = ch.write(new ChunkedFile(raf, 0, raf.length(), 8192)); } else { // No encryption - use zero-copy. final FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, raf.length()); writeFuture = ch.write(region); writeFuture.addListener(new ChannelFutureProgressListener() { public void operationComplete(ChannelFuture future) { region.releaseExternalResources(); } public void operationProgressed(ChannelFuture future, long amount, long current, long total) { LOG.info("{}", String.format("%s: %d / %d (+%d)%n", path, current, total, amount)); } }); } } // Decide whether to close the connection or not. if (!isKeepAlive(request)) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } private HttpResponse fileDownloadResponse(File file, RandomAccessFile raf) throws IOException { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); long fileLength = raf.length(); setContentLength(response, fileLength); setContentTypeHeader(response, file); setDateAndCacheHeaders(response, file); return response; } private HttpResponse directoryListingResponse(File file) { final HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); String directoryListing = directoryRenderer.renderDirectory(file); ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(); buffer.writeBytes(directoryListing.getBytes(Charsets.UTF_8)); response.setContent(buffer); setContentLength(response, buffer.readableBytes()); response.setHeader("Content-type", "text/html"); return response; } private RandomAccessFile openFileOrSendError(ChannelHandlerContext ctx, File file) { RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException fnfe) { sendError(ctx, NOT_FOUND); return null; } return raf; } private boolean sendRetrieveFromCacheIfNotModified(ChannelHandlerContext ctx, HttpRequest request, File file) throws ParseException { String ifModifiedSince = request.getHeader(IF_MODIFIED_SINCE); if (ifModifiedSince != null && ifModifiedSince.length() != 0) { 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; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { sendNotModified(ctx); return true; } } return false; } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { Channel ch = e.getChannel(); Throwable cause = e.getCause(); if (cause instanceof TooLongFrameException) { sendError(ctx, BAD_REQUEST); return; } cause.printStackTrace(); if (ch.isConnected()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } protected String sanitizeUri(String uri) { // Decode the path. try { uri = URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { try { uri = URLDecoder.decode(uri, "ISO-8859-1"); } catch (UnsupportedEncodingException e1) { throw new Error(); } } String normalizedRelativePath = FilenameUtils.normalize(uri); // Convert to absolute path. return new File(new File(rootUri), normalizedRelativePath).getAbsolutePath(); } /** * When file timestamp is the same as what the browser is sending up, send a "304 Not Modified" * * @param ctx Context */ private static void sendNotModified(ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, 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 static 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(DATE, dateFormatter.format(time.getTime())); } /** * Sets the Date and Cache headers for the HTTP Response * * @param response HTTP response * @param fileToCache file to extract content type */ private static 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(DATE, dateFormatter.format(time.getTime())); // Add cache headers time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); response.setHeader(EXPIRES, dateFormatter.format(time.getTime())); response.setHeader(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); response.setHeader(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 static void setContentTypeHeader(HttpResponse response, File file) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); response.setHeader(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); } }