View Javadoc

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("&lt;");
122                 break;
123             case '>':
124                 result.append("&gt;");
125                 break;
126             case '&':
127                 result.append("&amp;");
128                 break;
129             case '"':
130                 result.append("&quot;");
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