michael@0: /*
michael@0: * ====================================================================
michael@0: * Licensed to the Apache Software Foundation (ASF) under one
michael@0: * or more contributor license agreements. See the NOTICE file
michael@0: * distributed with this work for additional information
michael@0: * regarding copyright ownership. The ASF licenses this file
michael@0: * to you under the Apache License, Version 2.0 (the
michael@0: * "License"); you may not use this file except in compliance
michael@0: * with the License. You may obtain a copy of the License at
michael@0: *
michael@0: * http://www.apache.org/licenses/LICENSE-2.0
michael@0: *
michael@0: * Unless required by applicable law or agreed to in writing,
michael@0: * software distributed under the License is distributed on an
michael@0: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
michael@0: * KIND, either express or implied. See the License for the
michael@0: * specific language governing permissions and limitations
michael@0: * under the License.
michael@0: * ====================================================================
michael@0: *
michael@0: * This software consists of voluntary contributions made by many
michael@0: * individuals on behalf of the Apache Software Foundation. For more
michael@0: * information on the Apache Software Foundation, please see
michael@0: * .
michael@0: *
michael@0: */
michael@0:
michael@0: package ch.boye.httpclientandroidlib.impl.cookie;
michael@0:
michael@0: import java.util.Locale;
michael@0:
michael@0: import ch.boye.httpclientandroidlib.annotation.Immutable;
michael@0:
michael@0: import ch.boye.httpclientandroidlib.cookie.ClientCookie;
michael@0: import ch.boye.httpclientandroidlib.cookie.Cookie;
michael@0: import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
michael@0: import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
michael@0: import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
michael@0: import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
michael@0: import ch.boye.httpclientandroidlib.cookie.SetCookie;
michael@0:
michael@0: /**
michael@0: * "Domain" cookie attribute handler for RFC 2965 cookie spec.
michael@0: *
michael@0: *
michael@0: * @since 3.1
michael@0: */
michael@0: @Immutable
michael@0: public class RFC2965DomainAttributeHandler implements CookieAttributeHandler {
michael@0:
michael@0: public RFC2965DomainAttributeHandler() {
michael@0: super();
michael@0: }
michael@0:
michael@0: /**
michael@0: * Parse cookie domain attribute.
michael@0: */
michael@0: public void parse(final SetCookie cookie, String domain)
michael@0: throws MalformedCookieException {
michael@0: if (cookie == null) {
michael@0: throw new IllegalArgumentException("Cookie may not be null");
michael@0: }
michael@0: if (domain == null) {
michael@0: throw new MalformedCookieException(
michael@0: "Missing value for domain attribute");
michael@0: }
michael@0: if (domain.trim().length() == 0) {
michael@0: throw new MalformedCookieException(
michael@0: "Blank value for domain attribute");
michael@0: }
michael@0: domain = domain.toLowerCase(Locale.ENGLISH);
michael@0: if (!domain.startsWith(".")) {
michael@0: // Per RFC 2965 section 3.2.2
michael@0: // "... If an explicitly specified value does not start with
michael@0: // a dot, the user agent supplies a leading dot ..."
michael@0: // That effectively implies that the domain attribute
michael@0: // MAY NOT be an IP address of a host name
michael@0: domain = '.' + domain;
michael@0: }
michael@0: cookie.setDomain(domain);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Performs domain-match as defined by the RFC2965.
michael@0: *
michael@0: * Host A's name domain-matches host B's if
michael@0: *
michael@0: * their host name strings string-compare equal; or
michael@0: * A is a HDN string and has the form NB, where N is a non-empty
michael@0: * name string, B has the form .B', and B' is a HDN string. (So,
michael@0: * x.y.com domain-matches .Y.com but not Y.com.)
michael@0: *
michael@0: *
michael@0: * @param host host name where cookie is received from or being sent to.
michael@0: * @param domain The cookie domain attribute.
michael@0: * @return true if the specified host matches the given domain.
michael@0: */
michael@0: public boolean domainMatch(String host, String domain) {
michael@0: boolean match = host.equals(domain)
michael@0: || (domain.startsWith(".") && host.endsWith(domain));
michael@0:
michael@0: return match;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Validate cookie domain attribute.
michael@0: */
michael@0: public void validate(final Cookie cookie, final CookieOrigin origin)
michael@0: throws MalformedCookieException {
michael@0: if (cookie == null) {
michael@0: throw new IllegalArgumentException("Cookie may not be null");
michael@0: }
michael@0: if (origin == null) {
michael@0: throw new IllegalArgumentException("Cookie origin may not be null");
michael@0: }
michael@0: String host = origin.getHost().toLowerCase(Locale.ENGLISH);
michael@0: if (cookie.getDomain() == null) {
michael@0: throw new CookieRestrictionViolationException("Invalid cookie state: " +
michael@0: "domain not specified");
michael@0: }
michael@0: String cookieDomain = cookie.getDomain().toLowerCase(Locale.ENGLISH);
michael@0:
michael@0: if (cookie instanceof ClientCookie
michael@0: && ((ClientCookie) cookie).containsAttribute(ClientCookie.DOMAIN_ATTR)) {
michael@0: // Domain attribute must start with a dot
michael@0: if (!cookieDomain.startsWith(".")) {
michael@0: throw new CookieRestrictionViolationException("Domain attribute \"" +
michael@0: cookie.getDomain() + "\" violates RFC 2109: domain must start with a dot");
michael@0: }
michael@0:
michael@0: // Domain attribute must contain at least one embedded dot,
michael@0: // or the value must be equal to .local.
michael@0: int dotIndex = cookieDomain.indexOf('.', 1);
michael@0: if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
michael@0: && (!cookieDomain.equals(".local"))) {
michael@0: throw new CookieRestrictionViolationException(
michael@0: "Domain attribute \"" + cookie.getDomain()
michael@0: + "\" violates RFC 2965: the value contains no embedded dots "
michael@0: + "and the value is not .local");
michael@0: }
michael@0:
michael@0: // The effective host name must domain-match domain attribute.
michael@0: if (!domainMatch(host, cookieDomain)) {
michael@0: throw new CookieRestrictionViolationException(
michael@0: "Domain attribute \"" + cookie.getDomain()
michael@0: + "\" violates RFC 2965: effective host name does not "
michael@0: + "domain-match domain attribute.");
michael@0: }
michael@0:
michael@0: // effective host name minus domain must not contain any dots
michael@0: String effectiveHostWithoutDomain = host.substring(
michael@0: 0, host.length() - cookieDomain.length());
michael@0: if (effectiveHostWithoutDomain.indexOf('.') != -1) {
michael@0: throw new CookieRestrictionViolationException("Domain attribute \""
michael@0: + cookie.getDomain() + "\" violates RFC 2965: "
michael@0: + "effective host minus domain may not contain any dots");
michael@0: }
michael@0: } else {
michael@0: // Domain was not specified in header. In this case, domain must
michael@0: // string match request host (case-insensitive).
michael@0: if (!cookie.getDomain().equals(host)) {
michael@0: throw new CookieRestrictionViolationException("Illegal domain attribute: \""
michael@0: + cookie.getDomain() + "\"."
michael@0: + "Domain of origin: \""
michael@0: + host + "\"");
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Match cookie domain attribute.
michael@0: */
michael@0: public boolean match(final Cookie cookie, final CookieOrigin origin) {
michael@0: if (cookie == null) {
michael@0: throw new IllegalArgumentException("Cookie may not be null");
michael@0: }
michael@0: if (origin == null) {
michael@0: throw new IllegalArgumentException("Cookie origin may not be null");
michael@0: }
michael@0: String host = origin.getHost().toLowerCase(Locale.ENGLISH);
michael@0: String cookieDomain = cookie.getDomain();
michael@0:
michael@0: // The effective host name MUST domain-match the Domain
michael@0: // attribute of the cookie.
michael@0: if (!domainMatch(host, cookieDomain)) {
michael@0: return false;
michael@0: }
michael@0: // effective host name minus domain must not contain any dots
michael@0: String effectiveHostWithoutDomain = host.substring(
michael@0: 0, host.length() - cookieDomain.length());
michael@0: return effectiveHostWithoutDomain.indexOf('.') == -1;
michael@0: }
michael@0:
michael@0: }