1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-30 11:16:38 -05:00

http: Cookie improvements (denoland/deno_std#359)

Original: f111469103
This commit is contained in:
Vincent LE GOFF 2019-04-28 01:07:11 +02:00 committed by Ryan Dahl
parent 8d49022ef6
commit ce101a0f86
5 changed files with 386 additions and 14 deletions

View file

@ -1,4 +1,6 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { pad } from "../strings/pad.ts";
export type DateFormat = "mm-dd-yyyy" | "dd-mm-yyyy" | "yyyy-mm-dd";
/**
@ -105,3 +107,40 @@ export function dayOfYear(date: Date): number {
export function currentDayOfYear(): number {
return dayOfYear(new Date());
}
/**
* Parse a date to return a IMF formated string date
* RFC: https://tools.ietf.org/html/rfc7231#section-7.1.1.1
* IMF is the time format to use when generating times in HTTP
* headers. The time being formatted must be in UTC for Format to
* generate the correct format.
* @param date Date to parse
* @return IMF date formated string
*/
export function toIMF(date: Date): string {
function dtPad(v: string, lPad: number = 2): string {
return pad(v, lPad, { char: "0" });
}
const d = dtPad(date.getUTCDate().toString());
const h = dtPad(date.getUTCHours().toString());
const min = dtPad(date.getUTCMinutes().toString());
const s = dtPad(date.getUTCSeconds().toString());
const y = date.getUTCFullYear();
const days = ["Sun", "Mon", "Tue", "Wed", "Thus", "Fri", "Sat"];
const months = [
"Jan",
"Feb",
"Mar",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
return `${days[date.getDay()]}, ${d} ${
months[date.getUTCMonth()]
} ${y} ${h}:${min}:${s} GMT`;
}

View file

@ -74,3 +74,12 @@ test(function DayOfYear(): void {
test(function currentDayOfYear(): void {
assertEquals(datetime.currentDayOfYear(), datetime.dayOfYear(new Date()));
});
test({
name: "[DateTime] to IMF",
fn(): void {
const actual = datetime.toIMF(new Date(Date.UTC(1994, 3, 5, 15, 32)));
const expected = "Tue, 05 May 1994 15:32:00 GMT";
assertEquals(actual, expected);
}
});

View file

@ -2,6 +2,45 @@
A framework for creating HTTP/HTTPS server.
## Cookie
Helper to manipulate `Cookie` throught `ServerRequest` and `Response`.
```ts
import { getCookies } from "https://deno.land/std/http/cookie.ts";
let req = new ServerRequest();
req.headers = new Headers();
req.headers.set("Cookie", "full=of; tasty=chocolate");
const c = getCookies(request);
// c = { full: "of", tasty: "chocolate" }
```
To set a `Cookie` you can add `CookieOptions` to properly set your `Cookie`
```ts
import { setCookie } from "https://deno.land/std/http/cookie.ts";
let res: Response = {};
res.headers = new Headers();
setCookie(res, { name: "Space", value: "Cat" });
```
Deleting a `Cookie` will set its expiration date before now.
Forcing the browser to delete it.
```ts
import { delCookie } from "https://deno.land/std/http/cookie.ts";
let res = new Response();
delCookie(res, "deno");
// Will append this header in the response
// "Set-Cookie: deno=; Expires=Thus, 01 Jan 1970 00:00:00 GMT"
```
**Note**: At the moment multiple `Set-Cookie` in a `Response` is not handled.
## Example
```typescript

View file

@ -1,15 +1,81 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { ServerRequest } from "./server.ts";
// Structured similarly to Go's cookie.go
// https://github.com/golang/go/blob/master/src/net/http/cookie.go
import { ServerRequest, Response } from "./server.ts";
import { assert } from "../testing/asserts.ts";
import { toIMF } from "../datetime/mod.ts";
export interface Cookie {
export interface Cookies {
[key: string]: string;
}
/* Parse the cookie of the Server Request */
export function getCookie(rq: ServerRequest): Cookie {
if (rq.headers.has("Cookie")) {
const out: Cookie = {};
const c = rq.headers.get("Cookie").split(";");
export interface Cookie {
name: string;
value: string;
expires?: Date;
maxAge?: number;
domain?: string;
path?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: SameSite;
unparsed?: string[];
}
export type SameSite = "Strict" | "Lax";
function toString(cookie: Cookie): string {
const out: string[] = [];
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 (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) {
out.push(`Path=${cookie.path}`);
}
if (cookie.expires) {
let dateString = toIMF(cookie.expires);
out.push(`Expires=${dateString}`);
}
if (cookie.unparsed) {
out.push(cookie.unparsed.join("; "));
}
return out.join("; ");
}
/**
* Parse the cookies of the Server Request
* @param req Server Request
*/
export function getCookies(req: ServerRequest): Cookies {
if (req.headers.has("Cookie")) {
const out: Cookies = {};
const c = req.headers.get("Cookie").split(";");
for (const kv of c) {
const cookieVal = kv.split("=");
const key = cookieVal.shift().trim();
@ -19,3 +85,48 @@ export function getCookie(rq: ServerRequest): Cookie {
}
return {};
}
/**
* Set the cookie header properly in the Response
* @param res Server Response
* @param cookie Cookie to set
* @param [cookie.name] Name of the cookie
* @param [cookie.value] Value of the cookie
* @param [cookie.expires] Expiration Date of the cookie
* @param [cookie.maxAge] Max-Age of the Cookie. Must be integer superior to 0
* @param [cookie.domain] Specifies those hosts to which the cookie will be sent
* @param [cookie.path] Indicates a URL path that must exist in the request.
* @param [cookie.secure] Indicates if the cookie is made using SSL & HTTPS.
* @param [cookie.httpOnly] Indicates that cookie is not accessible via Javascript
* @param [cookie.sameSite] Allows servers to assert that a cookie ought not to be
* sent along with cross-site requests
* Example:
*
* setCookie(response, { name: 'deno', value: 'runtime',
* httpOnly: true, secure: true, maxAge: 2, domain: "deno.land" });
*/
export function setCookie(res: Response, 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
res.headers.set("Set-Cookie", toString(cookie));
}
/**
* Set the cookie header properly in the Response to delete it
* @param res Server Response
* @param name Name of the cookie to Delete
* Example:
*
* delCookie(res,'foo');
*/
export function delCookie(res: Response, name: string): void {
setCookie(res, {
name: name,
value: "",
expires: new Date(0)
});
}

View file

@ -1,7 +1,7 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { ServerRequest } from "./server.ts";
import { getCookie } from "./cookie.ts";
import { assertEquals } from "../testing/asserts.ts";
import { ServerRequest, Response } from "./server.ts";
import { getCookies, delCookie, setCookie } from "./cookie.ts";
import { assert, assertEquals } from "../testing/asserts.ts";
import { test } from "../testing/mod.ts";
test({
@ -9,17 +9,191 @@ test({
fn(): void {
let req = new ServerRequest();
req.headers = new Headers();
assertEquals(getCookie(req), {});
assertEquals(getCookies(req), {});
req.headers = new Headers();
req.headers.set("Cookie", "foo=bar");
assertEquals(getCookie(req), { foo: "bar" });
assertEquals(getCookies(req), { foo: "bar" });
req.headers = new Headers();
req.headers.set("Cookie", "full=of ; tasty=chocolate");
assertEquals(getCookie(req), { full: "of ", tasty: "chocolate" });
assertEquals(getCookies(req), { full: "of ", tasty: "chocolate" });
req.headers = new Headers();
req.headers.set("Cookie", "igot=99; problems=but...");
assertEquals(getCookie(req), { igot: "99", problems: "but..." });
assertEquals(getCookies(req), { igot: "99", problems: "but..." });
}
});
test({
name: "[HTTP] Cookie Delete",
fn(): void {
let res: Response = {};
delCookie(res, "deno");
assertEquals(
res.headers.get("Set-Cookie"),
"deno=; Expires=Thus, 01 Jan 1970 00:00:00 GMT"
);
}
});
test({
name: "[HTTP] Cookie Set",
fn(): void {
let res: Response = {};
res.headers = new Headers();
setCookie(res, { name: "Space", value: "Cat" });
assertEquals(res.headers.get("Set-Cookie"), "Space=Cat");
res.headers = new Headers();
setCookie(res, { name: "Space", value: "Cat", secure: true });
assertEquals(res.headers.get("Set-Cookie"), "Space=Cat; Secure");
res.headers = new Headers();
setCookie(res, { name: "Space", value: "Cat", httpOnly: true });
assertEquals(res.headers.get("Set-Cookie"), "Space=Cat; HttpOnly");
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true
});
assertEquals(res.headers.get("Set-Cookie"), "Space=Cat; Secure; HttpOnly");
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2"
);
let error = false;
res.headers = new Headers();
try {
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 0
});
} catch (e) {
error = true;
}
assert(error);
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2,
domain: "deno.land"
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land"
);
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2,
domain: "deno.land",
sameSite: "Strict"
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Strict"
);
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2,
domain: "deno.land",
sameSite: "Lax"
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Lax"
);
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2,
domain: "deno.land",
path: "/"
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/"
);
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2,
domain: "deno.land",
path: "/",
unparsed: ["unparsed=keyvalue", "batman=Bruce"]
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; unparsed=keyvalue; batman=Bruce"
);
res.headers = new Headers();
setCookie(res, {
name: "Space",
value: "Cat",
httpOnly: true,
secure: true,
maxAge: 2,
domain: "deno.land",
path: "/",
expires: new Date(Date.UTC(1983, 0, 7, 15, 32))
});
assertEquals(
res.headers.get("Set-Cookie"),
"Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; Expires=Fri, 07 Jan 1983 15:32:00 GMT"
);
res.headers = new Headers();
setCookie(res, { name: "__Secure-Kitty", value: "Meow" });
assertEquals(res.headers.get("Set-Cookie"), "__Secure-Kitty=Meow; Secure");
res.headers = new Headers();
setCookie(res, {
name: "__Host-Kitty",
value: "Meow",
domain: "deno.land"
});
assertEquals(
res.headers.get("Set-Cookie"),
"__Host-Kitty=Meow; Secure; Path=/"
);
}
});