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