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,15 +1,105 @@
// 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";
import { ServerRequest, Response } from "./server.ts";
import { assert } from "../testing/asserts.ts";
import { pad } from "../strings/pad.ts";

const SETCOOKIE = "Set-Cookie";
const COOKIE = "Cookie";

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

export interface CookieValue {
name: string;
value: string;
}

export interface CookieOptions {
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

I think this should be called "Cookie"

https://golang.org/pkg/net/http/#Cookie

This comment has been minimized.

Copy link
@zekth

zekth Apr 27, 2019

Author Contributor

Refactored

Expires?: Date;
MaxAge?: number;
Domain?: string;
Path?: string;
Secure?: boolean;
HttpOnly?: boolean;
SameSite?: SameSite;
}
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: CookieValue, opt: CookieOptions): string {
function dtPad(v: string, lPad: number = 2): string {
return pad(v, lPad, { char: "0" });
}

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 = "";
let d = dtPad(opt.Expires.getUTCDate().toString());
const h = dtPad(opt.Expires.getUTCHours().toString());
const min = dtPad(opt.Expires.getUTCMinutes().toString());
const s = dtPad(opt.Expires.getUTCSeconds().toString());
const y = opt.Expires.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"
];
dateString += `${days[opt.Expires.getDay()]}, ${d} ${
months[opt.Expires.getUTCMonth()]
} ${y} ${h}:${min}:${s} GMT`;
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?

}
return out.join("; ");
}

/* Parse the cookie of the Server Request */
export function getCookie(rq: ServerRequest): Cookie {
if (rq.headers.has("Cookie")) {
if (rq.headers.has(COOKIE)) {
const out: Cookie = {};
const c = rq.headers.get("Cookie").split(";");
const c = rq.headers.get(COOKIE).split(";");
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 25, 2019

Contributor

I don't think this constant makes thing easier or more maintainable.

This comment has been minimized.

Copy link
@zekth

zekth Apr 25, 2019

Author Contributor

Was because of the SETCOOKIE one just uniformise the module. But yep.

for (const kv of c) {
const cookieVal = kv.split("=");
const key = cookieVal.shift().trim();
@@ -19,3 +109,30 @@ export function getCookie(rq: ServerRequest): Cookie {
}
return {};
}

/* Set the cookie header properly in the Response */

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

Use jsdoc style comments.

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(SETCOOKIE, cookieStringFormat(cookie, opt));
}

/* Set the cookie header properly in the Response to delete it */
This conversation was marked as resolved by zekth

This comment has been minimized.

Copy link
@ry

ry Apr 27, 2019

Contributor

Use jsdoc style comments

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: CookieValue = {
name: CookieName,
value: ""
};
res.headers.set(SETCOOKIE, cookieStringFormat(c, { Expires: new Date(0) }));
}
@@ -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 { getCookie, delCookie, setCookie } from "./cookie.ts";
import { assert, assertEquals } from "../testing/asserts.ts";
import { test } from "../testing/mod.ts";

test({
@@ -23,3 +23,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=/"
);
}
});
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.