2021-01-10 21:59:07 -05:00
|
|
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
2019-04-27 19:07:11 -04:00
|
|
|
// Structured similarly to Go's cookie.go
|
|
|
|
// https://github.com/golang/go/blob/master/src/net/http/cookie.go
|
2020-06-07 09:20:33 -04:00
|
|
|
import { assert } from "../_util/assert.ts";
|
2019-04-27 19:07:11 -04:00
|
|
|
import { toIMF } from "../datetime/mod.ts";
|
2019-04-24 07:38:52 -04:00
|
|
|
|
2020-06-30 09:08:40 -04:00
|
|
|
export type Cookies = Record<string, string>;
|
2019-04-24 07:38:52 -04:00
|
|
|
|
2019-04-27 19:07:11 -04:00
|
|
|
export interface Cookie {
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Name of the cookie. */
|
2019-04-27 19:07:11 -04:00
|
|
|
name: string;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Value of the cookie. */
|
2019-04-27 19:07:11 -04:00
|
|
|
value: string;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Expiration date of the cookie. */
|
2019-04-27 19:07:11 -04:00
|
|
|
expires?: Date;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Max-Age of the Cookie. Must be integer superior to 0. */
|
2019-04-27 19:07:11 -04:00
|
|
|
maxAge?: number;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Specifies those hosts to which the cookie will be sent. */
|
2019-04-27 19:07:11 -04:00
|
|
|
domain?: string;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Indicates a URL path that must exist in the request. */
|
2019-04-27 19:07:11 -04:00
|
|
|
path?: string;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Indicates if the cookie is made using SSL & HTTPS. */
|
2019-04-27 19:07:11 -04:00
|
|
|
secure?: boolean;
|
2020-05-17 13:24:39 -04:00
|
|
|
/** Indicates that cookie is not accessible via JavaScript. **/
|
2019-04-27 19:07:11 -04:00
|
|
|
httpOnly?: boolean;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Allows servers to assert that a cookie ought not to
|
|
|
|
* be sent along with cross-site requests. */
|
2019-04-27 19:07:11 -04:00
|
|
|
sameSite?: SameSite;
|
2020-04-28 17:26:31 -04:00
|
|
|
/** Additional key value pairs with the form "key=value" */
|
2019-04-27 19:07:11 -04:00
|
|
|
unparsed?: string[];
|
|
|
|
}
|
|
|
|
|
2020-04-10 10:12:42 -04:00
|
|
|
export type SameSite = "Strict" | "Lax" | "None";
|
2019-04-27 19:07:11 -04:00
|
|
|
|
2020-11-17 15:06:06 -05:00
|
|
|
const FIELD_CONTENT_REGEXP = /^(?=[\x20-\x7E]*$)[^()@<>,;:\\"\[\]?={}\s]+$/;
|
|
|
|
|
2019-04-27 19:07:11 -04:00
|
|
|
function toString(cookie: Cookie): string {
|
2020-04-10 10:12:42 -04:00
|
|
|
if (!cookie.name) {
|
|
|
|
return "";
|
|
|
|
}
|
2019-04-27 19:07:11 -04:00
|
|
|
const out: string[] = [];
|
2020-11-17 15:06:06 -05:00
|
|
|
validateCookieName(cookie.name);
|
2020-12-01 08:23:03 -05:00
|
|
|
validateCookieValue(cookie.name, cookie.value);
|
2019-04-27 19:07:11 -04:00
|
|
|
out.push(`${cookie.name}=${cookie.value}`);
|
|
|
|
|
|
|
|
// Fallback for invalid Set-Cookie
|
|
|
|
// ref: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
|
|
|
|
if (cookie.name.startsWith("__Secure")) {
|
|
|
|
cookie.secure = true;
|
|
|
|
}
|
|
|
|
if (cookie.name.startsWith("__Host")) {
|
|
|
|
cookie.path = "/";
|
|
|
|
cookie.secure = true;
|
|
|
|
delete cookie.domain;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cookie.secure) {
|
|
|
|
out.push("Secure");
|
|
|
|
}
|
|
|
|
if (cookie.httpOnly) {
|
|
|
|
out.push("HttpOnly");
|
|
|
|
}
|
2020-02-19 15:36:18 -05:00
|
|
|
if (typeof cookie.maxAge === "number" && Number.isInteger(cookie.maxAge)) {
|
2020-02-07 02:23:38 -05:00
|
|
|
assert(cookie.maxAge > 0, "Max-Age must be an integer superior to 0");
|
2019-04-27 19:07:11 -04:00
|
|
|
out.push(`Max-Age=${cookie.maxAge}`);
|
|
|
|
}
|
|
|
|
if (cookie.domain) {
|
|
|
|
out.push(`Domain=${cookie.domain}`);
|
|
|
|
}
|
|
|
|
if (cookie.sameSite) {
|
|
|
|
out.push(`SameSite=${cookie.sameSite}`);
|
|
|
|
}
|
|
|
|
if (cookie.path) {
|
2020-11-22 09:34:31 -05:00
|
|
|
validatePath(cookie.path);
|
2019-04-27 19:07:11 -04:00
|
|
|
out.push(`Path=${cookie.path}`);
|
|
|
|
}
|
|
|
|
if (cookie.expires) {
|
2019-10-05 12:02:34 -04:00
|
|
|
const dateString = toIMF(cookie.expires);
|
2019-04-27 19:07:11 -04:00
|
|
|
out.push(`Expires=${dateString}`);
|
|
|
|
}
|
|
|
|
if (cookie.unparsed) {
|
|
|
|
out.push(cookie.unparsed.join("; "));
|
|
|
|
}
|
|
|
|
return out.join("; ");
|
|
|
|
}
|
|
|
|
|
2020-11-17 15:06:06 -05:00
|
|
|
/**
|
2020-11-21 10:53:23 -05:00
|
|
|
* Validate Cookie Name.
|
|
|
|
* @param name Cookie name.
|
2020-11-17 15:06:06 -05:00
|
|
|
*/
|
2020-11-21 10:53:23 -05:00
|
|
|
function validateCookieName(name: string | undefined | null): void {
|
|
|
|
if (name && !FIELD_CONTENT_REGEXP.test(name)) {
|
|
|
|
throw new TypeError(`Invalid cookie name: "${name}".`);
|
2020-11-17 15:06:06 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-22 09:34:31 -05:00
|
|
|
/**
|
|
|
|
* Validate Path Value.
|
|
|
|
* @see https://tools.ietf.org/html/rfc6265#section-4.1.2.4
|
|
|
|
* @param path Path value.
|
|
|
|
*/
|
|
|
|
function validatePath(path: string | null): void {
|
|
|
|
if (path == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (let i = 0; i < path.length; i++) {
|
|
|
|
const c = path.charAt(i);
|
|
|
|
if (
|
|
|
|
c < String.fromCharCode(0x20) || c > String.fromCharCode(0x7E) || c == ";"
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
path + ": Invalid cookie path char '" + c + "'",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-01 08:23:03 -05:00
|
|
|
/**
|
|
|
|
*Validate Cookie Value.
|
|
|
|
* @see https://tools.ietf.org/html/rfc6265#section-4.1
|
|
|
|
* @param value Cookie value.
|
|
|
|
*/
|
|
|
|
function validateCookieValue(name: string, value: string | null): void {
|
|
|
|
if (value == null || name == null) return;
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
|
|
const c = value.charAt(i);
|
|
|
|
if (
|
|
|
|
c < String.fromCharCode(0x21) || c == String.fromCharCode(0x22) ||
|
|
|
|
c == String.fromCharCode(0x2c) || c == String.fromCharCode(0x3b) ||
|
|
|
|
c == String.fromCharCode(0x5c) || c == String.fromCharCode(0x7f)
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
"RFC2616 cookie '" + name + "' cannot have '" + c + "' as value",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (c > String.fromCharCode(0x80)) {
|
|
|
|
throw new Error(
|
|
|
|
"RFC2616 cookie '" + name + "' can only have US-ASCII chars as value" +
|
|
|
|
c.charCodeAt(0).toString(16),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-27 19:07:11 -04:00
|
|
|
/**
|
|
|
|
* Parse the cookies of the Server Request
|
2020-06-30 09:08:40 -04:00
|
|
|
* @param req An object which has a `headers` property
|
2019-04-27 19:07:11 -04:00
|
|
|
*/
|
2020-06-30 09:08:40 -04:00
|
|
|
export function getCookies(req: { headers: Headers }): Cookies {
|
2020-02-07 02:23:38 -05:00
|
|
|
const cookie = req.headers.get("Cookie");
|
|
|
|
if (cookie != null) {
|
2019-04-27 19:07:11 -04:00
|
|
|
const out: Cookies = {};
|
2020-02-07 02:23:38 -05:00
|
|
|
const c = cookie.split(";");
|
2019-04-24 07:38:52 -04:00
|
|
|
for (const kv of c) {
|
2020-02-07 02:23:38 -05:00
|
|
|
const [cookieKey, ...cookieVal] = kv.split("=");
|
|
|
|
assert(cookieKey != null);
|
|
|
|
const key = cookieKey.trim();
|
2019-04-29 10:49:50 -04:00
|
|
|
out[key] = cookieVal.join("=");
|
2019-04-24 07:38:52 -04:00
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
2019-04-27 19:07:11 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the cookie header properly in the Response
|
2020-06-30 09:08:40 -04:00
|
|
|
* @param res An object which has a headers property
|
2019-04-27 19:07:11 -04:00
|
|
|
* @param cookie Cookie to set
|
2020-06-30 09:08:40 -04:00
|
|
|
*
|
2019-04-27 19:07:11 -04:00
|
|
|
* Example:
|
|
|
|
*
|
2020-06-30 09:08:40 -04:00
|
|
|
* ```ts
|
|
|
|
* setCookie(response, { name: 'deno', value: 'runtime',
|
|
|
|
* httpOnly: true, secure: true, maxAge: 2, domain: "deno.land" });
|
|
|
|
* ```
|
2019-04-27 19:07:11 -04:00
|
|
|
*/
|
2020-06-30 09:08:40 -04:00
|
|
|
export function setCookie(res: { headers?: Headers }, cookie: Cookie): void {
|
2019-04-27 19:07:11 -04:00
|
|
|
if (!res.headers) {
|
|
|
|
res.headers = new Headers();
|
|
|
|
}
|
2021-01-16 18:32:59 -05:00
|
|
|
// TODO(zekth) : Add proper parsing of Set-Cookie headers
|
2019-04-27 19:07:11 -04:00
|
|
|
// Parsing cookie headers to make consistent set-cookie header
|
|
|
|
// ref: https://tools.ietf.org/html/rfc6265#section-4.1.1
|
2020-04-10 10:12:42 -04:00
|
|
|
const v = toString(cookie);
|
|
|
|
if (v) {
|
|
|
|
res.headers.append("Set-Cookie", v);
|
|
|
|
}
|
2019-04-27 19:07:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the cookie header properly in the Response to delete it
|
|
|
|
* @param res Server Response
|
|
|
|
* @param name Name of the cookie to Delete
|
|
|
|
* Example:
|
|
|
|
*
|
2020-06-07 07:53:36 -04:00
|
|
|
* deleteCookie(res,'foo');
|
2019-04-27 19:07:11 -04:00
|
|
|
*/
|
2020-06-30 09:08:40 -04:00
|
|
|
export function deleteCookie(res: { headers?: Headers }, name: string): void {
|
2019-04-27 19:07:11 -04:00
|
|
|
setCookie(res, {
|
|
|
|
name: name,
|
|
|
|
value: "",
|
2020-03-28 13:03:49 -04:00
|
|
|
expires: new Date(0),
|
2019-04-27 19:07:11 -04:00
|
|
|
});
|
|
|
|
}
|