001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.cookie;
029
030import java.util.Locale;
031
032import org.apache.http.annotation.Contract;
033import org.apache.http.annotation.ThreadingBehavior;
034import org.apache.http.cookie.ClientCookie;
035import org.apache.http.cookie.CommonCookieAttributeHandler;
036import org.apache.http.cookie.Cookie;
037import org.apache.http.cookie.CookieOrigin;
038import org.apache.http.cookie.CookieRestrictionViolationException;
039import org.apache.http.cookie.MalformedCookieException;
040import org.apache.http.cookie.SetCookie;
041import org.apache.http.util.Args;
042
043/**
044 * {@code "Domain"} cookie attribute handler for RFC 2965 cookie spec.
045 *
046 *
047 * @since 3.1
048 */
049@Contract(threading = ThreadingBehavior.IMMUTABLE)
050public class RFC2965DomainAttributeHandler implements CommonCookieAttributeHandler {
051
052    public RFC2965DomainAttributeHandler() {
053        super();
054    }
055
056    /**
057     * Parse cookie domain attribute.
058     */
059    @Override
060    public void parse(
061            final SetCookie cookie, final String domain) throws MalformedCookieException {
062        Args.notNull(cookie, "Cookie");
063        if (domain == null) {
064            throw new MalformedCookieException(
065                    "Missing value for domain attribute");
066        }
067        if (domain.trim().isEmpty()) {
068            throw new MalformedCookieException(
069                    "Blank value for domain attribute");
070        }
071        String s = domain;
072        s = s.toLowerCase(Locale.ROOT);
073        if (!domain.startsWith(".")) {
074            // Per RFC 2965 section 3.2.2
075            // "... If an explicitly specified value does not start with
076            // a dot, the user agent supplies a leading dot ..."
077            // That effectively implies that the domain attribute
078            // MAY NOT be an IP address of a host name
079            s = '.' + s;
080        }
081        cookie.setDomain(s);
082    }
083
084    /**
085     * Performs domain-match as defined by the RFC2965.
086     * <p>
087     * Host A's name domain-matches host B's if
088     * </p>
089     * <ol>
090     *   <li>their host name strings string-compare equal; or</li>
091     *   <li>A is a HDN string and has the form NB, where N is a non-empty
092     *       name string, B has the form .B', and B' is a HDN string.  (So,
093     *       x.y.com domain-matches .Y.com but not Y.com.)</li>
094     * </ol>
095     *
096     * @param host host name where cookie is received from or being sent to.
097     * @param domain The cookie domain attribute.
098     * @return true if the specified host matches the given domain.
099     */
100    public boolean domainMatch(final String host, final String domain) {
101        final boolean match = host.equals(domain)
102                        || (domain.startsWith(".") && host.endsWith(domain));
103
104        return match;
105    }
106
107    /**
108     * Validate cookie domain attribute.
109     */
110    @Override
111    public void validate(final Cookie cookie, final CookieOrigin origin)
112            throws MalformedCookieException {
113        Args.notNull(cookie, "Cookie");
114        Args.notNull(origin, "Cookie origin");
115        final String host = origin.getHost().toLowerCase(Locale.ROOT);
116        if (cookie.getDomain() == null) {
117            throw new CookieRestrictionViolationException("Invalid cookie state: " +
118                                               "domain not specified");
119        }
120        final String cookieDomain = cookie.getDomain().toLowerCase(Locale.ROOT);
121
122        if (cookie instanceof ClientCookie
123                && ((ClientCookie) cookie).containsAttribute(ClientCookie.DOMAIN_ATTR)) {
124            // Domain attribute must start with a dot
125            if (!cookieDomain.startsWith(".")) {
126                throw new CookieRestrictionViolationException("Domain attribute \"" +
127                    cookie.getDomain() + "\" violates RFC 2109: domain must start with a dot");
128            }
129
130            // Domain attribute must contain at least one embedded dot,
131            // or the value must be equal to .local.
132            final int dotIndex = cookieDomain.indexOf('.', 1);
133            if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
134                && (!cookieDomain.equals(".local"))) {
135                throw new CookieRestrictionViolationException(
136                        "Domain attribute \"" + cookie.getDomain()
137                        + "\" violates RFC 2965: the value contains no embedded dots "
138                        + "and the value is not .local");
139            }
140
141            // The effective host name must domain-match domain attribute.
142            if (!domainMatch(host, cookieDomain)) {
143                throw new CookieRestrictionViolationException(
144                        "Domain attribute \"" + cookie.getDomain()
145                        + "\" violates RFC 2965: effective host name does not "
146                        + "domain-match domain attribute.");
147            }
148
149            // effective host name minus domain must not contain any dots
150            final String effectiveHostWithoutDomain = host.substring(
151                    0, host.length() - cookieDomain.length());
152            if (effectiveHostWithoutDomain.indexOf('.') != -1) {
153                throw new CookieRestrictionViolationException("Domain attribute \""
154                                                   + cookie.getDomain() + "\" violates RFC 2965: "
155                                                   + "effective host minus domain may not contain any dots");
156            }
157        } else {
158            // Domain was not specified in header. In this case, domain must
159            // string match request host (case-insensitive).
160            if (!cookie.getDomain().equals(host)) {
161                throw new CookieRestrictionViolationException("Illegal domain attribute: \""
162                                                   + cookie.getDomain() + "\"."
163                                                   + "Domain of origin: \""
164                                                   + host + "\"");
165            }
166        }
167    }
168
169    /**
170     * Match cookie domain attribute.
171     */
172    @Override
173    public boolean match(final Cookie cookie, final CookieOrigin origin) {
174        Args.notNull(cookie, "Cookie");
175        Args.notNull(origin, "Cookie origin");
176        final String host = origin.getHost().toLowerCase(Locale.ROOT);
177        final String cookieDomain = cookie.getDomain();
178
179        // The effective host name MUST domain-match the Domain
180        // attribute of the cookie.
181        if (!domainMatch(host, cookieDomain)) {
182            return false;
183        }
184        // effective host name minus domain must not contain any dots
185        final String effectiveHostWithoutDomain = host.substring(
186                0, host.length() - cookieDomain.length());
187        return effectiveHostWithoutDomain.indexOf('.') == -1;
188    }
189
190    @Override
191    public String getAttributeName() {
192        return ClientCookie.DOMAIN_ATTR;
193    }
194
195}