| /* |
| * Copyright 1999,2004 The Apache Software Foundation. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| package org.apache.catalina.util; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Map; |
| import java.util.TimeZone; |
| |
| import javax.servlet.http.Cookie; |
| |
| |
| /** |
| * General purpose request parsing and encoding utility methods. |
| * |
| * @author Craig R. McClanahan |
| * @author Tim Tye |
| * @version $Revision$ $Date$ |
| */ |
| |
| public final class RequestUtil { |
| |
| |
| /** |
| * The DateFormat to use for generating readable dates in cookies. |
| */ |
| private static SimpleDateFormat format = |
| new SimpleDateFormat(" EEEE, dd-MMM-yy kk:mm:ss zz"); |
| |
| static { |
| format.setTimeZone(TimeZone.getTimeZone("GMT")); |
| } |
| |
| |
| /** |
| * Encode a cookie as per RFC 2109. The resulting string can be used |
| * as the value for a <code>Set-Cookie</code> header. |
| * |
| * @param cookie The cookie to encode. |
| * @return A string following RFC 2109. |
| */ |
| public static String encodeCookie(Cookie cookie) { |
| |
| StringBuffer buf = new StringBuffer( cookie.getName() ); |
| buf.append("="); |
| buf.append(cookie.getValue()); |
| |
| if (cookie.getComment() != null) { |
| buf.append("; Comment=\""); |
| buf.append(cookie.getComment()); |
| buf.append("\""); |
| } |
| |
| if (cookie.getDomain() != null) { |
| buf.append("; Domain=\""); |
| buf.append(cookie.getDomain()); |
| buf.append("\""); |
| } |
| |
| long age = cookie.getMaxAge(); |
| if (cookie.getMaxAge() >= 0) { |
| buf.append("; Max-Age=\""); |
| buf.append(cookie.getMaxAge()); |
| buf.append("\""); |
| } |
| |
| if (cookie.getPath() != null) { |
| buf.append("; Path=\""); |
| buf.append(cookie.getPath()); |
| buf.append("\""); |
| } |
| |
| if (cookie.getSecure()) { |
| buf.append("; Secure"); |
| } |
| |
| if (cookie.getVersion() > 0) { |
| buf.append("; Version=\""); |
| buf.append(cookie.getVersion()); |
| buf.append("\""); |
| } |
| |
| return (buf.toString()); |
| } |
| |
| |
| /** |
| * Filter the specified message string for characters that are sensitive |
| * in HTML. This avoids potential attacks caused by including JavaScript |
| * codes in the request URL that is often reported in error messages. |
| * |
| * @param message The message string to be filtered |
| */ |
| public static String filter(String message) { |
| |
| if (message == null) |
| return (null); |
| |
| char content[] = new char[message.length()]; |
| message.getChars(0, message.length(), content, 0); |
| StringBuffer result = new StringBuffer(content.length + 50); |
| for (int i = 0; i < content.length; i++) { |
| switch (content[i]) { |
| case '<': |
| result.append("<"); |
| break; |
| case '>': |
| result.append(">"); |
| break; |
| case '&': |
| result.append("&"); |
| break; |
| case '"': |
| result.append("""); |
| break; |
| default: |
| result.append(content[i]); |
| } |
| } |
| return (result.toString()); |
| |
| } |
| |
| |
| /** |
| * Normalize a relative URI path that may have relative values ("/./", |
| * "/../", and so on ) it it. <strong>WARNING</strong> - This method is |
| * useful only for normalizing application-generated paths. It does not |
| * try to perform security checks for malicious input. |
| * |
| * @param path Relative path to be normalized |
| */ |
| public static String normalize(String path) { |
| |
| if (path == null) |
| return null; |
| |
| // Create a place for the normalized path |
| String normalized = path; |
| |
| if (normalized.equals("/.")) |
| return "/"; |
| |
| // Add a leading "/" if necessary |
| if (!normalized.startsWith("/")) |
| normalized = "/" + normalized; |
| |
| // Resolve occurrences of "//" in the normalized path |
| while (true) { |
| int index = normalized.indexOf("//"); |
| if (index < 0) |
| break; |
| normalized = normalized.substring(0, index) + |
| normalized.substring(index + 1); |
| } |
| |
| // Resolve occurrences of "/./" in the normalized path |
| while (true) { |
| int index = normalized.indexOf("/./"); |
| if (index < 0) |
| break; |
| normalized = normalized.substring(0, index) + |
| normalized.substring(index + 2); |
| } |
| |
| // Resolve occurrences of "/../" in the normalized path |
| while (true) { |
| int index = normalized.indexOf("/../"); |
| if (index < 0) |
| break; |
| if (index == 0) |
| return (null); // Trying to go outside our context |
| int index2 = normalized.lastIndexOf('/', index - 1); |
| normalized = normalized.substring(0, index2) + |
| normalized.substring(index + 3); |
| } |
| |
| // Return the normalized path that we have completed |
| return (normalized); |
| |
| } |
| |
| |
| /** |
| * Parse the character encoding from the specified content type header. |
| * If the content type is null, or there is no explicit character encoding, |
| * <code>null</code> is returned. |
| * |
| * @param contentType a content type header |
| */ |
| public static String parseCharacterEncoding(String contentType) { |
| |
| if (contentType == null) |
| return (null); |
| int start = contentType.indexOf("charset="); |
| if (start < 0) |
| return (null); |
| String encoding = contentType.substring(start + 8); |
| int end = encoding.indexOf(';'); |
| if (end >= 0) |
| encoding = encoding.substring(0, end); |
| encoding = encoding.trim(); |
| if ((encoding.length() > 2) && (encoding.startsWith("\"")) |
| && (encoding.endsWith("\""))) |
| encoding = encoding.substring(1, encoding.length() - 1); |
| return (encoding.trim()); |
| |
| } |
| |
| |
| /** |
| * Parse a cookie header into an array of cookies according to RFC 2109. |
| * |
| * @param header Value of an HTTP "Cookie" header |
| */ |
| public static Cookie[] parseCookieHeader(String header) { |
| |
| if ((header == null) || (header.length() < 1)) |
| return (new Cookie[0]); |
| |
| ArrayList cookies = new ArrayList(); |
| while (header.length() > 0) { |
| int semicolon = header.indexOf(';'); |
| if (semicolon < 0) |
| semicolon = header.length(); |
| if (semicolon == 0) |
| break; |
| String token = header.substring(0, semicolon); |
| if (semicolon < header.length()) |
| header = header.substring(semicolon + 1); |
| else |
| header = ""; |
| try { |
| int equals = token.indexOf('='); |
| if (equals > 0) { |
| String name = token.substring(0, equals).trim(); |
| String value = token.substring(equals+1).trim(); |
| cookies.add(new Cookie(name, value)); |
| } |
| } catch (Throwable e) { |
| ; |
| } |
| } |
| |
| return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()])); |
| |
| } |
| |
| |
| /** |
| * Append request parameters from the specified String to the specified |
| * Map. It is presumed that the specified Map is not accessed from any |
| * other thread, so no synchronization is performed. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed |
| * individually on the parsed name and value elements, rather than on |
| * the entire query string ahead of time, to properly deal with the case |
| * where the name or value includes an encoded "=" or "&" character |
| * that would otherwise be interpreted as a delimiter. |
| * |
| * @param map Map that accumulates the resulting parameters |
| * @param data Input string containing request parameters |
| * |
| * @exception IllegalArgumentException if the data is malformed |
| */ |
| public static void parseParameters(Map map, String data, String encoding) |
| throws UnsupportedEncodingException { |
| |
| if ((data != null) && (data.length() > 0)) { |
| |
| // use the specified encoding to extract bytes out of the |
| // given string so that the encoding is not lost. If an |
| // encoding is not specified, let it use platform default |
| byte[] bytes = null; |
| try { |
| if (encoding == null) { |
| bytes = data.getBytes(); |
| } else { |
| bytes = data.getBytes(encoding); |
| } |
| } catch (UnsupportedEncodingException uee) { |
| } |
| |
| parseParameters(map, bytes, encoding); |
| } |
| |
| } |
| |
| |
| /** |
| * Decode and return the specified URL-encoded String. |
| * When the byte array is converted to a string, the system default |
| * character encoding is used... This may be different than some other |
| * servers. |
| * |
| * @param str The url-encoded string |
| * |
| * @exception IllegalArgumentException if a '%' character is not followed |
| * by a valid 2-digit hexadecimal number |
| */ |
| public static String URLDecode(String str) { |
| |
| return URLDecode(str, null); |
| |
| } |
| |
| |
| /** |
| * Decode and return the specified URL-encoded String. |
| * |
| * @param str The url-encoded string |
| * @param enc The encoding to use; if null, the default encoding is used |
| * @exception IllegalArgumentException if a '%' character is not followed |
| * by a valid 2-digit hexadecimal number |
| */ |
| public static String URLDecode(String str, String enc) { |
| |
| if (str == null) |
| return (null); |
| |
| // use the specified encoding to extract bytes out of the |
| // given string so that the encoding is not lost. If an |
| // encoding is not specified, let it use platform default |
| byte[] bytes = null; |
| try { |
| if (enc == null) { |
| bytes = str.getBytes(); |
| } else { |
| bytes = str.getBytes(enc); |
| } |
| } catch (UnsupportedEncodingException uee) {} |
| |
| return URLDecode(bytes, enc); |
| |
| } |
| |
| |
| /** |
| * Decode and return the specified URL-encoded byte array. |
| * |
| * @param bytes The url-encoded byte array |
| * @exception IllegalArgumentException if a '%' character is not followed |
| * by a valid 2-digit hexadecimal number |
| */ |
| public static String URLDecode(byte[] bytes) { |
| return URLDecode(bytes, null); |
| } |
| |
| |
| /** |
| * Decode and return the specified URL-encoded byte array. |
| * |
| * @param bytes The url-encoded byte array |
| * @param enc The encoding to use; if null, the default encoding is used |
| * @exception IllegalArgumentException if a '%' character is not followed |
| * by a valid 2-digit hexadecimal number |
| */ |
| public static String URLDecode(byte[] bytes, String enc) { |
| |
| if (bytes == null) |
| return (null); |
| |
| int len = bytes.length; |
| int ix = 0; |
| int ox = 0; |
| while (ix < len) { |
| byte b = bytes[ix++]; // Get byte to test |
| if (b == '+') { |
| b = (byte)' '; |
| } else if (b == '%') { |
| b = (byte) ((convertHexDigit(bytes[ix++]) << 4) |
| + convertHexDigit(bytes[ix++])); |
| } |
| bytes[ox++] = b; |
| } |
| if (enc != null) { |
| try { |
| return new String(bytes, 0, ox, enc); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| return new String(bytes, 0, ox); |
| |
| } |
| |
| |
| /** |
| * Convert a byte character value to hexidecimal digit value. |
| * |
| * @param b the character value byte |
| */ |
| private static byte convertHexDigit( byte b ) { |
| if ((b >= '0') && (b <= '9')) return (byte)(b - '0'); |
| if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10); |
| if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10); |
| return 0; |
| } |
| |
| |
| /** |
| * Put name and value pair in map. When name already exist, add value |
| * to array of values. |
| * |
| * @param map The map to populate |
| * @param name The parameter name |
| * @param value The parameter value |
| */ |
| private static void putMapEntry( Map map, String name, String value) { |
| String[] newValues = null; |
| String[] oldValues = (String[]) map.get(name); |
| if (oldValues == null) { |
| newValues = new String[1]; |
| newValues[0] = value; |
| } else { |
| newValues = new String[oldValues.length + 1]; |
| System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); |
| newValues[oldValues.length] = value; |
| } |
| map.put(name, newValues); |
| } |
| |
| |
| /** |
| * Append request parameters from the specified String to the specified |
| * Map. It is presumed that the specified Map is not accessed from any |
| * other thread, so no synchronization is performed. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed |
| * individually on the parsed name and value elements, rather than on |
| * the entire query string ahead of time, to properly deal with the case |
| * where the name or value includes an encoded "=" or "&" character |
| * that would otherwise be interpreted as a delimiter. |
| * |
| * NOTE: byte array data is modified by this method. Caller beware. |
| * |
| * @param map Map that accumulates the resulting parameters |
| * @param data Input string containing request parameters |
| * @param encoding Encoding to use for converting hex |
| * |
| * @exception UnsupportedEncodingException if the data is malformed |
| */ |
| public static void parseParameters(Map map, byte[] data, String encoding) |
| throws UnsupportedEncodingException { |
| |
| if (data != null && data.length > 0) { |
| int pos = 0; |
| int ix = 0; |
| int ox = 0; |
| String key = null; |
| String value = null; |
| while (ix < data.length) { |
| byte c = data[ix++]; |
| switch ((char) c) { |
| case '&': |
| value = new String(data, 0, ox, encoding); |
| if (key != null) { |
| putMapEntry(map, key, value); |
| key = null; |
| } |
| ox = 0; |
| break; |
| case '=': |
| if (key == null) { |
| key = new String(data, 0, ox, encoding); |
| ox = 0; |
| } else { |
| data[ox++] = c; |
| } |
| break; |
| case '+': |
| data[ox++] = (byte)' '; |
| break; |
| case '%': |
| data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4) |
| + convertHexDigit(data[ix++])); |
| break; |
| default: |
| data[ox++] = c; |
| } |
| } |
| //The last value does not end in '&'. So save it now. |
| if (key != null) { |
| value = new String(data, 0, ox, encoding); |
| putMapEntry(map, key, value); |
| } |
| } |
| |
| } |
| |
| |
| |
| } |