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