1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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
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
142
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
168 ch.write(response);
169
170
171 ChannelFuture writeFuture;
172 if (ch.getPipeline().get(SslHandler.class) != null) {
173
174 writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
175 } else {
176
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
193 if (!isKeepAlive(request)) {
194
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
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
228 uri = uri.replace('/', File.separatorChar);
229
230
231
232 if (uri.contains(File.separator + ".") ||
233 uri.contains("." + File.separator) ||
234 uri.startsWith(".") || uri.endsWith(".")) {
235 return null;
236 }
237
238
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
250 ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
251 }
252
253
254
255
256
257
258
259 private static void sendNotModified(ChannelHandlerContext ctx) {
260 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED);
261 setDateHeader(response);
262
263
264 ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
265 }
266
267
268
269
270
271
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
283
284
285
286
287
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
294 Calendar time = new GregorianCalendar();
295 response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime()));
296
297
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
307
308
309
310
311
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 }