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 static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
106 static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
107 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.headers().get(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.err.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 Channel ch = e.getChannel();
200 Throwable cause = e.getCause();
201 if (cause instanceof TooLongFrameException) {
202 sendError(ctx, BAD_REQUEST);
203 return;
204 }
205
206 cause.printStackTrace();
207 if (ch.isConnected()) {
208 sendError(ctx, INTERNAL_SERVER_ERROR);
209 }
210 }
211
212 private static String sanitizeUri(String uri) {
213
214 try {
215 uri = URLDecoder.decode(uri, "UTF-8");
216 } catch (UnsupportedEncodingException e) {
217 try {
218 uri = URLDecoder.decode(uri, "ISO-8859-1");
219 } catch (UnsupportedEncodingException e1) {
220 throw new Error();
221 }
222 }
223
224
225 uri = uri.replace('/', File.separatorChar);
226
227
228
229 if (uri.contains(File.separator + '.') ||
230 uri.contains('.' + File.separator) ||
231 uri.startsWith(".") || uri.endsWith(".")) {
232 return null;
233 }
234
235
236 return System.getProperty("user.dir") + File.separator + uri;
237 }
238
239 private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
240 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
241 response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
242 response.setContent(ChannelBuffers.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
243
244
245 ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
246 }
247
248
249
250
251 private static void sendNotModified(ChannelHandlerContext ctx) {
252 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, NOT_MODIFIED);
253 setDateHeader(response);
254
255
256 ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
257 }
258
259
260
261
262 private static void setDateHeader(HttpResponse response) {
263 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
264 dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
265
266 Calendar time = new GregorianCalendar();
267 response.headers().set(DATE, dateFormatter.format(time.getTime()));
268 }
269
270
271
272
273
274
275 private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) {
276 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
277 dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
278
279
280 Calendar time = new GregorianCalendar();
281 response.headers().set(DATE, dateFormatter.format(time.getTime()));
282
283
284 time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
285 response.headers().set(EXPIRES, dateFormatter.format(time.getTime()));
286 response.headers().set(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
287 response.headers().set(LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
288 }
289
290
291
292
293
294
295 private static void setContentTypeHeader(HttpResponse response, File file) {
296 MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
297 response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
298 }
299 }