001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.catalina.util;
019:
020: import java.io.UnsupportedEncodingException;
021: import java.text.SimpleDateFormat;
022: import java.util.Map;
023: import java.util.TimeZone;
024:
025: /**
026: * General purpose request parsing and encoding utility methods.
027: *
028: * @author Craig R. McClanahan
029: * @author Tim Tye
030: * @version $Revision: 528897 $ $Date: 2007-04-15 02:24:49 +0200 (dim., 15 avr. 2007) $
031: */
032:
033: public final class RequestUtil {
034:
035: /**
036: * The DateFormat to use for generating readable dates in cookies.
037: */
038: private static SimpleDateFormat format = new SimpleDateFormat(
039: " EEEE, dd-MMM-yy kk:mm:ss zz");
040:
041: static {
042: format.setTimeZone(TimeZone.getTimeZone("GMT"));
043: }
044:
045: /**
046: * Filter the specified message string for characters that are sensitive
047: * in HTML. This avoids potential attacks caused by including JavaScript
048: * codes in the request URL that is often reported in error messages.
049: *
050: * @param message The message string to be filtered
051: */
052: public static String filter(String message) {
053:
054: if (message == null)
055: return (null);
056:
057: char content[] = new char[message.length()];
058: message.getChars(0, message.length(), content, 0);
059: StringBuffer result = new StringBuffer(content.length + 50);
060: for (int i = 0; i < content.length; i++) {
061: switch (content[i]) {
062: case '<':
063: result.append("<");
064: break;
065: case '>':
066: result.append(">");
067: break;
068: case '&':
069: result.append("&");
070: break;
071: case '"':
072: result.append(""");
073: break;
074: default:
075: result.append(content[i]);
076: }
077: }
078: return (result.toString());
079:
080: }
081:
082: /**
083: * Normalize a relative URI path that may have relative values ("/./",
084: * "/../", and so on ) it it. <strong>WARNING</strong> - This method is
085: * useful only for normalizing application-generated paths. It does not
086: * try to perform security checks for malicious input.
087: *
088: * @param path Relative path to be normalized
089: */
090: public static String normalize(String path) {
091:
092: if (path == null)
093: return null;
094:
095: // Create a place for the normalized path
096: String normalized = path;
097:
098: if (normalized.equals("/."))
099: return "/";
100:
101: // Add a leading "/" if necessary
102: if (!normalized.startsWith("/"))
103: normalized = "/" + normalized;
104:
105: // Resolve occurrences of "//" in the normalized path
106: while (true) {
107: int index = normalized.indexOf("//");
108: if (index < 0)
109: break;
110: normalized = normalized.substring(0, index)
111: + normalized.substring(index + 1);
112: }
113:
114: // Resolve occurrences of "/./" in the normalized path
115: while (true) {
116: int index = normalized.indexOf("/./");
117: if (index < 0)
118: break;
119: normalized = normalized.substring(0, index)
120: + normalized.substring(index + 2);
121: }
122:
123: // Resolve occurrences of "/../" in the normalized path
124: while (true) {
125: int index = normalized.indexOf("/../");
126: if (index < 0)
127: break;
128: if (index == 0)
129: return (null); // Trying to go outside our context
130: int index2 = normalized.lastIndexOf('/', index - 1);
131: normalized = normalized.substring(0, index2)
132: + normalized.substring(index + 3);
133: }
134:
135: // Return the normalized path that we have completed
136: return (normalized);
137:
138: }
139:
140: /**
141: * Append request parameters from the specified String to the specified
142: * Map. It is presumed that the specified Map is not accessed from any
143: * other thread, so no synchronization is performed.
144: * <p>
145: * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed
146: * individually on the parsed name and value elements, rather than on
147: * the entire query string ahead of time, to properly deal with the case
148: * where the name or value includes an encoded "=" or "&" character
149: * that would otherwise be interpreted as a delimiter.
150: *
151: * @param map Map that accumulates the resulting parameters
152: * @param data Input string containing request parameters
153: *
154: * @exception IllegalArgumentException if the data is malformed
155: */
156: public static void parseParameters(Map map, String data,
157: String encoding) throws UnsupportedEncodingException {
158:
159: if ((data != null) && (data.length() > 0)) {
160:
161: // use the specified encoding to extract bytes out of the
162: // given string so that the encoding is not lost. If an
163: // encoding is not specified, let it use platform default
164: byte[] bytes = null;
165: try {
166: if (encoding == null) {
167: bytes = data.getBytes();
168: } else {
169: bytes = data.getBytes(encoding);
170: }
171: } catch (UnsupportedEncodingException uee) {
172: }
173:
174: parseParameters(map, bytes, encoding);
175: }
176:
177: }
178:
179: /**
180: * Decode and return the specified URL-encoded String.
181: * When the byte array is converted to a string, the system default
182: * character encoding is used... This may be different than some other
183: * servers.
184: *
185: * @param str The url-encoded string
186: *
187: * @exception IllegalArgumentException if a '%' character is not followed
188: * by a valid 2-digit hexadecimal number
189: */
190: public static String URLDecode(String str) {
191:
192: return URLDecode(str, null);
193:
194: }
195:
196: /**
197: * Decode and return the specified URL-encoded String.
198: *
199: * @param str The url-encoded string
200: * @param enc The encoding to use; if null, the default encoding is used
201: * @exception IllegalArgumentException if a '%' character is not followed
202: * by a valid 2-digit hexadecimal number
203: */
204: public static String URLDecode(String str, String enc) {
205:
206: if (str == null)
207: return (null);
208:
209: // use the specified encoding to extract bytes out of the
210: // given string so that the encoding is not lost. If an
211: // encoding is not specified, let it use platform default
212: byte[] bytes = null;
213: try {
214: if (enc == null) {
215: bytes = str.getBytes();
216: } else {
217: bytes = str.getBytes(enc);
218: }
219: } catch (UnsupportedEncodingException uee) {
220: }
221:
222: return URLDecode(bytes, enc);
223:
224: }
225:
226: /**
227: * Decode and return the specified URL-encoded byte array.
228: *
229: * @param bytes The url-encoded byte array
230: * @exception IllegalArgumentException if a '%' character is not followed
231: * by a valid 2-digit hexadecimal number
232: */
233: public static String URLDecode(byte[] bytes) {
234: return URLDecode(bytes, null);
235: }
236:
237: /**
238: * Decode and return the specified URL-encoded byte array.
239: *
240: * @param bytes The url-encoded byte array
241: * @param enc The encoding to use; if null, the default encoding is used
242: * @exception IllegalArgumentException if a '%' character is not followed
243: * by a valid 2-digit hexadecimal number
244: */
245: public static String URLDecode(byte[] bytes, String enc) {
246:
247: if (bytes == null)
248: return (null);
249:
250: int len = bytes.length;
251: int ix = 0;
252: int ox = 0;
253: while (ix < len) {
254: byte b = bytes[ix++]; // Get byte to test
255: if (b == '+') {
256: b = (byte) ' ';
257: } else if (b == '%') {
258: b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + convertHexDigit(bytes[ix++]));
259: }
260: bytes[ox++] = b;
261: }
262: if (enc != null) {
263: try {
264: return new String(bytes, 0, ox, enc);
265: } catch (Exception e) {
266: e.printStackTrace();
267: }
268: }
269: return new String(bytes, 0, ox);
270:
271: }
272:
273: /**
274: * Convert a byte character value to hexidecimal digit value.
275: *
276: * @param b the character value byte
277: */
278: private static byte convertHexDigit(byte b) {
279: if ((b >= '0') && (b <= '9'))
280: return (byte) (b - '0');
281: if ((b >= 'a') && (b <= 'f'))
282: return (byte) (b - 'a' + 10);
283: if ((b >= 'A') && (b <= 'F'))
284: return (byte) (b - 'A' + 10);
285: return 0;
286: }
287:
288: /**
289: * Put name and value pair in map. When name already exist, add value
290: * to array of values.
291: *
292: * @param map The map to populate
293: * @param name The parameter name
294: * @param value The parameter value
295: */
296: private static void putMapEntry(Map map, String name, String value) {
297: String[] newValues = null;
298: String[] oldValues = (String[]) map.get(name);
299: if (oldValues == null) {
300: newValues = new String[1];
301: newValues[0] = value;
302: } else {
303: newValues = new String[oldValues.length + 1];
304: System.arraycopy(oldValues, 0, newValues, 0,
305: oldValues.length);
306: newValues[oldValues.length] = value;
307: }
308: map.put(name, newValues);
309: }
310:
311: /**
312: * Append request parameters from the specified String to the specified
313: * Map. It is presumed that the specified Map is not accessed from any
314: * other thread, so no synchronization is performed.
315: * <p>
316: * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed
317: * individually on the parsed name and value elements, rather than on
318: * the entire query string ahead of time, to properly deal with the case
319: * where the name or value includes an encoded "=" or "&" character
320: * that would otherwise be interpreted as a delimiter.
321: *
322: * NOTE: byte array data is modified by this method. Caller beware.
323: *
324: * @param map Map that accumulates the resulting parameters
325: * @param data Input string containing request parameters
326: * @param encoding Encoding to use for converting hex
327: *
328: * @exception UnsupportedEncodingException if the data is malformed
329: */
330: public static void parseParameters(Map map, byte[] data,
331: String encoding) throws UnsupportedEncodingException {
332:
333: if (data != null && data.length > 0) {
334: int ix = 0;
335: int ox = 0;
336: String key = null;
337: String value = null;
338: while (ix < data.length) {
339: byte c = data[ix++];
340: switch ((char) c) {
341: case '&':
342: value = new String(data, 0, ox, encoding);
343: if (key != null) {
344: putMapEntry(map, key, value);
345: key = null;
346: }
347: ox = 0;
348: break;
349: case '=':
350: if (key == null) {
351: key = new String(data, 0, ox, encoding);
352: ox = 0;
353: } else {
354: data[ox++] = c;
355: }
356: break;
357: case '+':
358: data[ox++] = (byte) ' ';
359: break;
360: case '%':
361: data[ox++] = (byte) ((convertHexDigit(data[ix++]) << 4) + convertHexDigit(data[ix++]));
362: break;
363: default:
364: data[ox++] = c;
365: }
366: }
367: //The last value does not end in '&'. So save it now.
368: if (key != null) {
369: value = new String(data, 0, ox, encoding);
370: putMapEntry(map, key, value);
371: }
372: }
373:
374: }
375:
376: }
|