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 }