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 }