Source code: gsoft/xervlet/MultipartRequest.java
1 /*************************************************************************
2 Copyright (C) 2003 Steve Gee
3 stevesgee@cox.net
4
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18 *************************************************************************/
19
20
21 package gsoft.xervlet;
22
23 import java.io.*;
24 import java.util.*;
25 import javax.servlet.*;
26
27 /**
28 * A utility class to handle <tt>multipart/form-data</tt> requests,
29 * the kind of requests that support file uploads. This class can
30 * receive arbitrarily large files (up to an artificial limit you can set),
31 * and fairly efficiently too.
32 * It cannot handle nested data (multipart content within multipart content)
33 * or internationalized content (such as non Latin-1 filenames).
34 * <p>
35 * It's used like this:
36 * <blockquote><pre>
37 * MultipartRequest multi = new MultipartRequest(req, ".");
38 *
39 * out.println("Params:");
40 * Enumeration params = multi.getParameterNames();
41 * while (params.hasMoreElements()) {
42 * String name = (String)params.nextElement();
43 * String value = multi.getParameter(name);
44 * out.println(name + " = " + value);
45 * }
46 * out.println();
47 *
48 * out.println("Files:");
49 * Enumeration files = multi.getFileNames();
50 * while (files.hasMoreElements()) {
51 * String name = (String)files.nextElement();
52 * String filename = multi.getFilesystemName(name);
53 * String type = multi.getContentType(name);
54 * File f = multi.getFile(name);
55 * out.println("name: " + name);
56 * out.println("filename: " + filename);
57 * out.println("type: " + type);
58 * if (f != null) {
59 * out.println("f.toString(): " + f.toString());
60 * out.println("f.getName(): " + f.getName());
61 * out.println("f.exists(): " + f.exists());
62 * out.println("f.length(): " + f.length());
63 * out.println();
64 * }
65 * }
66 * </pre></blockquote>
67 *
68 * A client can upload files using an HTML form with the following structure.
69 * Note that not all browsers support file uploads.
70 * <blockquote><pre>
71 * <FORM ACTION="/servlet/Handler" METHOD=POST
72 * ENCTYPE="multipart/form-data">
73 * What is your name? <INPUT TYPE=TEXT NAME=submitter> <BR>
74 * Which file to upload? <INPUT TYPE=FILE NAME=file> <BR>
75 * <INPUT TYPE=SUBMIT>
76 * </FORM>
77 * </pre></blockquote>
78 * <p>
79 * The full file upload specification is contained in experimental RFC 1867,
80 * available at <a href="http://ds.internic.net/rfc/rfc1867.txt">
81 * http://ds.internic.net/rfc/rfc1867.txt</a>.
82 *
83 */
84 public class MultipartRequest {
85
86 private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024; // 1 Meg
87
88 private ServletRequest req;
89 private File dir;
90 private int maxSize;
91
92 private Hashtable parameters = new Hashtable(); // name - value
93 private Hashtable files = new Hashtable(); // name - UploadedFile
94
95 public MultipartRequest(ServletRequest request,
96 String saveDirectory) throws IOException {
97 this(request, saveDirectory, DEFAULT_MAX_POST_SIZE);
98 }
99
100 public MultipartRequest(ServletRequest request,
101 String saveDirectory,
102 int maxPostSize) throws IOException {
103
104 // Sanity check values
105 if (request == null)
106 throw new IllegalArgumentException("request cannot be null");
107 if (saveDirectory == null)
108 throw new IllegalArgumentException("saveDirectory cannot be null");
109 if (maxPostSize <= 0) {
110 throw new IllegalArgumentException("maxPostSize must be positive");
111 }
112
113 // Save the request, dir, and max size
114 req = request;
115 dir = new File(saveDirectory);
116 maxSize = maxPostSize;
117
118 // Check saveDirectory is truly a directory
119 if (!dir.isDirectory())
120 throw new IllegalArgumentException("Not a directory: " + saveDirectory);
121
122 // Check saveDirectory is writable
123 if (!dir.canWrite())
124 throw new IllegalArgumentException("Not writable: " + saveDirectory);
125
126 // Now parse the request saving data to "parameters" and "files";
127 // write the file contents to the saveDirectory
128 readRequest();
129 }
130
131 public Enumeration getParameterNames() {
132 return parameters.keys();
133 }
134
135 public Enumeration getFileNames() {
136 return files.keys();
137 }
138
139 public String getParameter(String name) {
140 try {
141 String param = (String) parameters.get(name);
142 if (param.equals("")) return null;
143 return param;
144 } catch (Exception e) {
145 return null;
146 }
147 }
148
149 public String getFilesystemName(String name) {
150 try {
151 UploadedFile file = (UploadedFile) files.get(name);
152 return file.getFilesystemName(); // may be null
153 } catch (Exception e) {
154 return null;
155 }
156 }
157
158 public String getContentType(String name) {
159 try {
160 UploadedFile file = (UploadedFile) files.get(name);
161 return file.getContentType(); // may be null
162 } catch (Exception e) {
163 return null;
164 }
165 }
166
167 public File getFile(String name) {
168 try {
169 UploadedFile file = (UploadedFile) files.get(name);
170 return file.getFile(); // may be null
171 } catch (Exception e) {
172 return null;
173 }
174 }
175
176 protected void readRequest() throws IOException {
177 // Check the content type to make sure it's "multipart/form-data"
178 String type = req.getContentType();
179 if (type == null ||
180 !type.toLowerCase().startsWith("multipart/form-data")) {
181 throw new IOException("Posted content type isn't multipart/form-data");
182 }
183
184 // Check the content length to prevent denial of service attacks
185 int length = req.getContentLength();
186 if (length > maxSize) {
187 throw new IOException("Posted content length of " + length +
188 " exceeds limit of " + maxSize);
189 }
190
191 // Get the boundary string; it's included in the content type.
192 // Should look something like "------------------------12012133613061"
193 String boundary = extractBoundary(type);
194 if (boundary == null) {
195 throw new IOException("Separation boundary was not specified");
196 }
197
198 // Construct the special input stream we'll read from
199 MultipartInputStreamHandler in =
200 new MultipartInputStreamHandler(req.getInputStream(), boundary, length);
201
202 // Read the first line, should be the first boundary
203 String line = in.readLine();
204 if (line == null) {
205 throw new IOException("Corrupt form data: premature ending");
206 }
207
208 // Verify that the line is the boundary
209 if (!line.startsWith(boundary)) {
210 throw new IOException("Corrupt form data: no leading boundary");
211 }
212
213 // Now that we're just beyond the first boundary, loop over each part
214 boolean done = false;
215 while (!done) {
216 done = readNextPart(in, boundary);
217 }
218 }
219
220 protected boolean readNextPart(MultipartInputStreamHandler in,
221 String boundary) throws IOException {
222 // Read the first line, should look like this:
223 // content-disposition: form-data; name="field1"; filename="file1.txt"
224 String line = in.readLine();
225 if (line == null) {
226 // No parts left, we're done
227 return true;
228 }
229
230 // Parse the content-disposition line
231 String[] dispInfo = extractDispositionInfo(line);
232 String disposition = dispInfo[0];
233 String name = dispInfo[1];
234 String filename = dispInfo[2];
235
236 // Now onto the next line. This will either be empty
237 // or contain a Content-Type and then an empty line.
238 line = in.readLine();
239 if (line == null) {
240 // No parts left, we're done
241 return true;
242 }
243
244 // Get the content type, or null if none specified
245 String contentType = extractContentType(line);
246 if (contentType != null) {
247 // Eat the empty line
248 line = in.readLine();
249 if (line == null || line.length() > 0) { // line should be empty
250 throw new
251 IOException("Malformed line after content type: " + line);
252 }
253 } else {
254 // Assume a default content type
255 contentType = "application/octet-stream";
256 }
257
258 // Now, finally, we read the content (end after reading the boundary)
259 if (filename == null) {
260 // This is a parameter
261 String value = readParameter(in, boundary);
262 parameters.put(name, value);
263 } else {
264 // This is a file
265 readAndSaveFile(in, boundary, filename);
266 if (filename.equals("unknown")) {
267 files.put(name, new UploadedFile(null, null, null));
268 } else {
269 files.put(name,
270 new UploadedFile(dir.toString(), filename, contentType));
271 }
272 }
273 return false; // there's more to read
274 }
275
276 protected String readParameter(MultipartInputStreamHandler in,
277 String boundary) throws IOException {
278 StringBuffer sbuf = new StringBuffer();
279 String line;
280
281 while ((line = in.readLine()) != null) {
282 if (line.startsWith(boundary)) break;
283 sbuf.append(line + "\r\n"); // add the \r\n in case there are many lines
284 }
285
286 if (sbuf.length() == 0) {
287 return null; // nothing read
288 }
289
290 sbuf.setLength(sbuf.length() - 2); // cut off the last line's \r\n
291 return sbuf.toString(); // no URL decoding needed
292 }
293
294 protected void readAndSaveFile(MultipartInputStreamHandler in,
295 String boundary,
296 String filename) throws IOException {
297 File f = new File(dir + File.separator + filename);
298 FileOutputStream fos = new FileOutputStream(f);
299 BufferedOutputStream out = new BufferedOutputStream(fos, 8 * 1024); // 8K
300
301 byte[] bbuf = new byte[100 * 1024]; // 100K
302 int result;
303 String line;
304
305 // ServletInputStream.readLine() has the annoying habit of
306 // adding a \r\n to the end of the last line.
307 // Since we want a byte-for-byte transfer, we have to cut those chars.
308 boolean rnflag = false;
309 while ((result = in.readLine(bbuf, 0, bbuf.length)) != -1) {
310 // Check for boundary
311 if (result > 2 && bbuf[0] == '-' && bbuf[1] == '-') { // quick pre-check
312 line = new String(bbuf, 0, result, "ISO-8859-1");
313 if (line.startsWith(boundary)) break;
314 }
315 // Are we supposed to write \r\n for the last iteration?
316 if (rnflag) {
317 out.write('\r');
318 out.write('\n');
319 rnflag = false;
320 }
321 // Write the buffer, postpone any ending \r\n
322 if (result >= 2 &&
323 bbuf[result - 2] == '\r' &&
324 bbuf[result - 1] == '\n') {
325 out.write(bbuf, 0, result - 2); // skip the last 2 chars
326 rnflag = true; // make a note to write them on the next iteration
327 } else {
328 out.write(bbuf, 0, result);
329 }
330 }
331 out.flush();
332 out.close();
333 fos.close();
334 }
335
336 // Extracts and returns the boundary token from a line.
337 //
338 private String extractBoundary(String line) {
339 int index = line.indexOf("boundary=");
340 if (index == -1) {
341 return null;
342 }
343 String boundary = line.substring(index + 9); // 9 for "boundary="
344
345 // The real boundary is always preceeded by an extra "--"
346 boundary = "--" + boundary;
347
348 return boundary;
349 }
350
351 // Extracts and returns disposition info from a line, as a String array
352 // with elements: disposition, name, filename. Throws an IOException
353 // if the line is malformatted.
354 //
355 private String[] extractDispositionInfo(String line) throws IOException {
356 // Return the line's data as an array: disposition, name, filename
357 String[] retval = new String[3];
358
359 // Convert the line to a lowercase string without the ending \r\n
360 // Keep the original line for error messages and for variable names.
361 String origline = line;
362 line = origline.toLowerCase();
363
364 // Get the content disposition, should be "form-data"
365 int start = line.indexOf("content-disposition: ");
366 int end = line.indexOf(";");
367 if (start == -1 || end == -1) {
368 throw new IOException("Content disposition corrupt: " + origline);
369 }
370 String disposition = line.substring(start + 21, end);
371 if (!disposition.equals("form-data")) {
372 throw new IOException("Invalid content disposition: " + disposition);
373 }
374
375 // Get the field name
376 start = line.indexOf("name=\"", end); // start at last semicolon
377 end = line.indexOf("\"", start + 7); // skip name=\"
378 if (start == -1 || end == -1) {
379 throw new IOException("Content disposition corrupt: " + origline);
380 }
381 String name = origline.substring(start + 6, end);
382
383 // Get the filename, if given
384 String filename = null;
385 start = line.indexOf("filename=\"", end + 2); // start after name
386 end = line.indexOf("\"", start + 10); // skip filename=\"
387 if (start != -1 && end != -1) { // note the !=
388 filename = origline.substring(start + 10, end);
389 // The filename may contain a full path. Cut to just the filename.
390 int slash =
391 Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
392 if (slash > -1) {
393 filename = filename.substring(slash + 1); // past last slash
394 }
395 if (filename.equals("")) filename = "unknown"; // sanity check
396 }
397
398 // Return a String array: disposition, name, filename
399 retval[0] = disposition;
400 retval[1] = name;
401 retval[2] = filename;
402 return retval;
403 }
404
405 // Extracts and returns the content type from a line, or null if the
406 // line was empty. Throws an IOException if the line is malformatted.
407 //
408 private String extractContentType(String line) throws IOException {
409 String contentType = null;
410
411 // Convert the line to a lowercase string
412 String origline = line;
413 line = origline.toLowerCase();
414
415 // Get the content type, if any
416 if (line.startsWith("content-type")) {
417 int start = line.indexOf(" ");
418 if (start == -1) {
419 throw new IOException("Content type corrupt: " + origline);
420 }
421 contentType = line.substring(start + 1);
422 } else if (line.length() != 0) { // no content type, so should be empty
423 throw new IOException("Malformed line after disposition: " + origline);
424 }
425
426 return contentType;
427 }
428 }
429
430
431 // A class to hold information about an uploaded file.
432 //
433
434 class UploadedFile {
435
436 private String dir;
437 private String filename;
438 private String type;
439
440 UploadedFile(String dir, String filename, String type) {
441 this.dir = dir;
442 this.filename = filename;
443 this.type = type;
444 }
445
446 public String getContentType() {
447 return type;
448 }
449
450 public String getFilesystemName() {
451 return filename;
452 }
453
454 public File getFile() {
455 if (dir == null || filename == null) {
456 return null;
457 } else {
458 return new File(dir + File.separator + filename);
459 }
460 }
461
462 }
463
464
465