View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.http.impl.cookie;
29  
30  import java.util.ArrayList;
31  import java.util.BitSet;
32  import java.util.Collections;
33  import java.util.Date;
34  import java.util.LinkedHashMap;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.concurrent.ConcurrentHashMap;
39  
40  import org.apache.http.FormattedHeader;
41  import org.apache.http.Header;
42  import org.apache.http.annotation.Contract;
43  import org.apache.http.annotation.ThreadingBehavior;
44  import org.apache.http.cookie.ClientCookie;
45  import org.apache.http.cookie.CommonCookieAttributeHandler;
46  import org.apache.http.cookie.Cookie;
47  import org.apache.http.cookie.CookieAttributeHandler;
48  import org.apache.http.cookie.CookieOrigin;
49  import org.apache.http.cookie.CookiePriorityComparator;
50  import org.apache.http.cookie.CookieSpec;
51  import org.apache.http.cookie.MalformedCookieException;
52  import org.apache.http.cookie.SM;
53  import org.apache.http.message.BufferedHeader;
54  import org.apache.http.message.ParserCursor;
55  import org.apache.http.message.TokenParser;
56  import org.apache.http.util.Args;
57  import org.apache.http.util.CharArrayBuffer;
58  
59  /**
60   * Cookie management functions shared by RFC C6265 compliant specification.
61   *
62   * @since 4.5
63   */
64  @Contract(threading = ThreadingBehavior.SAFE)
65  public class RFC6265CookieSpec implements CookieSpec {
66  
67      private final static char PARAM_DELIMITER  = ';';
68      private final static char COMMA_CHAR       = ',';
69      private final static char EQUAL_CHAR       = '=';
70      private final static char DQUOTE_CHAR      = '"';
71      private final static char ESCAPE_CHAR      = '\\';
72  
73      // IMPORTANT!
74      // These private static variables must be treated as immutable and never exposed outside this class
75      private static final BitSet TOKEN_DELIMS = TokenParser.INIT_BITSET(EQUAL_CHAR, PARAM_DELIMITER);
76      private static final BitSet VALUE_DELIMS = TokenParser.INIT_BITSET(PARAM_DELIMITER);
77      private static final BitSet SPECIAL_CHARS = TokenParser.INIT_BITSET(' ',
78              DQUOTE_CHAR, COMMA_CHAR, PARAM_DELIMITER, ESCAPE_CHAR);
79  
80      private final CookieAttributeHandler[] attribHandlers;
81      private final Map<String, CookieAttributeHandler> attribHandlerMap;
82      private final TokenParser tokenParser;
83  
84      protected RFC6265CookieSpec(final CommonCookieAttributeHandler... handlers) {
85          super();
86          this.attribHandlers = handlers.clone();
87          this.attribHandlerMap = new ConcurrentHashMap<String, CookieAttributeHandler>(handlers.length);
88          for (final CommonCookieAttributeHandler handler: handlers) {
89              this.attribHandlerMap.put(handler.getAttributeName().toLowerCase(Locale.ROOT), handler);
90          }
91          this.tokenParser = TokenParser.INSTANCE;
92      }
93  
94      static String getDefaultPath(final CookieOrigin origin) {
95          String defaultPath = origin.getPath();
96          int lastSlashIndex = defaultPath.lastIndexOf('/');
97          if (lastSlashIndex >= 0) {
98              if (lastSlashIndex == 0) {
99                  //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 }