1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.shiro.web.servlet;
20
21 import org.apache.shiro.lang.util.StringUtils;
22 import org.owasp.encoder.Encode;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28 import java.text.DateFormat;
29 import java.text.SimpleDateFormat;
30 import java.util.Calendar;
31 import java.util.Date;
32 import java.util.Locale;
33 import java.util.TimeZone;
34
35
36
37
38
39
40
41
42
43 @SuppressWarnings({"checkstyle:MethodCount", "checkstyle:ParameterNumber"})
44 public class SimpleCookie implements Cookie {
45
46
47
48
49 public static final int DEFAULT_MAX_AGE = -1;
50
51
52
53
54 public static final int DEFAULT_VERSION = -1;
55
56
57 protected static final String NAME_VALUE_DELIMITER = "=";
58 protected static final String ATTRIBUTE_DELIMITER = "; ";
59
60 protected static final long DAY_MILLIS = 86400000;
61 protected static final String GMT_TIME_ZONE_ID = "GMT";
62 protected static final String COOKIE_DATE_FORMAT_STRING = "EEE, dd-MMM-yyyy HH:mm:ss z";
63
64 protected static final String COOKIE_HEADER_NAME = "Set-Cookie";
65 protected static final String PATH_ATTRIBUTE_NAME = "Path";
66 protected static final String EXPIRES_ATTRIBUTE_NAME = "Expires";
67 protected static final String MAXAGE_ATTRIBUTE_NAME = "Max-Age";
68 protected static final String DOMAIN_ATTRIBUTE_NAME = "Domain";
69 protected static final String VERSION_ATTRIBUTE_NAME = "Version";
70 protected static final String COMMENT_ATTRIBUTE_NAME = "Comment";
71 protected static final String SECURE_ATTRIBUTE_NAME = "Secure";
72 protected static final String HTTP_ONLY_ATTRIBUTE_NAME = "HttpOnly";
73 protected static final String SAME_SITE_ATTRIBUTE_NAME = "SameSite";
74
75 private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCookie.class);
76
77 private String name;
78 private String value;
79 private String comment;
80 private String domain;
81 private String path;
82 private int maxAge;
83 private int version;
84 private boolean secure;
85 private boolean httpOnly;
86 private SameSiteOptions sameSite;
87
88 public SimpleCookie() {
89 this.maxAge = DEFAULT_MAX_AGE;
90 this.version = DEFAULT_VERSION;
91
92 this.httpOnly = true;
93 this.sameSite = SameSiteOptions.LAX;
94 }
95
96 public SimpleCookie(String name) {
97 this();
98 this.name = name;
99 }
100
101 public SimpleCookie(Cookie cookie) {
102 this.name = cookie.getName();
103 this.value = cookie.getValue();
104 this.comment = cookie.getComment();
105 this.domain = cookie.getDomain();
106 this.path = cookie.getPath();
107 this.maxAge = Math.max(DEFAULT_MAX_AGE, cookie.getMaxAge());
108 this.version = Math.max(DEFAULT_VERSION, cookie.getVersion());
109 this.secure = cookie.isSecure();
110 this.httpOnly = cookie.isHttpOnly();
111 this.sameSite = cookie.getSameSite();
112 }
113
114 @Override
115 public String getName() {
116 return name;
117 }
118
119 @Override
120 public void setName(String name) {
121 if (!StringUtils.hasText(name)) {
122 throw new IllegalArgumentException("Name cannot be null/empty.");
123 }
124 this.name = name;
125 }
126
127 @Override
128 public String getValue() {
129 return value;
130 }
131
132 @Override
133 public void setValue(String value) {
134 this.value = value;
135 }
136
137 @Override
138 public String getComment() {
139 return comment;
140 }
141
142 @Override
143 public void setComment(String comment) {
144 this.comment = comment;
145 }
146
147 @Override
148 public String getDomain() {
149 return domain;
150 }
151
152 @Override
153 public void setDomain(String domain) {
154 this.domain = domain;
155 }
156
157 @Override
158 public String getPath() {
159 return path;
160 }
161
162 @Override
163 public void setPath(String path) {
164 this.path = path;
165 }
166
167 @Override
168 public int getMaxAge() {
169 return maxAge;
170 }
171
172 @Override
173 public void setMaxAge(int maxAge) {
174 this.maxAge = Math.max(DEFAULT_MAX_AGE, maxAge);
175 }
176
177 @Override
178 public int getVersion() {
179 return version;
180 }
181
182 @Override
183 public void setVersion(int version) {
184 this.version = Math.max(DEFAULT_VERSION, version);
185 }
186
187 @Override
188 public boolean isSecure() {
189 return secure;
190 }
191
192 @Override
193 public void setSecure(boolean secure) {
194 this.secure = secure;
195 }
196
197 @Override
198 public boolean isHttpOnly() {
199 return httpOnly;
200 }
201
202 @Override
203 public void setHttpOnly(boolean httpOnly) {
204 this.httpOnly = httpOnly;
205 }
206
207 @Override
208 public SameSiteOptions getSameSite() {
209 return sameSite;
210 }
211
212 @Override
213 public void setSameSite(SameSiteOptions sameSite) {
214 this.sameSite = sameSite;
215 if (this.sameSite == SameSiteOptions.NONE) {
216
217 setSecure(true);
218 }
219 }
220
221
222
223
224
225
226
227
228
229 private String calculatePath(HttpServletRequest request) {
230 String path = StringUtils.clean(getPath());
231 if (!StringUtils.hasText(path)) {
232 path = StringUtils.clean(request.getContextPath());
233 }
234
235
236 if (path == null) {
237 path = ROOT_PATH;
238 }
239 LOGGER.trace("calculated path: {}", path);
240 return path;
241 }
242
243 @Override
244 public void saveTo(HttpServletRequest request, HttpServletResponse response) {
245
246 String name = getName();
247 String value = getValue();
248 String comment = getComment();
249 String domain = getDomain();
250 String path = calculatePath(request);
251 int maxAge = getMaxAge();
252 int version = getVersion();
253 boolean secure = isSecure();
254 boolean httpOnly = isHttpOnly();
255 SameSiteOptions sameSite = getSameSite();
256
257 addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly, sameSite);
258 }
259
260 private void addCookieHeader(HttpServletResponse response, String name, String value, String comment,
261 String domain, String path, int maxAge, int version,
262 boolean secure, boolean httpOnly, SameSiteOptions sameSite) {
263
264 String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly, sameSite);
265 response.addHeader(COOKIE_HEADER_NAME, headerValue);
266
267 if (LOGGER.isDebugEnabled()) {
268 LOGGER.debug("Added HttpServletResponse Cookie [{}]", headerValue);
269 }
270 }
271
272
273
274
275
276
277
278
279 protected String buildHeaderValue(String name, String value, String comment,
280 String domain, String path, int maxAge, int version,
281 boolean secure, boolean httpOnly) {
282
283 return buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly, getSameSite());
284 }
285
286 protected String buildHeaderValue(String name, String value, String comment,
287 String domain, String path, int maxAge, int version,
288 boolean secure, boolean httpOnly, SameSiteOptions sameSite) {
289
290 if (!StringUtils.hasText(name)) {
291 throw new IllegalStateException("Cookie name cannot be null/empty.");
292 }
293
294 StringBuilder sb = new StringBuilder(name).append(NAME_VALUE_DELIMITER);
295
296 if (StringUtils.hasText(value)) {
297 sb.append(value);
298 }
299
300 appendComment(sb, comment);
301 appendDomain(sb, domain);
302 appendPath(sb, path);
303 appendExpires(sb, maxAge);
304 appendVersion(sb, version);
305 appendSecure(sb, secure);
306 appendHttpOnly(sb, httpOnly);
307 appendSameSite(sb, sameSite);
308
309 return sb.toString();
310
311 }
312
313 private void appendComment(StringBuilder sb, String comment) {
314 if (StringUtils.hasText(comment)) {
315 sb.append(ATTRIBUTE_DELIMITER);
316 sb.append(COMMENT_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(comment);
317 }
318 }
319
320 private void appendDomain(StringBuilder sb, String domain) {
321 if (StringUtils.hasText(domain)) {
322 sb.append(ATTRIBUTE_DELIMITER);
323 sb.append(DOMAIN_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(domain);
324 }
325 }
326
327 private void appendPath(StringBuilder sb, String path) {
328 if (StringUtils.hasText(path)) {
329 sb.append(ATTRIBUTE_DELIMITER);
330 sb.append(PATH_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(path);
331 }
332 }
333
334 private void appendExpires(StringBuilder sb, int maxAge) {
335
336
337
338
339
340
341 if (maxAge >= 0) {
342 sb.append(ATTRIBUTE_DELIMITER);
343 sb.append(MAXAGE_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(maxAge);
344 sb.append(ATTRIBUTE_DELIMITER);
345 Date expires;
346 if (maxAge == 0) {
347
348 expires = new Date(System.currentTimeMillis() - DAY_MILLIS);
349 } else {
350
351 Calendar cal = Calendar.getInstance();
352 cal.add(Calendar.SECOND, maxAge);
353 expires = cal.getTime();
354 }
355 String formatted = toCookieDate(expires);
356 sb.append(EXPIRES_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(formatted);
357 }
358 }
359
360 private void appendVersion(StringBuilder sb, int version) {
361 if (version > DEFAULT_VERSION) {
362 sb.append(ATTRIBUTE_DELIMITER);
363 sb.append(VERSION_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(version);
364 }
365 }
366
367 private void appendSecure(StringBuilder sb, boolean secure) {
368 if (secure) {
369 sb.append(ATTRIBUTE_DELIMITER);
370
371 sb.append(SECURE_ATTRIBUTE_NAME);
372 }
373 }
374
375 private void appendHttpOnly(StringBuilder sb, boolean httpOnly) {
376 if (httpOnly) {
377 sb.append(ATTRIBUTE_DELIMITER);
378
379 sb.append(HTTP_ONLY_ATTRIBUTE_NAME);
380 }
381 }
382
383 private void appendSameSite(StringBuilder sb, SameSiteOptions sameSite) {
384 if (sameSite != null) {
385 sb.append(ATTRIBUTE_DELIMITER);
386 sb.append(SAME_SITE_ATTRIBUTE_NAME)
387 .append(NAME_VALUE_DELIMITER)
388 .append(sameSite.toString().toLowerCase(Locale.ENGLISH));
389 }
390 }
391
392
393
394
395
396
397
398
399
400 private boolean pathMatches(String cookiePath, String requestPath) {
401 if (!requestPath.startsWith(cookiePath)) {
402 return false;
403 }
404
405 return requestPath.length() == cookiePath.length()
406 || cookiePath.charAt(cookiePath.length() - 1) == '/'
407 || requestPath.charAt(cookiePath.length()) == '/';
408 }
409
410
411
412
413
414
415
416 private static String toCookieDate(Date date) {
417 TimeZone tz = TimeZone.getTimeZone(GMT_TIME_ZONE_ID);
418 DateFormat fmt = new SimpleDateFormat(COOKIE_DATE_FORMAT_STRING, Locale.US);
419 fmt.setTimeZone(tz);
420 return fmt.format(date);
421 }
422
423 @Override
424 public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
425 String name = getName();
426 String value = DELETED_COOKIE_VALUE;
427
428 String comment = null;
429 String domain = getDomain();
430 String path = calculatePath(request);
431
432 int maxAge = 0;
433 int version = getVersion();
434 boolean secure = isSecure();
435
436 boolean httpOnly = false;
437 SameSiteOptions sameSite = getSameSite();
438
439 addCookieHeader(response, name, value, null, domain, path, maxAge, version, secure, httpOnly, sameSite);
440
441 LOGGER.trace("Removed '{}' cookie by setting maxAge=0", name);
442 }
443
444 @Override
445 public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
446 String name = getName();
447 String value = null;
448 javax.servlet.http.Cookie cookie = getCookie(request, name);
449 if (cookie != null) {
450
451 String path = StringUtils.clean(getPath());
452 if (path != null && !pathMatches(path, request.getRequestURI())) {
453 LOGGER.warn("Found '{}' cookie at path '{}', but should be only used for '{}'",
454 name, Encode.forHtml(request.getRequestURI()), path);
455 } else {
456 value = cookie.getValue();
457 LOGGER.debug("Found '{}' cookie value [{}]", name, Encode.forHtml(value));
458 }
459 } else {
460 LOGGER.trace("No '{}' cookie value", name);
461 }
462
463 return value;
464 }
465
466
467
468
469
470
471
472
473
474
475 private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
476 javax.servlet.http.Cookie[] cookies = request.getCookies();
477 if (cookies != null) {
478 for (javax.servlet.http.Cookie cookie : cookies) {
479 if (cookie.getName().equals(cookieName)) {
480 return cookie;
481 }
482 }
483 }
484 return null;
485 }
486 }