1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 19 package org.apache.tomcat.util.http.fileupload; 20 21 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.OutputStream; 25 import java.util.ArrayList; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Map; 29 import javax.servlet.http.HttpServletRequest; 30 31 32 /** 33 * <p>High level API for processing file uploads.</p> 34 * 35 * <p>This class handles multiple files per single HTML widget, sent using 36 * <code>multipart/mixed</code> encoding type, as specified by 37 * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link 38 * #parseRequest(HttpServletRequest)} to acquire a list of {@link 39 * org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML 40 * widget.</p> 41 * 42 * <p>How the data for individual parts is stored is determined by the factory 43 * used to create them; a given part may be in memory, on disk, or somewhere 44 * else.</p> 45 * 46 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a> 47 * @author <a href="mailto:dlr@collab.net">Daniel Rall</a> 48 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> 49 * @author <a href="mailto:jmcnally@collab.net">John McNally</a> 50 * @author <a href="mailto:martinc@apache.org">Martin Cooper</a> 51 * @author Sean C. Sullivan 52 * 53 * @version $Id: FileUploadBase.java 467222 2006-10-24 03:17:11Z markt $ 54 */ 55 public abstract class FileUploadBase 56 { 57 58 // ---------------------------------------------------------- Class methods 59 60 61 /** 62 * Utility method that determines whether the request contains multipart 63 * content. 64 * 65 * @param req The servlet request to be evaluated. Must be non-null. 66 * 67 * @return <code>true</code> if the request is multipart; 68 * <code>false</code> otherwise. 69 */ 70 public static final boolean isMultipartContent(HttpServletRequest req) 71 { 72 String contentType = req.getHeader(CONTENT_TYPE); 73 if (contentType == null) 74 { 75 return false; 76 } 77 if (contentType.startsWith(MULTIPART)) 78 { 79 return true; 80 } 81 return false; 82 } 83 84 85 // ----------------------------------------------------- Manifest constants 86 87 88 /** 89 * HTTP content type header name. 90 */ 91 public static final String CONTENT_TYPE = "Content-type"; 92 93 94 /** 95 * HTTP content disposition header name. 96 */ 97 public static final String CONTENT_DISPOSITION = "Content-disposition"; 98 99 100 /** 101 * Content-disposition value for form data. 102 */ 103 public static final String FORM_DATA = "form-data"; 104 105 106 /** 107 * Content-disposition value for file attachment. 108 */ 109 public static final String ATTACHMENT = "attachment"; 110 111 112 /** 113 * Part of HTTP content type header. 114 */ 115 public static final String MULTIPART = "multipart/"; 116 117 118 /** 119 * HTTP content type header for multipart forms. 120 */ 121 public static final String MULTIPART_FORM_DATA = "multipart/form-data"; 122 123 124 /** 125 * HTTP content type header for multiple uploads. 126 */ 127 public static final String MULTIPART_MIXED = "multipart/mixed"; 128 129 130 /** 131 * The maximum length of a single header line that will be parsed 132 * (1024 bytes). 133 */ 134 public static final int MAX_HEADER_SIZE = 1024; 135 136 137 // ----------------------------------------------------------- Data members 138 139 140 /** 141 * The maximum size permitted for an uploaded file. A value of -1 indicates 142 * no maximum. 143 */ 144 private long sizeMax = -1; 145 146 147 /** 148 * The content encoding to use when reading part headers. 149 */ 150 private String headerEncoding; 151 152 153 // ----------------------------------------------------- Property accessors 154 155 156 /** 157 * Returns the factory class used when creating file items. 158 * 159 * @return The factory class for new file items. 160 */ 161 public abstract FileItemFactory getFileItemFactory(); 162 163 164 /** 165 * Sets the factory class to use when creating file items. 166 * 167 * @param factory The factory class for new file items. 168 */ 169 public abstract void setFileItemFactory(FileItemFactory factory); 170 171 172 /** 173 * Returns the maximum allowed upload size. 174 * 175 * @return The maximum allowed size, in bytes. 176 * 177 * @see #setSizeMax(long) 178 * 179 */ 180 public long getSizeMax() 181 { 182 return sizeMax; 183 } 184 185 186 /** 187 * Sets the maximum allowed upload size. If negative, there is no maximum. 188 * 189 * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum. 190 * 191 * @see #getSizeMax() 192 * 193 */ 194 public void setSizeMax(long sizeMax) 195 { 196 this.sizeMax = sizeMax; 197 } 198 199 200 /** 201 * Retrieves the character encoding used when reading the headers of an 202 * individual part. When not specified, or <code>null</code>, the platform 203 * default encoding is used. 204 * 205 * @return The encoding used to read part headers. 206 */ 207 public String getHeaderEncoding() 208 { 209 return headerEncoding; 210 } 211 212 213 /** 214 * Specifies the character encoding to be used when reading the headers of 215 * individual parts. When not specified, or <code>null</code>, the platform 216 * default encoding is used. 217 * 218 * @param encoding The encoding used to read part headers. 219 */ 220 public void setHeaderEncoding(String encoding) 221 { 222 headerEncoding = encoding; 223 } 224 225 226 // --------------------------------------------------------- Public methods 227 228 229 /** 230 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a> 231 * compliant <code>multipart/form-data</code> stream. If files are stored 232 * on disk, the path is given by <code>getRepository()</code>. 233 * 234 * @param req The servlet request to be parsed. 235 * 236 * @return A list of <code>FileItem</code> instances parsed from the 237 * request, in the order that they were transmitted. 238 * 239 * @exception FileUploadException if there are problems reading/parsing 240 * the request or storing files. 241 */ 242 public List /* FileItem */ parseRequest(HttpServletRequest req) 243 throws FileUploadException 244 { 245 if (null == req) 246 { 247 throw new NullPointerException("req parameter"); 248 } 249 250 ArrayList items = new ArrayList(); 251 String contentType = req.getHeader(CONTENT_TYPE); 252 253 if ((null == contentType) || (!contentType.startsWith(MULTIPART))) 254 { 255 throw new InvalidContentTypeException( 256 "the request doesn't contain a " 257 + MULTIPART_FORM_DATA 258 + " or " 259 + MULTIPART_MIXED 260 + " stream, content type header is " 261 + contentType); 262 } 263 int requestSize = req.getContentLength(); 264 265 if (requestSize == -1) 266 { 267 throw new UnknownSizeException( 268 "the request was rejected because it's size is unknown"); 269 } 270 271 if (sizeMax >= 0 && requestSize > sizeMax) 272 { 273 throw new SizeLimitExceededException( 274 "the request was rejected because " 275 + "it's size exceeds allowed range"); 276 } 277 278 try 279 { 280 int boundaryIndex = contentType.indexOf("boundary="); 281 if (boundaryIndex < 0) 282 { 283 throw new FileUploadException( 284 "the request was rejected because " 285 + "no multipart boundary was found"); 286 } 287 byte[] boundary = contentType.substring( 288 boundaryIndex + 9).getBytes(); 289 290 InputStream input = req.getInputStream(); 291 292 MultipartStream multi = new MultipartStream(input, boundary); 293 multi.setHeaderEncoding(headerEncoding); 294 295 boolean nextPart = multi.skipPreamble(); 296 while (nextPart) 297 { 298 Map headers = parseHeaders(multi.readHeaders()); 299 String fieldName = getFieldName(headers); 300 if (fieldName != null) 301 { 302 String subContentType = getHeader(headers, CONTENT_TYPE); 303 if (subContentType != null && subContentType 304 .startsWith(MULTIPART_MIXED)) 305 { 306 // Multiple files. 307 byte[] subBoundary = 308 subContentType.substring( 309 subContentType 310 .indexOf("boundary=") + 9).getBytes(); 311 multi.setBoundary(subBoundary); 312 boolean nextSubPart = multi.skipPreamble(); 313 while (nextSubPart) 314 { 315 headers = parseHeaders(multi.readHeaders()); 316 if (getFileName(headers) != null) 317 { 318 FileItem item = 319 createItem(headers, false); 320 OutputStream os = item.getOutputStream(); 321 try 322 { 323 multi.readBodyData(os); 324 } 325 finally 326 { 327 os.close(); 328 } 329 items.add(item); 330 } 331 else 332 { 333 // Ignore anything but files inside 334 // multipart/mixed. 335 multi.discardBodyData(); 336 } 337 nextSubPart = multi.readBoundary(); 338 } 339 multi.setBoundary(boundary); 340 } 341 else 342 { 343 if (getFileName(headers) != null) 344 { 345 // A single file. 346 FileItem item = createItem(headers, false); 347 OutputStream os = item.getOutputStream(); 348 try 349 { 350 multi.readBodyData(os); 351 } 352 finally 353 { 354 os.close(); 355 } 356 items.add(item); 357 } 358 else 359 { 360 // A form field. 361 FileItem item = createItem(headers, true); 362 OutputStream os = item.getOutputStream(); 363 try 364 { 365 multi.readBodyData(os); 366 } 367 finally 368 { 369 os.close(); 370 } 371 items.add(item); 372 } 373 } 374 } 375 else 376 { 377 // Skip this part. 378 multi.discardBodyData(); 379 } 380 nextPart = multi.readBoundary(); 381 } 382 } 383 catch (IOException e) 384 { 385 throw new FileUploadException( 386 "Processing of " + MULTIPART_FORM_DATA 387 + " request failed. " + e.getMessage()); 388 } 389 390 return items; 391 } 392 393 394 // ------------------------------------------------------ Protected methods 395 396 397 /** 398 * Retrieves the file name from the <code>Content-disposition</code> 399 * header. 400 * 401 * @param headers A <code>Map</code> containing the HTTP request headers. 402 * 403 * @return The file name for the current <code>encapsulation</code>. 404 */ 405 protected String getFileName(Map /* String, String */ headers) 406 { 407 String fileName = null; 408 String cd = getHeader(headers, CONTENT_DISPOSITION); 409 if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT)) 410 { 411 int start = cd.indexOf("filename=\""); 412 int end = cd.indexOf('"', start + 10); 413 if (start != -1 && end != -1) 414 { 415 fileName = cd.substring(start + 10, end).trim(); 416 } 417 } 418 return fileName; 419 } 420 421 422 /** 423 * Retrieves the field name from the <code>Content-disposition</code> 424 * header. 425 * 426 * @param headers A <code>Map</code> containing the HTTP request headers. 427 * 428 * @return The field name for the current <code>encapsulation</code>. 429 */ 430 protected String getFieldName(Map /* String, String */ headers) 431 { 432 String fieldName = null; 433 String cd = getHeader(headers, CONTENT_DISPOSITION); 434 if (cd != null && cd.startsWith(FORM_DATA)) 435 { 436 int start = cd.indexOf("name=\""); 437 int end = cd.indexOf('"', start + 6); 438 if (start != -1 && end != -1) 439 { 440 fieldName = cd.substring(start + 6, end); 441 } 442 } 443 return fieldName; 444 } 445 446 447 /** 448 * Creates a new {@link FileItem} instance. 449 * 450 * @param headers A <code>Map</code> containing the HTTP request 451 * headers. 452 * @param isFormField Whether or not this item is a form field, as 453 * opposed to a file. 454 * 455 * @return A newly created <code>FileItem</code> instance. 456 * 457 * @exception FileUploadException if an error occurs. 458 */ 459 protected FileItem createItem(Map /* String, String */ headers, 460 boolean isFormField) 461 throws FileUploadException 462 { 463 return getFileItemFactory().createItem(getFieldName(headers), 464 getHeader(headers, CONTENT_TYPE), 465 isFormField, 466 getFileName(headers)); 467 } 468 469 470 /** 471 * <p> Parses the <code>header-part</code> and returns as key/value 472 * pairs. 473 * 474 * <p> If there are multiple headers of the same names, the name 475 * will map to a comma-separated list containing the values. 476 * 477 * @param headerPart The <code>header-part</code> of the current 478 * <code>encapsulation</code>. 479 * 480 * @return A <code>Map</code> containing the parsed HTTP request headers. 481 */ 482 protected Map /* String, String */ parseHeaders(String headerPart) 483 { 484 Map headers = new HashMap(); 485 char buffer[] = new char[MAX_HEADER_SIZE]; 486 boolean done = false; 487 int j = 0; 488 int i; 489 String header, headerName, headerValue; 490 try 491 { 492 while (!done) 493 { 494 i = 0; 495 // Copy a single line of characters into the buffer, 496 // omitting trailing CRLF. 497 while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n') 498 { 499 buffer[i++] = headerPart.charAt(j++); 500 } 501 header = new String(buffer, 0, i - 2); 502 if (header.equals("")) 503 { 504 done = true; 505 } 506 else 507 { 508 if (header.indexOf(':') == -1) 509 { 510 // This header line is malformed, skip it. 511 continue; 512 } 513 headerName = header.substring(0, header.indexOf(':')) 514 .trim().toLowerCase(); 515 headerValue = 516 header.substring(header.indexOf(':') + 1).trim(); 517 if (getHeader(headers, headerName) != null) 518 { 519 // More that one heder of that name exists, 520 // append to the list. 521 headers.put(headerName, 522 getHeader(headers, headerName) + ',' 523 + headerValue); 524 } 525 else 526 { 527 headers.put(headerName, headerValue); 528 } 529 } 530 } 531 } 532 catch (IndexOutOfBoundsException e) 533 { 534 // Headers were malformed. continue with all that was 535 // parsed. 536 } 537 return headers; 538 } 539 540 541 /** 542 * Returns the header with the specified name from the supplied map. The 543 * header lookup is case-insensitive. 544 * 545 * @param headers A <code>Map</code> containing the HTTP request headers. 546 * @param name The name of the header to return. 547 * 548 * @return The value of specified header, or a comma-separated list if 549 * there were multiple headers of that name. 550 */ 551 protected final String getHeader(Map /* String, String */ headers, 552 String name) 553 { 554 return (String) headers.get(name.toLowerCase()); 555 } 556 557 558 /** 559 * Thrown to indicate that the request is not a multipart request. 560 */ 561 public static class InvalidContentTypeException 562 extends FileUploadException 563 { 564 /** 565 * Constructs a <code>InvalidContentTypeException</code> with no 566 * detail message. 567 */ 568 public InvalidContentTypeException() 569 { 570 super(); 571 } 572 573 /** 574 * Constructs an <code>InvalidContentTypeException</code> with 575 * the specified detail message. 576 * 577 * @param message The detail message. 578 */ 579 public InvalidContentTypeException(String message) 580 { 581 super(message); 582 } 583 } 584 585 586 /** 587 * Thrown to indicate that the request size is not specified. 588 */ 589 public static class UnknownSizeException 590 extends FileUploadException 591 { 592 /** 593 * Constructs a <code>UnknownSizeException</code> with no 594 * detail message. 595 */ 596 public UnknownSizeException() 597 { 598 super(); 599 } 600 601 /** 602 * Constructs an <code>UnknownSizeException</code> with 603 * the specified detail message. 604 * 605 * @param message The detail message. 606 */ 607 public UnknownSizeException(String message) 608 { 609 super(message); 610 } 611 } 612 613 614 /** 615 * Thrown to indicate that the request size exceeds the configured maximum. 616 */ 617 public static class SizeLimitExceededException 618 extends FileUploadException 619 { 620 /** 621 * Constructs a <code>SizeExceededException</code> with no 622 * detail message. 623 */ 624 public SizeLimitExceededException() 625 { 626 super(); 627 } 628 629 /** 630 * Constructs an <code>SizeExceededException</code> with 631 * the specified detail message. 632 * 633 * @param message The detail message. 634 */ 635 public SizeLimitExceededException(String message) 636 { 637 super(message); 638 } 639 } 640 641 }