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