View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.example.http.upload;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.Channel;
20  import io.netty.channel.ChannelFuture;
21  import io.netty.channel.ChannelFutureListener;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.channel.SimpleChannelInboundHandler;
24  import io.netty.handler.codec.http.DefaultFullHttpResponse;
25  import io.netty.handler.codec.http.FullHttpResponse;
26  import io.netty.handler.codec.http.HttpContent;
27  import io.netty.handler.codec.http.HttpHeaderNames;
28  import io.netty.handler.codec.http.HttpUtil;
29  import io.netty.handler.codec.http.HttpHeaderValues;
30  import io.netty.handler.codec.http.HttpMethod;
31  import io.netty.handler.codec.http.HttpObject;
32  import io.netty.handler.codec.http.HttpRequest;
33  import io.netty.handler.codec.http.HttpResponseStatus;
34  import io.netty.handler.codec.http.HttpVersion;
35  import io.netty.handler.codec.http.LastHttpContent;
36  import io.netty.handler.codec.http.QueryStringDecoder;
37  import io.netty.handler.codec.http.cookie.Cookie;
38  import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
39  import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
40  import io.netty.handler.codec.http.multipart.Attribute;
41  import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
42  import io.netty.handler.codec.http.multipart.DiskAttribute;
43  import io.netty.handler.codec.http.multipart.DiskFileUpload;
44  import io.netty.handler.codec.http.multipart.FileUpload;
45  import io.netty.handler.codec.http.multipart.HttpData;
46  import io.netty.handler.codec.http.multipart.HttpDataFactory;
47  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
48  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
49  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
50  import io.netty.handler.codec.http.multipart.InterfaceHttpData;
51  import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType;
52  import io.netty.util.CharsetUtil;
53  
54  import java.io.IOException;
55  import java.net.URI;
56  import java.util.Collections;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Map.Entry;
60  import java.util.Set;
61  import java.util.logging.Level;
62  import java.util.logging.Logger;
63  
64  import static io.netty.buffer.Unpooled.*;
65  
66  public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObject> {
67  
68      private static final Logger logger = Logger.getLogger(HttpUploadServerHandler.class.getName());
69  
70      private HttpRequest request;
71  
72      private boolean readingChunks;
73  
74      private HttpData partialContent;
75  
76      private final StringBuilder responseContent = new StringBuilder();
77  
78      private static final HttpDataFactory factory =
79              new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // Disk if size exceed
80  
81      private HttpPostRequestDecoder decoder;
82  
83      static {
84          DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file
85                                                           // on exit (in normal
86                                                           // exit)
87          DiskFileUpload.baseDirectory = null; // system temp directory
88          DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on
89                                                          // exit (in normal exit)
90          DiskAttribute.baseDirectory = null; // system temp directory
91      }
92  
93      @Override
94      public void channelInactive(ChannelHandlerContext ctx) throws Exception {
95          if (decoder != null) {
96              decoder.cleanFiles();
97          }
98      }
99  
100     @Override
101     public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
102         if (msg instanceof HttpRequest) {
103             HttpRequest request = this.request = (HttpRequest) msg;
104             URI uri = new URI(request.uri());
105             if (!uri.getPath().startsWith("/form")) {
106                 // Write Menu
107                 writeMenu(ctx);
108                 return;
109             }
110             responseContent.setLength(0);
111             responseContent.append("WELCOME TO THE WILD WILD WEB SERVER\r\n");
112             responseContent.append("===================================\r\n");
113 
114             responseContent.append("VERSION: " + request.protocolVersion().text() + "\r\n");
115 
116             responseContent.append("REQUEST_URI: " + request.uri() + "\r\n\r\n");
117             responseContent.append("\r\n\r\n");
118 
119             // new getMethod
120             for (Entry<String, String> entry : request.headers()) {
121                 responseContent.append("HEADER: " + entry.getKey() + '=' + entry.getValue() + "\r\n");
122             }
123             responseContent.append("\r\n\r\n");
124 
125             // new getMethod
126             Set<Cookie> cookies;
127             String value = request.headers().get(HttpHeaderNames.COOKIE);
128             if (value == null) {
129                 cookies = Collections.emptySet();
130             } else {
131                 cookies = ServerCookieDecoder.STRICT.decode(value);
132             }
133             for (Cookie cookie : cookies) {
134                 responseContent.append("COOKIE: " + cookie + "\r\n");
135             }
136             responseContent.append("\r\n\r\n");
137 
138             QueryStringDecoder decoderQuery = new QueryStringDecoder(request.uri());
139             Map<String, List<String>> uriAttributes = decoderQuery.parameters();
140             for (Entry<String, List<String>> attr: uriAttributes.entrySet()) {
141                 for (String attrVal: attr.getValue()) {
142                     responseContent.append("URI: " + attr.getKey() + '=' + attrVal + "\r\n");
143                 }
144             }
145             responseContent.append("\r\n\r\n");
146 
147             // if GET Method: should not try to create a HttpPostRequestDecoder
148             if (request.method().equals(HttpMethod.GET)) {
149                 // GET Method: should not try to create a HttpPostRequestDecoder
150                 // So stop here
151                 responseContent.append("\r\n\r\nEND OF GET CONTENT\r\n");
152                 // Not now: LastHttpContent will be sent writeResponse(ctx.channel());
153                 return;
154             }
155             try {
156                 decoder = new HttpPostRequestDecoder(factory, request);
157             } catch (ErrorDataDecoderException e1) {
158                 e1.printStackTrace();
159                 responseContent.append(e1.getMessage());
160                 writeResponse(ctx.channel());
161                 ctx.channel().close();
162                 return;
163             }
164 
165             readingChunks = HttpUtil.isTransferEncodingChunked(request);
166             responseContent.append("Is Chunked: " + readingChunks + "\r\n");
167             responseContent.append("IsMultipart: " + decoder.isMultipart() + "\r\n");
168             if (readingChunks) {
169                 // Chunk version
170                 responseContent.append("Chunks: ");
171                 readingChunks = true;
172             }
173         }
174 
175         // check if the decoder was constructed before
176         // if not it handles the form get
177         if (decoder != null) {
178             if (msg instanceof HttpContent) {
179                 // New chunk is received
180                 HttpContent chunk = (HttpContent) msg;
181                 try {
182                     decoder.offer(chunk);
183                 } catch (ErrorDataDecoderException e1) {
184                     e1.printStackTrace();
185                     responseContent.append(e1.getMessage());
186                     writeResponse(ctx.channel());
187                     ctx.channel().close();
188                     return;
189                 }
190                 responseContent.append('o');
191                 // example of reading chunk by chunk (minimize memory usage due to
192                 // Factory)
193                 readHttpDataChunkByChunk();
194                 // example of reading only if at the end
195                 if (chunk instanceof LastHttpContent) {
196                     writeResponse(ctx.channel());
197                     readingChunks = false;
198 
199                     reset();
200                 }
201             }
202         } else {
203             writeResponse(ctx.channel());
204         }
205     }
206 
207     private void reset() {
208         request = null;
209 
210         // destroy the decoder to release all resources
211         decoder.destroy();
212         decoder = null;
213     }
214 
215     /**
216      * Example of reading request by chunk and getting values from chunk to chunk
217      */
218     private void readHttpDataChunkByChunk() {
219         try {
220             while (decoder.hasNext()) {
221                 InterfaceHttpData data = decoder.next();
222                 if (data != null) {
223                     // check if current HttpData is a FileUpload and previously set as partial
224                     if (partialContent == data) {
225                         logger.info(" 100% (FinalSize: " + partialContent.length() + ")");
226                         partialContent = null;
227                     }
228                     // new value
229                     writeHttpData(data);
230                 }
231             }
232             // Check partial decoding for a FileUpload
233             InterfaceHttpData data = decoder.currentPartialHttpData();
234             if (data != null) {
235                 StringBuilder builder = new StringBuilder();
236                 if (partialContent == null) {
237                     partialContent = (HttpData) data;
238                     if (partialContent instanceof FileUpload) {
239                         builder.append("Start FileUpload: ")
240                             .append(((FileUpload) partialContent).getFilename()).append(" ");
241                     } else {
242                         builder.append("Start Attribute: ")
243                             .append(partialContent.getName()).append(" ");
244                     }
245                     builder.append("(DefinedSize: ").append(partialContent.definedLength()).append(")");
246                 }
247                 if (partialContent.definedLength() > 0) {
248                     builder.append(" ").append(partialContent.length() * 100 / partialContent.definedLength())
249                         .append("% ");
250                     logger.info(builder.toString());
251                 } else {
252                     builder.append(" ").append(partialContent.length()).append(" ");
253                     logger.info(builder.toString());
254                 }
255             }
256         } catch (EndOfDataDecoderException e1) {
257             // end
258             responseContent.append("\r\n\r\nEND OF CONTENT CHUNK BY CHUNK\r\n\r\n");
259         }
260     }
261 
262     private void writeHttpData(InterfaceHttpData data) {
263         if (data.getHttpDataType() == HttpDataType.Attribute) {
264             Attribute attribute = (Attribute) data;
265             String value;
266             try {
267                 value = attribute.getValue();
268             } catch (IOException e1) {
269                 // Error while reading data from File, only print name and error
270                 e1.printStackTrace();
271                 responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": "
272                         + attribute.getName() + " Error while reading value: " + e1.getMessage() + "\r\n");
273                 return;
274             }
275             if (value.length() > 100) {
276                 responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": "
277                         + attribute.getName() + " data too long\r\n");
278             } else {
279                 responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": "
280                         + attribute + "\r\n");
281             }
282         } else {
283             responseContent.append("\r\nBODY FileUpload: " + data.getHttpDataType().name() + ": " + data
284                     + "\r\n");
285             if (data.getHttpDataType() == HttpDataType.FileUpload) {
286                 FileUpload fileUpload = (FileUpload) data;
287                 if (fileUpload.isCompleted()) {
288                     if (fileUpload.length() < 10000) {
289                         responseContent.append("\tContent of file\r\n");
290                         try {
291                             responseContent.append(fileUpload.getString(fileUpload.getCharset()));
292                         } catch (IOException e1) {
293                             // do nothing for the example
294                             e1.printStackTrace();
295                         }
296                         responseContent.append("\r\n");
297                     } else {
298                         responseContent.append("\tFile too long to be printed out:" + fileUpload.length() + "\r\n");
299                     }
300                     // fileUpload.isInMemory();// tells if the file is in Memory
301                     // or on File
302                     // fileUpload.renameTo(dest); // enable to move into another
303                     // File dest
304                     // decoder.removeFileUploadFromClean(fileUpload); //remove
305                     // the File of to delete file
306                 } else {
307                     responseContent.append("\tFile to be continued but should not!\r\n");
308                 }
309             }
310         }
311     }
312 
313     private void writeResponse(Channel channel) {
314         // Convert the response content to a ChannelBuffer.
315         ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
316         responseContent.setLength(0);
317 
318         // Decide whether to close the connection or not.
319         boolean close = request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true)
320                 || request.protocolVersion().equals(HttpVersion.HTTP_1_0)
321                 && !request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true);
322 
323         // Build the response object.
324         FullHttpResponse response = new DefaultFullHttpResponse(
325                 HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
326         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
327 
328         if (!close) {
329             // There's no need to add 'Content-Length' header
330             // if this is the last response.
331             response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
332         }
333 
334         Set<Cookie> cookies;
335         String value = request.headers().get(HttpHeaderNames.COOKIE);
336         if (value == null) {
337             cookies = Collections.emptySet();
338         } else {
339             cookies = ServerCookieDecoder.STRICT.decode(value);
340         }
341         if (!cookies.isEmpty()) {
342             // Reset the cookies if necessary.
343             for (Cookie cookie : cookies) {
344                 response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
345             }
346         }
347         // Write the response.
348         ChannelFuture future = channel.writeAndFlush(response);
349         // Close the connection after the write operation is done if necessary.
350         if (close) {
351             future.addListener(ChannelFutureListener.CLOSE);
352         }
353     }
354 
355     private void writeMenu(ChannelHandlerContext ctx) {
356         // print several HTML forms
357         // Convert the response content to a ChannelBuffer.
358         responseContent.setLength(0);
359 
360         // create Pseudo Menu
361         responseContent.append("<html>");
362         responseContent.append("<head>");
363         responseContent.append("<title>Netty Test Form</title>\r\n");
364         responseContent.append("</head>\r\n");
365         responseContent.append("<body bgcolor=white><style>td{font-size: 12pt;}</style>");
366 
367         responseContent.append("<table border=\"0\">");
368         responseContent.append("<tr>");
369         responseContent.append("<td>");
370         responseContent.append("<h1>Netty Test Form</h1>");
371         responseContent.append("Choose one FORM");
372         responseContent.append("</td>");
373         responseContent.append("</tr>");
374         responseContent.append("</table>\r\n");
375 
376         // GET
377         responseContent.append("<CENTER>GET FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
378         responseContent.append("<FORM ACTION=\"/formget\" METHOD=\"GET\">");
379         responseContent.append("<input type=hidden name=getform value=\"GET\">");
380         responseContent.append("<table border=\"0\">");
381         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
382         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
383         responseContent
384                 .append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
385         responseContent.append("</td></tr>");
386         responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
387         responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
388         responseContent.append("</table></FORM>\r\n");
389         responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
390 
391         // POST
392         responseContent.append("<CENTER>POST FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
393         responseContent.append("<FORM ACTION=\"/formpost\" METHOD=\"POST\">");
394         responseContent.append("<input type=hidden name=getform value=\"POST\">");
395         responseContent.append("<table border=\"0\">");
396         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
397         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
398         responseContent
399                 .append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
400         responseContent.append("<tr><td>Fill with file (only file name will be transmitted): <br> "
401                 + "<input type=file name=\"myfile\">");
402         responseContent.append("</td></tr>");
403         responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
404         responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
405         responseContent.append("</table></FORM>\r\n");
406         responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
407 
408         // POST with enctype="multipart/form-data"
409         responseContent.append("<CENTER>POST MULTIPART FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
410         responseContent.append("<FORM ACTION=\"/formpostmultipart\" ENCTYPE=\"multipart/form-data\" METHOD=\"POST\">");
411         responseContent.append("<input type=hidden name=getform value=\"POST\">");
412         responseContent.append("<table border=\"0\">");
413         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
414         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
415         responseContent
416                 .append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
417         responseContent.append("<tr><td>Fill with file: <br> <input type=file name=\"myfile\">");
418         responseContent.append("</td></tr>");
419         responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
420         responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
421         responseContent.append("</table></FORM>\r\n");
422         responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
423 
424         responseContent.append("</body>");
425         responseContent.append("</html>");
426 
427         ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
428         // Build the response object.
429         FullHttpResponse response = new DefaultFullHttpResponse(
430                 HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
431 
432         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
433         response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
434 
435         // Write the response.
436         ctx.channel().writeAndFlush(response);
437     }
438 
439     @Override
440     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
441         logger.log(Level.WARNING, responseContent.toString(), cause);
442         ctx.channel().close();
443     }
444 }