Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export { default as IHttpRequest } from "./lib/types/http/IHttpRequest";
export { default as IHttpResponse } from "./lib/types/http/IHttpResponse";
export { default as IHttpRoute } from "./lib/types/http/IHttpRoute";
export { default as IHttpRouterExecutor } from "./lib/types/http/IHttpRouterExecutor";
export { default as ICookie } from "./lib/types/http/ICookie";
export { default as Cookie } from "./lib/http/Cookie";

export { default as IBodyParser } from "./lib/types/http/IBodyParser";
export { default as JsonParser } from "./lib/http/bodyParsers/JsonParser";
Expand Down
2 changes: 1 addition & 1 deletion src/lib/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default class App implements IApp {

public handle(event: IRawEvent, callback: IRawCallback): void {
if (event.isHttp) {
const req: IHttpRequest = new HttpRequest(event);
const req: IHttpRequest = new HttpRequest(this, event);
const res: IHttpResponse = new HttpResponse(this, req, callback);
const done = httpFinalHandler(req, res, {
env: this.get(configuration.ENVIRONMENT),
Expand Down
46 changes: 46 additions & 0 deletions src/lib/http/Cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import ICookie from "./../types/http/ICookie";

/**
* This object represents a browser cookie.
*/
export default class Cookie implements ICookie {

private _name: string;

private _value: string|{[name: string]: any};

private _expires: Date;

private _path: string;

private _signed: boolean;

constructor(name: string, value: string|{[name: string]: any}, expires?: Date, path?: string, signed?: boolean) {
this._name = name;
this._value = value;
this._expires = expires;
this._path = path;
this._signed = signed;
}

get name(): string {
return this._name;
}

get value(): string|{[name: string]: any} {
return this._value;
}

get expires(): Date {
return this._expires;
}

get path(): string {
return this._path;
}

get signed(): boolean {
return this._signed;
}

}
18 changes: 16 additions & 2 deletions src/lib/http/HttpRequest.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as accepts from "accepts";
import * as fresh from "fresh";
import configuration from "./../configuration/configuration";
import ICookie from "./../types/http/ICookie";
import IHttpRequest from "./../types/http/IHttpRequest";
import IHttpResponse from "./../types/http/IHttpResponse";
import IHttpRoute from "./../types/http/IHttpRoute";
import IHttpUploadedFile from "./../types/http/IHttpUploadedFile";
import IApp from "./../types/IApp";
import INext from "./../types/INext";
import IRawEvent from "./../types/IRawEvent";
import { mergeParams, normalizeType } from "./../utils/utils";
import { getCookiesFromHeader, mergeParams, normalizeType } from "./../utils/utils";

/**
* A incoming request created when the event is APIGatewayEvent.
Expand All @@ -24,8 +27,9 @@ export default class HttpRequest implements IHttpRequest {
private _event: IRawEvent;
private _headers: { [name: string]: string };
private _context: { [name: string]: any };
private _cookies: { [name: string]: ICookie };

constructor(event: IRawEvent) {
constructor(app: IApp, event: IRawEvent) {
this.body = event.body; // Default body
this._event = event;
this._context = {};
Expand All @@ -35,6 +39,8 @@ export default class HttpRequest implements IHttpRequest {
for (const key of Object.keys(this._event.headers)) {
this._headers[key.toLowerCase()] = this._event.headers[key];
}

this._cookies = getCookiesFromHeader(this._headers.cookie, app.get(configuration.COOKIE_SECRET));
}

get headers(): { [name: string]: string } {
Expand Down Expand Up @@ -85,6 +91,10 @@ export default class HttpRequest implements IHttpRequest {
return this._context;
}

get cookies(): { [name: string]: ICookie } {
return this._cookies;
}

public header(key: string): string {
return this.headers[key.toLowerCase()];
}
Expand Down Expand Up @@ -162,4 +172,8 @@ export default class HttpRequest implements IHttpRequest {
return !this.fresh(response);
}

public cookie(name: string): ICookie {
return this._cookies[name];
}

}
50 changes: 26 additions & 24 deletions src/lib/http/HttpResponse.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { parse, serialize } from "cookie";
import { serialize } from "cookie";
import { sign } from "cookie-signature";
import * as encodeUrl from "encodeurl";
import * as escapeHtml from "escape-html";
import * as statuses from "statuses";
import configuration from "./../configuration/configuration";
import HttpError from "./../exceptions/HttpError";
import IHttpError from "./../types/exceptions/IHttpError";
import ICookie from "./../types/http/ICookie";
import IHttpHandler from "./../types/http/IHttpHandler";
import IHttpRequest from "./../types/http/IHttpRequest";
import IHttpResponse from "./../types/http/IHttpResponse";
Expand All @@ -15,6 +16,7 @@ import INext from "./../types/INext";
import IRawCallback from "./../types/IRawCallback";
import IRouter from "./../types/IRouter";
import { merge, normalizeType, setCharset, stringify } from "./../utils/utils";
import Cookie from "./Cookie";

/**
* This class represents an HTTP response, with the helpers to be sent.
Expand All @@ -30,13 +32,15 @@ export default class HttpResponse implements IHttpResponse {
private _headers: { [name: string]: string|string[] };
private _error: IHttpError;
private _isSent: boolean;
private _cookies: { [name: string]: ICookie };

constructor(app: IApp, request: IHttpRequest, callback: IRawCallback) {
this._app = app;
this._request = request;
this._callback = callback;
this._headers = {};
this._isSent = false;
this._cookies = {};
}

get statusCode(): number {
Expand Down Expand Up @@ -245,40 +249,43 @@ export default class HttpResponse implements IHttpResponse {
return this;
}

public addCookie(name: string, value: string|object, options?: object): IHttpResponse {
const opts = merge({}, options);
public addCookie(cookie: ICookie): IHttpResponse {
const opts: {[name: string]: any} = {};
const secret = this._app.get(configuration.COOKIE_SECRET);
const signed = opts.signed;
const signed = cookie.signed;

if (signed && !secret) {
throw new Error("app.set(\"cookie_secret\", \"SECRET\") required for signed cookies.");
}

let val = typeof value === "object"
? "j:" + JSON.stringify(value)
: String(value);
let val = typeof cookie.value === "object"
? "j:" + JSON.stringify(cookie.value)
: String(cookie.value);

if (signed) {
val = "s:" + sign(val, secret);
opts.signed = true;
}

if ("maxAge" in opts) {
opts.expires = new Date(Date.now() + opts.maxAge);
opts.maxAge /= 1000;
if (cookie.expires) {
opts.expires = cookie.expires;
}

if (opts.path == null) {
if (cookie.path == null) {
opts.path = "/";
} else {
opts.path = cookie.path;
}

this.appendHeader("Set-Cookie", serialize(name, String(val), opts));
this._cookies[cookie.name] = cookie;
this.appendHeader("Set-Cookie", serialize(cookie.name, String(val), opts));

return this;
}

public addCookies(obj: object, options?: object): IHttpResponse {
for (const key of Object.keys(obj)) {
this.addCookie(key, obj[key], options);
public addCookies(cookies: ICookie[]): IHttpResponse {
for (const cookie of cookies) {
this.addCookie(cookie);
}

return this;
Expand All @@ -287,19 +294,14 @@ export default class HttpResponse implements IHttpResponse {
public clearCookie(name: string, options?: object): IHttpResponse {
const opts = merge({ expires: new Date(1), path: "/" }, options);

this.addCookie(name, "", opts);
const cookie: ICookie = new Cookie(name, "", opts.expires, opts.path);
this.addCookie(cookie);

return this;
}

public cookie(name: string): string {
const cookiesHeader = this.header("Set-Cookie");

const cookies = parse(Array.isArray(cookiesHeader) ?
cookiesHeader.join("; ") : cookiesHeader
);

return cookies[name];
public cookie(name: string): ICookie {
return this._cookies[name];
}

public location(url: string): IHttpResponse {
Expand Down
16 changes: 16 additions & 0 deletions src/lib/types/http/ICookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* This object represents a browser cookie.
*/
export default interface ICookie {

readonly name: string;

readonly value: string|{[name: string]: any};

readonly expires: Date;

readonly path: string;

readonly signed: boolean;

}
17 changes: 17 additions & 0 deletions src/lib/types/http/IHttpRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import INext from "./../INext";
import ICookie from "./ICookie";
import IHttpResponse from "./IHttpResponse";
import IHttpRoute from "./IHttpRoute";
import IHttpUploadedFile from "./IHttpUploadedFile";
Expand Down Expand Up @@ -94,6 +95,13 @@ export default interface IHttpRequest {
*/
readonly context: { [name: string]: any };

/**
* Returns the all the cookies retrieving their values and
* options from the HTTP request header. The key will be the name of
* the cookie and the value the object representing the cookie.
*/
readonly cookies: {[name: string]: ICookie};

/**
* Return request header.
*
Expand Down Expand Up @@ -180,4 +188,13 @@ export default interface IHttpRequest {
*/
stale(response: IHttpResponse): boolean;

/**
* Returns the cookie with the given `name` retrieving the value and
* options from the HTTP request header.
*
* @param {string} name
* @return {ICookie}
*/
cookie(name: string): ICookie;

}
23 changes: 10 additions & 13 deletions src/lib/types/http/IHttpResponse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import IHttpError from "./../exceptions/IHttpError";
import INext from "./../INext";
import IRouter from "./../IRouter";
import ICookie from "./ICookie";
import IHttpHandler from "./IHttpHandler";

/**
Expand Down Expand Up @@ -146,32 +147,28 @@ export default interface IHttpResponse {
clearCookie(name: string, options?: object): IHttpResponse;

/**
* Set cookie `name` to `value`, with the given `options`.
* Add the given cookie to response header.
*
* @param {string} name
* @param {string|object} value
* @param {object} options
* @param {ICookie} cookie
* @return {IHttpResponse}
*/
addCookie(name: string, value: string|object, options?: object): IHttpResponse;
addCookie(cookie: ICookie): IHttpResponse;

/**
* Set each cookie indicated by the key of `object` to the value indicated
* by the value of the key.
* Add the given cookies to response header.
*
* @param {object} name
* @param {object} options
* @param {ICookie[]} cookies
* @return {IHttpResponse}
*/
addCookies(name: object, options?: object): IHttpResponse;
addCookies(cookies: ICookie[]): IHttpResponse;

/**
* Get the value of the cookie `name`.
* Get the cookie with the given `name`.
*
* @param {string} name
* @return {string}
* @return {ICookie}
*/
cookie(name: string): string;
cookie(name: string): ICookie;

/**
* Set the location header to `url`.
Expand Down
38 changes: 36 additions & 2 deletions src/lib/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { format, parse } from "content-type";
import { format, parse as parseContentType } from "content-type";
import { parse as parseCookie } from "cookie";
import { unsign } from "cookie-signature";
import { lookup } from "mime-types";
import Cookie from "./../http/Cookie";
import ICookie from "./../types/http/ICookie";
import IRawEvent from "./../types/IRawEvent";

/**
Expand All @@ -12,7 +16,7 @@ export function setCharset(contentType: string, charset: string): string {
}

// parse type
const parsed = parse(contentType);
const parsed = parseContentType(contentType);

// set charset
parsed.parameters.charset = charset;
Expand Down Expand Up @@ -71,3 +75,33 @@ export function normalizeType(type: string): string {
? lookup(type)
: type;
}

const parseCookieValue = (rawValue: string, secret: string): string | { [name: string]: any } => {
let result: string | { [name: string]: any } = rawValue;

if (result.startsWith("s:")) {
result = unsign(result.substr(2), secret) as string;
}

if (result.startsWith("j:")) {
result = JSON.parse(result.substr(2));
}

return result;
};

export function getCookiesFromHeader(header: string, secret: string): { [name: string]: ICookie } {
const result: { [name: string]: ICookie } = {};

if (header) {
const cookiesAsObject: { [name: string]: string } = parseCookie(header);

for (const cookieName of Object.keys(cookiesAsObject)) {
const cookieValue: string | { [name: string]: any } = parseCookieValue(cookiesAsObject[cookieName], secret);

result[cookieName] = new Cookie(cookieName, cookieValue);
}
}

return result;
}
Loading