View Javadoc

    1   /*
    2    * Copyright 2012 The Netty Project
    3    *
    4    * The Netty Project licenses this file to you under the Apache License,
    5    * version 2.0 (the "License"); you may not use this file except in compliance
    6    * with the License. You may obtain a copy of the License at:
    7    *
    8    *   http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    13   * License for the specific language governing permissions and limitations
    14   * under the License.
    15   */
    16  package org.jboss.netty.example.http.file;
    17
    18  import static org.jboss.netty.handler.codec.http.HttpHeaders.*;
    19  import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
    20  import static org.jboss.netty.handler.codec.http.HttpMethod.*;
    21  import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*;
    22  import static org.jboss.netty.handler.codec.http.HttpVersion.*;
    23
    24  import java.io.File;
    25  import java.io.FileNotFoundException;
    26  import java.io.RandomAccessFile;
    27  import java.io.UnsupportedEncodingException;
    28  import java.net.URLDecoder;
    29  import java.text.SimpleDateFormat;
    30  import java.util.Calendar;
    31  import java.util.Date;
    32  import java.util.GregorianCalendar;
    33  import java.util.Locale;
    34  import java.util.TimeZone;
    35
    36  import javax.activation.MimetypesFileTypeMap;
    37
    38  import org.jboss.netty.buffer.ChannelBuffers;
    39  import org.jboss.netty.channel.Channel;
    40  import org.jboss.netty.channel.ChannelFuture;
    41  import org.jboss.netty.channel.ChannelFutureListener;
    42  import org.jboss.netty.channel.ChannelFutureProgressListener;
    43  import org.jboss.netty.channel.ChannelHandlerContext;
    44  import org.jboss.netty.channel.DefaultFileRegion;
    45  import org.jboss.netty.channel.ExceptionEvent;
    46  import org.jboss.netty.channel.FileRegion;
    47  import org.jboss.netty.channel.MessageEvent;
    48  import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
    49  import org.jboss.netty.handler.codec.frame.TooLongFrameException;
    50  import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
    51  import org.jboss.netty.handler.codec.http.HttpHeaders;
    52  import org.jboss.netty.handler.codec.http.HttpRequest;
    53  import org.jboss.netty.handler.codec.http.HttpResponse;
    54  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
    55  import org.jboss.netty.handler.ssl.SslHandler;
    56  import org.jboss.netty.handler.stream.ChunkedFile;
    57  import org.jboss.netty.util.CharsetUtil;
    58
    59  /**
    60   * A simple handler that serves incoming HTTP requests to send their respective
    61   * HTTP responses.  It also implements {@code 'If-Modified-Since'} header to
    62   * take advantage of browser cache, as described in
    63   * <a href="http://tools.ietf.org/html/rfc2616#section-14.25">RFC 2616</a>.
    64   *
    65   * <h3>How Browser Caching Works</h3>
    66   *
    67   * Web browser caching works with HTTP headers as illustrated by the following
    68   * sample:
    69   * <ol>
    70   * <li>Request #1 returns the content of <code>/file1.txt</code>.</li>
    71   * <li>Contents of <code>/file1.txt</code> is cached by the browser.</li>
    72   * <li>Request #2 for <code>/file1.txt</code> does return the contents of the
    73   *     file again. Rather, a 304 Not Modified is returned. This tells the
    74   *     browser to use the contents stored in its cache.</li>
    75   * <li>The server knows the file has not been modified because the
    76   *     <code>If-Modified-Since</code> date is the same as the file's last
    77   *     modified date.</li>
    78   * </ol>
    79   *
    80   * <pre>
    81   * Request #1 Headers
    82   * ===================
    83   * GET /file1.txt HTTP/1.1
    84   *
    85   * Response #1 Headers
    86   * ===================
    87   * HTTP/1.1 200 OK
    88   * Date:               Tue, 01 Mar 2011 22:44:26 GMT
    89   * Last-Modified:      Wed, 30 Jun 2010 21:36:48 GMT
    90   * Expires:            Tue, 01 Mar 2012 22:44:26 GMT
    91   * Cache-Control:      private, max-age=31536000
    92   *
    93   * Request #2 Headers
    94   * ===================
    95   * GET /file1.txt HTTP/1.1
    96   * If-Modified-Since:  Wed, 30 Jun 2010 21:36:48 GMT
    97   *
    98   * Response #2 Headers
    99   * ===================
    100  * HTTP/1.1 304 Not Modified
    101  * Date:               Tue, 01 Mar 2011 22:44:28 GMT
    102  *
    103  * </pre>
    104  */
    105 public class HttpStaticFileServerHandler extends SimpleChannelUpstreamHandler {
    106
    107     public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
    108     public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
    109     public static final int HTTP_CACHE_SECONDS = 60;
    110
    111     @Override
    112     public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    113         HttpRequest request = (HttpRequest) e.getMessage();
    114         if (request.getMethod() != GET) {
    115             sendError(ctx, METHOD_NOT_ALLOWED);
    116             return;
    117         }
    118
    119         final String path = sanitizeUri(request.getUri());
    120         if (path == null) {
    121             sendError(ctx, FORBIDDEN);
    122             return;
    123         }
    124
    125         File file = new File(path);
    126         if (file.isHidden() || !file.exists()) {
    127             sendError(ctx, NOT_FOUND);
    128             return;
    129         }
    130         if (!file.isFile()) {
    131             sendError(ctx, FORBIDDEN);
    132             return;
    133         }
    134
    135         // Cache Validation
    136         String ifModifiedSince = request.getHeader(HttpHeaders.Names.IF_MODIFIED_SINCE);
    137         if (ifModifiedSince != null && !ifModifiedSince.equals("")) {
    138             SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
    139             Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);
    140
    141             // Only compare up to the second because the datetime format we send to the client does
    142             // not have milliseconds
    143             long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
    144             long fileLastModifiedSeconds = file.lastModified() / 1000;
    145             if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
    146                 sendNotModified(ctx);
    147                 return;
    148             }
    149         }
    150
    151         RandomAccessFile raf;
    152         try {
    153             raf = new RandomAccessFile(file, "r");
    154         } catch (FileNotFoundException fnfe) {
    155             sendError(ctx, NOT_FOUND);
    156             return;
    157         }
    158         long fileLength = raf.length();
    159
    160         HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
    161         setContentLength(response, fileLength);
    162         setContentTypeHeader(response, file);
    163         setDateAndCacheHeaders(response, file);
    164
    165         Channel ch = e.getChannel();
    166
    167         // Write the initial line and the header.
    168         ch.write(response);
    169
    170         // Write the content.
    171         ChannelFuture writeFuture;
    172         if (ch.getPipeline().get(SslHandler.class) != null) {
    173             // Cannot use zero-copy with HTTPS.
    174             writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
    175         } else {
    176             // No encryption - use zero-copy.
    177             final FileRegion region =
    178                 new DefaultFileRegion(raf.getChannel(), 0, fileLength);
    179             writeFuture = ch.write(region);
    180             writeFuture.addListener(new ChannelFutureProgressListener() {
    181                 public void operationComplete(ChannelFuture future) {
    182                     region.releaseExternalResources();
    183                 }
    184
    185                 public void operationProgressed(
    186                         ChannelFuture future, long amount, long current, long total) {
    187                     System.out.printf("%s: %d / %d (+%d)%n", path, current, total, amount);
    188                 }
    189             });
    190         }
    191
    192         // Decide whether to close the connection or not.
    193         if (!isKeepAlive(request)) {
    194             // Close the connection when the whole content is written out.
    195             writeFuture.addListener(ChannelFutureListener.CLOSE);
    196         }
    197     }
    198
    199     @Override
    200     public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
    201             throws Exception {
    202         Channel ch = e.getChannel();
    203         Throwable cause = e.getCause();
    204         if (cause instanceof TooLongFrameException) {
    205             sendError(ctx, BAD_REQUEST);
    206             return;
    207         }
    208
    209         cause.printStackTrace();
    210         if (ch.isConnected()) {
    211             sendError(ctx, INTERNAL_SERVER_ERROR);
    212         }
    213     }
    214
    215     private static String sanitizeUri(String uri) {
    216         // Decode the path.
    217         try {
    218             uri = URLDecoder.decode(uri, "UTF-8");
    219         } catch (UnsupportedEncodingException e) {
    220             try {
    221                 uri = URLDecoder.decode(uri, "ISO-8859-1");
    222             } catch (UnsupportedEncodingException e1) {
    223                 throw new Error();
    224             }
    225         }
    226
    227         // Convert file separators.
    228         uri = uri.replace('/', File.separatorChar);
    229
    230         // Simplistic dumb security check.
    231         // You will have to do something serious in the production environment.
    232         if (uri.contains(File.separator + ".") ||
    233             uri.contains("." + File.separator) ||
    234             uri.startsWith(".") || uri.endsWith(".")) {
    235             return null;
    236         }
    237
    238         // Convert to absolute path.
    239         return System.getProperty("user.dir") + File.separator + uri;
    240     }
    241
    242     private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
    243         HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
    244         response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
    245         response.setContent(ChannelBuffers.copiedBuffer(
    246                 "Failure: " + status.toString() + "\r\n",
    247                 CharsetUtil.UTF_8));
    248
    249         // Close the connection as soon as the error message is sent.
    250         ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
    251     }
    252
    253     /**
    254      * When file timestamp is the same as what the browser is sending up, send a "304 Not Modified"
    255      *
    256      * @param ctx
    257      *            Context
    258      */
    259     private static void sendNotModified(ChannelHandlerContext ctx) {
    260         HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED);
    261         setDateHeader(response);
    262
    263         // Close the connection as soon as the error message is sent.
    264         ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
    265     }
    266
    267     /**
    268      * Sets the Date header for the HTTP response
    269      *
    270      * @param response
    271      *            HTTP response
    272      */
    273     private static void setDateHeader(HttpResponse response) {
    274         SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
    275         dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
    276
    277         Calendar time = new GregorianCalendar();
    278         response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime()));
    279     }
    280
    281     /**
    282      * Sets the Date and Cache headers for the HTTP Response
    283      *
    284      * @param response
    285      *            HTTP response
    286      * @param fileToCache
    287      *            file to extract content type
    288      */
    289     private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) {
    290         SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
    291         dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
    292
    293         // Date header
    294         Calendar time = new GregorianCalendar();
    295         response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime()));
    296
    297         // Add cache headers
    298         time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
    299         response.setHeader(HttpHeaders.Names.EXPIRES, dateFormatter.format(time.getTime()));
    300         response.setHeader(HttpHeaders.Names.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
    301         response.setHeader(
    302                 HttpHeaders.Names.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
    303     }
    304
    305     /**
    306      * Sets the content type header for the HTTP Response
    307      *
    308      * @param response
    309      *            HTTP response
    310      * @param file
    311      *            file to extract content type
    312      */
    313     private static void setContentTypeHeader(HttpResponse response, File file) {
    314         MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
    315         response.setHeader(HttpHeaders.Names.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
    316     }
    317
    318 }