deno/std/http/cookie.ts

205 lines
5.6 KiB
TypeScript

// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// Structured similarly to Go's cookie.go
// https://github.com/golang/go/blob/master/src/net/http/cookie.go
import { assert } from "../_util/assert.ts";
import { toIMF } from "../datetime/mod.ts";
export type Cookies = Record<string, string>;
export interface Cookie {
/** Name of the cookie. */
name: string;
/** Value of the cookie. */
value: string;
/** Expiration date of the cookie. */
expires?: Date;
/** Max-Age of the Cookie. Must be integer superior to 0. */
maxAge?: number;
/** Specifies those hosts to which the cookie will be sent. */
domain?: string;
/** Indicates a URL path that must exist in the request. */
path?: string;
/** Indicates if the cookie is made using SSL & HTTPS. */
secure?: boolean;
/** Indicates that cookie is not accessible via JavaScript. **/
httpOnly?: boolean;
/** Allows servers to assert that a cookie ought not to
* be sent along with cross-site requests. */
sameSite?: SameSite;
/** Additional key value pairs with the form "key=value" */
unparsed?: string[];
}
export type SameSite = "Strict" | "Lax" | "None";
const FIELD_CONTENT_REGEXP = /^(?=[\x20-\x7E]*$)[^()@<>,;:\\"\[\]?={}\s]+$/;
function toString(cookie: Cookie): string {
if (!cookie.name) {
return "";
}
const out: string[] = [];
validateCookieName(cookie.name);
validateCookieValue(cookie.name, cookie.value);
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");
}
if (typeof cookie.maxAge === "number" && Number.isInteger(cookie.maxAge)) {
assert(cookie.maxAge > 0, "Max-Age must be an integer superior to 0");
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) {
validatePath(cookie.path);
out.push(`Path=${cookie.path}`);
}
if (cookie.expires) {
const dateString = toIMF(cookie.expires);
out.push(`Expires=${dateString}`);
}
if (cookie.unparsed) {
out.push(cookie.unparsed.join("; "));
}
return out.join("; ");
}
/**
* Validate Cookie Name.
* @param name Cookie name.
*/
function validateCookieName(name: string | undefined | null): void {
if (name && !FIELD_CONTENT_REGEXP.test(name)) {
throw new TypeError(`Invalid cookie name: "${name}".`);
}
}
/**
* 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 + "'",
);
}
}
}
/**
*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),
);
}
}
}
/**
* Parse the cookies of the Server Request
* @param req An object which has a `headers` property
*/
export function getCookies(req: { headers: Headers }): Cookies {
const cookie = req.headers.get("Cookie");
if (cookie != null) {
const out: Cookies = {};
const c = cookie.split(";");
for (const kv of c) {
const [cookieKey, ...cookieVal] = kv.split("=");
assert(cookieKey != null);
const key = cookieKey.trim();
out[key] = cookieVal.join("=");
}
return out;
}
return {};
}
/**
* Set the cookie header properly in the Response
* @param res An object which has a headers property
* @param cookie Cookie to set
*
* Example:
*
* ```ts
* setCookie(response, { name: 'deno', value: 'runtime',
* httpOnly: true, secure: true, maxAge: 2, domain: "deno.land" });
* ```
*/
export function setCookie(res: { headers?: Headers }, cookie: Cookie): void {
if (!res.headers) {
res.headers = new Headers();
}
// TODO(zekth) : Add proper parsing of Set-Cookie headers
// Parsing cookie headers to make consistent set-cookie header
// ref: https://tools.ietf.org/html/rfc6265#section-4.1.1
const v = toString(cookie);
if (v) {
res.headers.append("Set-Cookie", v);
}
}
/**
* Set the cookie header properly in the Response to delete it
* @param res Server Response
* @param name Name of the cookie to Delete
* Example:
*
* deleteCookie(res,'foo');
*/
export function deleteCookie(res: { headers?: Headers }, name: string): void {
setCookie(res, {
name: name,
value: "",
expires: new Date(0),
});
}