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 com.eincs.decanter.handler; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; 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.HttpMethod.GET; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.io.File; import java.io.FileNotFoundException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Date; import org.apache.commons.io.FilenameUtils; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; 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.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.ssl.SslHandler; import org.jboss.netty.handler.stream.ChunkedFile; import com.eincs.decanter.handler.codec.http.HttpHeaderValues; import com.eincs.decanter.message.DecanterChannels; import com.eincs.decanter.message.DecanterContentType; import com.eincs.decanter.message.DecanterRequest; /** * 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>. * * <h3>How Browser Caching Works</h3> * * Web browser caching works with HTTP headers as illustrated by the following * sample: * <ol> * <li>Request #1 returns the content of <code>/file1.txt</code>.</li> * <li>Contents of <code>/file1.txt</code> is cached by the browser.</li> * <li>Request #2 for <code>/file1.txt</code> 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</code> date is the same as the file's last modified * date.</li> * </ol> * * <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 StaticFileHandler extends SimpleChannelUpstreamHandler { public static final int HTTP_CACHE_SECONDS = 60; private final String path; private final File directory; /** * * @param path * @param directory */ public StaticFileHandler(String path, String directory) { this.path = path; this.directory = new File(directory); this.directory.mkdirs(); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { if (!(e.getMessage() instanceof DecanterRequest)) { super.messageReceived(ctx, e); return; } final DecanterRequest request = (DecanterRequest) e.getMessage(); final String path = request.getPath(); if (!path.startsWith(this.path)) { super.messageReceived(ctx, e); return; } if (request.getMethod() != GET) { DecanterChannels.writeError(ctx, request, METHOD_NOT_ALLOWED); return; } final String subPath = path.substring(this.path.length()); final String filePath = sanitizeUri(directory, subPath); if (filePath == null) { DecanterChannels.writeError(ctx, request, FORBIDDEN); return; } final File file = new File(filePath); if (file.isHidden() || !file.exists()) { DecanterChannels.writeError(ctx, request, NOT_FOUND); return; } if (!file.isFile()) { DecanterChannels.writeError(ctx, request, FORBIDDEN); return; } // Cache Validation String ifModifiedSince = request.getHeaders().get(IF_MODIFIED_SINCE); if (ifModifiedSince != null && ifModifiedSince.length() != 0) { Date ifModifiedSinceDate = HttpHeaderValues.parseDate(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) { DecanterChannels.writeNotModified(ctx, request); return; } } RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException fnfe) { DecanterChannels.writeError(ctx, request, NOT_FOUND); return; } long fileLength = raf.length(); // Add cache headers long timeMillis = System.currentTimeMillis(); long expireMillis = timeMillis + HTTP_CACHE_SECONDS * 1000; HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); response.setHeader(CONTENT_TYPE, DecanterContentType.create(file)); response.setHeader(CONTENT_LENGTH, String.valueOf(fileLength)); response.setHeader(DATE, HttpHeaderValues.getCurrentDate()); response.setHeader(EXPIRES, HttpHeaderValues.getDateString(expireMillis)); response.setHeader(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); response.setHeader(LAST_MODIFIED, HttpHeaderValues.getDateString(file.lastModified())); Channel ch = e.getChannel(); // Write the initial line and the header. ch.write(response); // Write the content. 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(); } public void operationProgressed(ChannelFuture future, long amount, long current, long total) { System.out.printf("%s: %d / %d (+%d)%n", filePath, current, total, amount); } }); } } private static String sanitizeUri(File directory, 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(); } } // Convert file separators. uri = uri.replace('/', File.separatorChar); // Simplistic dumb security check. // You will have to do something serious in the production environment. if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".")) { return null; } // Convert to absolute path. String filePath = new File(directory, uri).getAbsolutePath(); return FilenameUtils.normalize(filePath); } }