View Javadoc

    1   /*
    2    * Copyright 2009 Red Hat, Inc.
    3    *
    4    * Red Hat licenses this file to you under the Apache License, version 2.0
    5    * (the "License"); you may not use this file except in compliance with the
    6    * 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
    30  import org.jboss.netty.buffer.ChannelBuffers;
    31  import org.jboss.netty.channel.Channel;
    32  import org.jboss.netty.channel.ChannelFuture;
    33  import org.jboss.netty.channel.ChannelFutureListener;
    34  import org.jboss.netty.channel.ChannelFutureProgressListener;
    35  import org.jboss.netty.channel.ChannelHandlerContext;
    36  import org.jboss.netty.channel.DefaultFileRegion;
    37  import org.jboss.netty.channel.ExceptionEvent;
    38  import org.jboss.netty.channel.FileRegion;
    39  import org.jboss.netty.channel.MessageEvent;
    40  import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
    41  import org.jboss.netty.handler.codec.frame.TooLongFrameException;
    42  import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
    43  import org.jboss.netty.handler.codec.http.HttpRequest;
    44  import org.jboss.netty.handler.codec.http.HttpResponse;
    45  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
    46  import org.jboss.netty.handler.ssl.SslHandler;
    47  import org.jboss.netty.handler.stream.ChunkedFile;
    48  import org.jboss.netty.util.CharsetUtil;
    49
    50  /**
    51   * @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
    52   * @author <a href="http://gleamynode.net/">Trustin Lee</a>
    53   */
    54  public class HttpStaticFileServerHandler extends SimpleChannelUpstreamHandler {
    55
    56      @Override
    57      public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    58          HttpRequest request = (HttpRequest) e.getMessage();
    59          if (request.getMethod() != GET) {
    60              sendError(ctx, METHOD_NOT_ALLOWED);
    61              return;
    62          }
    63
    64          final String path = sanitizeUri(request.getUri());
    65          if (path == null) {
    66              sendError(ctx, FORBIDDEN);
    67              return;
    68          }
    69
    70          File file = new File(path);
    71          if (file.isHidden() || !file.exists()) {
    72              sendError(ctx, NOT_FOUND);
    73              return;
    74          }
    75          if (!file.isFile()) {
    76              sendError(ctx, FORBIDDEN);
    77              return;
    78          }
    79
    80          RandomAccessFile raf;
    81          try {
    82              raf = new RandomAccessFile(file, "r");
    83          } catch (FileNotFoundException fnfe) {
    84              sendError(ctx, NOT_FOUND);
    85              return;
    86          }
    87          long fileLength = raf.length();
    88
    89          HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
    90          setContentLength(response, fileLength);
    91
    92          Channel ch = e.getChannel();
    93
    94          // Write the initial line and the header.
    95          ch.write(response);
    96
    97          // Write the content.
    98          ChannelFuture writeFuture;
    99          if (ch.getPipeline().get(SslHandler.class) != null) {
    100             // Cannot use zero-copy with HTTPS.
    101             writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
    102         } else {
    103             // No encryption - use zero-copy.
    104             final FileRegion region =
    105                 new DefaultFileRegion(raf.getChannel(), 0, fileLength);
    106             writeFuture = ch.write(region);
    107             writeFuture.addListener(new ChannelFutureProgressListener() {
    108                 public void operationComplete(ChannelFuture future) {
    109                     region.releaseExternalResources();
    110                 }
    111
    112                 public void operationProgressed(
    113                         ChannelFuture future, long amount, long current, long total) {
    114                     System.out.printf("%s: %d / %d (+%d)%n", path, current, total, amount);
    115                 }
    116             });
    117         }
    118
    119         // Decide whether to close the connection or not.
    120         if (!isKeepAlive(request)) {
    121             // Close the connection when the whole content is written out.
    122             writeFuture.addListener(ChannelFutureListener.CLOSE);
    123         }
    124     }
    125
    126     @Override
    127     public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
    128             throws Exception {
    129         Channel ch = e.getChannel();
    130         Throwable cause = e.getCause();
    131         if (cause instanceof TooLongFrameException) {
    132             sendError(ctx, BAD_REQUEST);
    133             return;
    134         }
    135
    136         cause.printStackTrace();
    137         if (ch.isConnected()) {
    138             sendError(ctx, INTERNAL_SERVER_ERROR);
    139         }
    140     }
    141
    142     private String sanitizeUri(String uri) {
    143         // Decode the path.
    144         try {
    145             uri = URLDecoder.decode(uri, "UTF-8");
    146         } catch (UnsupportedEncodingException e) {
    147             try {
    148                 uri = URLDecoder.decode(uri, "ISO-8859-1");
    149             } catch (UnsupportedEncodingException e1) {
    150                 throw new Error();
    151             }
    152         }
    153
    154         // Convert file separators.
    155         uri = uri.replace('/', File.separatorChar);
    156
    157         // Simplistic dumb security check.
    158         // You will have to do something serious in the production environment.
    159         if (uri.contains(File.separator + ".") ||
    160             uri.contains("." + File.separator) ||
    161             uri.startsWith(".") || uri.endsWith(".")) {
    162             return null;
    163         }
    164
    165         // Convert to absolute path.
    166         return System.getProperty("user.dir") + File.separator + uri;
    167     }
    168
    169     private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
    170         HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
    171         response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
    172         response.setContent(ChannelBuffers.copiedBuffer(
    173                 "Failure: " + status.toString() + "\r\n",
    174                 CharsetUtil.UTF_8));
    175
    176         // Close the connection as soon as the error message is sent.
    177         ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
    178     }
    179 }