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