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                     try {
229                         // new value
230                         writeHttpData(data);
231                     } finally {
232                         data.release();
233                     }
234                 }
235             }
236             // Check partial decoding for a FileUpload
237             InterfaceHttpData data = decoder.currentPartialHttpData();
238             if (data != null) {
239                 StringBuilder builder = new StringBuilder();
240                 if (partialContent == null) {
241                     partialContent = (HttpData) data;
242                     if (partialContent instanceof FileUpload) {
243                         builder.append("Start FileUpload: ")
244                             .append(((FileUpload) partialContent).getFilename()).append(" ");
245                     } else {
246                         builder.append("Start Attribute: ")
247                             .append(partialContent.getName()).append(" ");
248                     }
249                     builder.append("(DefinedSize: ").append(partialContent.definedLength()).append(")");
250                 }
251                 if (partialContent.definedLength() > 0) {
252                     builder.append(" ").append(partialContent.length() * 100 / partialContent.definedLength())
253                         .append("% ");
254                     logger.info(builder.toString());
255                 } else {
256                     builder.append(" ").append(partialContent.length()).append(" ");
257                     logger.info(builder.toString());
258                 }
259             }
260         } catch (EndOfDataDecoderException e1) {
261             // end
262             responseContent.append("\r\n\r\nEND OF CONTENT CHUNK BY CHUNK\r\n\r\n");
263         }
264     }
265 
266     private void writeHttpData(InterfaceHttpData data) {
267         if (data.getHttpDataType() == HttpDataType.Attribute) {
268             Attribute attribute = (Attribute) data;
269             String value;
270             try {
271                 value = attribute.getValue();
272             } catch (IOException e1) {
273                 // Error while reading data from File, only print name and error
274                 e1.printStackTrace();
275                 responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": "
276                         + attribute.getName() + " Error while reading value: " + e1.getMessage() + "\r\n");
277                 return;
278             }
279             if (value.length() > 100) {
280                 responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": "
281                         + attribute.getName() + " data too long\r\n");
282             } else {
283                 responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": "
284                         + attribute + "\r\n");
285             }
286         } else {
287             responseContent.append("\r\nBODY FileUpload: " + data.getHttpDataType().name() + ": " + data
288                     + "\r\n");
289             if (data.getHttpDataType() == HttpDataType.FileUpload) {
290                 FileUpload fileUpload = (FileUpload) data;
291                 if (fileUpload.isCompleted()) {
292                     if (fileUpload.length() < 10000) {
293                         responseContent.append("\tContent of file\r\n");
294                         try {
295                             responseContent.append(fileUpload.getString(fileUpload.getCharset()));
296                         } catch (IOException e1) {
297                             // do nothing for the example
298                             e1.printStackTrace();
299                         }
300                         responseContent.append("\r\n");
301                     } else {
302                         responseContent.append("\tFile too long to be printed out:" + fileUpload.length() + "\r\n");
303                     }
304                     // fileUpload.isInMemory();// tells if the file is in Memory
305                     // or on File
306                     // fileUpload.renameTo(dest); // enable to move into another
307                     // File dest
308                     // decoder.removeFileUploadFromClean(fileUpload); //remove
309                     // the File of to delete file
310                 } else {
311                     responseContent.append("\tFile to be continued but should not!\r\n");
312                 }
313             }
314         }
315     }
316 
317     private void writeResponse(Channel channel) {
318         // Convert the response content to a ChannelBuffer.
319         ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
320         responseContent.setLength(0);
321 
322         // Decide whether to close the connection or not.
323         boolean close = request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true)
324                 || request.protocolVersion().equals(HttpVersion.HTTP_1_0)
325                 && !request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true);
326 
327         // Build the response object.
328         FullHttpResponse response = new DefaultFullHttpResponse(
329                 HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
330         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
331 
332         if (!close) {
333             // There's no need to add 'Content-Length' header
334             // if this is the last response.
335             response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
336         }
337 
338         Set<Cookie> cookies;
339         String value = request.headers().get(HttpHeaderNames.COOKIE);
340         if (value == null) {
341             cookies = Collections.emptySet();
342         } else {
343             cookies = ServerCookieDecoder.STRICT.decode(value);
344         }
345         if (!cookies.isEmpty()) {
346             // Reset the cookies if necessary.
347             for (Cookie cookie : cookies) {
348                 response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
349             }
350         }
351         // Write the response.
352         ChannelFuture future = channel.writeAndFlush(response);
353         // Close the connection after the write operation is done if necessary.
354         if (close) {
355             future.addListener(ChannelFutureListener.CLOSE);
356         }
357     }
358 
359     private void writeMenu(ChannelHandlerContext ctx) {
360         // print several HTML forms
361         // Convert the response content to a ChannelBuffer.
362         responseContent.setLength(0);
363 
364         // create Pseudo Menu
365         responseContent.append("<html>");
366         responseContent.append("<head>");
367         responseContent.append("<title>Netty Test Form</title>\r\n");
368         responseContent.append("</head>\r\n");
369         responseContent.append("<body bgcolor=white><style>td{font-size: 12pt;}</style>");
370 
371         responseContent.append("<table border=\"0\">");
372         responseContent.append("<tr>");
373         responseContent.append("<td>");
374         responseContent.append("<h1>Netty Test Form</h1>");
375         responseContent.append("Choose one FORM");
376         responseContent.append("</td>");
377         responseContent.append("</tr>");
378         responseContent.append("</table>\r\n");
379 
380         // GET
381         responseContent.append("<CENTER>GET FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
382         responseContent.append("<FORM ACTION=\"/formget\" METHOD=\"GET\">");
383         responseContent.append("<input type=hidden name=getform value=\"GET\">");
384         responseContent.append("<table border=\"0\">");
385         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
386         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
387         responseContent
388                 .append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
389         responseContent.append("</td></tr>");
390         responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
391         responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
392         responseContent.append("</table></FORM>\r\n");
393         responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
394 
395         // POST
396         responseContent.append("<CENTER>POST FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
397         responseContent.append("<FORM ACTION=\"/formpost\" METHOD=\"POST\">");
398         responseContent.append("<input type=hidden name=getform value=\"POST\">");
399         responseContent.append("<table border=\"0\">");
400         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
401         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
402         responseContent
403                 .append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
404         responseContent.append("<tr><td>Fill with file (only file name will be transmitted): <br> "
405                 + "<input type=file name=\"myfile\">");
406         responseContent.append("</td></tr>");
407         responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
408         responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
409         responseContent.append("</table></FORM>\r\n");
410         responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
411 
412         // POST with enctype="multipart/form-data"
413         responseContent.append("<CENTER>POST MULTIPART FORM<HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
414         responseContent.append("<FORM ACTION=\"/formpostmultipart\" ENCTYPE=\"multipart/form-data\" METHOD=\"POST\">");
415         responseContent.append("<input type=hidden name=getform value=\"POST\">");
416         responseContent.append("<table border=\"0\">");
417         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"info\" size=10></td></tr>");
418         responseContent.append("<tr><td>Fill with value: <br> <input type=text name=\"secondinfo\" size=20>");
419         responseContent
420                 .append("<tr><td>Fill with value: <br> <textarea name=\"thirdinfo\" cols=40 rows=10></textarea>");
421         responseContent.append("<tr><td>Fill with file: <br> <input type=file name=\"myfile\">");
422         responseContent.append("</td></tr>");
423         responseContent.append("<tr><td><INPUT TYPE=\"submit\" NAME=\"Send\" VALUE=\"Send\"></INPUT></td>");
424         responseContent.append("<td><INPUT TYPE=\"reset\" NAME=\"Clear\" VALUE=\"Clear\" ></INPUT></td></tr>");
425         responseContent.append("</table></FORM>\r\n");
426         responseContent.append("<CENTER><HR WIDTH=\"75%\" NOSHADE color=\"blue\"></CENTER>");
427 
428         responseContent.append("</body>");
429         responseContent.append("</html>");
430 
431         ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
432         // Build the response object.
433         FullHttpResponse response = new DefaultFullHttpResponse(
434                 HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
435 
436         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
437         response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
438 
439         // Write the response.
440         ctx.channel().writeAndFlush(response);
441     }
442 
443     @Override
444     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
445         logger.log(Level.WARNING, responseContent.toString(), cause);
446         ctx.channel().close();
447     }
448 }