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("&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