ed.net.CookieJar.java Source code

Java tutorial

Introduction

Here is the source code for ed.net.CookieJar.java

Source

/**
*      Copyright (C) 2008 10gen Inc.
*  
*    Licensed 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.
*/

/**
*  Portions of this code taken from Apache's HTTPClient and was licensed under the following terms :
*  
*  
*  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.
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation.  For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
* 
*/

package ed.net;

import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.Cookie;

import edu.emory.mathcs.backport.java.util.Collections;

public class CookieJar {
    //TODO: add clean method to remove stale & optionally nonpr
    public CookieJar() {
        this._creationDates = new HashMap<String, Date>();
        this._cookies = new HashMap<String, Cookie>();
    }

    /**
     * Validates & adds cookies to this object
     * 
     * @param source the origin server of the cookie
     * @param cookie the being added 
     */
    public void addCookie(URL source, Cookie cookie) {
        try {
            validate(source, cookie);
        } catch (MalformedCookieException e) {
            //TODO: invalid cookies
            return;
        } catch (IllegalArgumentException e) {
            //TODO: invalid cookies
            return;
        }

        if (cookie.getMaxAge() == 0) {
            remove(cookie.getName());
            return;
        } else {
            _cookies.put(cookie.getName(), cookie);
            _creationDates.put(cookie.getName(), new Date());
        }
    }

    /**
     * Returns all applicable cookies for the given url.
     * @param requestingUrl
     * @return
     */
    public Map<String, Cookie> getActiveCookies(URL requestingUrl) {
        Map<String, Cookie> cookiesToSend = new HashMap<String, Cookie>();

        for (Cookie c : _cookies.values()) {
            if (match(requestingUrl, c))
                cookiesToSend.put(c.getName(), c);
        }

        return cookiesToSend;
    }

    public List<Cookie> clean() {
        return clean(true);
    }

    public Map<String, Cookie> getAll() {
        return Collections.unmodifiableMap(_cookies);
    }

    public List<Cookie> clean(boolean removeNonpersistent) {
        List<Cookie> deadCookies = new ArrayList<Cookie>();

        for (Cookie c : _cookies.values()) {
            if (isExpired(c))
                deadCookies.add(c);

            if (removeNonpersistent && c.getMaxAge() < 0)
                deadCookies.add(c);
        }
        for (Cookie deadCookie : deadCookies)
            _cookies.remove(deadCookie.getName());

        return deadCookies;
    }

    public Cookie remove(String name) {
        _creationDates.remove(name);
        return _cookies.remove(name);
    }

    public void removeAll() {
        _creationDates.clear();
        _cookies.clear();
    }

    /**
     * Performs RFC 2109 {@link Cookie} validation
     * 
     * @param url the source of the cookie
     * @param cookie The cookie to validate.
     * @throws IllegalArgumentException if an exception occurs during validation
     */
    private void validate(URL url, Cookie cookie) {
        String host = url.getHost();
        int port = url.getPort();
        String path = url.getPath();

        // based on org.apache.commons.httpclient.cookie.CookieSpecBase
        if (host == null) {
            throw new IllegalArgumentException("Host of origin may not be null");
        }
        if (host.trim().equals("")) {
            throw new IllegalArgumentException("Host of origin may not be blank");
        }
        if (port < 0)
            port = 80;

        if (path == null) {
            throw new IllegalArgumentException("Path of origin may not be null.");
        }
        if (path.trim().equals("")) {
            path = "/";
        }
        host = host.toLowerCase();
        // check version
        if (cookie.getVersion() < 0) {
            throw new MalformedCookieException("Illegal version number " + cookie.getValue());
        }

        // security check... we musn't allow the server to give us an
        // invalid domain scope

        // Validate the cookies domain attribute. NOTE: Domains without
        // any dots are allowed to support hosts on private LANs that don't
        // have DNS names. Since they have no dots, to domain-match the
        // request-host and domain must be identical for the cookie to sent
        // back to the origin-server.
        if (host.indexOf(".") >= 0) {
            // Not required to have at least two dots. RFC 2965.
            // A Set-Cookie2 with Domain=ajax.com will be accepted.

            // domain must match host
            if (!host.endsWith(cookie.getDomain())) {
                String s = cookie.getDomain();
                if (s.startsWith(".")) {
                    s = s.substring(1, s.length());
                }
                if (!host.equals(s)) {
                    throw new MalformedCookieException("Illegal domain attribute \"" + cookie.getDomain()
                            + "\". Domain of origin: \"" + host + "\"");
                }
            }
        } else {
            if (!host.equals(cookie.getDomain())) {
                throw new MalformedCookieException("Illegal domain attribute \"" + cookie.getDomain()
                        + "\". Domain of origin: \"" + host + "\"");
            }
        }

        // another security check... we musn't allow the server to give us a
        // cookie that doesn't match this path
        if (!path.startsWith(cookie.getPath())) {
            throw new MalformedCookieException(
                    "Illegal path attribute \"" + cookie.getPath() + "\". Path of origin: \"" + path + "\"");
        }

        // Validate using RFC 2109
        // --------------------------------------------------------
        if (cookie.getName().indexOf(' ') != -1) {
            throw new MalformedCookieException("Cookie name may not contain blanks");
        }
        if (cookie.getName().startsWith("$")) {
            throw new MalformedCookieException("Cookie name may not start with $");
        }

        if (cookie.getDomain() != null && (!cookie.getDomain().equals(host))) {

            // domain must start with dot
            if (!cookie.getDomain().startsWith(".")) {
                throw new MalformedCookieException("Domain attribute \"" + cookie.getDomain()
                        + "\" violates RFC 2109: domain must start with a dot");
            }
            // domain must have at least one embedded dot
            int dotIndex = cookie.getDomain().indexOf('.', 1);
            if (dotIndex < 0 || dotIndex == cookie.getDomain().length() - 1) {
                throw new MalformedCookieException("Domain attribute \"" + cookie.getDomain()
                        + "\" violates RFC 2109: domain must contain an embedded dot");
            }
            host = host.toLowerCase();
            if (!host.endsWith(cookie.getDomain())) {
                throw new MalformedCookieException("Illegal domain attribute \"" + cookie.getDomain()
                        + "\". Domain of origin: \"" + host + "\"");
            }
            // host minus domain may not contain any dots
            String hostWithoutDomain = host.substring(0, host.length() - cookie.getDomain().length());
            if (hostWithoutDomain.indexOf('.') != -1) {
                throw new MalformedCookieException("Domain attribute \"" + cookie.getDomain()
                        + "\" violates RFC 2109: host minus domain may not contain any dots");
            }
        }
    }

    /**
     * Return <tt>true</tt> if the cookie should be submitted with a request
     * with given attributes, <tt>false</tt> otherwise.
     * @param destination the destination of the request
     * @param cookie {@link Cookie} to be matched
     * @return true if the cookie matches the criterium
     */
    private boolean match(URL destination, final Cookie cookie) {
        String host = destination.getHost();
        int port = destination.getPort();
        String path = destination.getPath();
        boolean secure = "https".equals(destination.getProtocol());

        if (host == null) {
            throw new IllegalArgumentException("Host of origin may not be null");
        }
        if (host.trim().equals("")) {
            throw new IllegalArgumentException("Host of origin may not be blank");
        }
        if (port < 0) {
            port = 80;
        }
        if (path == null) {
            throw new IllegalArgumentException("Path of origin may not be null.");
        }
        if (cookie == null) {
            throw new IllegalArgumentException("Cookie may not be null");
        }
        if (path.trim().equals("")) {
            path = "/";
        }
        host = host.toLowerCase();
        if (cookie.getDomain() == null) {
            return false;
        }
        if (cookie.getPath() == null) {
            return false;
        }

        return
        // only add the cookie if it hasn't yet expired
        !isExpired(cookie)
                // and the domain pattern matches
                && (domainMatch(host, cookie.getDomain()))
                // and the path is null or matching
                && (pathMatch(path, cookie.getPath()))
                // and if the secure flag is set, only if the request is
                // actually secure
                && (cookie.getSecure() ? secure : true);
    }

    /**
     * Performs domain-match as implemented in common browsers.
     * @param host The target host.
     * @param domain The cookie domain attribute.
     * @return true if the specified host matches the given domain.
     */
    private boolean domainMatch(final String host, String domain) {
        if (host.equals(domain)) {
            return true;
        }
        if (!domain.startsWith(".")) {
            domain = "." + domain;
        }
        return host.endsWith(domain) || host.equals(domain.substring(1));
    }

    /**
     * Performs path-match as implemented in common browsers.
     * @param path The target path.
     * @param topmostPath The cookie path attribute.
     * @return true if the paths match
     */
    private boolean pathMatch(final String path, final String topmostPath) {
        boolean match = path.startsWith(topmostPath);
        // if there is a match and these values are not exactly the same we have
        // to make sure we're not matcing "/foobar" and "/foo"
        if (match && path.length() != topmostPath.length()) {
            if (!topmostPath.endsWith("/")) {
                match = (path.charAt(topmostPath.length()) == '/');
            }
        }
        return match;
    }

    /**
     * Checks if the cookie has expired
     * @param cookie the cookie to check
     * @return true, if the cookie has an expiration date that has been reached
     */
    private boolean isExpired(Cookie cookie) {
        if (cookie.getMaxAge() < 0)
            return false;

        if (cookie.getMaxAge() == 0)
            return true;

        Date createDate = _creationDates.get(cookie.getName());
        Date expirationDate = new Date(createDate.getTime() + (cookie.getMaxAge() * 1000));

        return expirationDate.getTime() <= System.currentTimeMillis();
    }

    public static class MalformedCookieException extends RuntimeException {
        public MalformedCookieException() {
            super();
        }

        /** 
         * Creates a new MalformedCookieException with a specified message string.
         * 
         * @param message The exception detail message
         */
        public MalformedCookieException(String message) {
            super(message);
        }

        /**
         * Creates a new MalformedCookieException with the specified detail message and cause.
         * 
         * @param message the exception detail message
         * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
         * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
         * 
         */
        public MalformedCookieException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    private final Map<String, Date> _creationDates;
    private final Map<String, Cookie> _cookies;

}