View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.servlets;
20  
21  import java.io.BufferedOutputStream;
22  import java.io.BufferedReader;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileOutputStream;
26  import java.io.FilterInputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.io.OutputStream;
31  import java.io.UnsupportedEncodingException;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.Enumeration;
35  import java.util.HashMap;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  
41  import javax.servlet.Filter;
42  import javax.servlet.FilterChain;
43  import javax.servlet.FilterConfig;
44  import javax.servlet.ServletContext;
45  import javax.servlet.ServletException;
46  import javax.servlet.ServletRequest;
47  import javax.servlet.ServletResponse;
48  import javax.servlet.http.HttpServletRequest;
49  import javax.servlet.http.HttpServletRequestWrapper;
50  
51  import org.eclipse.jetty.http.MimeTypes;
52  import org.eclipse.jetty.io.ByteArrayBuffer;
53  import org.eclipse.jetty.util.B64Code;
54  import org.eclipse.jetty.util.LazyList;
55  import org.eclipse.jetty.util.MultiMap;
56  import org.eclipse.jetty.util.QuotedStringTokenizer;
57  import org.eclipse.jetty.util.ReadLineInputStream;
58  import org.eclipse.jetty.util.StringUtil;
59  import org.eclipse.jetty.util.TypeUtil;
60  import org.eclipse.jetty.util.log.Log;
61  import org.eclipse.jetty.util.log.Logger;
62  
63  /* ------------------------------------------------------------ */
64  /**
65   * Multipart Form Data Filter.
66   * <p>
67   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
68   * item.  Any files sent are stored to a temporary file and a File object added to the request 
69   * as an attribute.  All other values are made available via the normal getParameter API and
70   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
71   * <p>
72   * If the init parameter "delete" is set to "true", any files created will be deleted when the
73   * current request returns.
74   * <p>
75   * The init parameter maxFormKeys sets the maximum number of keys that may be present in a 
76   * form (default set by system property org.eclipse.jetty.server.Request.maxFormKeys or 1000) to protect 
77   * against DOS attacks by bad hash keys. 
78   * <p>
79   * The init parameter deleteFiles controls if uploaded files are automatically deleted after the request
80   * completes.
81   * 
82   */
83  public class MultiPartFilter implements Filter
84  {
85      private static final Logger LOG = Log.getLogger(MultiPartFilter.class);
86      public final static String CONTENT_TYPE_SUFFIX=".org.eclipse.jetty.servlet.contentType";
87      private final static String FILES ="org.eclipse.jetty.servlet.MultiPartFilter.files";
88      private File tempdir;
89      private boolean _deleteFiles;
90      private ServletContext _context;
91      private int _fileOutputBuffer = 0;
92      private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys",1000).intValue();
93  
94      /* ------------------------------------------------------------------------------- */
95      /**
96       * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
97       */
98      public void init(FilterConfig filterConfig) throws ServletException
99      {
100         tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
101         _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
102         String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
103         if(fileOutputBuffer!=null)
104             _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
105         _context=filterConfig.getServletContext();
106         String mfks = filterConfig.getInitParameter("maxFormKeys");
107         if (mfks!=null)
108             _maxFormKeys=Integer.parseInt(mfks);
109     }
110 
111     /* ------------------------------------------------------------------------------- */
112     /**
113      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
114      *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
115      */
116     public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) 
117         throws IOException, ServletException
118     {
119         HttpServletRequest srequest=(HttpServletRequest)request;
120         if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
121         {
122             chain.doFilter(request,response);
123             return;
124         }
125 
126         InputStream in = new ReadLineInputStream(request.getInputStream());
127         String content_type=srequest.getContentType();
128 
129         // TODO - handle encodings
130         String contentTypeBoundary = "";
131         int bstart = content_type.indexOf("boundary=");
132         if (bstart >= 0)
133         {
134             int bend = content_type.indexOf(";", bstart);
135             bend = (bend < 0? content_type.length(): bend);
136             contentTypeBoundary = QuotedStringTokenizer.unquote(value(content_type.substring(bstart,bend)).trim());
137         }
138 
139         String boundary="--"+contentTypeBoundary;
140         
141         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
142         
143         MultiMap params = new MultiMap();
144         for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
145         {
146             Map.Entry entry=(Map.Entry)i.next();
147             Object value=entry.getValue();
148             if (value instanceof String[])
149                 params.addValues(entry.getKey(),(String[])value);
150             else
151                 params.add(entry.getKey(),value);
152         }
153         
154         try
155         {
156             // Get first boundary
157             String line=((ReadLineInputStream)in).readLine();
158 
159             if (line == null)
160                 throw new IOException("Missing content for multipart request");
161 
162             line = line.trim();
163             boolean badFormatLogged = false;
164             while (line != null && !line.equals(boundary))
165             {
166                 if (!badFormatLogged)
167                 {
168                     LOG.warn("Badly formatted multipart request");
169                     badFormatLogged = true;
170                 }
171                 line=((ReadLineInputStream)in).readLine();
172                 line=(line==null?line:line.trim());
173             }
174             
175             if (line == null)
176                 throw new IOException("Missing initial multi part boundary");
177             
178             // Read each part
179             boolean lastPart=false;
180       
181             outer:while(!lastPart && params.size()<_maxFormKeys)
182             {
183                 String type_content=null;
184                 String content_disposition=null;
185                 String content_transfer_encoding=null;
186                 
187                 while(true)
188                 {
189                     // read a line
190                     line=((ReadLineInputStream)in).readLine();
191                     
192                     //No more input
193                     if (line==null)
194                         break outer;
195                     
196                     // If blank line, end of part headers
197                     if("".equals(line))
198                         break;
199                     
200                     // place part header key and value in map
201                     int c=line.indexOf(':',0);
202                     if(c>0)
203                     {
204                         String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
205                         String value=line.substring(c+1,line.length()).trim();
206                         if(key.equals("content-disposition"))
207                             content_disposition=value;
208                         else if(key.equals("content-transfer-encoding"))
209                             content_transfer_encoding=value;
210                         else if (key.equals("content-type"))
211                              type_content = value;  
212                     }
213                 }
214                 // Extract content-disposition
215                 boolean form_data=false;
216                 if(content_disposition==null)
217                 {
218                     throw new IOException("Missing content-disposition");
219                 }
220                 
221                 LOG.debug("Content-Disposition: {}", content_disposition);
222                 QuotedStringTokenizer tok=new QuotedStringTokenizer(content_disposition,";",false,true);
223                 String name=null;
224                 String filename=null;
225                 while(tok.hasMoreTokens())
226                 {
227                     String t=tok.nextToken().trim();
228                     String tl=t.toLowerCase();
229                     if(t.startsWith("form-data"))
230                         form_data=true;
231                     else if(tl.startsWith("name="))
232                         name=value(t);
233                     else if(tl.startsWith("filename="))
234                         filename=filenameValue(t);
235                 }
236                 
237                 // Check disposition
238                 if(!form_data)
239                 {
240                     continue;
241                 }
242                 //It is valid for reset and submit buttons to have an empty name.
243                 //If no name is supplied, the browser skips sending the info for that field.
244                 //However, if you supply the empty string as the name, the browser sends the
245                 //field, with name as the empty string. So, only continue this loop if we
246                 //have not yet seen a name field.
247                 if(name==null)
248                 {
249                     continue;
250                 }
251                 
252                 OutputStream out=null;
253                 File file=null;
254                 try
255                 {
256                     if (filename!=null && filename.length()>0)
257                     {
258                         LOG.debug("filename = \"{}\"", filename);
259                         file = File.createTempFile("MultiPart", "", tempdir);
260                         out = new FileOutputStream(file);
261                         if(_fileOutputBuffer>0)
262                             out = new BufferedOutputStream(out, _fileOutputBuffer);
263                         request.setAttribute(name,file);
264                         params.add(name, filename);
265                         if (type_content != null)
266                             params.add(name+CONTENT_TYPE_SUFFIX, type_content);
267                         
268                         if (_deleteFiles)
269                         {
270                             file.deleteOnExit();
271                             ArrayList files = (ArrayList)request.getAttribute(FILES);
272                             if (files==null)
273                             {
274                                 files=new ArrayList();
275                                 request.setAttribute(FILES,files);
276                             }
277                             files.add(file);
278                         }   
279                     }
280                     else
281                     {
282                         out=new ByteArrayOutputStream();
283                     }
284                     
285                     
286                     if ("base64".equalsIgnoreCase(content_transfer_encoding))
287                     {
288                         in = new Base64InputStream((ReadLineInputStream)in);   
289                     }
290                     else if ("quoted-printable".equalsIgnoreCase(content_transfer_encoding))
291                     {
292                         in = new FilterInputStream(in)
293                         {
294                             @Override
295                             public int read() throws IOException
296                             {
297                                 int c = in.read();
298                                 if (c >= 0 && c == '=')
299                                 {
300                                     int hi = in.read();
301                                     int lo = in.read();
302                                     if (hi < 0 || lo < 0)
303                                     {
304                                         throw new IOException("Unexpected end to quoted-printable byte");
305                                     }
306                                     char[] chars = new char[] { (char)hi, (char)lo };
307                                     c = Integer.parseInt(new String(chars),16);
308                                 }
309                                 return c;
310                             }
311                         };
312                     }
313 
314                     int state=-2;
315                     int c;
316                     boolean cr=false;
317                     boolean lf=false;
318                     
319                     // loop for all lines`
320                     while(true)
321                     {
322                         int b=0;
323                         while((c=(state!=-2)?state:in.read())!=-1)
324                         {
325                             state=-2;
326                             // look for CR and/or LF
327                             if(c==13||c==10)
328                             {
329                                 if(c==13)
330                                 {
331                                     in.mark(1);
332                                     int tmp=in.read();
333                                     if (tmp!=10)
334                                         in.reset();
335                                     else
336                                         state=tmp;
337                                 }
338                                 break;
339                             }
340                             // look for boundary
341                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
342                                 b++;
343                             else
344                             {
345                                 // this is not a boundary
346                                 if(cr)
347                                     out.write(13);
348                                 if(lf)
349                                     out.write(10);
350                                 cr=lf=false;
351                                 if(b>0)
352                                     out.write(byteBoundary,0,b);
353                                 b=-1;
354                                 out.write(c);
355                             }
356                         }
357                         // check partial boundary
358                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
359                         {
360                             if(cr)
361                                 out.write(13);
362                             if(lf)
363                                 out.write(10);
364                             cr=lf=false;
365                             out.write(byteBoundary,0,b);
366                             b=-1;
367                         }
368                         // boundary match
369                         if(b>0||c==-1)
370                         {
371                             if(b==byteBoundary.length)
372                                 lastPart=true;
373                             if(state==10)
374                                 state=-2;
375                             break;
376                         }
377                         // handle CR LF
378                         if(cr)
379                             out.write(13);
380                         if(lf)
381                             out.write(10);
382                         cr=(c==13);
383                         lf=(c==10||state==10);
384                         if(state==10)
385                             state=-2;
386                     }
387                 }
388                 finally
389                 {
390                     out.close();
391                 }
392                 
393                 if (file==null)
394                 {
395                     byte[] bytes = ((ByteArrayOutputStream)out).toByteArray();
396                     params.add(name,bytes);
397                     if (type_content != null)
398                         params.add(name+CONTENT_TYPE_SUFFIX, type_content);
399                 }
400             }
401         
402             // handle request
403             chain.doFilter(new Wrapper(srequest,params),response);
404         }
405         finally
406         {
407             deleteFiles(request);
408         }
409     }
410 
411     private void deleteFiles(ServletRequest request)
412     {
413         ArrayList files = (ArrayList)request.getAttribute(FILES);
414         if (files!=null)
415         {
416             Iterator iter = files.iterator();
417             while (iter.hasNext())
418             {
419                 File file=(File)iter.next();
420                 try
421                 {
422                     file.delete();
423                 }
424                 catch(Exception e)
425                 {
426                     _context.log("failed to delete "+file,e);
427                 }
428             }
429         }
430     }
431     
432     /* ------------------------------------------------------------ */
433     private String value(String nameEqualsValue)
434     {
435         int idx = nameEqualsValue.indexOf('=');
436         String value = nameEqualsValue.substring(idx+1).trim();
437         return QuotedStringTokenizer.unquoteOnly(value);
438     }
439     
440     
441     /* ------------------------------------------------------------ */
442     private String filenameValue(String nameEqualsValue)
443     {
444         int idx = nameEqualsValue.indexOf('=');
445         String value = nameEqualsValue.substring(idx+1).trim();   
446 
447         if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
448         {
449             //incorrectly escaped IE filenames that have the whole path
450             //we just strip any leading & trailing quotes and leave it as is
451             char first=value.charAt(0);
452             if (first=='"' || first=='\'')
453                 value=value.substring(1);
454             char last=value.charAt(value.length()-1);
455             if (last=='"' || last=='\'')
456                 value = value.substring(0,value.length()-1);
457 
458             return value;
459         }
460         else
461             //unquote the string, but allow any backslashes that don't
462             //form a valid escape sequence to remain as many browsers
463             //even on *nix systems will not escape a filename containing
464             //backslashes
465             return QuotedStringTokenizer.unquoteOnly(value, true);
466     }
467 
468     /* ------------------------------------------------------------------------------- */
469     /**
470      * @see javax.servlet.Filter#destroy()
471      */
472     public void destroy()
473     {
474     }
475 
476     /* ------------------------------------------------------------------------------- */
477     /* ------------------------------------------------------------------------------- */
478     private static class Wrapper extends HttpServletRequestWrapper
479     {
480         String _encoding=StringUtil.__UTF8;
481         MultiMap _params;
482         
483         /* ------------------------------------------------------------------------------- */
484         /** Constructor.
485          * @param request
486          */
487         public Wrapper(HttpServletRequest request, MultiMap map)
488         {
489             super(request);
490             this._params=map;
491         }
492         
493         /* ------------------------------------------------------------------------------- */
494         /**
495          * @see javax.servlet.ServletRequest#getContentLength()
496          */
497         @Override
498         public int getContentLength()
499         {
500             return 0;
501         }
502         
503         /* ------------------------------------------------------------------------------- */
504         /**
505          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
506          */
507         @Override
508         public String getParameter(String name)
509         {
510             Object o=_params.get(name);
511             if (!(o instanceof byte[]) && LazyList.size(o)>0)
512                 o=LazyList.get(o,0);
513             
514             if (o instanceof byte[])
515             {
516                 try
517                 {
518                    return getParameterBytesAsString(name, (byte[])o);
519                 }
520                 catch(Exception e)
521                 {
522                     LOG.warn(e);
523                 }
524             }
525             else if (o!=null)
526                 return String.valueOf(o);
527             return null;
528         }
529         
530         /* ------------------------------------------------------------------------------- */
531         /**
532          * @see javax.servlet.ServletRequest#getParameterMap()
533          */
534         @Override
535         public Map getParameterMap()
536         {
537             Map<String, String[]> cmap = new HashMap<String,String[]>();
538             
539             for ( Object key : _params.keySet() )
540             {
541                 cmap.put((String)key,getParameterValues((String)key));
542             }
543             
544             return Collections.unmodifiableMap(cmap);
545         }
546         
547         /* ------------------------------------------------------------------------------- */
548         /**
549          * @see javax.servlet.ServletRequest#getParameterNames()
550          */
551         @Override
552         public Enumeration getParameterNames()
553         {
554             return Collections.enumeration(_params.keySet());
555         }
556         
557         /* ------------------------------------------------------------------------------- */
558         /**
559          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
560          */
561         @Override
562         public String[] getParameterValues(String name)
563         {
564             List l=_params.getValues(name);
565             if (l==null || l.size()==0)
566                 return new String[0];
567             String[] v = new String[l.size()];
568             for (int i=0;i<l.size();i++)
569             {
570                 Object o=l.get(i);
571                 if (o instanceof byte[])
572                 {
573                     try
574                     {
575                         v[i]=getParameterBytesAsString(name, (byte[])o);
576                     }
577                     catch(Exception e)
578                     {
579                         throw new RuntimeException(e);
580                     }
581                 }
582                 else if (o instanceof String)
583                     v[i]=(String)o;
584             }
585             return v;
586         }
587         
588         /* ------------------------------------------------------------------------------- */
589         /**
590          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
591          */
592         @Override
593         public void setCharacterEncoding(String enc) 
594             throws UnsupportedEncodingException
595         {
596             _encoding=enc;
597         }
598         
599         
600         /* ------------------------------------------------------------------------------- */
601         private String getParameterBytesAsString (String name, byte[] bytes) 
602         throws UnsupportedEncodingException
603         {
604             //check if there is a specific encoding for the parameter
605             Object ct = _params.get(name+CONTENT_TYPE_SUFFIX);
606             //use default if not
607             String contentType = _encoding;
608             if (ct != null)
609             {
610                 String tmp = MimeTypes.getCharsetFromContentType(new ByteArrayBuffer((String)ct));
611                 contentType = (tmp == null?_encoding:tmp);
612             }
613             
614             return new String(bytes,contentType);
615         }
616     }
617     
618     private static class Base64InputStream extends InputStream
619     {
620         ReadLineInputStream _in;
621         String _line;
622         byte[] _buffer;
623         int _pos;
624         
625         public Base64InputStream (ReadLineInputStream in)
626         {
627             _in = in;
628         }
629 
630         @Override
631         public int read() throws IOException
632         {
633             if (_buffer==null || _pos>= _buffer.length)
634             {
635                 _line = _in.readLine();
636                 System.err.println("LINE: "+_line);
637                 if (_line==null)
638                     return -1;
639                 if (_line.startsWith("--"))
640                     _buffer=(_line+"\r\n").getBytes();
641                 else if (_line.length()==0)
642                     _buffer="\r\n".getBytes();
643                 else
644                 {
645                     ByteArrayOutputStream bout = new ByteArrayOutputStream(4*_line.length()/3);  
646                     B64Code.decode(_line, bout);    
647                     bout.write(13);
648                     bout.write(10);
649                     _buffer = bout.toByteArray();
650                 }
651                 
652                 _pos=0;
653             }
654             return _buffer[_pos++];
655         } 
656     }
657 }