001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/util/RequestUtil.java,v 1.19 2002/02/21 22:51:55 remm Exp $
003: * $Revision: 1.19 $
004: * $Date: 2002/02/21 22:51:55 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.util;
065:
066: import java.io.UnsupportedEncodingException;
067: import java.text.SimpleDateFormat;
068: import java.util.ArrayList;
069: import java.util.Date;
070: import java.util.Map;
071: import java.util.TimeZone;
072: import javax.servlet.http.Cookie;
073:
074: /**
075: * General purpose request parsing and encoding utility methods.
076: *
077: * @author Craig R. McClanahan
078: * @author Tim Tye
079: * @version $Revision: 1.19 $ $Date: 2002/02/21 22:51:55 $
080: */
081:
082: public final class RequestUtil {
083:
084: /**
085: * The DateFormat to use for generating readable dates in cookies.
086: */
087: private static SimpleDateFormat format = new SimpleDateFormat(
088: " EEEE, dd-MMM-yy kk:mm:ss zz");
089:
090: static {
091: format.setTimeZone(TimeZone.getTimeZone("GMT"));
092: }
093:
094: /**
095: * Encode a cookie as per RFC 2109. The resulting string can be used
096: * as the value for a <code>Set-Cookie</code> header.
097: *
098: * @param cookie The cookie to encode.
099: * @return A string following RFC 2109.
100: */
101: public static String encodeCookie(Cookie cookie) {
102:
103: StringBuffer buf = new StringBuffer(cookie.getName());
104: buf.append("=");
105: buf.append(cookie.getValue());
106:
107: if (cookie.getComment() != null) {
108: buf.append("; Comment=\"");
109: buf.append(cookie.getComment());
110: buf.append("\"");
111: }
112:
113: if (cookie.getDomain() != null) {
114: buf.append("; Domain=\"");
115: buf.append(cookie.getDomain());
116: buf.append("\"");
117: }
118:
119: long age = cookie.getMaxAge();
120: if (cookie.getMaxAge() >= 0) {
121: buf.append("; Max-Age=\"");
122: buf.append(cookie.getMaxAge());
123: buf.append("\"");
124: }
125:
126: if (cookie.getPath() != null) {
127: buf.append("; Path=\"");
128: buf.append(cookie.getPath());
129: buf.append("\"");
130: }
131:
132: if (cookie.getSecure()) {
133: buf.append("; Secure");
134: }
135:
136: if (cookie.getVersion() > 0) {
137: buf.append("; Version=\"");
138: buf.append(cookie.getVersion());
139: buf.append("\"");
140: }
141:
142: return (buf.toString());
143: }
144:
145: /**
146: * Filter the specified message string for characters that are sensitive
147: * in HTML. This avoids potential attacks caused by including JavaScript
148: * codes in the request URL that is often reported in error messages.
149: *
150: * @param message The message string to be filtered
151: */
152: public static String filter(String message) {
153:
154: if (message == null)
155: return (null);
156:
157: char content[] = new char[message.length()];
158: message.getChars(0, message.length(), content, 0);
159: StringBuffer result = new StringBuffer(content.length + 50);
160: for (int i = 0; i < content.length; i++) {
161: switch (content[i]) {
162: case '<':
163: result.append("<");
164: break;
165: case '>':
166: result.append(">");
167: break;
168: case '&':
169: result.append("&");
170: break;
171: case '"':
172: result.append(""");
173: break;
174: default:
175: result.append(content[i]);
176: }
177: }
178: return (result.toString());
179:
180: }
181:
182: /**
183: * Normalize a relative URI path that may have relative values ("/./",
184: * "/../", and so on ) it it. <strong>WARNING</strong> - This method is
185: * useful only for normalizing application-generated paths. It does not
186: * try to perform security checks for malicious input.
187: *
188: * @param path Relative path to be normalized
189: */
190: public static String normalize(String path) {
191:
192: if (path == null)
193: return null;
194:
195: // Create a place for the normalized path
196: String normalized = path;
197:
198: if (normalized.equals("/."))
199: return "/";
200:
201: // Add a leading "/" if necessary
202: if (!normalized.startsWith("/"))
203: normalized = "/" + normalized;
204:
205: // Resolve occurrences of "//" in the normalized path
206: while (true) {
207: int index = normalized.indexOf("//");
208: if (index < 0)
209: break;
210: normalized = normalized.substring(0, index)
211: + normalized.substring(index + 1);
212: }
213:
214: // Resolve occurrences of "/./" in the normalized path
215: while (true) {
216: int index = normalized.indexOf("/./");
217: if (index < 0)
218: break;
219: normalized = normalized.substring(0, index)
220: + normalized.substring(index + 2);
221: }
222:
223: // Resolve occurrences of "/../" in the normalized path
224: while (true) {
225: int index = normalized.indexOf("/../");
226: if (index < 0)
227: break;
228: if (index == 0)
229: return (null); // Trying to go outside our context
230: int index2 = normalized.lastIndexOf('/', index - 1);
231: normalized = normalized.substring(0, index2)
232: + normalized.substring(index + 3);
233: }
234:
235: // Return the normalized path that we have completed
236: return (normalized);
237:
238: }
239:
240: /**
241: * Parse the character encoding from the specified content type header.
242: * If the content type is null, or there is no explicit character encoding,
243: * <code>null</code> is returned.
244: *
245: * @param contentType a content type header
246: */
247: public static String parseCharacterEncoding(String contentType) {
248:
249: if (contentType == null)
250: return (null);
251: int start = contentType.indexOf("charset=");
252: if (start < 0)
253: return (null);
254: String encoding = contentType.substring(start + 8);
255: int end = encoding.indexOf(';');
256: if (end >= 0)
257: encoding = encoding.substring(0, end);
258: encoding = encoding.trim();
259: if ((encoding.length() > 2) && (encoding.startsWith("\""))
260: && (encoding.endsWith("\"")))
261: encoding = encoding.substring(1, encoding.length() - 1);
262: return (encoding.trim());
263:
264: }
265:
266: /**
267: * Parse a cookie header into an array of cookies according to RFC 2109.
268: *
269: * @param header Value of an HTTP "Cookie" header
270: */
271: public static Cookie[] parseCookieHeader(String header) {
272:
273: if ((header == null) || (header.length() < 1))
274: return (new Cookie[0]);
275:
276: ArrayList cookies = new ArrayList();
277: while (header.length() > 0) {
278: int semicolon = header.indexOf(';');
279: if (semicolon < 0)
280: semicolon = header.length();
281: if (semicolon == 0)
282: break;
283: String token = header.substring(0, semicolon);
284: if (semicolon < header.length())
285: header = header.substring(semicolon + 1);
286: else
287: header = "";
288: try {
289: int equals = token.indexOf('=');
290: if (equals > 0) {
291: String name = token.substring(0, equals).trim();
292: String value = token.substring(equals + 1).trim();
293: cookies.add(new Cookie(name, value));
294: }
295: } catch (Throwable e) {
296: ;
297: }
298: }
299:
300: return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()]));
301:
302: }
303:
304: /**
305: * Append request parameters from the specified String to the specified
306: * Map. It is presumed that the specified Map is not accessed from any
307: * other thread, so no synchronization is performed.
308: * <p>
309: * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed
310: * individually on the parsed name and value elements, rather than on
311: * the entire query string ahead of time, to properly deal with the case
312: * where the name or value includes an encoded "=" or "&" character
313: * that would otherwise be interpreted as a delimiter.
314: *
315: * @param map Map that accumulates the resulting parameters
316: * @param data Input string containing request parameters
317: * @param urlParameters true if we're parsing parameters on the URL
318: *
319: * @exception IllegalArgumentException if the data is malformed
320: */
321: public static void parseParameters(Map map, String data,
322: String encoding) throws UnsupportedEncodingException {
323:
324: if ((data != null) && (data.length() > 0)) {
325: int len = data.length();
326: byte[] bytes = new byte[len];
327: data.getBytes(0, len, bytes, 0);
328: parseParameters(map, bytes, encoding);
329: }
330:
331: }
332:
333: /**
334: * Decode and return the specified URL-encoded String.
335: * When the byte array is converted to a string, the system default
336: * character encoding is used... This may be different than some other
337: * servers.
338: *
339: * @param str The url-encoded string
340: *
341: * @exception IllegalArgumentException if a '%' character is not followed
342: * by a valid 2-digit hexadecimal number
343: */
344: public static String URLDecode(String str) {
345:
346: return URLDecode(str, null);
347:
348: }
349:
350: /**
351: * Decode and return the specified URL-encoded String.
352: *
353: * @param str The url-encoded string
354: * @param enc The encoding to use; if null, the default encoding is used
355: * @exception IllegalArgumentException if a '%' character is not followed
356: * by a valid 2-digit hexadecimal number
357: */
358: public static String URLDecode(String str, String enc) {
359:
360: if (str == null)
361: return (null);
362:
363: int len = str.length();
364: byte[] bytes = new byte[len];
365: str.getBytes(0, len, bytes, 0);
366:
367: return URLDecode(bytes, enc);
368:
369: }
370:
371: /**
372: * Decode and return the specified URL-encoded byte array.
373: *
374: * @param bytes The url-encoded byte array
375: * @exception IllegalArgumentException if a '%' character is not followed
376: * by a valid 2-digit hexadecimal number
377: */
378: public static String URLDecode(byte[] bytes) {
379: return URLDecode(bytes, null);
380: }
381:
382: /**
383: * Decode and return the specified URL-encoded byte array.
384: *
385: * @param bytes The url-encoded byte array
386: * @param enc The encoding to use; if null, the default encoding is used
387: * @exception IllegalArgumentException if a '%' character is not followed
388: * by a valid 2-digit hexadecimal number
389: */
390: public static String URLDecode(byte[] bytes, String enc) {
391:
392: if (bytes == null)
393: return (null);
394:
395: int len = bytes.length;
396: int ix = 0;
397: int ox = 0;
398: while (ix < len) {
399: byte b = bytes[ix++]; // Get byte to test
400: if (b == '+') {
401: b = (byte) ' ';
402: } else if (b == '%') {
403: b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + convertHexDigit(bytes[ix++]));
404: }
405: bytes[ox++] = b;
406: }
407: if (enc != null) {
408: try {
409: return new String(bytes, 0, ox, enc);
410: } catch (Exception e) {
411: e.printStackTrace();
412: }
413: }
414: return new String(bytes, 0, ox);
415:
416: }
417:
418: /**
419: * Convert a byte character value to hexidecimal digit value.
420: *
421: * @param b the character value byte
422: */
423: private static byte convertHexDigit(byte b) {
424: if ((b >= '0') && (b <= '9'))
425: return (byte) (b - '0');
426: if ((b >= 'a') && (b <= 'f'))
427: return (byte) (b - 'a' + 10);
428: if ((b >= 'A') && (b <= 'F'))
429: return (byte) (b - 'A' + 10);
430: return 0;
431: }
432:
433: /**
434: * Put name value pair in map.
435: *
436: * @param b the character value byte
437: *
438: * Put name and value pair in map. When name already exist, add value
439: * to array of values.
440: */
441: private static void putMapEntry(Map map, String name, String value) {
442: String[] newValues = null;
443: String[] oldValues = (String[]) map.get(name);
444: if (oldValues == null) {
445: newValues = new String[1];
446: newValues[0] = value;
447: } else {
448: newValues = new String[oldValues.length + 1];
449: System.arraycopy(oldValues, 0, newValues, 0,
450: oldValues.length);
451: newValues[oldValues.length] = value;
452: }
453: map.put(name, newValues);
454: }
455:
456: /**
457: * Append request parameters from the specified String to the specified
458: * Map. It is presumed that the specified Map is not accessed from any
459: * other thread, so no synchronization is performed.
460: * <p>
461: * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed
462: * individually on the parsed name and value elements, rather than on
463: * the entire query string ahead of time, to properly deal with the case
464: * where the name or value includes an encoded "=" or "&" character
465: * that would otherwise be interpreted as a delimiter.
466: *
467: * NOTE: byte array data is modified by this method. Caller beware.
468: *
469: * @param map Map that accumulates the resulting parameters
470: * @param data Input string containing request parameters
471: * @param encoding Encoding to use for converting hex
472: *
473: * @exception UnsupportedEncodingException if the data is malformed
474: */
475: public static void parseParameters(Map map, byte[] data,
476: String encoding) throws UnsupportedEncodingException {
477:
478: if (data != null && data.length > 0) {
479: int pos = 0;
480: int ix = 0;
481: int ox = 0;
482: String key = null;
483: String value = null;
484: while (ix < data.length) {
485: byte c = data[ix++];
486: switch ((char) c) {
487: case '&':
488: value = new String(data, 0, ox, encoding);
489: if (key != null) {
490: putMapEntry(map, key, value);
491: key = null;
492: }
493: ox = 0;
494: break;
495: case '=':
496: key = new String(data, 0, ox, encoding);
497: ox = 0;
498: break;
499: case '+':
500: data[ox++] = (byte) ' ';
501: break;
502: case '%':
503: data[ox++] = (byte) ((convertHexDigit(data[ix++]) << 4) + convertHexDigit(data[ix++]));
504: break;
505: default:
506: data[ox++] = c;
507: }
508: }
509: //The last value does not end in '&'. So save it now.
510: if (key != null) {
511: value = new String(data, 0, ox, encoding);
512: putMapEntry(map, key, value);
513: }
514: }
515:
516: }
517:
518: }
|