1 /*
2 * Copyright 1999,2004 The Apache Software Foundation.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18 package org.apache.commons.messagelet.impl;
19
20 import java.io.UnsupportedEncodingException;
21 import java.text.SimpleDateFormat;
22 import java.util.ArrayList;
23 import java.util.Map;
24 import java.util.TimeZone;
25
26 import javax.servlet.http.Cookie;
27
28
29 /**
30 * General purpose request parsing and encoding utility methods.
31 *
32 * @author Craig R. McClanahan
33 * @author Tim Tye
34 * @version $Revision: 155459 $ $Date: 2005-02-26 13:24:44 +0000 (Sat, 26 Feb 2005) $
35 */
36
37 public final class RequestUtil {
38
39
40 /**
41 * The DateFormat to use for generating readable dates in cookies.
42 */
43 private static SimpleDateFormat format =
44 new SimpleDateFormat(" EEEE, dd-MMM-yy kk:mm:ss zz");
45
46 static {
47 format.setTimeZone(TimeZone.getTimeZone("GMT"));
48 }
49
50
51 /**
52 * Encode a cookie as per RFC 2109. The resulting string can be used
53 * as the value for a <code>Set-Cookie</code> header.
54 *
55 * @param cookie The cookie to encode.
56 * @return A string following RFC 2109.
57 */
58 public static String encodeCookie(Cookie cookie) {
59
60 StringBuffer buf = new StringBuffer( cookie.getName() );
61 buf.append("=");
62 buf.append(cookie.getValue());
63
64 if (cookie.getComment() != null) {
65 buf.append("; Comment=\"");
66 buf.append(cookie.getComment());
67 buf.append("\"");
68 }
69
70 if (cookie.getDomain() != null) {
71 buf.append("; Domain=\"");
72 buf.append(cookie.getDomain());
73 buf.append("\"");
74 }
75
76 long age = cookie.getMaxAge();
77 if (cookie.getMaxAge() >= 0) {
78 buf.append("; Max-Age=\"");
79 buf.append(cookie.getMaxAge());
80 buf.append("\"");
81 }
82
83 if (cookie.getPath() != null) {
84 buf.append("; Path=\"");
85 buf.append(cookie.getPath());
86 buf.append("\"");
87 }
88
89 if (cookie.getSecure()) {
90 buf.append("; Secure");
91 }
92
93 if (cookie.getVersion() > 0) {
94 buf.append("; Version=\"");
95 buf.append(cookie.getVersion());
96 buf.append("\"");
97 }
98
99 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