001package votorola.g.web; // Copyright 2012, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import java.io.*;
004import java.net.*;
005import javax.servlet.http.*;
006import votorola.g.lang.*;
007
008
009/** An extended cookie implementation that rejects illegal values.  The superclass accepts
010  * illegal values and depending on the servlet container may encode them by RFC 2965
011  * quoting, which is not supported by all browsers.  So we do our own testing and
012  * encoding here in compliance with both the original and existing standards.
013  *
014  *     @see <a href='http://curl.haxx.se/rfc/cookie_spec.html' target='_top'
015  *       >Persistent client state: HTTP cookies</a>
016  *     @see <a href='http://tools.ietf.org/html/rfc2965' target='_top'>RFC 2965</a>
017  *     @see <a href='http://stackoverflow.com/questions/1969232/allowed-characters-in-cookies' target='_top'
018  *                                                             >Allowed characters in cookies</a>
019  */
020public class CookieX extends Cookie
021{
022
023
024    /** Constructs a CookieX without encoding the value.
025      *
026      *     @see #getName()
027      *     @see #getValue()
028      *
029      *     @throws IllegalArgumentException if either the name or value contains an
030      *       illegal character.
031      *     @see Cookie#Cookie(String,String)
032      */
033    public CookieX( String name, String value ) { super( name, testedValue(value) ); }
034
035
036
037    /** Constructs a CookieX.
038      *
039      *     @see #getName()
040      *     @see #getValue()
041      *     @param toEncode whether to {@linkplain #encodedValue(String) encode the
042      *       value}.  If
043      *
044      *     @throws IllegalArgumentException if the name contains an illegal character, or
045      *       if the value contains an illegal character and assertions are enabled.
046      *     @see Cookie#Cookie(String,String)
047      */
048    public CookieX( final String name, String value, final boolean toEncode )
049    {
050        super( name, toEncode? value=encodedValue(value): value );
051     // testName( name );
052     /// superclass does that already
053        assert test( value ); // test only when assertions enabled
054    }
055
056
057
058   // ------------------------------------------------------------------------------------
059
060
061    /** Decodes a previously {@link #encodedValue encodedValue} and returns the result.
062      *
063      *     @param encodedValue the value to decode.  Null values decode to null.
064      *
065      *     @see #encodedValue(String)
066      */
067    public static @ThreadSafe String decodedValue( final String encodedValue )
068    {
069        final String decodedValue;
070        if( encodedValue == null ) decodedValue = null;
071        else
072        {
073            try { decodedValue = URLDecoder.decode( encodedValue, "UTF-8" ); }
074            catch( UnsupportedEncodingException x ) { throw new RuntimeException( x ); }
075        }
076        return decodedValue;
077    }
078
079
080
081    /** A max-age value of roughly one year as measured in seconds.
082      */
083    public static final int DURATION_YEAR_S = 86400/*s per day*/ * 365/*day*/;
084
085
086
087    /** Encodes the value such that it has no illegal characters and returns the result.
088      * This method uses URI "percent" escaping based on UTF-8 ordinals for any non-ASCII
089      * characters.  This is also the encoding method recommended for URIs.
090      *
091      *     @param decodedValue the value to encode.  Null values encode to null.
092      *
093      *     @see #decodedValue(String)
094      *     @see <a href='http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars'
095      *      >Non-ASCII characters in URI attribute values</a>
096      */
097    public static @ThreadSafe String encodedValue( final String decodedValue )
098    {
099        // encoded name would be similar, provided it encodes any leading '$' char
100        final String encodedValue;
101        if( decodedValue == null ) encodedValue = null;
102        else
103        {
104            try { encodedValue = URLEncoder.encode( decodedValue, "UTF-8" ); } // encodes all illegal characters (tested JDK 1.7)
105            catch( UnsupportedEncodingException x ) { throw new RuntimeException( x ); }
106        }
107        return encodedValue;
108    }
109
110
111
112   // - C o o k i e ----------------------------------------------------------------------
113
114
115    /** @throws IllegalArgumentException if the value contains an illegal character.
116      */
117    public @Override final void setValue( final String value )
118    {
119        testedValue( value );
120        super.setValue( value );
121    }
122
123
124        /** @throws IllegalArgumentException if the value contains an illegal character
125          *   and assertions are enabled.
126          */
127        public final void setValue( String value, final boolean toEncode )
128        {
129            if( toEncode ) value = encodedValue( value );
130            assert test( value ); // test only when assertions enabled
131            super.setValue( value );
132        }
133
134
135
136    /** @throws IllegalArgumentException if the version is not zero.  The argument
137      *   checking performed by this class is probably not necessary for later versions.
138      */
139    public @Override final void setVersion( final int version )
140    {
141        if( version != 0 ) throw new IllegalArgumentException( "non-zero version: " + version );
142
143        super.setVersion( version );
144    }
145
146
147
148//// P r i v a t e ///////////////////////////////////////////////////////////////////////
149
150
151    private static boolean test( final String value )
152    {
153        testedValue( value );
154        return true;
155    }
156
157
158
159    private static String testedValue( final String value )
160    {
161        // Testing a name would be the same except that a leading '$' char is also illegal
162        // per RFC 2965 3.2.2.  But superclass constructor does its own name testing.
163        if( value != null )
164        {
165            final int cN = value.length();
166            if( cN == 0 ) throw new IllegalArgumentException( "empty cookie value" );
167              // API for #setValue says, "Empty values may not behave the same way on all browsers."
168
169            for( int c = 0;; )
170            {
171                char ch = value.charAt( c );
172                if( ch < '!' || ch > '~' // only ASCII [1] and no control characters [2]
173                 || ch == '"' // [2]
174                 || ch == ',' // [1] [2]
175                 || ch >= ':' && ch <= '@' // [2]  : ; < = > ? @
176                 || ch >= '[' && ch <= ']' // [2]  [ \ ]
177                 || ch == '(' || ch == ')' // [2]
178                 || ch == '/' // [2]
179                 || ch == '{' || ch == '}' // [2]
180                 || Character.isWhitespace( ch )) // [1], and [2] disallows the ASCII space
181                {
182                    throw new IllegalArgumentException( "illegal character in cookie value : '" + ch
183                      + "'" );
184                }
185                ++c;
186                if( c >= cN ) break;
187            }
188        }
189        return value;
190
191        // [1] Per Netscape.
192        // [2] Per RFC 2965, which requires token form per RFC 2616.
193    }
194
195
196
197}