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