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}