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.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
60
61
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
73
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
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
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
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
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 }