All Downloads are FREE. Search and download functionalities are using the official Maven repository.

okhttp3.Cookie Maven / Gradle / Ivy

/*
 * Copyright (C) 2015 Square, 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.
 */
package okhttp3;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import okhttp3.internal.Util;
import okhttp3.internal.http.HttpDate;
import okhttp3.internal.publicsuffix.PublicSuffixDatabase;

import static okhttp3.internal.Util.UTC;
import static okhttp3.internal.Util.canonicalizeHost;
import static okhttp3.internal.Util.delimiterOffset;
import static okhttp3.internal.Util.indexOfControlOrNonAscii;
import static okhttp3.internal.Util.trimSubstring;
import static okhttp3.internal.Util.verifyAsIpAddress;

/**
 * An RFC 6265 Cookie.
 *
 * 

This class doesn't support additional attributes on cookies, like Chromium's Priority=HIGH * extension. */ public final class Cookie { private static final Pattern YEAR_PATTERN = Pattern.compile("(\\d{2,4})[^\\d]*"); private static final Pattern MONTH_PATTERN = Pattern.compile("(?i)(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*"); private static final Pattern DAY_OF_MONTH_PATTERN = Pattern.compile("(\\d{1,2})[^\\d]*"); private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})[^\\d]*"); private final String name; private final String value; private final long expiresAt; private final String domain; private final String path; private final boolean secure; private final boolean httpOnly; private final boolean persistent; // True if 'expires' or 'max-age' is present. private final boolean hostOnly; // True unless 'domain' is present. private Cookie(String name, String value, long expiresAt, String domain, String path, boolean secure, boolean httpOnly, boolean hostOnly, boolean persistent) { this.name = name; this.value = value; this.expiresAt = expiresAt; this.domain = domain; this.path = path; this.secure = secure; this.httpOnly = httpOnly; this.hostOnly = hostOnly; this.persistent = persistent; } Cookie(Builder builder) { if (builder.name == null) throw new NullPointerException("builder.name == null"); if (builder.value == null) throw new NullPointerException("builder.value == null"); if (builder.domain == null) throw new NullPointerException("builder.domain == null"); this.name = builder.name; this.value = builder.value; this.expiresAt = builder.expiresAt; this.domain = builder.domain; this.path = builder.path; this.secure = builder.secure; this.httpOnly = builder.httpOnly; this.persistent = builder.persistent; this.hostOnly = builder.hostOnly; } /** Returns a non-empty string with this cookie's name. */ public String name() { return name; } /** Returns a possibly-empty string with this cookie's value. */ public String value() { return value; } /** Returns true if this cookie does not expire at the end of the current session. */ public boolean persistent() { return persistent; } /** * Returns the time that this cookie expires, in the same format as {@link * System#currentTimeMillis()}. This is December 31, 9999 if the cookie is {@linkplain * #persistent() not persistent}, in which case it will expire at the end of the current session. * *

This may return a value less than the current time, in which case the cookie is already * expired. Webservers may return expired cookies as a mechanism to delete previously set cookies * that may or may not themselves be expired. */ public long expiresAt() { return expiresAt; } /** * Returns true if this cookie's domain should be interpreted as a single host name, or false if * it should be interpreted as a pattern. This flag will be false if its {@code Set-Cookie} header * included a {@code domain} attribute. * *

For example, suppose the cookie's domain is {@code example.com}. If this flag is true it * matches only {@code example.com}. If this flag is false it matches {@code * example.com} and all subdomains including {@code api.example.com}, {@code www.example.com}, and * {@code beta.api.example.com}. */ public boolean hostOnly() { return hostOnly; } /** * Returns the cookie's domain. If {@link #hostOnly()} returns true this is the only domain that * matches this cookie; otherwise it matches this domain and all subdomains. */ public String domain() { return domain; } /** * Returns this cookie's path. This cookie matches URLs prefixed with path segments that match * this path's segments. For example, if this path is {@code /foo} this cookie matches requests to * {@code /foo} and {@code /foo/bar}, but not {@code /} or {@code /football}. */ public String path() { return path; } /** * Returns true if this cookie should be limited to only HTTP APIs. In web browsers this prevents * the cookie from being accessible to scripts. */ public boolean httpOnly() { return httpOnly; } /** Returns true if this cookie should be limited to only HTTPS requests. */ public boolean secure() { return secure; } /** * Returns true if this cookie should be included on a request to {@code url}. In addition to this * check callers should also confirm that this cookie has not expired. */ public boolean matches(HttpUrl url) { boolean domainMatch = hostOnly ? url.host().equals(domain) : domainMatch(url.host(), domain); if (!domainMatch) return false; if (!pathMatch(url, path)) return false; if (secure && !url.isHttps()) return false; return true; } private static boolean domainMatch(String urlHost, String domain) { if (urlHost.equals(domain)) { return true; // As in 'example.com' matching 'example.com'. } if (urlHost.endsWith(domain) && urlHost.charAt(urlHost.length() - domain.length() - 1) == '.' && !verifyAsIpAddress(urlHost)) { return true; // As in 'example.com' matching 'www.example.com'. } return false; } private static boolean pathMatch(HttpUrl url, String path) { String urlPath = url.encodedPath(); if (urlPath.equals(path)) { return true; // As in '/foo' matching '/foo'. } if (urlPath.startsWith(path)) { if (path.endsWith("/")) return true; // As in '/' matching '/foo'. if (urlPath.charAt(path.length()) == '/') return true; // As in '/foo' matching '/foo/bar'. } return false; } /** * Attempt to parse a {@code Set-Cookie} HTTP header value {@code setCookie} as a cookie. Returns * null if {@code setCookie} is not a well-formed cookie. */ public static @Nullable Cookie parse(HttpUrl url, String setCookie) { return parse(System.currentTimeMillis(), url, setCookie); } static @Nullable Cookie parse(long currentTimeMillis, HttpUrl url, String setCookie) { int pos = 0; int limit = setCookie.length(); int cookiePairEnd = delimiterOffset(setCookie, pos, limit, ';'); int pairEqualsSign = delimiterOffset(setCookie, pos, cookiePairEnd, '='); if (pairEqualsSign == cookiePairEnd) return null; String cookieName = trimSubstring(setCookie, pos, pairEqualsSign); if (cookieName.isEmpty() || indexOfControlOrNonAscii(cookieName) != -1) return null; String cookieValue = trimSubstring(setCookie, pairEqualsSign + 1, cookiePairEnd); if (indexOfControlOrNonAscii(cookieValue) != -1) return null; long expiresAt = HttpDate.MAX_DATE; long deltaSeconds = -1L; String domain = null; String path = null; boolean secureOnly = false; boolean httpOnly = false; boolean hostOnly = true; boolean persistent = false; pos = cookiePairEnd + 1; while (pos < limit) { int attributePairEnd = delimiterOffset(setCookie, pos, limit, ';'); int attributeEqualsSign = delimiterOffset(setCookie, pos, attributePairEnd, '='); String attributeName = trimSubstring(setCookie, pos, attributeEqualsSign); String attributeValue = attributeEqualsSign < attributePairEnd ? trimSubstring(setCookie, attributeEqualsSign + 1, attributePairEnd) : ""; if (attributeName.equalsIgnoreCase("expires")) { try { expiresAt = parseExpires(attributeValue, 0, attributeValue.length()); persistent = true; } catch (IllegalArgumentException e) { // Ignore this attribute, it isn't recognizable as a date. } } else if (attributeName.equalsIgnoreCase("max-age")) { try { deltaSeconds = parseMaxAge(attributeValue); persistent = true; } catch (NumberFormatException e) { // Ignore this attribute, it isn't recognizable as a max age. } } else if (attributeName.equalsIgnoreCase("domain")) { try { domain = parseDomain(attributeValue); hostOnly = false; } catch (IllegalArgumentException e) { // Ignore this attribute, it isn't recognizable as a domain. } } else if (attributeName.equalsIgnoreCase("path")) { path = attributeValue; } else if (attributeName.equalsIgnoreCase("secure")) { secureOnly = true; } else if (attributeName.equalsIgnoreCase("httponly")) { httpOnly = true; } pos = attributePairEnd + 1; } // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two // attributes are declared in the cookie string. if (deltaSeconds == Long.MIN_VALUE) { expiresAt = Long.MIN_VALUE; } else if (deltaSeconds != -1L) { long deltaMilliseconds = deltaSeconds <= (Long.MAX_VALUE / 1000) ? deltaSeconds * 1000 : Long.MAX_VALUE; expiresAt = currentTimeMillis + deltaMilliseconds; if (expiresAt < currentTimeMillis || expiresAt > HttpDate.MAX_DATE) { expiresAt = HttpDate.MAX_DATE; // Handle overflow & limit the date range. } } // If the domain is present, it must domain match. Otherwise we have a host-only cookie. String urlHost = url.host(); if (domain == null) { domain = urlHost; } else if (!domainMatch(urlHost, domain)) { return null; // No domain match? This is either incompetence or malice! } // If the domain is a suffix of the url host, it must not be a public suffix. if (urlHost.length() != domain.length() && PublicSuffixDatabase.get().getEffectiveTldPlusOne(domain) == null) { return null; } // If the path is absent or didn't start with '/', use the default path. It's a string like // '/foo/bar' for a URL like 'http://example.com/foo/bar/baz'. It always starts with '/'. if (path == null || !path.startsWith("/")) { String encodedPath = url.encodedPath(); int lastSlash = encodedPath.lastIndexOf('/'); path = lastSlash != 0 ? encodedPath.substring(0, lastSlash) : "/"; } return new Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly, hostOnly, persistent); } /** Parse a date as specified in RFC 6265, section 5.1.1. */ private static long parseExpires(String s, int pos, int limit) { pos = dateCharacterOffset(s, pos, limit, false); int hour = -1; int minute = -1; int second = -1; int dayOfMonth = -1; int month = -1; int year = -1; Matcher matcher = TIME_PATTERN.matcher(s); while (pos < limit) { int end = dateCharacterOffset(s, pos + 1, limit, true); matcher.region(pos, end); if (hour == -1 && matcher.usePattern(TIME_PATTERN).matches()) { hour = Integer.parseInt(matcher.group(1)); minute = Integer.parseInt(matcher.group(2)); second = Integer.parseInt(matcher.group(3)); } else if (dayOfMonth == -1 && matcher.usePattern(DAY_OF_MONTH_PATTERN).matches()) { dayOfMonth = Integer.parseInt(matcher.group(1)); } else if (month == -1 && matcher.usePattern(MONTH_PATTERN).matches()) { String monthString = matcher.group(1).toLowerCase(Locale.US); month = MONTH_PATTERN.pattern().indexOf(monthString) / 4; // Sneaky! jan=1, dec=12. } else if (year == -1 && matcher.usePattern(YEAR_PATTERN).matches()) { year = Integer.parseInt(matcher.group(1)); } pos = dateCharacterOffset(s, end + 1, limit, false); } // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015. if (year >= 70 && year <= 99) year += 1900; if (year >= 0 && year <= 69) year += 2000; // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap // seconds are not supported by this syntax. if (year < 1601) throw new IllegalArgumentException(); if (month == -1) throw new IllegalArgumentException(); if (dayOfMonth < 1 || dayOfMonth > 31) throw new IllegalArgumentException(); if (hour < 0 || hour > 23) throw new IllegalArgumentException(); if (minute < 0 || minute > 59) throw new IllegalArgumentException(); if (second < 0 || second > 59) throw new IllegalArgumentException(); Calendar calendar = new GregorianCalendar(UTC); calendar.setLenient(false); calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month - 1); calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTimeInMillis(); } /** * Returns the index of the next date character in {@code input}, or if {@code invert} the index * of the next non-date character in {@code input}. */ private static int dateCharacterOffset(String input, int pos, int limit, boolean invert) { for (int i = pos; i < limit; i++) { int c = input.charAt(i); boolean dateCharacter = (c < ' ' && c != '\t') || (c >= '\u007f') || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c == ':'); if (dateCharacter == !invert) return i; } return limit; } /** * Returns the positive value if {@code attributeValue} is positive, or {@link Long#MIN_VALUE} if * it is either 0 or negative. If the value is positive but out of range, this returns {@link * Long#MAX_VALUE}. * * @throws NumberFormatException if {@code s} is not an integer of any precision. */ private static long parseMaxAge(String s) { try { long parsed = Long.parseLong(s); return parsed <= 0L ? Long.MIN_VALUE : parsed; } catch (NumberFormatException e) { // Check if the value is an integer (positive or negative) that's too big for a long. if (s.matches("-?\\d+")) { return s.startsWith("-") ? Long.MIN_VALUE : Long.MAX_VALUE; } throw e; } } /** * Returns a domain string like {@code example.com} for an input domain like {@code EXAMPLE.COM} * or {@code .example.com}. */ private static String parseDomain(String s) { if (s.endsWith(".")) { throw new IllegalArgumentException(); } if (s.startsWith(".")) { s = s.substring(1); } String canonicalDomain = canonicalizeHost(s); if (canonicalDomain == null) { throw new IllegalArgumentException(); } return canonicalDomain; } /** Returns all of the cookies from a set of HTTP response headers. */ public static List parseAll(HttpUrl url, Headers headers) { List cookieStrings = headers.values("Set-Cookie"); List cookies = null; for (int i = 0, size = cookieStrings.size(); i < size; i++) { Cookie cookie = Cookie.parse(url, cookieStrings.get(i)); if (cookie == null) continue; if (cookies == null) cookies = new ArrayList<>(); cookies.add(cookie); } return cookies != null ? Collections.unmodifiableList(cookies) : Collections.emptyList(); } /** * Builds a cookie. The {@linkplain #name() name}, {@linkplain #value() value}, and {@linkplain * #domain() domain} values must all be set before calling {@link #build}. */ public static final class Builder { @Nullable String name; @Nullable String value; long expiresAt = HttpDate.MAX_DATE; @Nullable String domain; String path = "/"; boolean secure; boolean httpOnly; boolean persistent; boolean hostOnly; public Builder name(String name) { if (name == null) throw new NullPointerException("name == null"); if (!name.trim().equals(name)) throw new IllegalArgumentException("name is not trimmed"); this.name = name; return this; } public Builder value(String value) { if (value == null) throw new NullPointerException("value == null"); if (!value.trim().equals(value)) throw new IllegalArgumentException("value is not trimmed"); this.value = value; return this; } public Builder expiresAt(long expiresAt) { if (expiresAt <= 0) expiresAt = Long.MIN_VALUE; if (expiresAt > HttpDate.MAX_DATE) expiresAt = HttpDate.MAX_DATE; this.expiresAt = expiresAt; this.persistent = true; return this; } /** * Set the domain pattern for this cookie. The cookie will match {@code domain} and all of its * subdomains. */ public Builder domain(String domain) { return domain(domain, false); } /** * Set the host-only domain for this cookie. The cookie will match {@code domain} but none of * its subdomains. */ public Builder hostOnlyDomain(String domain) { return domain(domain, true); } private Builder domain(String domain, boolean hostOnly) { if (domain == null) throw new NullPointerException("domain == null"); String canonicalDomain = Util.canonicalizeHost(domain); if (canonicalDomain == null) { throw new IllegalArgumentException("unexpected domain: " + domain); } this.domain = canonicalDomain; this.hostOnly = hostOnly; return this; } public Builder path(String path) { if (!path.startsWith("/")) throw new IllegalArgumentException("path must start with '/'"); this.path = path; return this; } public Builder secure() { this.secure = true; return this; } public Builder httpOnly() { this.httpOnly = true; return this; } public Cookie build() { return new Cookie(this); } } @Override public String toString() { return toString(false); } /** * @param forObsoleteRfc2965 true to include a leading {@code .} on the domain pattern. This is * necessary for {@code example.com} to match {@code www.example.com} under RFC 2965. This * extra dot is ignored by more recent specifications. */ String toString(boolean forObsoleteRfc2965) { StringBuilder result = new StringBuilder(); result.append(name); result.append('='); result.append(value); if (persistent) { if (expiresAt == Long.MIN_VALUE) { result.append("; max-age=0"); } else { result.append("; expires=").append(HttpDate.format(new Date(expiresAt))); } } if (!hostOnly) { result.append("; domain="); if (forObsoleteRfc2965) { result.append("."); } result.append(domain); } result.append("; path=").append(path); if (secure) { result.append("; secure"); } if (httpOnly) { result.append("; httponly"); } return result.toString(); } @Override public boolean equals(@Nullable Object other) { if (!(other instanceof Cookie)) return false; Cookie that = (Cookie) other; return that.name.equals(name) && that.value.equals(value) && that.domain.equals(domain) && that.path.equals(path) && that.expiresAt == expiresAt && that.secure == secure && that.httpOnly == httpOnly && that.persistent == persistent && that.hostOnly == hostOnly; } @Override public int hashCode() { int hash = 17; hash = 31 * hash + name.hashCode(); hash = 31 * hash + value.hashCode(); hash = 31 * hash + domain.hashCode(); hash = 31 * hash + path.hashCode(); hash = 31 * hash + (int) (expiresAt ^ (expiresAt >>> 32)); hash = 31 * hash + (secure ? 0 : 1); hash = 31 * hash + (httpOnly ? 0 : 1); hash = 31 * hash + (persistent ? 0 : 1); hash = 31 * hash + (hostOnly ? 0 : 1); return hash; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy