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