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