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.hc.client5.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.hc.client5.http.cookie.CommonCookieAttributeHandler;
41  import org.apache.hc.client5.http.cookie.Cookie;
42  import org.apache.hc.client5.http.cookie.CookieAttributeHandler;
43  import org.apache.hc.client5.http.cookie.CookieOrigin;
44  import org.apache.hc.client5.http.cookie.CookiePriorityComparator;
45  import org.apache.hc.client5.http.cookie.CookieSpec;
46  import org.apache.hc.client5.http.cookie.MalformedCookieException;
47  import org.apache.hc.core5.annotation.Contract;
48  import org.apache.hc.core5.annotation.ThreadingBehavior;
49  import org.apache.hc.core5.http.FormattedHeader;
50  import org.apache.hc.core5.http.Header;
51  import org.apache.hc.core5.http.ParseException;
52  import org.apache.hc.core5.http.message.BufferedHeader;
53  import org.apache.hc.core5.http.message.ParserCursor;
54  import org.apache.hc.core5.http.message.TokenParser;
55  import org.apache.hc.core5.util.Args;
56  import org.apache.hc.core5.util.CharArrayBuffer;
57  
58  /**
59   * Cookie management functions shared by RFC C6265 compliant specification.
60   *
61   * @since 4.5
62   */
63  @Contract(threading = ThreadingBehavior.SAFE)
64  public class RFC6265CookieSpec implements CookieSpec {
65  
66      private final static char PARAM_DELIMITER  = ';';
67      private final static char COMMA_CHAR       = ',';
68      private final static char EQUAL_CHAR       = '=';
69      private final static char DQUOTE_CHAR      = '"';
70      private final static char ESCAPE_CHAR      = '\\';
71  
72      // IMPORTANT!
73      // These private static variables must be treated as immutable and never exposed outside this class
74      private static final BitSet TOKEN_DELIMS = TokenParser.INIT_BITSET(EQUAL_CHAR, PARAM_DELIMITER);
75      private static final BitSet VALUE_DELIMS = TokenParser.INIT_BITSET(PARAM_DELIMITER);
76      private static final BitSet SPECIAL_CHARS = TokenParser.INIT_BITSET(' ',
77              DQUOTE_CHAR, COMMA_CHAR, PARAM_DELIMITER, ESCAPE_CHAR);
78  
79      private final CookieAttributeHandler[] attribHandlers;
80      private final Map<String, CookieAttributeHandler> attribHandlerMap;
81      private final TokenParser tokenParser;
82  
83      protected RFC6265CookieSpec(final CommonCookieAttributeHandler... handlers) {
84          super();
85          this.attribHandlers = handlers.clone();
86          this.attribHandlerMap = new ConcurrentHashMap<>(handlers.length);
87          for (final CommonCookieAttributeHandler handler: handlers) {
88              this.attribHandlerMap.put(handler.getAttributeName().toLowerCase(Locale.ROOT), handler);
89          }
90          this.tokenParser = TokenParser.INSTANCE;
91      }
92  
93      static String getDefaultPath(final CookieOrigin origin) {
94          String defaultPath = origin.getPath();
95          int lastSlashIndex = defaultPath.lastIndexOf('/');
96          if (lastSlashIndex >= 0) {
97              if (lastSlashIndex == 0) {
98                  //Do not remove the very first slash
99                  lastSlashIndex = 1;
100             }
101             defaultPath = defaultPath.substring(0, lastSlashIndex);
102         }
103         return defaultPath;
104     }
105 
106     static String getDefaultDomain(final CookieOrigin origin) {
107         return origin.getHost();
108     }
109 
110     @Override
111     public final List<Cookie> parse(final Header header, final CookieOrigin origin) throws MalformedCookieException {
112         Args.notNull(header, "Header");
113         Args.notNull(origin, "Cookie origin");
114         if (!header.getName().equalsIgnoreCase("Set-Cookie")) {
115             throw new MalformedCookieException("Unrecognized cookie header: '" + header + "'");
116         }
117         final CharArrayBuffer buffer;
118         final ParserCursor cursor;
119         if (header instanceof FormattedHeader) {
120             buffer = ((FormattedHeader) header).getBuffer();
121             cursor = new ParserCursor(((FormattedHeader) header).getValuePos(), buffer.length());
122         } else {
123             final String s = header.getValue();
124             if (s == null) {
125                 throw new MalformedCookieException("Header value is null");
126             }
127             buffer = new CharArrayBuffer(s.length());
128             buffer.append(s);
129             cursor = new ParserCursor(0, buffer.length());
130         }
131         final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
132         if (name.isEmpty()) {
133             return Collections.emptyList();
134         }
135         if (cursor.atEnd()) {
136             return Collections.emptyList();
137         }
138         final int valueDelim = buffer.charAt(cursor.getPos());
139         cursor.updatePos(cursor.getPos() + 1);
140         if (valueDelim != '=') {
141             throw new MalformedCookieException("Cookie value is invalid: '" + header + "'");
142         }
143         final String value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
144         if (!cursor.atEnd()) {
145             cursor.updatePos(cursor.getPos() + 1);
146         }
147         final BasicClientCookiel/cookie/BasicClientCookie.html#BasicClientCookie">BasicClientCookie cookie = new BasicClientCookie(name, value);
148         cookie.setPath(getDefaultPath(origin));
149         cookie.setDomain(getDefaultDomain(origin));
150         cookie.setCreationDate(new Date());
151 
152         final Map<String, String> attribMap = new LinkedHashMap<>();
153         while (!cursor.atEnd()) {
154             final String paramName = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS)
155                     .toLowerCase(Locale.ROOT);
156             String paramValue = null;
157             if (!cursor.atEnd()) {
158                 final int paramDelim = buffer.charAt(cursor.getPos());
159                 cursor.updatePos(cursor.getPos() + 1);
160                 if (paramDelim == EQUAL_CHAR) {
161                     paramValue = tokenParser.parseToken(buffer, cursor, VALUE_DELIMS);
162                     if (!cursor.atEnd()) {
163                         cursor.updatePos(cursor.getPos() + 1);
164                     }
165                 }
166             }
167             cookie.setAttribute(paramName, paramValue);
168             attribMap.put(paramName, paramValue);
169         }
170         // Ignore 'Expires' if 'Max-Age' is present
171         if (attribMap.containsKey(Cookie.MAX_AGE_ATTR)) {
172             attribMap.remove(Cookie.EXPIRES_ATTR);
173         }
174 
175         for (final Map.Entry<String, String> entry: attribMap.entrySet()) {
176             final String paramName = entry.getKey();
177             final String paramValue = entry.getValue();
178             final CookieAttributeHandler handler = this.attribHandlerMap.get(paramName);
179             if (handler != null) {
180                 handler.parse(cookie, paramValue);
181             }
182         }
183 
184         return Collections.<Cookie>singletonList(cookie);
185     }
186 
187     @Override
188     public final void validate(final Cookie cookie, final CookieOrigin origin)
189             throws MalformedCookieException {
190         Args.notNull(cookie, "Cookie");
191         Args.notNull(origin, "Cookie origin");
192         for (final CookieAttributeHandler handler: this.attribHandlers) {
193             handler.validate(cookie, origin);
194         }
195     }
196 
197     @Override
198     public final boolean match(final Cookie cookie, final CookieOrigin origin) {
199         Args.notNull(cookie, "Cookie");
200         Args.notNull(origin, "Cookie origin");
201         for (final CookieAttributeHandler handler: this.attribHandlers) {
202             if (!handler.match(cookie, origin)) {
203                 return false;
204             }
205         }
206         return true;
207     }
208 
209     @Override
210     public List<Header> formatCookies(final List<Cookie> cookies) {
211         Args.notEmpty(cookies, "List of cookies");
212         final List<? extends Cookie> sortedCookies;
213         if (cookies.size() > 1) {
214             // Create a mutable copy and sort the copy.
215             sortedCookies = new ArrayList<>(cookies);
216             Collections.sort(sortedCookies, CookiePriorityComparator.INSTANCE);
217         } else {
218             sortedCookies = cookies;
219         }
220         final CharArrayBuffer buffer = new CharArrayBuffer(20 * sortedCookies.size());
221         buffer.append("Cookie");
222         buffer.append(": ");
223         for (int n = 0; n < sortedCookies.size(); n++) {
224             final Cookie cookie = sortedCookies.get(n);
225             if (n > 0) {
226                 buffer.append(PARAM_DELIMITER);
227                 buffer.append(' ');
228             }
229             buffer.append(cookie.getName());
230             final String s = cookie.getValue();
231             if (s != null) {
232                 buffer.append(EQUAL_CHAR);
233                 if (containsSpecialChar(s)) {
234                     buffer.append(DQUOTE_CHAR);
235                     for (int i = 0; i < s.length(); i++) {
236                         final char ch = s.charAt(i);
237                         if (ch == DQUOTE_CHAR || ch == ESCAPE_CHAR) {
238                             buffer.append(ESCAPE_CHAR);
239                         }
240                         buffer.append(ch);
241                     }
242                     buffer.append(DQUOTE_CHAR);
243                 } else {
244                     buffer.append(s);
245                 }
246             }
247         }
248         final List<Header> headers = new ArrayList<>(1);
249         try {
250             headers.add(new BufferedHeader(buffer));
251         } catch (final ParseException ignore) {
252             // should never happen
253         }
254         return headers;
255     }
256 
257     boolean containsSpecialChar(final CharSequence s) {
258         return containsChars(s, SPECIAL_CHARS);
259     }
260 
261     boolean containsChars(final CharSequence s, final BitSet chars) {
262         for (int i = 0; i < s.length(); i++) {
263             final char ch = s.charAt(i);
264             if (chars.get(ch)) {
265                 return true;
266             }
267         }
268         return false;
269     }
270 
271 }