Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
RequestUtil |
|
| 6.076923076923077;6.077 |
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 | 0 | public final class RequestUtil { |
38 | ||
39 | ||
40 | /** | |
41 | * The DateFormat to use for generating readable dates in cookies. | |
42 | */ | |
43 | 0 | private static SimpleDateFormat format = |
44 | new SimpleDateFormat(" EEEE, dd-MMM-yy kk:mm:ss zz"); | |
45 | ||
46 | static { | |
47 | 0 | format.setTimeZone(TimeZone.getTimeZone("GMT")); |
48 | 0 | } |
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 | 0 | StringBuffer buf = new StringBuffer( cookie.getName() ); |
61 | 0 | buf.append("="); |
62 | 0 | buf.append(cookie.getValue()); |
63 | ||
64 | 0 | if (cookie.getComment() != null) { |
65 | 0 | buf.append("; Comment=\""); |
66 | 0 | buf.append(cookie.getComment()); |
67 | 0 | buf.append("\""); |
68 | } | |
69 | ||
70 | 0 | if (cookie.getDomain() != null) { |
71 | 0 | buf.append("; Domain=\""); |
72 | 0 | buf.append(cookie.getDomain()); |
73 | 0 | buf.append("\""); |
74 | } | |
75 | ||
76 | 0 | long age = cookie.getMaxAge(); |
77 | 0 | if (cookie.getMaxAge() >= 0) { |
78 | 0 | buf.append("; Max-Age=\""); |
79 | 0 | buf.append(cookie.getMaxAge()); |
80 | 0 | buf.append("\""); |
81 | } | |
82 | ||
83 | 0 | if (cookie.getPath() != null) { |
84 | 0 | buf.append("; Path=\""); |
85 | 0 | buf.append(cookie.getPath()); |
86 | 0 | buf.append("\""); |
87 | } | |
88 | ||
89 | 0 | if (cookie.getSecure()) { |
90 | 0 | buf.append("; Secure"); |
91 | } | |
92 | ||
93 | 0 | if (cookie.getVersion() > 0) { |
94 | 0 | buf.append("; Version=\""); |
95 | 0 | buf.append(cookie.getVersion()); |
96 | 0 | buf.append("\""); |
97 | } | |
98 | ||
99 | 0 | 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 | 0 | if (message == null) |
113 | 0 | return (null); |
114 | ||
115 | 0 | char content[] = new char[message.length()]; |
116 | 0 | message.getChars(0, message.length(), content, 0); |
117 | 0 | StringBuffer result = new StringBuffer(content.length + 50); |
118 | 0 | for (int i = 0; i < content.length; i++) { |
119 | 0 | switch (content[i]) { |
120 | case '<': | |
121 | 0 | result.append("<"); |
122 | 0 | break; |
123 | case '>': | |
124 | 0 | result.append(">"); |
125 | 0 | break; |
126 | case '&': | |
127 | 0 | result.append("&"); |
128 | 0 | break; |
129 | case '"': | |
130 | 0 | result.append("""); |
131 | 0 | break; |
132 | default: | |
133 | 0 | result.append(content[i]); |
134 | } | |
135 | } | |
136 | 0 | 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 | 0 | if (path == null) |
152 | 0 | return null; |
153 | ||
154 | // Create a place for the normalized path | |
155 | 0 | String normalized = path; |
156 | ||
157 | 0 | if (normalized.equals("/.")) |
158 | 0 | return "/"; |
159 | ||
160 | // Add a leading "/" if necessary | |
161 | 0 | if (!normalized.startsWith("/")) |
162 | 0 | normalized = "/" + normalized; |
163 | ||
164 | // Resolve occurrences of "//" in the normalized path | |
165 | while (true) { | |
166 | 0 | int index = normalized.indexOf("//"); |
167 | 0 | if (index < 0) |
168 | 0 | break; |
169 | 0 | normalized = normalized.substring(0, index) + |
170 | normalized.substring(index + 1); | |
171 | 0 | } |
172 | ||
173 | // Resolve occurrences of "/./" in the normalized path | |
174 | while (true) { | |
175 | 0 | int index = normalized.indexOf("/./"); |
176 | 0 | if (index < 0) |
177 | 0 | break; |
178 | 0 | normalized = normalized.substring(0, index) + |
179 | normalized.substring(index + 2); | |
180 | 0 | } |
181 | ||
182 | // Resolve occurrences of "/../" in the normalized path | |
183 | while (true) { | |
184 | 0 | int index = normalized.indexOf("/../"); |
185 | 0 | if (index < 0) |
186 | 0 | break; |
187 | 0 | if (index == 0) |
188 | 0 | return (null); // Trying to go outside our context |
189 | 0 | int index2 = normalized.lastIndexOf('/', index - 1); |
190 | 0 | normalized = normalized.substring(0, index2) + |
191 | normalized.substring(index + 3); | |
192 | 0 | } |
193 | ||
194 | // Return the normalized path that we have completed | |
195 | 0 | 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 | 0 | if (contentType == null) |
210 | 0 | return (null); |
211 | 0 | int start = contentType.indexOf("charset="); |
212 | 0 | if (start < 0) |
213 | 0 | return (null); |
214 | 0 | String encoding = contentType.substring(start + 8); |
215 | 0 | int end = encoding.indexOf(';'); |
216 | 0 | if (end >= 0) |
217 | 0 | encoding = encoding.substring(0, end); |
218 | 0 | encoding = encoding.trim(); |
219 | 0 | if ((encoding.length() > 2) && (encoding.startsWith("\"")) |
220 | && (encoding.endsWith("\""))) | |
221 | 0 | encoding = encoding.substring(1, encoding.length() - 1); |
222 | 0 | 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 | 0 | if ((header == null) || (header.length() < 1)) |
235 | 0 | return (new Cookie[0]); |
236 | ||
237 | 0 | ArrayList cookies = new ArrayList(); |
238 | 0 | while (header.length() > 0) { |
239 | 0 | int semicolon = header.indexOf(';'); |
240 | 0 | if (semicolon < 0) |
241 | 0 | semicolon = header.length(); |
242 | 0 | if (semicolon == 0) |
243 | 0 | break; |
244 | 0 | String token = header.substring(0, semicolon); |
245 | 0 | if (semicolon < header.length()) |
246 | 0 | header = header.substring(semicolon + 1); |
247 | else | |
248 | 0 | header = ""; |
249 | try { | |
250 | 0 | int equals = token.indexOf('='); |
251 | 0 | if (equals > 0) { |
252 | 0 | String name = URLDecode(token.substring(0, equals).trim()); |
253 | 0 | String value = URLDecode(token.substring(equals+1).trim()); |
254 | 0 | cookies.add(new Cookie(name, value)); |
255 | } | |
256 | 0 | } catch (Throwable e) { |
257 | ; | |
258 | 0 | } |
259 | 0 | } |
260 | ||
261 | 0 | 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 | 0 | if ((data != null) && (data.length() > 0)) { |
287 | 0 | int len = data.length(); |
288 | 0 | byte[] bytes = new byte[len]; |
289 | 0 | data.getBytes(0, len, bytes, 0); |
290 | 0 | parseParameters(map, bytes, encoding); |
291 | } | |
292 | ||
293 | 0 | } |
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 | 0 | 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 | 0 | if (str == null) |
325 | 0 | return (null); |
326 | ||
327 | 0 | int len = str.length(); |
328 | 0 | byte[] bytes = new byte[len]; |
329 | 0 | str.getBytes(0, len, bytes, 0); |
330 | ||
331 | 0 | 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 | 0 | 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 | 0 | if (bytes == null) |
359 | 0 | return (null); |
360 | ||
361 | 0 | int len = bytes.length; |
362 | 0 | int ix = 0; |
363 | 0 | int ox = 0; |
364 | 0 | while (ix < len) { |
365 | 0 | byte b = bytes[ix++]; // Get byte to test |
366 | 0 | if (b == '+') { |
367 | 0 | b = (byte)' '; |
368 | 0 | } else if (b == '%') { |
369 | 0 | b = (byte) ((convertHexDigit(bytes[ix++]) << 4) |
370 | + convertHexDigit(bytes[ix++])); | |
371 | } | |
372 | 0 | bytes[ox++] = b; |
373 | 0 | } |
374 | 0 | if (enc != null) { |
375 | try { | |
376 | 0 | return new String(bytes, 0, ox, enc); |
377 | 0 | } catch (Exception e) { |
378 | 0 | e.printStackTrace(); |
379 | } | |
380 | } | |
381 | 0 | 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 | 0 | if ((b >= '0') && (b <= '9')) return (byte)(b - '0'); |
393 | 0 | if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10); |
394 | 0 | if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10); |
395 | 0 | 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 | 0 | String[] newValues = null; |
409 | 0 | String[] oldValues = (String[]) map.get(name); |
410 | 0 | if (oldValues == null) { |
411 | 0 | newValues = new String[1]; |
412 | 0 | newValues[0] = value; |
413 | } else { | |
414 | 0 | newValues = new String[oldValues.length + 1]; |
415 | 0 | System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); |
416 | 0 | newValues[oldValues.length] = value; |
417 | } | |
418 | 0 | map.put(name, newValues); |
419 | 0 | } |
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 | 0 | if (data != null && data.length > 0) { |
445 | 0 | int pos = 0; |
446 | 0 | int ix = 0; |
447 | 0 | int ox = 0; |
448 | 0 | String key = null; |
449 | 0 | String value = null; |
450 | 0 | while (ix < data.length) { |
451 | 0 | byte c = data[ix++]; |
452 | 0 | switch ((char) c) { |
453 | case '&': | |
454 | 0 | value = new String(data, 0, ox, encoding); |
455 | 0 | if (key != null) { |
456 | 0 | putMapEntry(map, key, value); |
457 | 0 | key = null; |
458 | } | |
459 | 0 | ox = 0; |
460 | 0 | break; |
461 | case '=': | |
462 | 0 | key = new String(data, 0, ox, encoding); |
463 | 0 | ox = 0; |
464 | 0 | break; |
465 | case '+': | |
466 | 0 | data[ox++] = (byte)' '; |
467 | 0 | break; |
468 | case '%': | |
469 | 0 | data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4) |
470 | + convertHexDigit(data[ix++])); | |
471 | 0 | break; |
472 | default: | |
473 | 0 | data[ox++] = c; |
474 | } | |
475 | 0 | } |
476 | //The last value does not end in '&'. So save it now. | |
477 | 0 | if (key != null) { |
478 | 0 | value = new String(data, 0, ox, encoding); |
479 | 0 | putMapEntry(map, key, value); |
480 | } | |
481 | } | |
482 | ||
483 | 0 | } |
484 | ||
485 | ||
486 | ||
487 | } | |
488 |