Extended Cookie Implementation for HttpOnly Cookies


package com.developi.openntf;

/**
 * Extended Cookie Snippet v1.00: 
 * (C) 2013 Serdar Basegmez 
 * 
 * This class is a derivative work of SimpleCookie class from Apache Shiro 
 * project licensed under Apache License V2.0. 
 * 
 * Apache Shiro 
 * Copyright 2008-2012 The Apache Software Foundation 
 * 
 * This product includes software developed at 
 * The Apache Software Foundation (http://www.apache.org/). 
 * 
 * Certain parts (StringUtils etc.) of the source code for this 
 * product was copied for simplicity and to reduce dependencies 
 * from the source code developed by the Spring Framework Project 
 * (http://www.springframework.org). 
 * 
 */

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ibm.commons.util.StringUtil;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class CookieEx extends Cookie {

	/**
	 * Usage:
	 * 
	 * This snippet contains a Java class extending the standard Cookie of J2EE. The problem
	 * of current JRE we are using in XPages is that it doesn't support HttpOnly cookies. 
	 * HttpOnly cookies are very important for security practices. In summary, if you mark a 
	 * cookie as HttpOnly, browser will not let the client-side JavaScript code to access the
	 * value of that cookie. So it's a suggested method to protect the content of a cookie 
	 * against possible client-side attack such as XSS or malicious browser plugins.
	 * 
	 * I have also seen many XPages developers having difficulties on removing cookies. To 
	 * remove a cookie, you should add an expired cookie to the servlet response. This class 
	 * contains an helper method to ease cookie removal.
	 * 
	 * To add a new cookie:
	 * 
	 * CookieEx c = new CookieEx("Test", "SomeValue");  // Create a cookie 'Test=SomeValue'
	 * c.setPath("/");  // Set its path. By default, it will be the relative path of your app.
	 * c.setMaxAge(120); // Set its max-age in seconds
	 * c.setSecure(true); // Set the cookie secure so it will only be submitted over HTTPS
	 * c.setHttpOnly(true);  // Set the cookie HttpOnly so it will not be accessible from CSJS
	 * c.save(); // Save... It will be added to the response stream.
	 * 
	 * 
	 * To remove a cookie - Method 1:
	 * 
	 * CookieEx.removeCookie("Test");  // Remove the cookie with the name 'Test' and default path. 
	 * CookieEx.removeCookie("Test", "/");  // Remove the cookie with the name 'Test' and path '/' 
	 * 
	 * To remove a cookie - Method 2:
	 * 
	 * CookieEx c=new CookieEx("Test"); // Create a cookie with name 'Test' (you may have the cookie)
	 * c.setPath("/");	// Set its path. Don't forget if you are not using the default path.
	 * c.remove(); 	// Remove... It will add an empty cookie with zero-age and browser will remove it.
	 * 
	 * 
	 * Remember, server side components cannot know the incoming cookies' path or domain values.
	 * So you should explicitly give the correct path if it's not the default one.
	 * 
	 * No need to say these methods works only for XPages. Because it's using FacesContext to get the
	 * servlet request and response object. But there are also methods with appropriate parameters. 
	 * So if you want to use it for servlets or plugins, you can remove methods commented as 
	 * 'XPages Specific' and use the following ones:
	 * 
	 * public void saveTo(HttpServletRequest request, HttpServletResponse response)
	 * public void removeFrom(HttpServletRequest request, HttpServletResponse response)
	 * public String readValue(HttpServletRequest request, HttpServletResponse ignored)
	 * 
	 * Sorry for long comments here and no comments on the code :)
	 * 
	 */

	public static final int DEFAULT_VERSION = -1;

    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 boolean httpOnly;

    public CookieEx(String name, String value) {
        super(name, value);
        this.httpOnly = false; // We are setting it false by default
    }

    public CookieEx(String name) {
    	this(name, "");
    }
    
    // Missing methods in Cookie
    public boolean getHttpOnly() {
        return httpOnly;
    }

    public void setHttpOnly(boolean httpOnly) {
        this.httpOnly = httpOnly;
    }

    private String calculatePath(HttpServletRequest request) {
    	
    	String path = clean(getPath());
        if (StringUtil.isEmpty(path)) {
            path = clean(request.getContextPath());
        }

        if (path == null) {
            path = "/";
        }

        return path;
    }

    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);

    }

    /*
     * 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.
     */

    private String buildHeaderValue(String name, String value, String comment,
                                      String domain, String path, int maxAge, int version,
                                      boolean secure, boolean httpOnly) {

        if (StringUtil.isEmpty(name)) {
            throw new IllegalStateException("Cookie name cannot be null/empty.");
        }

        StringBuilder sb = new StringBuilder(name).append(NAME_VALUE_DELIMITER);

        if (StringUtil.isNotEmpty(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 (StringUtil.isNotEmpty(comment)) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(COMMENT_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(comment);
        }
    }

    private void appendDomain(StringBuilder sb, String domain) {
        if (StringUtil.isNotEmpty(domain)) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(DOMAIN_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(domain);
        }
    }

    private void appendPath(StringBuilder sb, String path) {
        if (StringUtil.isNotEmpty(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
        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
        }
    }

    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);
    }

    private static Cookie getCookie(HttpServletRequest request, String cookieName) {
        Cookie cookies[] = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(cookieName)) {
                    return cookie;
                }
            }
        }
        return null;
    }

    // Imported from org.apache.shiro.util.StringUtils    
    private static String clean(String in) {
        String out = in;

        if (in != null) {
            out = in.trim();
            if (out.equals("")) {
                out = null;
            }
        }

        return out;
    }

    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 = getSecure();
        boolean httpOnly = getHttpOnly();

        addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
    }

    // Important! You need to set the correct path before trying to remove it. 
    public void removeFrom(HttpServletRequest request, HttpServletResponse response) {

    	String name = getName();
        String value = "deleteMe";
        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 = getSecure();
        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);
    }

    public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
        String name = getName();
        String value = null;
        javax.servlet.http.Cookie cookie = getCookie(request, name);
        if (cookie != null) {
            value = cookie.getValue();
        }

        return value;
    }

    // XPages-specific methods
    private HttpServletRequest extractRequest() {
    	FacesContext context=FacesContext.getCurrentInstance();
		ExternalContext extContext=context.getExternalContext();

		return (HttpServletRequest) extContext.getRequest();
    }

    private HttpServletResponse extractResponse() {
    	FacesContext context=FacesContext.getCurrentInstance();
		ExternalContext extContext=context.getExternalContext();

		return (HttpServletResponse) extContext.getResponse();    	
    }

    public void save() {
    	saveTo(extractRequest(), extractResponse());
    }
    
    public void remove() {
    	removeFrom(extractRequest(), extractResponse());
    }
    
    public String readValue() {
    	return readValue(extractRequest(), extractResponse());
    }
    		
    public static void removeCookie(String name) {
    	removeCookie(name, "");
    }

    public static void removeCookie(String name, String path) {
    	CookieEx c=new CookieEx(name);
    	c.setPath(path);
    	c.remove();
    }
    // end of XPages specific methods
    
}
All code submitted to OpenNTF XSnippets, whether submitted as a "Snippet" or in the body of a Comment, is provided under the Apache License Version 2.0. See Terms of Use for full details.
No comments yetLogin first to comment...