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