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.BufferedInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.UnsupportedEncodingException;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.charset.UnsupportedCharsetException;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.Enumeration;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  
38  import javax.servlet.Filter;
39  import javax.servlet.FilterChain;
40  import javax.servlet.FilterConfig;
41  import javax.servlet.MultipartConfigElement;
42  import javax.servlet.ServletContext;
43  import javax.servlet.ServletException;
44  import javax.servlet.ServletRequest;
45  import javax.servlet.ServletResponse;
46  import javax.servlet.http.HttpServletRequest;
47  import javax.servlet.http.HttpServletRequestWrapper;
48  import javax.servlet.http.Part;
49  
50  import org.eclipse.jetty.util.IO;
51  import org.eclipse.jetty.http.MimeTypes;
52  import org.eclipse.jetty.util.LazyList;
53  import org.eclipse.jetty.util.MultiMap;
54  import org.eclipse.jetty.util.MultiPartInputStreamParser;
55  import org.eclipse.jetty.util.StringUtil;
56  import org.eclipse.jetty.util.TypeUtil;
57  import org.eclipse.jetty.util.log.Log;
58  import org.eclipse.jetty.util.log.Logger;
59  
60  
61  /* ------------------------------------------------------------ */
62  /**
63   * Multipart Form Data Filter.
64   * <p>
65   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
66   * item.  Any files sent are stored to a temporary file and a File object added to the request
67   * as an attribute.  All other values are made available via the normal getParameter API and
68   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
69   * <p>
70   * If the init parameter "delete" is set to "true", any files created will be deleted when the
71   * current request returns.
72   * <p>
73   * The init parameter maxFormKeys sets the maximum number of keys that may be present in a
74   * form (default set by system property org.eclipse.jetty.server.Request.maxFormKeys or 1000) to protect
75   * against DOS attacks by bad hash keys.
76   * <p>
77   * The init parameter deleteFiles controls if uploaded files are automatically deleted after the request
78   * completes.
79   *
80   * Use init parameter "maxFileSize" to set the max size file that can be uploaded.
81   *
82   * Use init parameter "maxRequestSize" to limit the size of the multipart request.
83   *
84   */
85  public class MultiPartFilter implements Filter
86  {
87      private static final Logger LOG = Log.getLogger(MultiPartFilter.class);
88      public final static String CONTENT_TYPE_SUFFIX=".org.eclipse.jetty.servlet.contentType";
89      private final static String MULTIPART = "org.eclipse.jetty.servlet.MultiPartFile.multiPartInputStream";
90      private File tempdir;
91      private boolean _deleteFiles;
92      private ServletContext _context;
93      private int _fileOutputBuffer = 0;
94      private long _maxFileSize = -1L;
95      private long _maxRequestSize = -1L;
96      private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys", 1000);
97  
98      /* ------------------------------------------------------------------------------- */
99      /**
100      * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
101      */
102     public void init(FilterConfig filterConfig) throws ServletException
103     {
104         tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
105         _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
106         String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
107         if(fileOutputBuffer!=null)
108             _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
109         String maxFileSize = filterConfig.getInitParameter("maxFileSize");
110         if (maxFileSize != null)
111             _maxFileSize = Long.parseLong(maxFileSize.trim());
112         String maxRequestSize = filterConfig.getInitParameter("maxRequestSize");
113         if (maxRequestSize != null)
114             _maxRequestSize = Long.parseLong(maxRequestSize.trim());
115 
116         _context=filterConfig.getServletContext();
117         String mfks = filterConfig.getInitParameter("maxFormKeys");
118         if (mfks!=null)
119             _maxFormKeys=Integer.parseInt(mfks);
120     }
121 
122     /* ------------------------------------------------------------------------------- */
123     /**
124      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
125      *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
126      */
127     public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
128         throws IOException, ServletException
129     {
130         HttpServletRequest srequest=(HttpServletRequest)request;
131         if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
132         {
133             chain.doFilter(request,response);
134             return;
135         }
136 
137         InputStream in = new BufferedInputStream(request.getInputStream());
138         String content_type=srequest.getContentType();
139 
140         //Get current parameters so we can merge into them
141         MultiMap params = new MultiMap();
142         for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet())
143         {
144             Object value = entry.getValue();
145             if (value instanceof String[])
146                 params.addValues(entry.getKey(), (String[])value);
147             else
148                 params.add(entry.getKey(), value);
149         }
150 
151         MultipartConfigElement config = new MultipartConfigElement(tempdir.getCanonicalPath(), _maxFileSize, _maxRequestSize, _fileOutputBuffer);
152         MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(in, content_type, config, tempdir);
153         mpis.setDeleteOnExit(_deleteFiles);
154         request.setAttribute(MULTIPART, mpis);
155         try
156         {
157             Collection<Part> parts = mpis.getParts();
158             if (parts != null)
159             {
160                 Iterator<Part> itor = parts.iterator();
161                 while (itor.hasNext() && params.size() < _maxFormKeys)
162                 {
163                     Part p = itor.next();
164                     MultiPartInputStreamParser.MultiPart mp = (MultiPartInputStreamParser.MultiPart)p;
165                     if (mp.getFile() != null)
166                     {
167                         request.setAttribute(mp.getName(),mp.getFile());
168                         if (mp.getContentDispositionFilename() != null)
169                         {
170                             params.add(mp.getName(), mp.getContentDispositionFilename());
171                             if (mp.getContentType() != null)
172                                 params.add(mp.getName()+CONTENT_TYPE_SUFFIX, mp.getContentType());
173                         }
174                     }
175                     else
176                     {
177                         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
178                         IO.copy(p.getInputStream(), bytes);
179                         params.add(p.getName(), bytes.toByteArray());
180                         if (p.getContentType() != null)
181                             params.add(p.getName()+CONTENT_TYPE_SUFFIX, p.getContentType());
182                     }
183                 }
184             }
185 
186             // handle request
187             chain.doFilter(new Wrapper(srequest,params),response);
188         }
189         finally
190         {
191             deleteFiles(request);
192         }
193     }
194     
195     
196     /* ------------------------------------------------------------ */
197     private void deleteFiles(ServletRequest request)
198     {
199         if (!_deleteFiles)
200             return;
201         
202         MultiPartInputStreamParser mpis = (MultiPartInputStreamParser)request.getAttribute(MULTIPART);
203         if (mpis != null)
204         {
205             try
206             {
207                 mpis.deleteParts();
208             }
209             catch (Exception e)
210             {
211                 _context.log("Error deleting multipart tmp files", e);
212             }
213         }
214         request.removeAttribute(MULTIPART);
215     }
216 
217     /* ------------------------------------------------------------------------------- */
218     /**
219      * @see javax.servlet.Filter#destroy()
220      */
221     public void destroy()
222     {
223     }
224 
225     /* ------------------------------------------------------------------------------- */
226     /* ------------------------------------------------------------------------------- */
227     private static class Wrapper extends HttpServletRequestWrapper
228     {
229         Charset _encoding=StandardCharsets.UTF_8;
230         MultiMap<Object> _params;
231 
232         /* ------------------------------------------------------------------------------- */
233         /** Constructor.
234          * @param request
235          */
236         public Wrapper(HttpServletRequest request, MultiMap map)
237         {
238             super(request);
239             this._params=map;
240         }
241 
242         /* ------------------------------------------------------------------------------- */
243         /**
244          * @see javax.servlet.ServletRequest#getContentLength()
245          */
246         @Override
247         public int getContentLength()
248         {
249             return 0;
250         }
251 
252         /* ------------------------------------------------------------------------------- */
253         /**
254          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
255          */
256         @Override
257         public String getParameter(String name)
258         {
259             Object o=_params.get(name);
260             if (!(o instanceof byte[]) && LazyList.size(o)>0)
261                 o=LazyList.get(o,0);
262 
263             if (o instanceof byte[])
264             {
265                 try
266                 {
267                     return getParameterBytesAsString(name, (byte[])o);
268                 }
269                 catch(Exception e)
270                 {
271                     LOG.warn(e);
272                 }
273             }
274             else if (o!=null)
275                 return String.valueOf(o);
276             return null;
277         }
278 
279         /* ------------------------------------------------------------------------------- */
280         /**
281          * @see javax.servlet.ServletRequest#getParameterMap()
282          */
283         @Override
284         public Map<String, String[]> getParameterMap()
285         {
286             Map<String, String[]> cmap = new HashMap<String,String[]>();
287             
288             for ( Object key : _params.keySet() )
289             {
290                 cmap.put((String)key,getParameterValues((String)key));
291             }
292 
293             return Collections.unmodifiableMap(cmap);
294         }
295 
296         /* ------------------------------------------------------------------------------- */
297         /**
298          * @see javax.servlet.ServletRequest#getParameterNames()
299          */
300         @Override
301         public Enumeration<String> getParameterNames()
302         {
303             return Collections.enumeration(_params.keySet());
304         }
305 
306         /* ------------------------------------------------------------------------------- */
307         /**
308          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
309          */
310         @Override
311         public String[] getParameterValues(String name)
312         {
313             List l=_params.getValues(name);
314             if (l==null || l.size()==0)
315                 return new String[0];
316             String[] v = new String[l.size()];
317             for (int i=0;i<l.size();i++)
318             {
319                 Object o=l.get(i);
320                 if (o instanceof byte[])
321                 {
322                     try
323                     {
324                         v[i]=getParameterBytesAsString(name, (byte[])o);
325                     }
326                     catch(Exception e)
327                     {
328                         throw new RuntimeException(e);
329                     }
330                 }
331                 else if (o instanceof String)
332                     v[i]=(String)o;
333             }
334             return v;
335         }
336 
337         /* ------------------------------------------------------------------------------- */
338         /**
339          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
340          */
341         @Override
342         public void setCharacterEncoding(String enc)
343             throws UnsupportedEncodingException
344         {
345             try
346             {
347                 _encoding=Charset.forName(enc);
348             }
349             catch (UnsupportedCharsetException e)
350             {
351                 throw new UnsupportedEncodingException(e.getMessage());
352             }
353         }
354         
355         
356         /* ------------------------------------------------------------------------------- */
357         private String getParameterBytesAsString (String name, byte[] bytes) 
358         throws UnsupportedEncodingException
359         {
360             //check if there is a specific encoding for the parameter
361             Object ct = _params.getValue(name+CONTENT_TYPE_SUFFIX,0);
362             //use default if not
363             Charset contentType = _encoding;
364             if (ct != null)
365             {
366                 String tmp = MimeTypes.getCharsetFromContentType((String)ct);
367                 try
368                 {
369                     contentType = (tmp == null?_encoding:Charset.forName(tmp));
370                 }
371                 catch (UnsupportedCharsetException e)
372                 {
373                     throw new UnsupportedEncodingException(e.getMessage());
374                 }
375             }
376             
377             return new String(bytes,contentType);
378         }
379     }
380 }