/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.web.servlet; import org.apache.shiro.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** * Default {@link Cookie Cookie} implementation. 'HttpOnly' is supported out of the box, even on * Servlet {@code 2.4} and {@code 2.5} container implementations, using raw header writing logic and not * {@link javax.servlet.http.Cookie javax.servlet.http.Cookie} objects (which only has 'HttpOnly' support in Servlet * {@code 2.6} specifications and above). * * @since 1.0 */ public class SimpleCookie implements Cookie { /** * {@code -1}, indicating the cookie should expire when the browser closes. */ public static final int DEFAULT_MAX_AGE = -1; /** * {@code -1} indicating that no version property should be set on the cookie. */ public static final int DEFAULT_VERSION = -1; //These constants are protected on purpose so that the test case can use them protected static final String NAME_VALUE_DELIMITER = "="; protected static final String ATTRIBUTE_DELIMITER = "; "; protected static final long DAY_MILLIS = 86400000; //1 day = 86,400,000 milliseconds protected static final String GMT_TIME_ZONE_ID = "GMT"; protected static final String COOKIE_DATE_FORMAT_STRING = "EEE, dd-MMM-yyyy HH:mm:ss z"; protected static final String COOKIE_HEADER_NAME = "Set-Cookie"; protected static final String PATH_ATTRIBUTE_NAME = "Path"; protected static final String EXPIRES_ATTRIBUTE_NAME = "Expires"; protected static final String MAXAGE_ATTRIBUTE_NAME = "Max-Age"; protected static final String DOMAIN_ATTRIBUTE_NAME = "Domain"; protected static final String VERSION_ATTRIBUTE_NAME = "Version"; protected static final String COMMENT_ATTRIBUTE_NAME = "Comment"; protected static final String SECURE_ATTRIBUTE_NAME = "Secure"; protected static final String HTTP_ONLY_ATTRIBUTE_NAME = "HttpOnly"; private static final transient Logger log = LoggerFactory.getLogger(SimpleCookie.class); private String name; private String value; private String comment; private String domain; private String path; private int maxAge; private int version; private boolean secure; private boolean httpOnly; public SimpleCookie() { this.maxAge = DEFAULT_MAX_AGE; this.version = DEFAULT_VERSION; this.httpOnly = true; //most of the cookies ever used by Shiro should be as secure as possible. } public SimpleCookie(String name) { this(); this.name = name; } public SimpleCookie(Cookie cookie) { this.name = cookie.getName(); this.value = cookie.getValue(); this.comment = cookie.getComment(); this.domain = cookie.getDomain(); this.path = cookie.getPath(); this.maxAge = Math.max(DEFAULT_MAX_AGE, cookie.getMaxAge()); this.version = Math.max(DEFAULT_VERSION, cookie.getVersion()); this.secure = cookie.isSecure(); this.httpOnly = cookie.isHttpOnly(); } public String getName() { return name; } public void setName(String name) { if (!StringUtils.hasText(name)) { throw new IllegalArgumentException("Name cannot be null/empty."); } this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public int getMaxAge() { return maxAge; } public void setMaxAge(int maxAge) { this.maxAge = Math.max(DEFAULT_MAX_AGE, maxAge); } public int getVersion() { return version; } public void setVersion(int version) { this.version = Math.max(DEFAULT_VERSION, version); } public boolean isSecure() { return secure; } public void setSecure(boolean secure) { this.secure = secure; } public boolean isHttpOnly() { return httpOnly; } public void setHttpOnly(boolean httpOnly) { this.httpOnly = httpOnly; } /** * Returns the Cookie's calculated path setting. If the {@link javax.servlet.http.Cookie#getPath() path} is {@code null}, then the * {@code request}'s {@link javax.servlet.http.HttpServletRequest#getContextPath() context path} * will be returned. If getContextPath() is the empty string or null then the ROOT_PATH constant is returned. * * @param request the incoming HttpServletRequest * @return the path to be used as the path when the cookie is created or removed */ private String calculatePath(HttpServletRequest request) { String path = StringUtils.clean(getPath()); if (!StringUtils.hasText(path)) { path = StringUtils.clean(request.getContextPath()); } //fix for http://issues.apache.org/jira/browse/SHIRO-9: if (path == null) { path = ROOT_PATH; } log.trace("calculated path: {}", path); return path; } public void saveTo(HttpServletRequest request, HttpServletResponse response) { String name = getName(); String value = getValue(); String comment = getComment(); String domain = getDomain(); String path = calculatePath(request); int maxAge = getMaxAge(); int version = getVersion(); boolean secure = isSecure(); boolean httpOnly = isHttpOnly(); addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly); } private void addCookieHeader(HttpServletResponse response, String name, String value, String comment, String domain, String path, int maxAge, int version, boolean secure, boolean httpOnly) { String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly); response.addHeader(COOKIE_HEADER_NAME, headerValue); if (log.isDebugEnabled()) { log.debug("Added HttpServletResponse Cookie [{}]", headerValue); } } /* * This implementation followed the grammar defined here for convenience: * <a href="http://github.com/abarth/http-state/blob/master/notes/2009-11-07-Yui-Naruse.txt">Cookie grammar</a>. * * @return the 'Set-Cookie' header value for this cookie instance. */ protected String buildHeaderValue(String name, String value, String comment, String domain, String path, int maxAge, int version, boolean secure, boolean httpOnly) { if (!StringUtils.hasText(name)) { throw new IllegalStateException("Cookie name cannot be null/empty."); } StringBuilder sb = new StringBuilder(name).append(NAME_VALUE_DELIMITER); if (StringUtils.hasText(value)) { sb.append(value); } appendComment(sb, comment); appendDomain(sb, domain); appendPath(sb, path); appendExpires(sb, maxAge); appendVersion(sb, version); appendSecure(sb, secure); appendHttpOnly(sb, httpOnly); return sb.toString(); } private void appendComment(StringBuilder sb, String comment) { if (StringUtils.hasText(comment)) { sb.append(ATTRIBUTE_DELIMITER); sb.append(COMMENT_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(comment); } } private void appendDomain(StringBuilder sb, String domain) { if (StringUtils.hasText(domain)) { sb.append(ATTRIBUTE_DELIMITER); sb.append(DOMAIN_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(domain); } } private void appendPath(StringBuilder sb, String path) { if (StringUtils.hasText(path)) { sb.append(ATTRIBUTE_DELIMITER); sb.append(PATH_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(path); } } private void appendExpires(StringBuilder sb, int maxAge) { // if maxAge is negative, cookie should should expire when browser closes // Don't write the maxAge cookie value if it's negative - at least on Firefox it'll cause the // cookie to be deleted immediately // Write the expires header used by older browsers, but may be unnecessary // and it is not by the spec, see http://www.faqs.org/rfcs/rfc2965.html // TODO consider completely removing the following if (maxAge >= 0) { sb.append(ATTRIBUTE_DELIMITER); sb.append(MAXAGE_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(maxAge); sb.append(ATTRIBUTE_DELIMITER); Date expires; if (maxAge == 0) { //delete the cookie by specifying a time in the past (1 day ago): expires = new Date(System.currentTimeMillis() - DAY_MILLIS); } else { //Value is in seconds. So take 'now' and add that many seconds, and that's our expiration date: Calendar cal = Calendar.getInstance(); cal.add(Calendar.SECOND, maxAge); expires = cal.getTime(); } String formatted = toCookieDate(expires); sb.append(EXPIRES_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(formatted); } } private void appendVersion(StringBuilder sb, int version) { if (version > DEFAULT_VERSION) { sb.append(ATTRIBUTE_DELIMITER); sb.append(VERSION_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(version); } } private void appendSecure(StringBuilder sb, boolean secure) { if (secure) { sb.append(ATTRIBUTE_DELIMITER); sb.append(SECURE_ATTRIBUTE_NAME); //No value for this attribute } } private void appendHttpOnly(StringBuilder sb, boolean httpOnly) { if (httpOnly) { sb.append(ATTRIBUTE_DELIMITER); sb.append(HTTP_ONLY_ATTRIBUTE_NAME); //No value for this attribute } } /** * Check whether the given {@code cookiePath} matches the {@code requestPath} * * @param cookiePath * @param requestPath * @return * @see <a href="https://tools.ietf.org/html/rfc6265#section-5.1.4">RFC 6265, Section 5.1.4 "Paths and Path-Match"</a> */ private boolean pathMatches(String cookiePath, String requestPath) { if (!requestPath.startsWith(cookiePath)) { return false; } return requestPath.length() == cookiePath.length() || cookiePath.charAt(cookiePath.length() - 1) == '/' || requestPath.charAt(cookiePath.length()) == '/'; } /** * Formats a date into a cookie date compatible string (Netscape's specification). * * @param date the date to format * @return an HTTP 1.0/1.1 Cookie compatible date string (GMT-based). */ private static String toCookieDate(Date date) { TimeZone tz = TimeZone.getTimeZone(GMT_TIME_ZONE_ID); DateFormat fmt = new SimpleDateFormat(COOKIE_DATE_FORMAT_STRING, Locale.US); fmt.setTimeZone(tz); return fmt.format(date); } public void removeFrom(HttpServletRequest request, HttpServletResponse response) { String name = getName(); String value = DELETED_COOKIE_VALUE; String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions String domain = getDomain(); String path = calculatePath(request); int maxAge = 0; //always zero for deletion int version = getVersion(); boolean secure = isSecure(); boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly); log.trace("Removed '{}' cookie by setting maxAge=0", name); } public String readValue(HttpServletRequest request, HttpServletResponse ignored) { String name = getName(); String value = null; javax.servlet.http.Cookie cookie = getCookie(request, name); if (cookie != null) { // Validate that the cookie is used at the correct place. String path = StringUtils.clean(getPath()); if (path != null && !pathMatches(path, request.getRequestURI())) { log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path}); } else { value = cookie.getValue(); log.debug("Found '{}' cookie value [{}]", name, value); } } else { log.trace("No '{}' cookie value", name); } return value; } /** * Returns the cookie with the given name from the request or {@code null} if no cookie * with that name could be found. * * @param request the current executing http request. * @param cookieName the name of the cookie to find and return. * @return the cookie with the given name from the request or {@code null} if no cookie * with that name could be found. */ private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) { javax.servlet.http.Cookie cookies[] = request.getCookies(); if (cookies != null) { for (javax.servlet.http.Cookie cookie : cookies) { if (cookie.getName().equals(cookieName)) { return cookie; } } } return null; } }