1// 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} 373