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             outer:while(!lastPart)
    132             {
    133                 while(true)
    134                 {
    135                     bytes=TypeUtil.readLine(in);
    136                     // If blank line, end of part headers
    137                     if(bytes==null)
    138                         break outer;
    139                     if (bytes.length==0)
    140                         break;
    141                     line=new String(bytes,"UTF-8");
    142
    143                     // place part header key and value in map
    144                     int c=line.indexOf(':',0);
    145                     if(c>0)
    146                     {
    147                         String key=line.substring(0,c).trim().toLowerCase();
    148                         String value=line.substring(c+1,line.length()).trim();
    149                         if(key.equals("content-disposition"))
    150                             content_disposition=value;
    151                     }
    152                 }
    153                 // Extract content-disposition
    154                 boolean form_data=false;
    155                 if(content_disposition==null)
    156                 {
    157                     throw new IOException("Missing content-disposition");
    158                 }
    159
    160                 StringTokenizer tok=new StringTokenizer(content_disposition,";");
    161                 String name=null;
    162                 String filename=null;
    163                 while(tok.hasMoreTokens())
    164                 {
    165                     String t=tok.nextToken().trim();
    166                     String tl=t.toLowerCase();
    167                     if(t.startsWith("form-data"))
    168                         form_data=true;
    169                     else if(tl.startsWith("name="))
    170                         name=value(t);
    171                     else if(tl.startsWith("filename="))
    172                         filename=value(t);
    173                 }
    174
    175                 // Check disposition
    176                 if(!form_data)
    177                 {
    178                     continue;
    179                 }
    180
    181                 //It is valid for reset and submit buttons to have an empty name.
    182                 //If no name is supplied, the browser skips sending the info for that field.
    183                 //However, if you supply the empty string as the name, the browser sends the
    184                 //field, with name as the empty string. So, only continue this loop if we
    185                 //have not yet seen a name field.
    186                 if(name==null)
    187                 {
    188                     continue;
    189                 }
    190
    191                 OutputStream out=null;
    192                 File file=null;
    193                 try
    194                 {
    195                     if (filename!=null && filename.length()>0)
    196                     {
    197                         file = File.createTempFile("MultiPart", "", tempdir);
    198                         out = new FileOutputStream(file);
    199                         if(_fileOutputBuffer>0)
    200                             out = new BufferedOutputStream(out, _fileOutputBuffer);
    201                         request.setAttribute(name,file);
    202                         params.add(name, filename);
    203
    204                         if (_deleteFiles)
    205                         {
    206                             file.deleteOnExit();
    207                             ArrayList files = (ArrayList)request.getAttribute(FILES);
    208                             if (files==null)
    209                             {
    210                                 files=new ArrayList();
    211                                 request.setAttribute(FILES,files);
    212                             }
    213                             files.add(file);
    214                         }
    215
    216                     }
    217                     else
    218                         out=new ByteArrayOutputStream();
    219
    220                     int state=-2;
    221                     int c;
    222                     boolean cr=false;
    223                     boolean lf=false;
    224
    225                     // loop for all lines`
    226                     while(true)
    227                     {
    228                         int b=0;
    229                         while((c=(state!=-2)?state:in.read())!=-1)
    230                         {
    231                             state=-2;
    232                             // look for CR and/or LF
    233                             if(c==13||c==10)
    234                             {
    235                                 if(c==13)
    236                                     state=in.read();
    237                                 break;
    238                             }
    239                             // look for boundary
    240                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
    241                                 b++;
    242                             else
    243                             {
    244                                 // this is not a boundary
    245                                 if(cr)
    246                                     out.write(13);
    247                                 if(lf)
    248                                     out.write(10);
    249                                 cr=lf=false;
    250                                 if(b>0)
    251                                     out.write(byteBoundary,0,b);
    252                                 b=-1;
    253                                 out.write(c);
    254                             }
    255                         }
    256                         // check partial boundary
    257                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
    258                         {
    259                             if(cr)
    260                                 out.write(13);
    261                             if(lf)
    262                                 out.write(10);
    263                             cr=lf=false;
    264                             out.write(byteBoundary,0,b);
    265                             b=-1;
    266                         }
    267                         // boundary match
    268                         if(b>0||c==-1)
    269                         {
    270                             if(b==byteBoundary.length)
    271                                 lastPart=true;
    272                             if(state==10)
    273                                 state=-2;
    274                             break;
    275                         }
    276                         // handle CR LF
    277                         if(cr)
    278                             out.write(13);
    279                         if(lf)
    280                             out.write(10);
    281                         cr=(c==13);
    282                         lf=(c==10||state==10);
    283                         if(state==10)
    284                             state=-2;
    285                     }
    286                 }
    287                 finally
    288                 {
    289                     out.close();
    290                 }
    291
    292                 if (file==null)
    293                 {
    294                     bytes = ((ByteArrayOutputStream)out).toByteArray();
    295                     params.add(name,bytes);
    296                 }
    297             }
    298
    299             // handle request
    300             chain.doFilter(new Wrapper(srequest,params),response);
    301         }
    302         finally
    303         {
    304             deleteFiles(request);
    305         }
    306     }
    307
    308     private void deleteFiles(ServletRequest request)
    309     {
    310         ArrayList files = (ArrayList)request.getAttribute(FILES);
    311         if (files!=null)
    312         {
    313             Iterator iter = files.iterator();
    314             while (iter.hasNext())
    315             {
    316                 File file=(File)iter.next();
    317                 try
    318                 {
    319                     file.delete();
    320                 }
    321                 catch(Exception e)
    322                 {
    323                     _context.log("failed to delete "+file,e);
    324                 }
    325             }
    326         }
    327     }
    328     /* ------------------------------------------------------------ */
    329     private String value(String nameEqualsValue)
    330     {
    331         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
    332         int i=value.indexOf(';');
    333         if(i>0)
    334             value=value.substring(0,i);
    335         if(value.startsWith("\""))
    336         {
    337             value=value.substring(1,value.indexOf('"',1));
    338         }
    339         else
    340         {
    341             i=value.indexOf(' ');
    342             if(i>0)
    343                 value=value.substring(0,i);
    344         }
    345         return value;
    346     }
    347
    348     /* ------------------------------------------------------------------------------- */
    349     /**
    350      * @see javax.servlet.Filter#destroy()
    351      */
    352     public void destroy()
    353     {
    354     }
    355
    356     private static class Wrapper extends HttpServletRequestWrapper
    357     {
    358         String encoding="UTF-8";
    359         MultiMap map;
    360
    361         /* ------------------------------------------------------------------------------- */
    362         /** Constructor.
    363          * @param request
    364          */
    365         public Wrapper(HttpServletRequest request, MultiMap map)
    366         {
    367             super(request);
    368             this.map=map;
    369         }
    370
    371         /* ------------------------------------------------------------------------------- */
    372         /**
    373          * @see javax.servlet.ServletRequest#getContentLength()
    374          */
    375         public int getContentLength()
    376         {
    377             return 0;
    378         }
    379
    380         /* ------------------------------------------------------------------------------- */
    381         /**
    382          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
    383          */
    384         public String getParameter(String name)
    385         {
    386             Object o=map.get(name);
    387             if (!(o instanceof byte[]) && LazyList.size(o)>0)
    388                 o=LazyList.get(o,0);
    389
    390             if (o instanceof byte[])
    391             {
    392                 try
    393                 {
    394                     String s=new String((byte[])o,encoding);
    395                     return s;
    396                 }
    397                 catch(Exception e)
    398                 {
    399                     e.printStackTrace();
    400                 }
    401             }
    402             else if (o!=null)
    403                 return String.valueOf(o);
    404             return null;
    405         }
    406
    407         /* ------------------------------------------------------------------------------- */
    408         /**
    409          * @see javax.servlet.ServletRequest#getParameterMap()
    410          */
    411         public Map getParameterMap()
    412         {
    413             return Collections.unmodifiableMap(map.toStringArrayMap());
    414         }
    415
    416         /* ------------------------------------------------------------------------------- */
    417         /**
    418          * @see javax.servlet.ServletRequest#getParameterNames()
    419          */
    420         public Enumeration getParameterNames()
    421         {
    422             return Collections.enumeration(map.keySet());
    423         }
    424
    425         /* ------------------------------------------------------------------------------- */
    426         /**
    427          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
    428          */
    429         public String[] getParameterValues(String name)
    430         {
    431             List l=map.getValues(name);
    432             if (l==null || l.size()==0)
    433                 return new String[0];
    434             String[] v = new String[l.size()];
    435             for (int i=0;i<l.size();i++)
    436             {
    437                 Object o=l.get(i);
    438                 if (o instanceof byte[])
    439                 {
    440                     try
    441                     {
    442                         v[i]=new String((byte[])o,encoding);
    443                     }
    444                     catch(Exception e)
    445                     {
    446                         e.printStackTrace();
    447                     }
    448                 }
    449                 else if (o instanceof String)
    450                     v[i]=(String)o;
    451             }
    452             return v;
    453         }
    454
    455         /* ------------------------------------------------------------------------------- */
    456         /**
    457          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
    458          */
    459         public void setCharacterEncoding(String enc)
    460             throws UnsupportedEncodingException
    461         {
    462             encoding=enc;
    463         }
    464     }
    465 }