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.commons.messagelet.impl; 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: 155459 $ $Date: 2005-02-26 13:24:44 +0000 (Sat, 26 Feb 2005) $ 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 = URLDecode(token.substring(0, equals).trim()); 253 String value = URLDecode(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 * @param urlParameters true if we're parsing parameters on the URL 280 * 281 * @exception IllegalArgumentException if the data is malformed 282 */ 283 public static void parseParameters(Map map, String data, String encoding) 284 throws UnsupportedEncodingException { 285 286 if ((data != null) && (data.length() > 0)) { 287 int len = data.length(); 288 byte[] bytes = new byte[len]; 289 data.getBytes(0, len, bytes, 0); 290 parseParameters(map, bytes, encoding); 291 } 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 /** 315 * Decode and return the specified URL-encoded String. 316 * 317 * @param str The url-encoded string 318 * @param enc The encoding to use; if null, the default encoding is used 319 * @exception IllegalArgumentException if a '%' character is not followed 320 * by a valid 2-digit hexadecimal number 321 */ 322 public static String URLDecode(String str, String enc) { 323 324 if (str == null) 325 return (null); 326 327 int len = str.length(); 328 byte[] bytes = new byte[len]; 329 str.getBytes(0, len, bytes, 0); 330 331 return URLDecode(bytes, enc); 332 333 } 334 335 336 /** 337 * Decode and return the specified URL-encoded byte array. 338 * 339 * @param bytes The url-encoded byte array 340 * @exception IllegalArgumentException if a '%' character is not followed 341 * by a valid 2-digit hexadecimal number 342 */ 343 public static String URLDecode(byte[] bytes) { 344 return URLDecode(bytes, null); 345 } 346 347 348 /** 349 * Decode and return the specified URL-encoded byte array. 350 * 351 * @param bytes The url-encoded byte array 352 * @param enc The encoding to use; if null, the default encoding is used 353 * @exception IllegalArgumentException if a '%' character is not followed 354 * by a valid 2-digit hexadecimal number 355 */ 356 public static String URLDecode(byte[] bytes, String enc) { 357 358 if (bytes == null) 359 return (null); 360 361 int len = bytes.length; 362 int ix = 0; 363 int ox = 0; 364 while (ix < len) { 365 byte b = bytes[ix++]; // Get byte to test 366 if (b == '+') { 367 b = (byte)' '; 368 } else if (b == '%') { 369 b = (byte) ((convertHexDigit(bytes[ix++]) << 4) 370 + convertHexDigit(bytes[ix++])); 371 } 372 bytes[ox++] = b; 373 } 374 if (enc != null) { 375 try { 376 return new String(bytes, 0, ox, enc); 377 } catch (Exception e) { 378 e.printStackTrace(); 379 } 380 } 381 return new String(bytes, 0, ox); 382 383 } 384 385 386 /** 387 * Convert a byte character value to hexidecimal digit value. 388 * 389 * @param b the character value byte 390 */ 391 private static byte convertHexDigit( byte b ) { 392 if ((b >= '0') && (b <= '9')) return (byte)(b - '0'); 393 if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10); 394 if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10); 395 return 0; 396 } 397 398 399 /** 400 * Put name value pair in map. 401 * 402 * @param b the character value byte 403 * 404 * Put name and value pair in map. When name already exist, add value 405 * to array of values. 406 */ 407 private static void putMapEntry( Map map, String name, String value) { 408 String[] newValues = null; 409 String[] oldValues = (String[]) map.get(name); 410 if (oldValues == null) { 411 newValues = new String[1]; 412 newValues[0] = value; 413 } else { 414 newValues = new String[oldValues.length + 1]; 415 System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); 416 newValues[oldValues.length] = value; 417 } 418 map.put(name, newValues); 419 } 420 421 422 /** 423 * Append request parameters from the specified String to the specified 424 * Map. It is presumed that the specified Map is not accessed from any 425 * other thread, so no synchronization is performed. 426 * <p> 427 * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed 428 * individually on the parsed name and value elements, rather than on 429 * the entire query string ahead of time, to properly deal with the case 430 * where the name or value includes an encoded "=" or "&" character 431 * that would otherwise be interpreted as a delimiter. 432 * 433 * NOTE: byte array data is modified by this method. Caller beware. 434 * 435 * @param map Map that accumulates the resulting parameters 436 * @param data Input string containing request parameters 437 * @param encoding Encoding to use for converting hex 438 * 439 * @exception UnsupportedEncodingException if the data is malformed 440 */ 441 public static void parseParameters(Map map, byte[] data, String encoding) 442 throws UnsupportedEncodingException { 443 444 if (data != null && data.length > 0) { 445 int pos = 0; 446 int ix = 0; 447 int ox = 0; 448 String key = null; 449 String value = null; 450 while (ix < data.length) { 451 byte c = data[ix++]; 452 switch ((char) c) { 453 case '&': 454 value = new String(data, 0, ox, encoding); 455 if (key != null) { 456 putMapEntry(map, key, value); 457 key = null; 458 } 459 ox = 0; 460 break; 461 case '=': 462 key = new String(data, 0, ox, encoding); 463 ox = 0; 464 break; 465 case '+': 466 data[ox++] = (byte)' '; 467 break; 468 case '%': 469 data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4) 470 + convertHexDigit(data[ix++])); 471 break; 472 default: 473 data[ox++] = c; 474 } 475 } 476 //The last value does not end in '&'. So save it now. 477 if (key != null) { 478 value = new String(data, 0, ox, encoding); 479 putMapEntry(map, key, value); 480 } 481 } 482 483 } 484 485 486 487 } 488