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