001: /*
002: * Copyright 1999,2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.apache.catalina.util;
018:
019: import java.io.UnsupportedEncodingException;
020: import java.text.SimpleDateFormat;
021: import java.util.ArrayList;
022: import java.util.Map;
023: import java.util.TimeZone;
024:
025: import javax.servlet.http.Cookie;
026:
027: /**
028: * General purpose request parsing and encoding utility methods.
029: *
030: * @author Craig R. McClanahan
031: * @author Tim Tye
032: * @version $Revision: 1.8 $ $Date: 2004/05/26 16:26:10 $
033: */
034:
035: public final class RequestUtil {
036:
037: /**
038: * The DateFormat to use for generating readable dates in cookies.
039: */
040: private static SimpleDateFormat format = new SimpleDateFormat(
041: " EEEE, dd-MMM-yy kk:mm:ss zz");
042:
043: static {
044: format.setTimeZone(TimeZone.getTimeZone("GMT"));
045: }
046:
047: /**
048: * Encode a cookie as per RFC 2109. The resulting string can be used
049: * as the value for a <code>Set-Cookie</code> header.
050: *
051: * @param cookie The cookie to encode.
052: * @return A string following RFC 2109.
053: */
054: public static String encodeCookie(Cookie cookie) {
055:
056: StringBuffer buf = new StringBuffer(cookie.getName());
057: buf.append("=");
058: buf.append(cookie.getValue());
059:
060: if (cookie.getComment() != null) {
061: buf.append("; Comment=\"");
062: buf.append(cookie.getComment());
063: buf.append("\"");
064: }
065:
066: if (cookie.getDomain() != null) {
067: buf.append("; Domain=\"");
068: buf.append(cookie.getDomain());
069: buf.append("\"");
070: }
071:
072: long age = cookie.getMaxAge();
073: if (cookie.getMaxAge() >= 0) {
074: buf.append("; Max-Age=\"");
075: buf.append(cookie.getMaxAge());
076: buf.append("\"");
077: }
078:
079: if (cookie.getPath() != null) {
080: buf.append("; Path=\"");
081: buf.append(cookie.getPath());
082: buf.append("\"");
083: }
084:
085: if (cookie.getSecure()) {
086: buf.append("; Secure");
087: }
088:
089: if (cookie.getVersion() > 0) {
090: buf.append("; Version=\"");
091: buf.append(cookie.getVersion());
092: buf.append("\"");
093: }
094:
095: return (buf.toString());
096: }
097:
098: /**
099: * Filter the specified message string for characters that are sensitive
100: * in HTML. This avoids potential attacks caused by including JavaScript
101: * codes in the request URL that is often reported in error messages.
102: *
103: * @param message The message string to be filtered
104: */
105: public static String filter(String message) {
106:
107: if (message == null)
108: return (null);
109:
110: char content[] = new char[message.length()];
111: message.getChars(0, message.length(), content, 0);
112: StringBuffer result = new StringBuffer(content.length + 50);
113: for (int i = 0; i < content.length; i++) {
114: switch (content[i]) {
115: case '<':
116: result.append("<");
117: break;
118: case '>':
119: result.append(">");
120: break;
121: case '&':
122: result.append("&");
123: break;
124: case '"':
125: result.append(""");
126: break;
127: default:
128: result.append(content[i]);
129: }
130: }
131: return (result.toString());
132:
133: }
134:
135: /**
136: * Normalize a relative URI path that may have relative values ("/./",
137: * "/../", and so on ) it it. <strong>WARNING</strong> - This method is
138: * useful only for normalizing application-generated paths. It does not
139: * try to perform security checks for malicious input.
140: *
141: * @param path Relative path to be normalized
142: */
143: public static String normalize(String path) {
144:
145: if (path == null)
146: return null;
147:
148: // Create a place for the normalized path
149: String normalized = path;
150:
151: if (normalized.equals("/."))
152: return "/";
153:
154: // Add a leading "/" if necessary
155: if (!normalized.startsWith("/"))
156: normalized = "/" + normalized;
157:
158: // Resolve occurrences of "//" in the normalized path
159: while (true) {
160: int index = normalized.indexOf("//");
161: if (index < 0)
162: break;
163: normalized = normalized.substring(0, index)
164: + normalized.substring(index + 1);
165: }
166:
167: // Resolve occurrences of "/./" in the normalized path
168: while (true) {
169: int index = normalized.indexOf("/./");
170: if (index < 0)
171: break;
172: normalized = normalized.substring(0, index)
173: + normalized.substring(index + 2);
174: }
175:
176: // Resolve occurrences of "/../" in the normalized path
177: while (true) {
178: int index = normalized.indexOf("/../");
179: if (index < 0)
180: break;
181: if (index == 0)
182: return (null); // Trying to go outside our context
183: int index2 = normalized.lastIndexOf('/', index - 1);
184: normalized = normalized.substring(0, index2)
185: + normalized.substring(index + 3);
186: }
187:
188: // Return the normalized path that we have completed
189: return (normalized);
190:
191: }
192:
193: /**
194: * Parse the character encoding from the specified content type header.
195: * If the content type is null, or there is no explicit character encoding,
196: * <code>null</code> is returned.
197: *
198: * @param contentType a content type header
199: */
200: public static String parseCharacterEncoding(String contentType) {
201:
202: if (contentType == null)
203: return (null);
204: int start = contentType.indexOf("charset=");
205: if (start < 0)
206: return (null);
207: String encoding = contentType.substring(start + 8);
208: int end = encoding.indexOf(';');
209: if (end >= 0)
210: encoding = encoding.substring(0, end);
211: encoding = encoding.trim();
212: if ((encoding.length() > 2) && (encoding.startsWith("\""))
213: && (encoding.endsWith("\"")))
214: encoding = encoding.substring(1, encoding.length() - 1);
215: return (encoding.trim());
216:
217: }
218:
219: /**
220: * Parse a cookie header into an array of cookies according to RFC 2109.
221: *
222: * @param header Value of an HTTP "Cookie" header
223: */
224: public static Cookie[] parseCookieHeader(String header) {
225:
226: if ((header == null) || (header.length() < 1))
227: return (new Cookie[0]);
228:
229: ArrayList cookies = new ArrayList();
230: while (header.length() > 0) {
231: int semicolon = header.indexOf(';');
232: if (semicolon < 0)
233: semicolon = header.length();
234: if (semicolon == 0)
235: break;
236: String token = header.substring(0, semicolon);
237: if (semicolon < header.length())
238: header = header.substring(semicolon + 1);
239: else
240: header = "";
241: try {
242: int equals = token.indexOf('=');
243: if (equals > 0) {
244: String name = token.substring(0, equals).trim();
245: String value = token.substring(equals + 1).trim();
246: cookies.add(new Cookie(name, value));
247: }
248: } catch (Throwable e) {
249: ;
250: }
251: }
252:
253: return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()]));
254:
255: }
256:
257: /**
258: * Append request parameters from the specified String to the specified
259: * Map. It is presumed that the specified Map is not accessed from any
260: * other thread, so no synchronization is performed.
261: * <p>
262: * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed
263: * individually on the parsed name and value elements, rather than on
264: * the entire query string ahead of time, to properly deal with the case
265: * where the name or value includes an encoded "=" or "&" character
266: * that would otherwise be interpreted as a delimiter.
267: *
268: * @param map Map that accumulates the resulting parameters
269: * @param data Input string containing request parameters
270: *
271: * @exception IllegalArgumentException if the data is malformed
272: */
273: public static void parseParameters(Map map, String data,
274: String encoding) throws UnsupportedEncodingException {
275:
276: if ((data != null) && (data.length() > 0)) {
277:
278: // use the specified encoding to extract bytes out of the
279: // given string so that the encoding is not lost. If an
280: // encoding is not specified, let it use platform default
281: byte[] bytes = null;
282: try {
283: if (encoding == null) {
284: bytes = data.getBytes();
285: } else {
286: bytes = data.getBytes(encoding);
287: }
288: } catch (UnsupportedEncodingException uee) {
289: }
290:
291: parseParameters(map, bytes, encoding);
292: }
293:
294: }
295:
296: /**
297: * Decode and return the specified URL-encoded String.
298: * When the byte array is converted to a string, the system default
299: * character encoding is used... This may be different than some other
300: * servers.
301: *
302: * @param str The url-encoded string
303: *
304: * @exception IllegalArgumentException if a '%' character is not followed
305: * by a valid 2-digit hexadecimal number
306: */
307: public static String URLDecode(String str) {
308:
309: return URLDecode(str, null);
310:
311: }
312:
313: /**
314: * Decode and return the specified URL-encoded String.
315: *
316: * @param str The url-encoded string
317: * @param enc The encoding to use; if null, the default encoding is used
318: * @exception IllegalArgumentException if a '%' character is not followed
319: * by a valid 2-digit hexadecimal number
320: */
321: public static String URLDecode(String str, String enc) {
322:
323: if (str == null)
324: return (null);
325:
326: // use the specified encoding to extract bytes out of the
327: // given string so that the encoding is not lost. If an
328: // encoding is not specified, let it use platform default
329: byte[] bytes = null;
330: try {
331: if (enc == null) {
332: bytes = str.getBytes();
333: } else {
334: bytes = str.getBytes(enc);
335: }
336: } catch (UnsupportedEncodingException uee) {
337: }
338:
339: return URLDecode(bytes, enc);
340:
341: }
342:
343: /**
344: * Decode and return the specified URL-encoded byte array.
345: *
346: * @param bytes The url-encoded byte array
347: * @exception IllegalArgumentException if a '%' character is not followed
348: * by a valid 2-digit hexadecimal number
349: */
350: public static String URLDecode(byte[] bytes) {
351: return URLDecode(bytes, null);
352: }
353:
354: /**
355: * Decode and return the specified URL-encoded byte array.
356: *
357: * @param bytes The url-encoded byte array
358: * @param enc The encoding to use; if null, the default encoding is used
359: * @exception IllegalArgumentException if a '%' character is not followed
360: * by a valid 2-digit hexadecimal number
361: */
362: public static String URLDecode(byte[] bytes, String enc) {
363:
364: if (bytes == null)
365: return (null);
366:
367: int len = bytes.length;
368: int ix = 0;
369: int ox = 0;
370: while (ix < len) {
371: byte b = bytes[ix++]; // Get byte to test
372: if (b == '+') {
373: b = (byte) ' ';
374: } else if (b == '%') {
375: b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + convertHexDigit(bytes[ix++]));
376: }
377: bytes[ox++] = b;
378: }
379: if (enc != null) {
380: try {
381: return new String(bytes, 0, ox, enc);
382: } catch (Exception e) {
383: e.printStackTrace();
384: }
385: }
386: return new String(bytes, 0, ox);
387:
388: }
389:
390: /**
391: * Convert a byte character value to hexidecimal digit value.
392: *
393: * @param b the character value byte
394: */
395: private static byte convertHexDigit(byte b) {
396: if ((b >= '0') && (b <= '9'))
397: return (byte) (b - '0');
398: if ((b >= 'a') && (b <= 'f'))
399: return (byte) (b - 'a' + 10);
400: if ((b >= 'A') && (b <= 'F'))
401: return (byte) (b - 'A' + 10);
402: return 0;
403: }
404:
405: /**
406: * Put name and value pair in map. When name already exist, add value
407: * to array of values.
408: *
409: * @param map The map to populate
410: * @param name The parameter name
411: * @param value The parameter value
412: */
413: private static void putMapEntry(Map map, String name, String value) {
414: String[] newValues = null;
415: String[] oldValues = (String[]) map.get(name);
416: if (oldValues == null) {
417: newValues = new String[1];
418: newValues[0] = value;
419: } else {
420: newValues = new String[oldValues.length + 1];
421: System.arraycopy(oldValues, 0, newValues, 0,
422: oldValues.length);
423: newValues[oldValues.length] = value;
424: }
425: map.put(name, newValues);
426: }
427:
428: /**
429: * Append request parameters from the specified String to the specified
430: * Map. It is presumed that the specified Map is not accessed from any
431: * other thread, so no synchronization is performed.
432: * <p>
433: * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed
434: * individually on the parsed name and value elements, rather than on
435: * the entire query string ahead of time, to properly deal with the case
436: * where the name or value includes an encoded "=" or "&" character
437: * that would otherwise be interpreted as a delimiter.
438: *
439: * NOTE: byte array data is modified by this method. Caller beware.
440: *
441: * @param map Map that accumulates the resulting parameters
442: * @param data Input string containing request parameters
443: * @param encoding Encoding to use for converting hex
444: *
445: * @exception UnsupportedEncodingException if the data is malformed
446: */
447: public static void parseParameters(Map map, byte[] data,
448: String encoding) throws UnsupportedEncodingException {
449:
450: if (data != null && data.length > 0) {
451: int pos = 0;
452: int ix = 0;
453: int ox = 0;
454: String key = null;
455: String value = null;
456: while (ix < data.length) {
457: byte c = data[ix++];
458: switch ((char) c) {
459: case '&':
460: value = new String(data, 0, ox, encoding);
461: if (key != null) {
462: putMapEntry(map, key, value);
463: key = null;
464: }
465: ox = 0;
466: break;
467: case '=':
468: if (key == null) {
469: key = new String(data, 0, ox, encoding);
470: ox = 0;
471: } else {
472: data[ox++] = c;
473: }
474: break;
475: case '+':
476: data[ox++] = (byte) ' ';
477: break;
478: case '%':
479: data[ox++] = (byte) ((convertHexDigit(data[ix++]) << 4) + convertHexDigit(data[ix++]));
480: break;
481: default:
482: data[ox++] = c;
483: }
484: }
485: //The last value does not end in '&'. So save it now.
486: if (key != null) {
487: value = new String(data, 0, ox, encoding);
488: putMapEntry(map, key, value);
489: }
490: }
491:
492: }
493:
494: }
|