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
@@ -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`;
}
@@ -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);
}
});
@@ -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
@@ -1,14 +1,80 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

Add

// Structured similarly to Go's cookie.go
// https://github.com/golang/go/blob/master/src/net/http/cookie.go
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 {
export interface Cookie {
name: string;
value: string;
expires?: Date;
maxAge?: number;
domain?: string;
path?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: SameSite;
unparsed?: string[];
}
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

Use lower-case for the first letter.

It would be nice to have an element like this (as is done in Go)

        unparsed string[] // Raw text of unparsed attribute-value pairs

This comment has been minimized.

Copy link
@zekth

zekth Apr 27, 2019

Author Contributor

Refactored and added tests.


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

function cookieStringFormat(cookie: Cookie): string {
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

s/cookieStringFormat/toString/

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}`);
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

Could you break the date formatting out into a standalone function with tests of its own?

}
if (cookie.unparsed) {
out.push(cookie.unparsed.join("; "));
}
return out.join("; ");
}

/**
* Parse the cookies of the Server Request
* @param rq Server Request
*/
export function getCookies(rq: ServerRequest): Cookies {
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

s/rq/req/

if (rq.headers.has("Cookie")) {
const out: Cookie = {};
const out: Cookies = {};
const c = rq.headers.get("Cookie").split(";");
for (const kv of c) {
const cookieVal = kv.split("=");
@@ -19,3 +85,52 @@ 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", cookieStringFormat(cookie));
}

/**
* 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 {
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

s/CookieName/name/

if (!res.headers) {
res.headers = new Headers();
}
const c: Cookie = {
name: CookieName,
value: "",
expires: new Date(0)
};
res.headers.set("Set-Cookie", cookieStringFormat(c));
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

So it seems delCookie could just call setCookie with max-age set to zero?

Something like this:

export function delCookie(res: Response, name: string): void {
  setCookie(res, {
    name,
    value: "",
    expires: new Date(0)
  });
}

This comment has been minimized.

Copy link
@zekth

zekth Apr 27, 2019

Author Contributor

To delete a cookie you have to set the expire time to earlier. Like golang does is a fallback. Looking at rfc the max age under 0 is invalid. Refactoring.

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

Ok but I think delCookie should be a thin wrapper around setCookie, because code like this

  if (!res.headers) {
    res.headers = new Headers();
  }

is repeated.

Sorry for so many nitpicks :)

This comment has been minimized.

Copy link
@zekth

zekth Apr 27, 2019

Author Contributor

no my bad!

}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.