View Javadoc

    1   // ========================================================================
    2   // Copyright 1996-2005 Mort Bay Consulting Pty. Ltd.
    3   // ------------------------------------------------------------------------
    4   // Licensed under the Apache License, Version 2.0 (the "License");
    5   // you may not use this file except in compliance with the License.
    6   // You may obtain a copy of the License at
    7   // http://www.apache.org/licenses/LICENSE-2.0
    8   // Unless required by applicable law or agreed to in writing, software
    9   // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  // ========================================================================
    14  package org.mortbay.servlet;
    15
    16  import java.io.BufferedInputStream;
    17  import java.io.BufferedOutputStream;
    18  import java.io.ByteArrayOutputStream;
    19  import java.io.File;
    20  import java.io.FileOutputStream;
    21  import java.io.IOException;
    22  import java.io.OutputStream;
    23  import java.io.UnsupportedEncodingException;
    24  import java.util.ArrayList;
    25  import java.util.Collections;
    26  import java.util.Enumeration;
    27  import java.util.Iterator;
    28  import java.util.List;
    29  import java.util.Map;
    30  import java.util.StringTokenizer;
    31
    32  import javax.servlet.Filter;
    33  import javax.servlet.FilterChain;
    34  import javax.servlet.FilterConfig;
    35  import javax.servlet.ServletContext;
    36  import javax.servlet.ServletException;
    37  import javax.servlet.ServletRequest;
    38  import javax.servlet.ServletResponse;
    39  import javax.servlet.http.HttpServletRequest;
    40  import javax.servlet.http.HttpServletRequestWrapper;
    41
    42  import org.mortbay.util.LazyList;
    43  import org.mortbay.util.MultiMap;
    44  import org.mortbay.util.StringUtil;
    45  import org.mortbay.util.TypeUtil;
    46
    47  /* ------------------------------------------------------------ */
    48  /**
    49   * Multipart Form Data Filter.
    50   * <p>
    51   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
    52   * item.  Any files sent are stored to a tempary file and a File object added to the request
    53   * as an attribute.  All other values are made available via the normal getParameter API and
    54   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
    55   *
    56   * If the init paramter "delete" is set to "true", any files created will be deleted when the
    57   * current request returns.
    58   *
    59   * @author Greg Wilkins
    60   * @author Jim Crossley
    61   */
    62  public class MultiPartFilter implements Filter
    63  {
    64      private final static String FILES ="org.mortbay.servlet.MultiPartFilter.files";
    65      private File tempdir;
    66      private boolean _deleteFiles;
    67      private ServletContext _context;
    68      private int _fileOutputBuffer = 0;
    69
    70      /* ------------------------------------------------------------------------------- */
    71      /**
    72       * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
    73       */
    74      public void init(FilterConfig filterConfig) throws ServletException
    75      {
    76          tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
    77          _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
    78          String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
    79          if(fileOutputBuffer!=null)
    80              _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
    81          _context=filterConfig.getServletContext();
    82      }
    83
    84      /* ------------------------------------------------------------------------------- */
    85      /**
    86       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
    87       *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
    88       */
    89      public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
    90          throws IOException, ServletException
    91      {
    92          HttpServletRequest srequest=(HttpServletRequest)request;
    93          if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
    94          {
    95              chain.doFilter(request,response);
    96              return;
    97          }
    98
    99          BufferedInputStream in = new BufferedInputStream(request.getInputStream());
    100         String content_type=srequest.getContentType();
    101
    102         // TODO - handle encodings
    103
    104         String boundary="--"+value(content_type.substring(content_type.indexOf("boundary=")));
    105         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
    106
    107         MultiMap params = new MultiMap();
    108         for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
    109         {
    110             Map.Entry entry=(Map.Entry)i.next();
    111             Object value=entry.getValue();
    112             if (value instanceof String[])
    113                 params.addValues(entry.getKey(),(String[])value);
    114             else
    115                 params.add(entry.getKey(),value);
    116         }
    117
    118         try
    119         {
    120             // Get first boundary
    121             byte[] bytes=TypeUtil.readLine(in);
    122             String line=bytes==null?null:new String(bytes,"UTF-8");
    123             if(line==null || !line.equals(boundary))
    124             {
    125                 throw new IOException("Missing initial multi part boundary");
    126             }
    127
    128             // Read each part
    129             boolean lastPart=false;
    130             String content_disposition=null;
    131             while(!lastPart)
    132             {
    133                 while(true)
    134                 {
    135                     bytes=TypeUtil.readLine(in);
    136                     // If blank line, end of part headers
    137                     if(bytes==null || bytes.length==0)
    138                         break;
    139                     line=new String(bytes,"UTF-8");
    140
    141                     // place part header key and value in map
    142                     int c=line.indexOf(':',0);
    143                     if(c>0)
    144                     {
    145                         String key=line.substring(0,c).trim().toLowerCase();
    146                         String value=line.substring(c+1,line.length()).trim();
    147                         if(key.equals("content-disposition"))
    148                             content_disposition=value;
    149                     }
    150                 }
    151                 // Extract content-disposition
    152                 boolean form_data=false;
    153                 if(content_disposition==null)
    154                 {
    155                     throw new IOException("Missing content-disposition");
    156                 }
    157
    158                 StringTokenizer tok=new StringTokenizer(content_disposition,";");
    159                 String name=null;
    160                 String filename=null;
    161                 while(tok.hasMoreTokens())
    162                 {
    163                     String t=tok.nextToken().trim();
    164                     String tl=t.toLowerCase();
    165                     if(t.startsWith("form-data"))
    166                         form_data=true;
    167                     else if(tl.startsWith("name="))
    168                         name=value(t);
    169                     else if(tl.startsWith("filename="))
    170                         filename=value(t);
    171                 }
    172
    173                 // Check disposition
    174                 if(!form_data)
    175                 {
    176                     continue;
    177                 }
    178
    179                 //It is valid for reset and submit buttons to have an empty name.
    180                 //If no name is supplied, the browser skips sending the info for that field.
    181                 //However, if you supply the empty string as the name, the browser sends the
    182                 //field, with name as the empty string. So, only continue this loop if we
    183                 //have not yet seen a name field.
    184                 if(name==null)
    185                 {
    186                     continue;
    187                 }
    188
    189                 OutputStream out=null;
    190                 File file=null;
    191                 try
    192                 {
    193                     if (filename!=null && filename.length()>0)
    194                     {
    195                         file = File.createTempFile("MultiPart", "", tempdir);
    196                         out = new FileOutputStream(file);
    197                         if(_fileOutputBuffer>0)
    198                             out = new BufferedOutputStream(out, _fileOutputBuffer);
    199                         request.setAttribute(name,file);
    200                         params.add(name, filename);
    201
    202                         if (_deleteFiles)
    203                         {
    204                             file.deleteOnExit();
    205                             ArrayList files = (ArrayList)request.getAttribute(FILES);
    206                             if (files==null)
    207                             {
    208                                 files=new ArrayList();
    209                                 request.setAttribute(FILES,files);
    210                             }
    211                             files.add(file);
    212                         }
    213
    214                     }
    215                     else
    216                         out=new ByteArrayOutputStream();
    217
    218                     int state=-2;
    219                     int c;
    220                     boolean cr=false;
    221                     boolean lf=false;
    222
    223                     // loop for all lines`
    224                     while(true)
    225                     {
    226                         int b=0;
    227                         while((c=(state!=-2)?state:in.read())!=-1)
    228                         {
    229                             state=-2;
    230                             // look for CR and/or LF
    231                             if(c==13||c==10)
    232                             {
    233                                 if(c==13)
    234                                     state=in.read();
    235                                 break;
    236                             }
    237                             // look for boundary
    238                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
    239                                 b++;
    240                             else
    241                             {
    242                                 // this is not a boundary
    243                                 if(cr)
    244                                     out.write(13);
    245                                 if(lf)
    246                                     out.write(10);
    247                                 cr=lf=false;
    248                                 if(b>0)
    249                                     out.write(byteBoundary,0,b);
    250                                 b=-1;
    251                                 out.write(c);
    252                             }
    253                         }
    254                         // check partial boundary
    255                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
    256                         {
    257                             if(cr)
    258                                 out.write(13);
    259                             if(lf)
    260                                 out.write(10);
    261                             cr=lf=false;
    262                             out.write(byteBoundary,0,b);
    263                             b=-1;
    264                         }
    265                         // boundary match
    266                         if(b>0||c==-1)
    267                         {
    268                             if(b==byteBoundary.length)
    269                                 lastPart=true;
    270                             if(state==10)
    271                                 state=-2;
    272                             break;
    273                         }
    274                         // handle CR LF
    275                         if(cr)
    276                             out.write(13);
    277                         if(lf)
    278                             out.write(10);
    279                         cr=(c==13);
    280                         lf=(c==10||state==10);
    281                         if(state==10)
    282                             state=-2;
    283                     }
    284                 }
    285                 finally
    286                 {
    287                     out.close();
    288                 }
    289
    290                 if (file==null)
    291                 {
    292                     bytes = ((ByteArrayOutputStream)out).toByteArray();
    293                     params.add(name,bytes);
    294                 }
    295             }
    296
    297             // handle request
    298             chain.doFilter(new Wrapper(srequest,params),response);
    299         }
    300         finally
    301         {
    302             deleteFiles(request);
    303         }
    304     }
    305
    306     private void deleteFiles(ServletRequest request)
    307     {
    308         ArrayList files = (ArrayList)request.getAttribute(FILES);
    309         if (files!=null)
    310         {
    311             Iterator iter = files.iterator();
    312             while (iter.hasNext())
    313             {
    314                 File file=(File)iter.next();
    315                 try
    316                 {
    317                     file.delete();
    318                 }
    319                 catch(Exception e)
    320                 {
    321                     _context.log("failed to delete "+file,e);
    322                 }
    323             }
    324         }
    325     }
    326     /* ------------------------------------------------------------ */
    327     private String value(String nameEqualsValue)
    328     {
    329         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
    330         int i=value.indexOf(';');
    331         if(i>0)
    332             value=value.substring(0,i);
    333         if(value.startsWith("\""))
    334         {
    335             value=value.substring(1,value.indexOf('"',1));
    336         }
    337         else
    338         {
    339             i=value.indexOf(' ');
    340             if(i>0)
    341                 value=value.substring(0,i);
    342         }
    343         return value;
    344     }
    345
    346     /* ------------------------------------------------------------------------------- */
    347     /**
    348      * @see javax.servlet.Filter#destroy()
    349      */
    350     public void destroy()
    351     {
    352     }
    353
    354     private static class Wrapper extends HttpServletRequestWrapper
    355     {
    356         String encoding="UTF-8";
    357         MultiMap map;
    358
    359         /* ------------------------------------------------------------------------------- */
    360         /** Constructor.
    361          * @param request
    362          */
    363         public Wrapper(HttpServletRequest request, MultiMap map)
    364         {
    365             super(request);
    366             this.map=map;
    367         }
    368
    369         /* ------------------------------------------------------------------------------- */
    370         /**
    371          * @see javax.servlet.ServletRequest#getContentLength()
    372          */
    373         public int getContentLength()
    374         {
    375             return 0;
    376         }
    377
    378         /* ------------------------------------------------------------------------------- */
    379         /**
    380          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
    381          */
    382         public String getParameter(String name)
    383         {
    384             Object o=map.get(name);
    385             if (!(o instanceof byte[]) && LazyList.size(o)>0)
    386                 o=LazyList.get(o,0);
    387
    388             if (o instanceof byte[])
    389             {
    390                 try
    391                 {
    392                     String s=new String((byte[])o,encoding);
    393                     return s;
    394                 }
    395                 catch(Exception e)
    396                 {
    397                     e.printStackTrace();
    398                 }
    399             }
    400             else if (o!=null)
    401                 return String.valueOf(o);
    402             return null;
    403         }
    404
    405         /* ------------------------------------------------------------------------------- */
    406         /**
    407          * @see javax.servlet.ServletRequest#getParameterMap()
    408          */
    409         public Map getParameterMap()
    410         {
    411             return Collections.unmodifiableMap(map.toStringArrayMap());
    412         }
    413
    414         /* ------------------------------------------------------------------------------- */
    415         /**
    416          * @see javax.servlet.ServletRequest#getParameterNames()
    417          */
    418         public Enumeration getParameterNames()
    419         {
    420             return Collections.enumeration(map.keySet());
    421         }
    422
    423         /* ------------------------------------------------------------------------------- */
    424         /**
    425          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
    426          */
    427         public String[] getParameterValues(String name)
    428         {
    429             List l=map.getValues(name);
    430             if (l==null || l.size()==0)
    431                 return new String[0];
    432             String[] v = new String[l.size()];
    433             for (int i=0;i<l.size();i++)
    434             {
    435                 Object o=l.get(i);
    436                 if (o instanceof byte[])
    437                 {
    438                     try
    439                     {
    440                         v[i]=new String((byte[])o,encoding);
    441                     }
    442                     catch(Exception e)
    443                     {
    444                         e.printStackTrace();
    445                     }
    446                 }
    447                 else if (o instanceof String)
    448                     v[i]=(String)o;
    449             }
    450             return v;
    451         }
    452
    453         /* ------------------------------------------------------------------------------- */
    454         /**
    455          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
    456          */
    457         public void setCharacterEncoding(String enc)
    458             throws UnsupportedEncodingException
    459         {
    460             encoding=enc;
    461         }
    462     }
    463 }