1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
59
60
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
72
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
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
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
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
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 }