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