1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.example.http.file;
17
18 import io.netty.buffer.ByteBuf;
19 import io.netty.buffer.Unpooled;
20 import io.netty.channel.ChannelFuture;
21 import io.netty.channel.ChannelFutureListener;
22 import io.netty.channel.ChannelHandlerContext;
23 import io.netty.channel.ChannelProgressiveFuture;
24 import io.netty.channel.ChannelProgressiveFutureListener;
25 import io.netty.channel.DefaultFileRegion;
26 import io.netty.channel.SimpleChannelInboundHandler;
27 import io.netty.handler.codec.http.DefaultFullHttpResponse;
28 import io.netty.handler.codec.http.DefaultHttpResponse;
29 import io.netty.handler.codec.http.FullHttpRequest;
30 import io.netty.handler.codec.http.FullHttpResponse;
31 import io.netty.handler.codec.http.HttpChunkedInput;
32 import io.netty.handler.codec.http.HttpHeaderNames;
33 import io.netty.handler.codec.http.HttpUtil;
34 import io.netty.handler.codec.http.HttpHeaderValues;
35 import io.netty.handler.codec.http.HttpResponse;
36 import io.netty.handler.codec.http.HttpResponseStatus;
37 import io.netty.handler.codec.http.LastHttpContent;
38 import io.netty.handler.ssl.SslHandler;
39 import io.netty.handler.stream.ChunkedFile;
40 import io.netty.util.CharsetUtil;
41 import io.netty.util.internal.SystemPropertyUtil;
42
43 import javax.activation.MimetypesFileTypeMap;
44 import java.io.File;
45 import java.io.FileNotFoundException;
46 import java.io.RandomAccessFile;
47 import java.io.UnsupportedEncodingException;
48 import java.net.URLDecoder;
49 import java.text.SimpleDateFormat;
50 import java.util.Calendar;
51 import java.util.Date;
52 import java.util.GregorianCalendar;
53 import java.util.Locale;
54 import java.util.TimeZone;
55 import java.util.regex.Pattern;
56
57 import static io.netty.handler.codec.http.HttpMethod.*;
58 import static io.netty.handler.codec.http.HttpResponseStatus.*;
59 import static io.netty.handler.codec.http.HttpVersion.*;
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
106
107 public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
108
109 public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
110 public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
111 public static final int HTTP_CACHE_SECONDS = 60;
112
113 private FullHttpRequest request;
114
115 @Override
116 public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
117 this.request = request;
118 if (!request.decoderResult().isSuccess()) {
119 sendError(ctx, BAD_REQUEST);
120 return;
121 }
122
123 if (!GET.equals(request.method())) {
124 sendError(ctx, METHOD_NOT_ALLOWED);
125 return;
126 }
127
128 final boolean keepAlive = HttpUtil.isKeepAlive(request);
129 final String uri = request.uri();
130 final String path = sanitizeUri(uri);
131 if (path == null) {
132 sendError(ctx, FORBIDDEN);
133 return;
134 }
135
136 File file = new File(path);
137 if (file.isHidden() || !file.exists()) {
138 sendError(ctx, NOT_FOUND);
139 return;
140 }
141
142 if (file.isDirectory()) {
143 if (uri.endsWith("/")) {
144 sendListing(ctx, file, uri);
145 } else {
146 sendRedirect(ctx, uri + '/');
147 }
148 return;
149 }
150
151 if (!file.isFile()) {
152 sendError(ctx, FORBIDDEN);
153 return;
154 }
155
156
157 String ifModifiedSince = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
158 if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) {
159 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
160 Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);
161
162
163
164 long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
165 long fileLastModifiedSeconds = file.lastModified() / 1000;
166 if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
167 sendNotModified(ctx);
168 return;
169 }
170 }
171
172 RandomAccessFile raf;
173 try {
174 raf = new RandomAccessFile(file, "r");
175 } catch (FileNotFoundException ignore) {
176 sendError(ctx, NOT_FOUND);
177 return;
178 }
179 long fileLength = raf.length();
180
181 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
182 HttpUtil.setContentLength(response, fileLength);
183 setContentTypeHeader(response, file);
184 setDateAndCacheHeaders(response, file);
185
186 if (!keepAlive) {
187 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
188 } else if (request.protocolVersion().equals(HTTP_1_0)) {
189 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
190 }
191
192
193 ctx.write(response);
194
195
196 ChannelFuture sendFileFuture;
197 ChannelFuture lastContentFuture;
198 if (ctx.pipeline().get(SslHandler.class) == null) {
199 sendFileFuture =
200 ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
201
202 lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
203 } else {
204 sendFileFuture =
205 ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)),
206 ctx.newProgressivePromise());
207
208 lastContentFuture = sendFileFuture;
209 }
210
211 sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
212 @Override
213 public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
214 if (total < 0) {
215 System.err.println(future.channel() + " Transfer progress: " + progress);
216 } else {
217 System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total);
218 }
219 }
220
221 @Override
222 public void operationComplete(ChannelProgressiveFuture future) {
223 System.err.println(future.channel() + " Transfer complete.");
224 }
225 });
226
227
228 if (!keepAlive) {
229
230 lastContentFuture.addListener(ChannelFutureListener.CLOSE);
231 }
232 }
233
234 @Override
235 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
236 cause.printStackTrace();
237 if (ctx.channel().isActive()) {
238 sendError(ctx, INTERNAL_SERVER_ERROR);
239 }
240 }
241
242 private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
243
244 private static String sanitizeUri(String uri) {
245
246 try {
247 uri = URLDecoder.decode(uri, "UTF-8");
248 } catch (UnsupportedEncodingException e) {
249 throw new Error(e);
250 }
251
252 if (uri.isEmpty() || uri.charAt(0) != '/') {
253 return null;
254 }
255
256
257 uri = uri.replace('/', File.separatorChar);
258
259
260
261 if (uri.contains(File.separator + '.') ||
262 uri.contains('.' + File.separator) ||
263 uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' ||
264 INSECURE_URI.matcher(uri).matches()) {
265 return null;
266 }
267
268
269 return SystemPropertyUtil.get("user.dir") + File.separator + uri;
270 }
271
272 private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");
273
274 private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) {
275 StringBuilder buf = new StringBuilder()
276 .append("<!DOCTYPE html>\r\n")
277 .append("<html><head><meta charset='utf-8' /><title>")
278 .append("Listing of: ")
279 .append(dirPath)
280 .append("</title></head><body>\r\n")
281
282 .append("<h3>Listing of: ")
283 .append(dirPath)
284 .append("</h3>\r\n")
285
286 .append("<ul>")
287 .append("<li><a href=\"../\">..</a></li>\r\n");
288
289 File[] files = dir.listFiles();
290 if (files != null) {
291 for (File f: files) {
292 if (f.isHidden() || !f.canRead()) {
293 continue;
294 }
295
296 String name = f.getName();
297 if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
298 continue;
299 }
300
301 buf.append("<li><a href=\"")
302 .append(name)
303 .append("\">")
304 .append(name)
305 .append("</a></li>\r\n");
306 }
307 }
308
309 buf.append("</ul></body></html>\r\n");
310
311 ByteBuf buffer = ctx.alloc().buffer(buf.length());
312 buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8);
313
314 FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer);
315 response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
316
317 sendAndCleanupConnection(ctx, response);
318 }
319
320 private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
321 FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER);
322 response.headers().set(HttpHeaderNames.LOCATION, newUri);
323
324 sendAndCleanupConnection(ctx, response);
325 }
326
327 private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
328 FullHttpResponse response = new DefaultFullHttpResponse(
329 HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
330 response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
331
332 sendAndCleanupConnection(ctx, response);
333 }
334
335
336
337
338
339
340
341 private void sendNotModified(ChannelHandlerContext ctx) {
342 FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER);
343 setDateHeader(response);
344
345 sendAndCleanupConnection(ctx, response);
346 }
347
348
349
350
351
352 private void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response) {
353 final FullHttpRequest request = this.request;
354 final boolean keepAlive = HttpUtil.isKeepAlive(request);
355 HttpUtil.setContentLength(response, response.content().readableBytes());
356 if (!keepAlive) {
357
358
359 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
360 } else if (request.protocolVersion().equals(HTTP_1_0)) {
361 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
362 }
363
364 ChannelFuture flushPromise = ctx.writeAndFlush(response);
365
366 if (!keepAlive) {
367
368 flushPromise.addListener(ChannelFutureListener.CLOSE);
369 }
370 }
371
372
373
374
375
376
377
378 private static void setDateHeader(FullHttpResponse response) {
379 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
380 dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
381
382 Calendar time = new GregorianCalendar();
383 response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
384 }
385
386
387
388
389
390
391
392
393
394 private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) {
395 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
396 dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
397
398
399 Calendar time = new GregorianCalendar();
400 response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
401
402
403 time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
404 response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime()));
405 response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
406 response.headers().set(
407 HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
408 }
409
410
411
412
413
414
415
416
417
418 private static void setContentTypeHeader(HttpResponse response, File file) {
419 MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
420 response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
421 }
422 }