Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: Cookie extend #359

Merged
merged 14 commits into from
Apr 27, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@

A framework for creating HTTP/HTTPS server.

## Cookie

Helper to manipulate `Cookie` throught `ServerRequest` and `Response`.

```ts
import { getCookie } 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 = getCookie(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
Expand Down
150 changes: 148 additions & 2 deletions http/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,103 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
zekth marked this conversation as resolved.
Show resolved Hide resolved
import { ServerRequest } from "./server.ts";
import { ServerRequest, Response } from "./server.ts";
import { assert } from "../testing/asserts.ts";
import { pad } from "../strings/pad.ts";

export interface Cookie {
[key: string]: string;
}

/* Parse the cookie of the Server Request */
export interface CookieValue {
name: string;
value: string;
}

export interface CookieOptions {
zekth marked this conversation as resolved.
Show resolved Hide resolved
Expires?: Date;
MaxAge?: number;
Domain?: string;
Path?: string;
Secure?: boolean;
HttpOnly?: boolean;
SameSite?: SameSite;
}
zekth marked this conversation as resolved.
Show resolved Hide resolved

export type SameSite = "Strict" | "Lax";

export function cookieDateFormat(date: Date): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a specific date format for cookies? Does it have a name? If it's not just for cookies, consider moving this to //datetime

Copy link
Contributor Author

@zekth zekth Apr 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking it was specific but no. It's called IMF-fixdate. I'll move it to datetime. Also is date.toIMF() good naming for you?

ref: https://tools.ietf.org/html/rfc7231#section-7.1.1.1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a name for this in Go? Perhaps you can borrow that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the only mention of it i can find is IMF-fixdate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can call it "RFC1123_GMT"

We don't have the same way of serializing datetime formats as Go. I'd like to slowly move in that direction tho. So I think you can just keep the toIMF() where it is now. But please familiarize yourself with how I think this would be ideally structured:
https://github.com/golang/go/blob/f67f5511ee0f225bcc8943994ba6139eed375e85/src/net/http/server.go#L903-L909
https://github.com/golang/go/blob/f67f5511ee0f225bcc8943994ba6139eed375e85/src/net/http/cookie.go#L200-L204
Consider adding some of the commentary from Go to the jsdoc of "toIMF()"

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();
// See Date format: https://tools.ietf.org/html/rfc7231#section-7.1.1.1
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`;
}

function cookieStringFormat(cookie: CookieValue, opt: CookieOptions): 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")) {
opt.Secure = true;
}
if (cookie.name.startsWith("__Host")) {
opt.Path = "/";
opt.Secure = true;
delete opt.Domain;
}

if (opt.Secure) {
out.push("Secure");
}
if (opt.HttpOnly) {
out.push("HttpOnly");
}
if (Number.isInteger(opt.MaxAge)) {
assert(opt.MaxAge > 0, "Max-Age must be an integer superior to 0");
out.push(`Max-Age=${opt.MaxAge}`);
}
if (opt.Domain) {
out.push(`Domain=${opt.Domain}`);
}
if (opt.SameSite) {
out.push(`SameSite=${opt.SameSite}`);
}
if (opt.Path) {
out.push(`Path=${opt.Path}`);
}
if (opt.Expires) {
let dateString = cookieDateFormat(opt.Expires);
out.push(`Expires=${dateString}`);
zekth marked this conversation as resolved.
Show resolved Hide resolved
}
return out.join("; ");
}

/**
* Parse the cookie of the Server Request
* @param rq Server Request
*/
export function getCookie(rq: ServerRequest): Cookie {
if (rq.headers.has("Cookie")) {
const out: Cookie = {};
Expand All @@ -19,3 +111,57 @@ 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 opt Cookie options
* @param [opt.Expires] Expiration Date of the cookie
* @param [opt.MaxAge] Max-Age of the Cookie. Must be integer superior to 0
* @param [opt.Domain] Specifies those hosts to which the cookie will be sent
* @param [opt.Path] Indicates a URL path that must exist in the request.
* @param [opt.Secure] Indicates if the cookie is made using SSL & HTTPS.
* @param [opt.HttpOnly] Indicates that cookie is not accessible via Javascript
* @param [opt.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: CookieValue,
opt: CookieOptions = {}
): 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", cookieStringFormat(cookie, opt));
}

/**
* Set the cookie header properly in the Response to delete it
* @param res Server Response
* @param CookieName Name of the cookie to Delete
* Example:
*
* delCookie(res,'foo');
*/
export function delCookie(res: Response, CookieName: string): void {
zekth marked this conversation as resolved.
Show resolved Hide resolved
if (!res.headers) {
res.headers = new Headers();
}
const c: CookieValue = {
name: CookieName,
value: ""
};
res.headers.set(
"Set-Cookie",
cookieStringFormat(c, { Expires: new Date(0) })
);
}
173 changes: 170 additions & 3 deletions http/cookie_test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// 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 { getCookie, delCookie, setCookie, cookieDateFormat } from "./cookie.ts";
import { assert, assertEquals } from "../testing/asserts.ts";
import { test } from "../testing/mod.ts";

test({
name: "[HTTP] Cookie Date Format",
fn(): void {
const actual = cookieDateFormat(new Date(Date.UTC(1994, 3, 5, 15, 32)));
const expected = "Tue, 05 May 1994 15:32:00 GMT";
assertEquals(actual, expected);
}
});

test({
name: "[HTTP] Cookie parser",
fn(): void {
Expand All @@ -23,3 +32,161 @@ test({
assertEquals(getCookie(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: "/",
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=/"
);
}
});