323 lines
11 KiB
JavaScript
323 lines
11 KiB
JavaScript
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Cookie = class Cookie
|
|
{
|
|
constructor(type, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite} = {})
|
|
{
|
|
console.assert(Object.values(WI.Cookie.Type).includes(type));
|
|
console.assert(typeof name === "string");
|
|
console.assert(typeof value === "string");
|
|
console.assert(!header || typeof header === "string");
|
|
console.assert(!expires || expires instanceof Date);
|
|
console.assert(!session || typeof session === "boolean");
|
|
console.assert(!maxAge || typeof maxAge === "number");
|
|
console.assert(!path || typeof path === "string");
|
|
console.assert(!domain || typeof domain === "string");
|
|
console.assert(!secure || typeof secure === "boolean");
|
|
console.assert(!httpOnly || typeof httpOnly === "boolean");
|
|
console.assert(!sameSite || Object.values(WI.Cookie.SameSiteType).includes(sameSite));
|
|
|
|
this._type = type;
|
|
this._name = name;
|
|
this._value = value;
|
|
this._size = this._name.length + this._value.length;
|
|
|
|
if (this._type === WI.Cookie.Type.Response) {
|
|
this._header = header || "";
|
|
this._expires = (!session && expires) || null;
|
|
this._session = session || false;
|
|
this._maxAge = maxAge || null;
|
|
this._path = path || null;
|
|
this._domain = domain || null;
|
|
this._secure = secure || false;
|
|
this._httpOnly = httpOnly || false;
|
|
this._sameSite = sameSite || WI.Cookie.SameSiteType.None;
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
let {name, value, ...options} = payload;
|
|
options.expires = options.expires ? new Date(options.expires.maxDecimals(-3)) : null;
|
|
|
|
return new WI.Cookie(WI.Cookie.Type.Response, name, value, options);
|
|
}
|
|
|
|
// RFC 6265 defines the HTTP Cookie and Set-Cookie header fields:
|
|
// https://www.ietf.org/rfc/rfc6265.txt
|
|
|
|
static parseCookieRequestHeader(header)
|
|
{
|
|
if (!header)
|
|
return [];
|
|
|
|
header = header.trim();
|
|
if (!header)
|
|
return [];
|
|
|
|
let cookies = [];
|
|
|
|
// Cookie: <name> = <value> ( ";" SP <name> = <value> )*?
|
|
// NOTE: Just name/value pairs.
|
|
|
|
let pairs = header.split(/; /);
|
|
for (let pair of pairs) {
|
|
let match = pair.match(/^(?<name>[^\s=]+)[ \t]*=[ \t]*(?<value>.*)$/);
|
|
if (!match) {
|
|
WI.reportInternalError("Failed to parse Cookie pair", {header, pair});
|
|
continue;
|
|
}
|
|
|
|
let {name, value} = match.groups;
|
|
cookies.push(new WI.Cookie(WI.Cookie.Type.Request, name, value));
|
|
}
|
|
|
|
return cookies;
|
|
}
|
|
|
|
static displayNameForSameSiteType(sameSiteType)
|
|
{
|
|
switch (sameSiteType) {
|
|
case WI.Cookie.SameSiteType.None:
|
|
return WI.unlocalizedString("None");
|
|
case WI.Cookie.SameSiteType.Lax:
|
|
return WI.unlocalizedString("Lax");
|
|
case WI.Cookie.SameSiteType.Strict:
|
|
return WI.unlocalizedString("Strict");
|
|
default:
|
|
console.error("Invalid SameSite type", sameSiteType);
|
|
return sameSiteType;
|
|
}
|
|
}
|
|
|
|
// <https://httpwg.org/http-extensions/rfc6265bis.html#the-samesite-attribute-1>
|
|
static parseSameSiteAttributeValue(attributeValue)
|
|
{
|
|
if (!attributeValue)
|
|
return WI.Cookie.SameSiteType.None;
|
|
|
|
switch (attributeValue.toLowerCase()) {
|
|
case "lax":
|
|
return WI.Cookie.SameSiteType.Lax;
|
|
case "strict":
|
|
return WI.Cookie.SameSiteType.Strict;
|
|
}
|
|
|
|
return WI.Cookie.SameSiteType.None;
|
|
}
|
|
|
|
static parseSetCookieResponseHeader(header)
|
|
{
|
|
if (!header)
|
|
return null;
|
|
|
|
// Set-Cookie: <name> = <value> ( ";" SP <attr-maybe-pair> )*?
|
|
// NOTE: Some attributes can have pairs (e.g. "Path=/"), some are only a
|
|
// single word (e.g. "Secure").
|
|
|
|
// Parse name/value.
|
|
let nameValueMatch = header.match(/^(?<name>[^\s=]+)[ \t]*=[ \t]*(?<value>[^;]*)/);
|
|
if (!nameValueMatch) {
|
|
WI.reportInternalError("Failed to parse Set-Cookie header", {header});
|
|
return null;
|
|
}
|
|
|
|
let {name, value} = nameValueMatch.groups;
|
|
let expires = null;
|
|
let session = false;
|
|
let maxAge = null;
|
|
let path = null;
|
|
let domain = null;
|
|
let secure = false;
|
|
let httpOnly = false;
|
|
let sameSite = WI.Cookie.SameSiteType.None;
|
|
|
|
// Parse Attributes
|
|
let remaining = header.substr(nameValueMatch[0].length);
|
|
let attributes = remaining.split(/; ?/);
|
|
for (let attribute of attributes) {
|
|
if (!attribute)
|
|
continue;
|
|
|
|
let match = attribute.match(/^(?<name>[^\s=]+)(?:=(?<value>.*))?$/);
|
|
if (!match) {
|
|
console.error("Failed to parse Set-Cookie attribute:", attribute);
|
|
continue;
|
|
}
|
|
|
|
let attributeName = match.groups.name;
|
|
let attributeValue = match.groups.value;
|
|
switch (attributeName.toLowerCase()) {
|
|
case "expires":
|
|
console.assert(attributeValue);
|
|
expires = new Date(attributeValue);
|
|
if (isNaN(expires.getTime())) {
|
|
console.warn("Invalid Expires date:", attributeValue);
|
|
expires = null;
|
|
}
|
|
break;
|
|
case "max-age":
|
|
console.assert(attributeValue);
|
|
maxAge = parseInt(attributeValue, 10);
|
|
if (isNaN(maxAge) || !/^\d+$/.test(attributeValue)) {
|
|
console.warn("Invalid MaxAge value:", attributeValue);
|
|
maxAge = null;
|
|
}
|
|
break;
|
|
case "path":
|
|
console.assert(attributeValue);
|
|
path = attributeValue;
|
|
break;
|
|
case "domain":
|
|
console.assert(attributeValue);
|
|
domain = attributeValue;
|
|
break;
|
|
case "secure":
|
|
console.assert(!attributeValue);
|
|
secure = true;
|
|
break;
|
|
case "httponly":
|
|
console.assert(!attributeValue);
|
|
httpOnly = true;
|
|
break;
|
|
case "samesite":
|
|
sameSite = WI.Cookie.parseSameSiteAttributeValue(attributeValue);
|
|
break;
|
|
default:
|
|
console.warn("Unknown Cookie attribute:", attribute);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!expires)
|
|
session = true;
|
|
|
|
return new WI.Cookie(WI.Cookie.Type.Response, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite});
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get name() { return this._name; }
|
|
get value() { return this._value; }
|
|
get header() { return this._header; }
|
|
get expires() { return this._expires; }
|
|
get session() { return this._session; }
|
|
get maxAge() { return this._maxAge; }
|
|
get path() { return this._path; }
|
|
get domain() { return this._domain; }
|
|
get secure() { return this._secure; }
|
|
get httpOnly() { return this._httpOnly; }
|
|
get sameSite() { return this._sameSite; }
|
|
get size() { return this._size; }
|
|
|
|
get url()
|
|
{
|
|
let url = this._secure ? "https://" : "http://";
|
|
url += this._domain || "";
|
|
url += this._path || "";
|
|
return url;
|
|
}
|
|
|
|
expirationDate(requestSentDate)
|
|
{
|
|
if (this._session)
|
|
return null;
|
|
|
|
if (this._maxAge) {
|
|
let startDate = requestSentDate || new Date;
|
|
return new Date(startDate.getTime() + (this._maxAge * 1000));
|
|
}
|
|
|
|
return this._expires;
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
return this._type === other.type
|
|
&& this._name === other.name
|
|
&& this._value === other.value
|
|
&& this._header === other.header
|
|
&& this._expires?.getTime() === other.expires?.getTime()
|
|
&& this._session === other.session
|
|
&& this._maxAge === other.maxAge
|
|
&& this._path === other.path
|
|
&& this._domain === other.domain
|
|
&& this._secure === other.secure
|
|
&& this._httpOnly === other.httpOnly
|
|
&& this._sameSite === other.sameSite;
|
|
}
|
|
|
|
toProtocol()
|
|
{
|
|
if (typeof this._name !== "string")
|
|
return null;
|
|
|
|
if (typeof this._value !== "string")
|
|
return null;
|
|
|
|
if (typeof this._domain !== "string")
|
|
return null;
|
|
|
|
if (typeof this._path !== "string")
|
|
return null;
|
|
|
|
if (!this._session && !this._expires)
|
|
return null;
|
|
|
|
if (!Object.values(WI.Cookie.SameSiteType).includes(this._sameSite))
|
|
return null;
|
|
|
|
let json = {
|
|
name: this._name,
|
|
value: this._value,
|
|
domain: this._domain,
|
|
path: this._path,
|
|
expires: this._expires?.getTime(),
|
|
session: this._session,
|
|
httpOnly: !!this._httpOnly,
|
|
secure: !!this._secure,
|
|
sameSite: this._sameSite,
|
|
};
|
|
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.Cookie.Type = {
|
|
Request: "request",
|
|
Response: "response",
|
|
};
|
|
|
|
// Keep these in sync with the "CookieSameSitePolicy" enum defined by the "Page" domain.
|
|
WI.Cookie.SameSiteType = {
|
|
None: "None",
|
|
Lax: "Lax",
|
|
Strict: "Strict",
|
|
};
|