001/* 002 * ==================================================================== 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * ==================================================================== 020 * 021 * This software consists of voluntary contributions made by many 022 * individuals on behalf of the Apache Software Foundation. For more 023 * information on the Apache Software Foundation, please see 024 * <http://www.apache.org/>. 025 * 026 */ 027 028package org.apache.http.impl.cookie; 029 030import java.util.ArrayList; 031import java.util.BitSet; 032import java.util.Collections; 033import java.util.Date; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Locale; 037import java.util.Map; 038import java.util.concurrent.ConcurrentHashMap; 039 040import org.apache.http.FormattedHeader; 041import org.apache.http.Header; 042import org.apache.http.annotation.Contract; 043import org.apache.http.annotation.ThreadingBehavior; 044import org.apache.http.cookie.ClientCookie; 045import org.apache.http.cookie.CommonCookieAttributeHandler; 046import org.apache.http.cookie.Cookie; 047import org.apache.http.cookie.CookieAttributeHandler; 048import org.apache.http.cookie.CookieOrigin; 049import org.apache.http.cookie.CookiePriorityComparator; 050import org.apache.http.cookie.CookieSpec; 051import org.apache.http.cookie.MalformedCookieException; 052import org.apache.http.cookie.SM; 053import org.apache.http.message.BufferedHeader; 054import org.apache.http.message.ParserCursor; 055import org.apache.http.message.TokenParser; 056import org.apache.http.util.Args; 057import org.apache.http.util.CharArrayBuffer; 058 059/** 060 * Cookie management functions shared by RFC C6265 compliant specification. 061 * 062 * @since 4.5 063 */ 064@Contract(threading = ThreadingBehavior.SAFE) 065public class RFC6265CookieSpec implements CookieSpec { 066 067 private final static char PARAM_DELIMITER = ';'; 068 private final static char COMMA_CHAR = ','; 069 private final static char EQUAL_CHAR = '='; 070 private final static char DQUOTE_CHAR = '"'; 071 private final static char ESCAPE_CHAR = '\\'; 072 073 // IMPORTANT! 074 // These private static variables must be treated as immutable and never exposed outside this class 075 private static final BitSet TOKEN_DELIMS = TokenParser.INIT_BITSET(EQUAL_CHAR, PARAM_DELIMITER); 076 private static final BitSet VALUE_DELIMS = TokenParser.INIT_BITSET(PARAM_DELIMITER); 077 private static final BitSet SPECIAL_CHARS = TokenParser.INIT_BITSET(' ', 078 DQUOTE_CHAR, COMMA_CHAR, PARAM_DELIMITER, ESCAPE_CHAR); 079 080 private final CookieAttributeHandler[] attribHandlers; 081 private final Map<String, CookieAttributeHandler> attribHandlerMap; 082 private final TokenParser tokenParser; 083 084 protected RFC6265CookieSpec(final CommonCookieAttributeHandler... handlers) { 085 super(); 086 this.attribHandlers = handlers.clone(); 087 this.attribHandlerMap = new ConcurrentHashMap<String, CookieAttributeHandler>(handlers.length); 088 for (final CommonCookieAttributeHandler handler: handlers) { 089 this.attribHandlerMap.put(handler.getAttributeName().toLowerCase(Locale.ROOT), handler); 090 } 091 this.tokenParser = TokenParser.INSTANCE; 092 } 093 094 static String getDefaultPath(final CookieOrigin origin) { 095 String defaultPath = origin.getPath(); 096 int lastSlashIndex = defaultPath.lastIndexOf('/'); 097 if (lastSlashIndex >= 0) { 098 if (lastSlashIndex == 0) { 099 //Do not remove the very first slash 100 lastSlashIndex = 1; 101 } 102 defaultPath = defaultPath.substring(0, lastSlashIndex); 103 } 104 return defaultPath; 105 } 106 107 static String getDefaultDomain(final CookieOrigin origin) { 108 return origin.getHost(); 109 } 110 111 @Override 112 public final List<Cookie> parse(final Header header, final CookieOrigin origin) throws MalformedCookieException { 113 Args.notNull(header, "Header"); 114 Args.notNull(origin, "Cookie origin"); 115 if (!header.getName().equalsIgnoreCase(SM.SET_COOKIE)) { 116 throw new MalformedCookieException("Unrecognized cookie header: '" + header.toString() + "'"); 117 } 118 final CharArrayBuffer buffer; 119 final ParserCursor cursor; 120 if (header instanceof FormattedHeader) { 121 buffer = ((FormattedHeader) header).getBuffer(); 122 cursor = new ParserCursor(((FormattedHeader) header).getValuePos(), buffer.length()); 123 } else { 124 final String s = header.getValue(); 125 if (s == null) { 126 throw new MalformedCookieException("Header value is null"); 127 } 128 buffer = new CharArrayBuffer(s.length()); 129 buffer.append(s); 130 cursor = new ParserCursor(0, buffer.length()); 131 } 132 final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS); 133 if (name.length() == 0) { 134 return Collections.emptyList(); 135 } 136 if (cursor.atEnd()) { 137 return Collections.emptyList(); 138 } 139 final int valueDelim = buffer.charAt(cursor.getPos()); 140 cursor.updatePos(cursor.getPos() + 1); 141 if (valueDelim != '=') { 142 throw new MalformedCookieException("Cookie value is invalid: '" + header.toString() + "'"); 143 } 144 final String value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS); 145 if (!cursor.atEnd()) { 146 cursor.updatePos(cursor.getPos() + 1); 147 } 148 final BasicClientCookie cookie = new BasicClientCookie(name, value); 149 cookie.setPath(getDefaultPath(origin)); 150 cookie.setDomain(getDefaultDomain(origin)); 151 cookie.setCreationDate(new Date()); 152 153 final Map<String, String> attribMap = new LinkedHashMap<String, String>(); 154 while (!cursor.atEnd()) { 155 final String paramName = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS) 156 .toLowerCase(Locale.ROOT); 157 String paramValue = null; 158 if (!cursor.atEnd()) { 159 final int paramDelim = buffer.charAt(cursor.getPos()); 160 cursor.updatePos(cursor.getPos() + 1); 161 if (paramDelim == EQUAL_CHAR) { 162 paramValue = tokenParser.parseToken(buffer, cursor, VALUE_DELIMS); 163 if (!cursor.atEnd()) { 164 cursor.updatePos(cursor.getPos() + 1); 165 } 166 } 167 } 168 cookie.setAttribute(paramName, paramValue); 169 attribMap.put(paramName, paramValue); 170 } 171 // Ignore 'Expires' if 'Max-Age' is present 172 if (attribMap.containsKey(ClientCookie.MAX_AGE_ATTR)) { 173 attribMap.remove(ClientCookie.EXPIRES_ATTR); 174 } 175 176 for (final Map.Entry<String, String> entry: attribMap.entrySet()) { 177 final String paramName = entry.getKey(); 178 final String paramValue = entry.getValue(); 179 final CookieAttributeHandler handler = this.attribHandlerMap.get(paramName); 180 if (handler != null) { 181 handler.parse(cookie, paramValue); 182 } 183 } 184 185 return Collections.<Cookie>singletonList(cookie); 186 } 187 188 @Override 189 public final void validate(final Cookie cookie, final CookieOrigin origin) 190 throws MalformedCookieException { 191 Args.notNull(cookie, "Cookie"); 192 Args.notNull(origin, "Cookie origin"); 193 for (final CookieAttributeHandler handler: this.attribHandlers) { 194 handler.validate(cookie, origin); 195 } 196 } 197 198 @Override 199 public final boolean match(final Cookie cookie, final CookieOrigin origin) { 200 Args.notNull(cookie, "Cookie"); 201 Args.notNull(origin, "Cookie origin"); 202 for (final CookieAttributeHandler handler: this.attribHandlers) { 203 if (!handler.match(cookie, origin)) { 204 return false; 205 } 206 } 207 return true; 208 } 209 210 @Override 211 public List<Header> formatCookies(final List<Cookie> cookies) { 212 Args.notEmpty(cookies, "List of cookies"); 213 final List<? extends Cookie> sortedCookies; 214 if (cookies.size() > 1) { 215 // Create a mutable copy and sort the copy. 216 sortedCookies = new ArrayList<Cookie>(cookies); 217 Collections.sort(sortedCookies, CookiePriorityComparator.INSTANCE); 218 } else { 219 sortedCookies = cookies; 220 } 221 final CharArrayBuffer buffer = new CharArrayBuffer(20 * sortedCookies.size()); 222 buffer.append(SM.COOKIE); 223 buffer.append(": "); 224 for (int n = 0; n < sortedCookies.size(); n++) { 225 final Cookie cookie = sortedCookies.get(n); 226 if (n > 0) { 227 buffer.append(PARAM_DELIMITER); 228 buffer.append(' '); 229 } 230 buffer.append(cookie.getName()); 231 final String s = cookie.getValue(); 232 if (s != null) { 233 buffer.append(EQUAL_CHAR); 234 if (containsSpecialChar(s)) { 235 buffer.append(DQUOTE_CHAR); 236 for (int i = 0; i < s.length(); i++) { 237 final char ch = s.charAt(i); 238 if (ch == DQUOTE_CHAR || ch == ESCAPE_CHAR) { 239 buffer.append(ESCAPE_CHAR); 240 } 241 buffer.append(ch); 242 } 243 buffer.append(DQUOTE_CHAR); 244 } else { 245 buffer.append(s); 246 } 247 } 248 } 249 final List<Header> headers = new ArrayList<Header>(1); 250 headers.add(new BufferedHeader(buffer)); 251 return headers; 252 } 253 254 boolean containsSpecialChar(final CharSequence s) { 255 return containsChars(s, SPECIAL_CHARS); 256 } 257 258 boolean containsChars(final CharSequence s, final BitSet chars) { 259 for (int i = 0; i < s.length(); i++) { 260 final char ch = s.charAt(i); 261 if (chars.get(ch)) { 262 return true; 263 } 264 } 265 return false; 266 } 267 268 @Override 269 public final int getVersion() { 270 return 0; 271 } 272 273 @Override 274 public final Header getVersionHeader() { 275 return null; 276 } 277 278}