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 org.jboss.netty.buffer.ChannelBuffers;
19 import org.jboss.netty.channel.Channel;
20 import org.jboss.netty.channel.ChannelFuture;
21 import org.jboss.netty.channel.ChannelFutureListener;
22 import org.jboss.netty.channel.ChannelFutureProgressListener;
23 import org.jboss.netty.channel.ChannelHandlerContext;
24 import org.jboss.netty.channel.DefaultFileRegion;
25 import org.jboss.netty.channel.ExceptionEvent;
26 import org.jboss.netty.channel.FileRegion;
27 import org.jboss.netty.channel.MessageEvent;
28 import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
29 import org.jboss.netty.handler.codec.frame.TooLongFrameException;
30 import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
31 import org.jboss.netty.handler.codec.http.HttpRequest;
32 import org.jboss.netty.handler.codec.http.HttpResponse;
33 import org.jboss.netty.handler.codec.http.HttpResponseStatus;
34 import org.jboss.netty.handler.ssl.SslHandler;
35 import org.jboss.netty.handler.stream.ChunkedFile;
36 import org.jboss.netty.util.CharsetUtil;
37
38 import javax.activation.MimetypesFileTypeMap;
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.RandomAccessFile;
42 import java.io.UnsupportedEncodingException;
43 import java.net.URLDecoder;
44 import java.text.SimpleDateFormat;
45 import java.util.Calendar;
46 import java.util.Date;
47 import java.util.GregorianCalendar;
48 import java.util.Locale;
49 import java.util.TimeZone;
50
51 import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
52 import static org.jboss.netty.handler.codec.http.HttpHeaders.*;
53 import static org.jboss.netty.handler.codec.http.HttpMethod.*;
54 import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*;
55 import static org.jboss.netty.handler.codec.http.HttpVersion.*;
56
57
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 public class HttpStaticFileServerHandler extends SimpleChannelUpstreamHandler {
104
105 public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
106 public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
107 public static final int HTTP_CACHE_SECONDS = 60;
108
109 @Override
110 public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
111 HttpRequest request = (HttpRequest) e.getMessage();
112 if (request.getMethod() != GET) {
113 sendError(ctx, METHOD_NOT_ALLOWED);
114 return;
115 }
116
117 final String path = sanitizeUri(request.getUri());
118 if (path == null) {
119 sendError(ctx, FORBIDDEN);
120 return;
121 }
122
123 File file = new File(path);
124 if (file.isHidden() || !file.exists()) {
125 sendError(ctx, NOT_FOUND);
126 return;
127 }
128 if (!file.isFile()) {
129 sendError(ctx, FORBIDDEN);
130 return;
131 }
132
133
134 String ifModifiedSince = request.getHeader(IF_MODIFIED_SINCE);
135 if (ifModifiedSince != null && ifModifiedSince.length() != 0) {
136 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
137 Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);
138
139
140
141 long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
142 long fileLastModifiedSeconds = file.lastModified() / 1000;
143 if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
144 sendNotModified(ctx);
145 return;
146 }
147 }
148
149 RandomAccessFile raf;
150 try {
151 raf = new RandomAccessFile(file, "r");
152 } catch (FileNotFoundException fnfe) {
153 sendError(ctx, NOT_FOUND);
154 return;
155 }
156 long fileLength = raf.length();
157
158 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
159 setContentLength(response, fileLength);
160 setContentTypeHeader(response, file);
161 setDateAndCacheHeaders(response, file);
162
163 Channel ch = e.getChannel();
164
165
166 ch.write(response);
167
168
169 ChannelFuture writeFuture;
170 if (ch.getPipeline().get(SslHandler.class) != null) {
171
172 writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
173 } else {
174
175 final FileRegion region =
176 new DefaultFileRegion(raf.getChannel(), 0, fileLength);
177 writeFuture = ch.write(region);
178 writeFuture.addListener(new ChannelFutureProgressListener() {
179 public void operationComplete(ChannelFuture future) {
180 region.releaseExternalResources();
181 }
182
183 public void operationProgressed(
184 ChannelFuture future, long amount, long current, long total) {
185 System.out.printf("%s: %d / %d (+%d)%n", path, current, total, amount);
186 }
187 });
188 }
189
190
191 if (!isKeepAlive(request)) {
192
193 writeFuture.addListener(ChannelFutureListener.CLOSE);
194 }
195 }
196
197 @Override
198 public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
199 throws Exception {
200 Channel ch = e.getChannel();
201 Throwable cause = e.getCause();
202 if (cause instanceof TooLongFrameException) {
203 sendError(ctx, BAD_REQUEST);
204 return;
205 }
206
207 cause.printStackTrace();
208 if (ch.isConnected()) {
209 sendError(ctx, INTERNAL_SERVER_ERROR);
210 }
211 }
212
213 private static String sanitizeUri(String uri) {
214
215 try {
216 uri = URLDecoder.decode(uri, "UTF-8");
217 } catch (UnsupportedEncodingException e) {
218 try {
219 uri = URLDecoder.decode(uri, "ISO-8859-1");
220 } catch (UnsupportedEncodingException e1) {
221 throw new Error();
222 }
223 }
224
225
226 uri = uri.replace('/', File.separatorChar);
227
228
229
230 if (uri.contains(File.separator + '.') ||
231 uri.contains('.' + File.separator) ||
232 uri.startsWith(".") || uri.endsWith(".")) {
233 return null;
234 }
235
236
237 return System.getProperty("user.dir") + File.separator + uri;
238 }
239
240 private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
241 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
242 response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
243 response.setContent(ChannelBuffers.copiedBuffer(
244 "Failure: " + status.toString() + "\r\n",
245 CharsetUtil.UTF_8));
246
247
248 ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
249 }
250
251
252
253
254
255
256
257 private static void sendNotModified(ChannelHandlerContext ctx) {
258 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, NOT_MODIFIED);
259 setDateHeader(response);
260
261
262 ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
263 }
264
265
266
267
268
269
270
271 private static void setDateHeader(HttpResponse response) {
272 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
273 dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
274
275 Calendar time = new GregorianCalendar();
276 response.setHeader(DATE, dateFormatter.format(time.getTime()));
277 }
278
279
280
281
282
283
284
285
286
287 private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) {
288 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
289 dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
290
291
292 Calendar time = new GregorianCalendar();
293 response.setHeader(DATE, dateFormatter.format(time.getTime()));
294
295
296 time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
297 response.setHeader(EXPIRES, dateFormatter.format(time.getTime()));
298 response.setHeader(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
299 response.setHeader(
300 LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
301 }
302
303
304
305
306
307
308
309
310
311 private static void setContentTypeHeader(HttpResponse response, File file) {
312 MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
313 response.setHeader(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
314 }
315
316 }