summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
authorVincent LE GOFF <g_n_s@hotmail.fr>2019-04-28 01:07:11 +0200
committerRyan Dahl <ry@tinyclouds.org>2019-04-27 16:07:11 -0700
commitce101a0f8632f6b5390af247f3df2002e86becdf (patch)
tree5232eb3cbd21c8773881327f98162f0567dfe543 /http
parent8d49022ef67b590d8ee7b3ff984307b6c0a69d4f (diff)
http: Cookie improvements (denoland/deno_std#359)
Original: https://github.com/denoland/deno_std/commit/f1114691038888fc3d8995b64a8028f072569672
Diffstat (limited to 'http')
-rw-r--r--http/README.md39
-rw-r--r--http/cookie.ts125
-rw-r--r--http/cookie_test.ts188
3 files changed, 338 insertions, 14 deletions
diff --git a/http/README.md b/http/README.md
index 448692a68..c7bac3f09 100644
--- a/http/README.md
+++ b/http/README.md
@@ -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
diff --git a/http/cookie.ts b/http/cookie.ts
index e78d48238..b7019ae20 100644
--- a/http/cookie.ts
+++ b/http/cookie.ts
@@ -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)
+ });
+}
diff --git a/http/cookie_test.ts b/http/cookie_test.ts
index e8f920b31..ae99b2707 100644
--- a/http/cookie_test.ts
+++ b/http/cookie_test.ts
@@ -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=/"
+ );
}
});