Coverage Report - org.apache.shiro.web.servlet.SimpleCookie
 
Classes in this File Line Coverage Branch Coverage Complexity
SimpleCookie
86%
131/151
77%
28/36
1.583
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one
 3  
  * or more contributor license agreements.  See the NOTICE file
 4  
  * distributed with this work for additional information
 5  
  * regarding copyright ownership.  The ASF licenses this file
 6  
  * to you under the Apache License, Version 2.0 (the
 7  
  * "License"); you may not use this file except in compliance
 8  
  * with the License.  You may obtain a copy of the License at
 9  
  *
 10  
  *     http://www.apache.org/licenses/LICENSE-2.0
 11  
  *
 12  
  * Unless required by applicable law or agreed to in writing,
 13  
  * software distributed under the License is distributed on an
 14  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15  
  * KIND, either express or implied.  See the License for the
 16  
  * specific language governing permissions and limitations
 17  
  * under the License.
 18  
  */
 19  
 package org.apache.shiro.web.servlet;
 20  
 
 21  
 import org.apache.shiro.util.StringUtils;
 22  
 import org.slf4j.Logger;
 23  
 import org.slf4j.LoggerFactory;
 24  
 
 25  
 import javax.servlet.http.HttpServletRequest;
 26  
 import javax.servlet.http.HttpServletResponse;
 27  
 import java.text.DateFormat;
 28  
 import java.text.SimpleDateFormat;
 29  
 import java.util.Calendar;
 30  
 import java.util.Date;
 31  
 import java.util.Locale;
 32  
 import java.util.TimeZone;
 33  
 
 34  
 /**
 35  
  * Default {@link Cookie Cookie} implementation.  'HttpOnly' is supported out of the box, even on
 36  
  * Servlet {@code 2.4} and {@code 2.5} container implementations, using raw header writing logic and not
 37  
  * {@link javax.servlet.http.Cookie javax.servlet.http.Cookie} objects (which only has 'HttpOnly' support in Servlet
 38  
  * {@code 2.6} specifications and above).
 39  
  *
 40  
  * @since 1.0
 41  
  */
 42  
 public class SimpleCookie implements Cookie {
 43  
 
 44  
     /**
 45  
      * {@code -1}, indicating the cookie should expire when the browser closes.
 46  
      */
 47  
     public static final int DEFAULT_MAX_AGE = -1;
 48  
 
 49  
     /**
 50  
      * {@code -1} indicating that no version property should be set on the cookie.
 51  
      */
 52  
     public static final int DEFAULT_VERSION = -1;
 53  
 
 54  
     //These constants are protected on purpose so that the test case can use them
 55  
     protected static final String NAME_VALUE_DELIMITER = "=";
 56  
     protected static final String ATTRIBUTE_DELIMITER = "; ";
 57  
     protected static final long DAY_MILLIS = 86400000; //1 day = 86,400,000 milliseconds
 58  
     protected static final String GMT_TIME_ZONE_ID = "GMT";
 59  
     protected static final String COOKIE_DATE_FORMAT_STRING = "EEE, dd-MMM-yyyy HH:mm:ss z";
 60  
 
 61  
     protected static final String COOKIE_HEADER_NAME = "Set-Cookie";
 62  
     protected static final String PATH_ATTRIBUTE_NAME = "Path";
 63  
     protected static final String EXPIRES_ATTRIBUTE_NAME = "Expires";
 64  
     protected static final String MAXAGE_ATTRIBUTE_NAME = "Max-Age";
 65  
     protected static final String DOMAIN_ATTRIBUTE_NAME = "Domain";
 66  
     protected static final String VERSION_ATTRIBUTE_NAME = "Version";
 67  
     protected static final String COMMENT_ATTRIBUTE_NAME = "Comment";
 68  
     protected static final String SECURE_ATTRIBUTE_NAME = "Secure";
 69  
     protected static final String HTTP_ONLY_ATTRIBUTE_NAME = "HttpOnly";
 70  
 
 71  1
     private static final transient Logger log = LoggerFactory.getLogger(SimpleCookie.class);
 72  
 
 73  
     private String name;
 74  
     private String value;
 75  
     private String comment;
 76  
     private String domain;
 77  
     private String path;
 78  
     private int maxAge;
 79  
     private int version;
 80  
     private boolean secure;
 81  
     private boolean httpOnly;
 82  
 
 83  42
     public SimpleCookie() {
 84  42
         this.maxAge = DEFAULT_MAX_AGE;
 85  42
         this.version = DEFAULT_VERSION;
 86  42
         this.httpOnly = true; //most of the cookies ever used by Shiro should be as secure as possible.
 87  42
     }
 88  
 
 89  
     public SimpleCookie(String name) {
 90  41
         this();
 91  41
         this.name = name;
 92  41
     }
 93  
 
 94  5
     public SimpleCookie(Cookie cookie) {
 95  5
         this.name = cookie.getName();
 96  5
         this.value = cookie.getValue();
 97  5
         this.comment = cookie.getComment();
 98  5
         this.domain = cookie.getDomain();
 99  5
         this.path = cookie.getPath();
 100  5
         this.maxAge = Math.max(DEFAULT_MAX_AGE, cookie.getMaxAge());
 101  5
         this.version = Math.max(DEFAULT_VERSION, cookie.getVersion());
 102  5
         this.secure = cookie.isSecure();
 103  5
         this.httpOnly = cookie.isHttpOnly();
 104  5
     }
 105  
 
 106  
     public String getName() {
 107  33
         return name;
 108  
     }
 109  
 
 110  
     public void setName(String name) {
 111  0
         if (!StringUtils.hasText(name)) {
 112  0
             throw new IllegalArgumentException("Name cannot be null/empty.");
 113  
         }
 114  0
         this.name = name;
 115  0
     }
 116  
 
 117  
     public String getValue() {
 118  10
         return value;
 119  
     }
 120  
 
 121  
     public void setValue(String value) {
 122  7
         this.value = value;
 123  7
     }
 124  
 
 125  
     public String getComment() {
 126  10
         return comment;
 127  
     }
 128  
 
 129  
     public void setComment(String comment) {
 130  0
         this.comment = comment;
 131  0
     }
 132  
 
 133  
     public String getDomain() {
 134  15
         return domain;
 135  
     }
 136  
 
 137  
     public void setDomain(String domain) {
 138  0
         this.domain = domain;
 139  0
     }
 140  
 
 141  
     public String getPath() {
 142  15
         return path;
 143  
     }
 144  
 
 145  
     public void setPath(String path) {
 146  0
         this.path = path;
 147  0
     }
 148  
 
 149  
     public int getMaxAge() {
 150  10
         return maxAge;
 151  
     }
 152  
 
 153  
     public void setMaxAge(int maxAge) {
 154  20
         this.maxAge = Math.max(DEFAULT_MAX_AGE, maxAge);
 155  20
     }
 156  
 
 157  
     public int getVersion() {
 158  15
         return version;
 159  
     }
 160  
 
 161  
     public void setVersion(int version) {
 162  0
         this.version = Math.max(DEFAULT_VERSION, version);
 163  0
     }
 164  
 
 165  
     public boolean isSecure() {
 166  15
         return secure;
 167  
     }
 168  
 
 169  
     public void setSecure(boolean secure) {
 170  0
         this.secure = secure;
 171  0
     }
 172  
 
 173  
     public boolean isHttpOnly() {
 174  10
         return httpOnly;
 175  
     }
 176  
 
 177  
     public void setHttpOnly(boolean httpOnly) {
 178  38
         this.httpOnly = httpOnly;
 179  38
     }
 180  
 
 181  
     /**
 182  
      * Returns the Cookie's calculated path setting.  If the {@link javax.servlet.http.Cookie#getPath() path} is {@code null}, then the
 183  
      * {@code request}'s {@link javax.servlet.http.HttpServletRequest#getContextPath() context path}
 184  
      * will be returned. If getContextPath() is the empty string or null then the ROOT_PATH constant is returned.
 185  
      *
 186  
      * @param request the incoming HttpServletRequest
 187  
      * @return the path to be used as the path when the cookie is created or removed
 188  
      */
 189  
     private String calculatePath(HttpServletRequest request) {
 190  12
         String path = StringUtils.clean(getPath());
 191  12
         if (!StringUtils.hasText(path)) {
 192  11
             path = StringUtils.clean(request.getContextPath());
 193  
         }
 194  
 
 195  
         //fix for http://issues.apache.org/jira/browse/SHIRO-9:
 196  12
         if (path == null) {
 197  7
             path = ROOT_PATH;
 198  
         }
 199  12
         log.trace("calculated path: {}", path);
 200  12
         return path;
 201  
     }
 202  
 
 203  
     public void saveTo(HttpServletRequest request, HttpServletResponse response) {
 204  
 
 205  7
         String name = getName();
 206  7
         String value = getValue();
 207  7
         String comment = getComment();
 208  7
         String domain = getDomain();
 209  7
         String path = calculatePath(request);
 210  7
         int maxAge = getMaxAge();
 211  7
         int version = getVersion();
 212  7
         boolean secure = isSecure();
 213  7
         boolean httpOnly = isHttpOnly();
 214  
 
 215  7
         addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
 216  7
     }
 217  
 
 218  
     private void addCookieHeader(HttpServletResponse response, String name, String value, String comment,
 219  
                                  String domain, String path, int maxAge, int version,
 220  
                                  boolean secure, boolean httpOnly) {
 221  
 
 222  12
         String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
 223  12
         response.addHeader(COOKIE_HEADER_NAME, headerValue);
 224  
 
 225  12
         if (log.isDebugEnabled()) {
 226  12
             log.debug("Added HttpServletResponse Cookie [{}]", headerValue);
 227  
         }
 228  12
     }
 229  
 
 230  
     /*
 231  
      * This implementation followed the grammar defined here for convenience:
 232  
      * <a href="http://github.com/abarth/http-state/blob/master/notes/2009-11-07-Yui-Naruse.txt">Cookie grammar</a>.
 233  
      *
 234  
      * @return the 'Set-Cookie' header value for this cookie instance.
 235  
      */
 236  
 
 237  
     protected String buildHeaderValue(String name, String value, String comment,
 238  
                                       String domain, String path, int maxAge, int version,
 239  
                                       boolean secure, boolean httpOnly) {
 240  
 
 241  13
         if (!StringUtils.hasText(name)) {
 242  0
             throw new IllegalStateException("Cookie name cannot be null/empty.");
 243  
         }
 244  
 
 245  13
         StringBuilder sb = new StringBuilder(name).append(NAME_VALUE_DELIMITER);
 246  
 
 247  13
         if (StringUtils.hasText(value)) {
 248  13
             sb.append(value);
 249  
         }
 250  
 
 251  13
         appendComment(sb, comment);
 252  13
         appendDomain(sb, domain);
 253  13
         appendPath(sb, path);
 254  13
         appendExpires(sb, maxAge);
 255  13
         appendVersion(sb, version);
 256  13
         appendSecure(sb, secure);
 257  13
         appendHttpOnly(sb, httpOnly);
 258  
 
 259  13
         return sb.toString();
 260  
 
 261  
     }
 262  
 
 263  
     private void appendComment(StringBuilder sb, String comment) {
 264  13
         if (StringUtils.hasText(comment)) {
 265  1
             sb.append(ATTRIBUTE_DELIMITER);
 266  1
             sb.append(COMMENT_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(comment);
 267  
         }
 268  13
     }
 269  
 
 270  
     private void appendDomain(StringBuilder sb, String domain) {
 271  13
         if (StringUtils.hasText(domain)) {
 272  1
             sb.append(ATTRIBUTE_DELIMITER);
 273  1
             sb.append(DOMAIN_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(domain);
 274  
         }
 275  13
     }
 276  
 
 277  
     private void appendPath(StringBuilder sb, String path) {
 278  13
         if (StringUtils.hasText(path)) {
 279  13
             sb.append(ATTRIBUTE_DELIMITER);
 280  13
             sb.append(PATH_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(path);
 281  
         }
 282  13
     }
 283  
 
 284  
     private void appendExpires(StringBuilder sb, int maxAge) {
 285  
         // if maxAge is negative, cookie should should expire when browser closes
 286  
         // Don't write the maxAge cookie value if it's negative - at least on Firefox it'll cause the 
 287  
         // cookie to be deleted immediately
 288  
         // Write the expires header used by older browsers, but may be unnecessary
 289  
         // and it is not by the spec, see http://www.faqs.org/rfcs/rfc2965.html
 290  
         // TODO consider completely removing the following 
 291  13
         if (maxAge >= 0) {
 292  6
             sb.append(ATTRIBUTE_DELIMITER);
 293  6
             sb.append(MAXAGE_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(maxAge);
 294  6
             sb.append(ATTRIBUTE_DELIMITER);
 295  
             Date expires;
 296  6
             if (maxAge == 0) {
 297  
                 //delete the cookie by specifying a time in the past (1 day ago):
 298  6
                 expires = new Date(System.currentTimeMillis() - DAY_MILLIS);
 299  
             } else {
 300  
                 //Value is in seconds.  So take 'now' and add that many seconds, and that's our expiration date:
 301  0
                 Calendar cal = Calendar.getInstance();
 302  0
                 cal.add(Calendar.SECOND, maxAge);
 303  0
                 expires = cal.getTime();
 304  
             }
 305  6
             String formatted = toCookieDate(expires);
 306  6
             sb.append(EXPIRES_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(formatted);
 307  
         }
 308  13
     }
 309  
 
 310  
     private void appendVersion(StringBuilder sb, int version) {
 311  13
         if (version > DEFAULT_VERSION) {
 312  0
             sb.append(ATTRIBUTE_DELIMITER);
 313  0
             sb.append(VERSION_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(version);
 314  
         }
 315  13
     }
 316  
 
 317  
     private void appendSecure(StringBuilder sb, boolean secure) {
 318  13
         if (secure) {
 319  1
             sb.append(ATTRIBUTE_DELIMITER);
 320  1
             sb.append(SECURE_ATTRIBUTE_NAME); //No value for this attribute
 321  
         }
 322  13
     }
 323  
 
 324  
     private void appendHttpOnly(StringBuilder sb, boolean httpOnly) {
 325  13
         if (httpOnly) {
 326  7
             sb.append(ATTRIBUTE_DELIMITER);
 327  7
             sb.append(HTTP_ONLY_ATTRIBUTE_NAME); //No value for this attribute
 328  
         }
 329  13
     }
 330  
 
 331  
     /**
 332  
      * Formats a date into a cookie date compatible string (Netscape's specification).
 333  
      *
 334  
      * @param date the date to format
 335  
      * @return an HTTP 1.0/1.1 Cookie compatible date string (GMT-based).
 336  
      */
 337  
     private static String toCookieDate(Date date) {
 338  6
         TimeZone tz = TimeZone.getTimeZone(GMT_TIME_ZONE_ID);
 339  6
         DateFormat fmt = new SimpleDateFormat(COOKIE_DATE_FORMAT_STRING, Locale.US);
 340  6
         fmt.setTimeZone(tz);
 341  6
         return fmt.format(date);
 342  
     }
 343  
 
 344  
     public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
 345  5
         String name = getName();
 346  5
         String value = DELETED_COOKIE_VALUE;
 347  5
         String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
 348  5
         String domain = getDomain();
 349  5
         String path = calculatePath(request);
 350  5
         int maxAge = 0; //always zero for deletion
 351  5
         int version = getVersion();
 352  5
         boolean secure = isSecure();
 353  5
         boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all
 354  
 
 355  5
         addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
 356  
 
 357  5
         log.trace("Removed '{}' cookie by setting maxAge=0", name);
 358  5
     }
 359  
 
 360  
     public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
 361  14
         String name = getName();
 362  14
         String value = null;
 363  14
         javax.servlet.http.Cookie cookie = getCookie(request, name);
 364  14
         if (cookie != null) {
 365  5
             value = cookie.getValue();
 366  5
             log.debug("Found '{}' cookie value [{}]", name, value);
 367  
         } else {
 368  9
             log.trace("No '{}' cookie value", name);
 369  
         }
 370  
 
 371  14
         return value;
 372  
     }
 373  
 
 374  
     /**
 375  
      * Returns the cookie with the given name from the request or {@code null} if no cookie
 376  
      * with that name could be found.
 377  
      *
 378  
      * @param request    the current executing http request.
 379  
      * @param cookieName the name of the cookie to find and return.
 380  
      * @return the cookie with the given name from the request or {@code null} if no cookie
 381  
      *         with that name could be found.
 382  
      */
 383  
     private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
 384  14
         javax.servlet.http.Cookie cookies[] = request.getCookies();
 385  14
         if (cookies != null) {
 386  7
             for (javax.servlet.http.Cookie cookie : cookies) {
 387  6
                 if (cookie.getName().equals(cookieName)) {
 388  5
                     return cookie;
 389  
                 }
 390  
             }
 391  
         }
 392  9
         return null;
 393  
     }
 394  
 }