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