From 228448e464d9fb3cda5641c0f92bcf4d69ca39a6 Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Wed, 25 Oct 2017 21:17:46 +0200 Subject: [PATCH 1/9] Body parsers scafolder. --- src/lib/http/HttpRequest.ts | 3 ++- src/lib/http/bodyParsers/JsonParser.ts | 15 +++++++++++++++ src/lib/http/bodyParsers/MultipartParser.ts | 15 +++++++++++++++ src/lib/http/bodyParsers/UrlEncodedParser.ts | 15 +++++++++++++++ src/lib/http/bodyParsers/XmlParser.ts | 15 +++++++++++++++ src/lib/types/http/IBodyParser.ts | 10 ++++++++++ src/lib/utils/utils.ts | 3 +-- 7 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/lib/http/bodyParsers/JsonParser.ts create mode 100644 src/lib/http/bodyParsers/MultipartParser.ts create mode 100644 src/lib/http/bodyParsers/UrlEncodedParser.ts create mode 100644 src/lib/http/bodyParsers/XmlParser.ts create mode 100644 src/lib/types/http/IBodyParser.ts diff --git a/src/lib/http/HttpRequest.ts b/src/lib/http/HttpRequest.ts index df45a10..53a5571 100644 --- a/src/lib/http/HttpRequest.ts +++ b/src/lib/http/HttpRequest.ts @@ -12,7 +12,7 @@ import { mergeParams } from "./../utils/utils"; */ export default class HttpRequest implements IHttpRequest { - public body: object|string; + public body: { [name: string]: string }|string; public basePath: string; public originalBasePath: string; public next: INext; @@ -24,6 +24,7 @@ export default class HttpRequest implements IHttpRequest { private _context: { [name: string]: any }; constructor(event: APIGatewayEvent) { + this.body = event.body; // Default body this._event = event; this._context = {}; this.params = mergeParams(event); diff --git a/src/lib/http/bodyParsers/JsonParser.ts b/src/lib/http/bodyParsers/JsonParser.ts new file mode 100644 index 0000000..8e51400 --- /dev/null +++ b/src/lib/http/bodyParsers/JsonParser.ts @@ -0,0 +1,15 @@ +import IBodyParser from "./../../types/http/IBodyParser"; +import IHttpHandler from "./../../types/http/IHttpHandler"; + +/** + * A layer that set the request body depending of its type. + */ +export default class JsonParser implements IBodyParser { + + public create(): IHttpHandler { + return (req, res, next) => { + + }; + } + +} diff --git a/src/lib/http/bodyParsers/MultipartParser.ts b/src/lib/http/bodyParsers/MultipartParser.ts new file mode 100644 index 0000000..f3db1cb --- /dev/null +++ b/src/lib/http/bodyParsers/MultipartParser.ts @@ -0,0 +1,15 @@ +import IBodyParser from "./../../types/http/IBodyParser"; +import IHttpHandler from "./../../types/http/IHttpHandler"; + +/** + * A layer that set the request body depending of its type. + */ +export default class MultipartParser implements IBodyParser { + + public create(): IHttpHandler { + return (req, res, next) => { + + }; + } + +} diff --git a/src/lib/http/bodyParsers/UrlEncodedParser.ts b/src/lib/http/bodyParsers/UrlEncodedParser.ts new file mode 100644 index 0000000..5ef101b --- /dev/null +++ b/src/lib/http/bodyParsers/UrlEncodedParser.ts @@ -0,0 +1,15 @@ +import IBodyParser from "./../../types/http/IBodyParser"; +import IHttpHandler from "./../../types/http/IHttpHandler"; + +/** + * A layer that set the request body depending of its type. + */ +export default class UrlEncodedParser implements IBodyParser { + + public create(): IHttpHandler { + return (req, res, next) => { + + }; + } + +} diff --git a/src/lib/http/bodyParsers/XmlParser.ts b/src/lib/http/bodyParsers/XmlParser.ts new file mode 100644 index 0000000..79a12f5 --- /dev/null +++ b/src/lib/http/bodyParsers/XmlParser.ts @@ -0,0 +1,15 @@ +import IBodyParser from "./../../types/http/IBodyParser"; +import IHttpHandler from "./../../types/http/IHttpHandler"; + +/** + * A layer that set the request body depending of its type. + */ +export default class XmlParser implements IBodyParser { + + public create(): IHttpHandler { + return (req, res, next) => { + + }; + } + +} diff --git a/src/lib/types/http/IBodyParser.ts b/src/lib/types/http/IBodyParser.ts new file mode 100644 index 0000000..f8b1f27 --- /dev/null +++ b/src/lib/types/http/IBodyParser.ts @@ -0,0 +1,10 @@ +import IHttpHandler from "./IHttpHandler"; + +/** + * A layer that set the request body depending of its type. + */ +export default interface IBodyParser { + + create(): IHttpHandler; + +} diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts index 0344dfd..33ffc92 100644 --- a/src/lib/utils/utils.ts +++ b/src/lib/utils/utils.ts @@ -83,11 +83,10 @@ export function getEventType(obj: any): string { } export function mergeParams(event: APIGatewayEvent): {[name: string]: string} { - const body = typeof event.body === "object" ? event.body : {}; const query = event.queryStringParameters || {}; const stageVariables = event.stageVariables || {}; - return merge(body, query, stageVariables); + return merge(query, stageVariables); } export function stringify(value: {}, replacer: (string[]|number[]), spaces: string|number, escape: boolean): string { From 9fddf787f5b472aff7b45ae687a51b2cb36422a9 Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Thu, 26 Oct 2017 20:27:40 +0200 Subject: [PATCH 2/9] JSON and XML parser. --- src/lib/http/HttpRequest.ts | 2 +- src/lib/http/bodyParsers/JsonParser.ts | 9 +++-- src/lib/http/bodyParsers/XmlParser.ts | 51 ++++++++++++++++++++++-- src/lib/http/bodyParsers/parserHelper.ts | 26 ++++++++++++ 4 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/lib/http/bodyParsers/parserHelper.ts diff --git a/src/lib/http/HttpRequest.ts b/src/lib/http/HttpRequest.ts index 53a5571..250f9ad 100644 --- a/src/lib/http/HttpRequest.ts +++ b/src/lib/http/HttpRequest.ts @@ -12,7 +12,7 @@ import { mergeParams } from "./../utils/utils"; */ export default class HttpRequest implements IHttpRequest { - public body: { [name: string]: string }|string; + public body: { [name: string]: any }|string; public basePath: string; public originalBasePath: string; public next: INext; diff --git a/src/lib/http/bodyParsers/JsonParser.ts b/src/lib/http/bodyParsers/JsonParser.ts index 8e51400..e013779 100644 --- a/src/lib/http/bodyParsers/JsonParser.ts +++ b/src/lib/http/bodyParsers/JsonParser.ts @@ -1,15 +1,16 @@ import IBodyParser from "./../../types/http/IBodyParser"; import IHttpHandler from "./../../types/http/IHttpHandler"; +import parserHelper from "./parserHelper"; /** * A layer that set the request body depending of its type. */ export default class JsonParser implements IBodyParser { - public create(): IHttpHandler { - return (req, res, next) => { - - }; + public create(reviver?: (key: string, value: string) => any): IHttpHandler { + return parserHelper((initialBody: string) => { + return JSON.parse(initialBody, reviver); + }); } } diff --git a/src/lib/http/bodyParsers/XmlParser.ts b/src/lib/http/bodyParsers/XmlParser.ts index 79a12f5..aec2c2d 100644 --- a/src/lib/http/bodyParsers/XmlParser.ts +++ b/src/lib/http/bodyParsers/XmlParser.ts @@ -1,5 +1,52 @@ import IBodyParser from "./../../types/http/IBodyParser"; import IHttpHandler from "./../../types/http/IHttpHandler"; +import parserHelper from "./parserHelper"; + +// Changes String to XML +const xmlToJsonFromString = (initialBody: string, contentType: string): { [name: string]: any } => { + const parser = new DOMParser(); + const xml = parser.parseFromString(initialBody, contentType || "text/xml"); + + return xmlToJson(xml); +} + +// Changes XML to JSON +const xmlToJson = (xml): { [name: string]: any } => { + // Create the return object + var obj = {}; + + if (xml.nodeType == 1) { // element + // do attributes + if (xml.attributes.length > 0) { + obj["@attributes"] = {}; + for (var j = 0; j < xml.attributes.length; j++) { + var attribute = xml.attributes.item(j); + obj["@attributes"][attribute.nodeName] = attribute.nodeValue; + } + } + } else if (xml.nodeType == 3) { // text + obj = xml.nodeValue; + } + + // do children + if (xml.hasChildNodes()) { + for(var i = 0; i < xml.childNodes.length; i++) { + var item = xml.childNodes.item(i); + var nodeName = item.nodeName; + if (typeof(obj[nodeName]) == "undefined") { + obj[nodeName] = xmlToJson(item); + } else { + if (typeof(obj[nodeName].push) == "undefined") { + var old = obj[nodeName]; + obj[nodeName] = []; + obj[nodeName].push(old); + } + obj[nodeName].push(xmlToJson(item)); + } + } + } + return obj; +}; /** * A layer that set the request body depending of its type. @@ -7,9 +54,7 @@ import IHttpHandler from "./../../types/http/IHttpHandler"; export default class XmlParser implements IBodyParser { public create(): IHttpHandler { - return (req, res, next) => { - - }; + return parserHelper(xmlToJsonFromString); } } diff --git a/src/lib/http/bodyParsers/parserHelper.ts b/src/lib/http/bodyParsers/parserHelper.ts new file mode 100644 index 0000000..8919955 --- /dev/null +++ b/src/lib/http/bodyParsers/parserHelper.ts @@ -0,0 +1,26 @@ +import IHttpHandler from "./../../types/http/IHttpHandler"; +import IHttpRequest from "./../../types/http/IHttpRequest"; +import IHttpResponse from "./../../types/http/IHttpResponse"; +import HttpError from "./../../exceptions/HttpError"; +import INext from "./../../types/INext"; + +const parserHelper = (func: (body: string, contentType?: string) => { [name: string]: any }|string): IHttpHandler => { + return (req: IHttpRequest, res: IHttpResponse, next: INext) => { + let error; + + if(req.body) { + const contentType = req.header("content-type"); + try { + req.body = func(req.event.body, contentType); + } catch(e) { + if(contentType) { + error = new HttpError("Body can not be parsed.", 400); + } + } + } + + next(error); + }; +}; + +export default parserHelper; From ec4bf32f3ec2adf7eaaf90e2b51ac9c39d29982d Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 20:36:15 +0200 Subject: [PATCH 3/9] Added parsers and tests. --- package-lock.json | 187 +++++++++++++++++- package.json | 8 +- src/lib/http/HttpRequest.ts | 24 ++- src/lib/http/HttpResponse.ts | 9 +- src/lib/http/HttpUploadedFile.ts | 46 +++++ src/lib/http/bodyParsers/JsonParser.ts | 10 +- src/lib/http/bodyParsers/MultiPart.ts | 55 ++++++ src/lib/http/bodyParsers/MultipartParser.ts | 114 ++++++++++- src/lib/http/bodyParsers/UrlEncodedParser.ts | 100 +++++++++- src/lib/http/bodyParsers/XmlParser.ts | 61 ++---- src/lib/http/bodyParsers/parserHelper.ts | 33 +++- src/lib/types/http/IHttpRequest.ts | 6 + src/lib/types/http/IHttpUploadedFile.ts | 16 ++ src/lib/utils/utils.ts | 7 + test/http/HttpRequest.spec.ts | 1 + test/http/bodyParsers/JsonParser.spec.ts | 85 ++++++++ test/http/bodyParsers/MultipartParser.spec.ts | 86 ++++++++ .../http/bodyParsers/UrlEncodedParser.spec.ts | 74 +++++++ test/http/bodyParsers/XmlParser.spec.ts | 93 +++++++++ test/http/bodyParsers/parserHelper.spec.ts | 139 +++++++++++++ 20 files changed, 1068 insertions(+), 86 deletions(-) create mode 100644 src/lib/http/HttpUploadedFile.ts create mode 100644 src/lib/http/bodyParsers/MultiPart.ts create mode 100644 src/lib/types/http/IHttpUploadedFile.ts create mode 100644 test/http/bodyParsers/JsonParser.spec.ts create mode 100644 test/http/bodyParsers/MultipartParser.spec.ts create mode 100644 test/http/bodyParsers/UrlEncodedParser.spec.ts create mode 100644 test/http/bodyParsers/XmlParser.spec.ts create mode 100644 test/http/bodyParsers/parserHelper.spec.ts diff --git a/package-lock.json b/package-lock.json index 52b1c16..84c1c47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lambda-framework", - "version": "1.0.8", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -28,6 +28,11 @@ "integrity": "sha512-HupkFXEv3O3KSzcr3Ylfajg0kaerBg1DyaZzRBBQfrU3NN1mTBRE7sCveqHwXLS5Yrjvww8qFzkzYQQakG9FuQ==", "dev": true }, + "@types/sinon": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-2.3.7.tgz", + "integrity": "sha512-w+LjztaZbgZWgt/y/VMP5BUAWLtSyoIJhXyW279hehLPyubDoBNwvhcj3WaSptcekuKYeTCVxrq60rdLc6ImJA==" + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -167,6 +172,11 @@ "tweetnacl": "0.14.5" } }, + "bindings": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", + "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" + }, "boom": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", @@ -192,6 +202,11 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -417,8 +432,7 @@ "diff": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", - "dev": true + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=" }, "ecc-jsbn": { "version": "0.1.1", @@ -526,6 +540,14 @@ "mime-types": "2.1.17" } }, + "formatio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", + "requires": { + "samsam": "1.3.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -652,8 +674,7 @@ "hoek": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", - "dev": true + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" }, "http-signature": { "version": "1.2.0", @@ -694,6 +715,16 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isemail": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-2.2.1.tgz", + "integrity": "sha1-A1PT2aYpUQgMJiwqoKQrjqjp4qY=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -736,6 +767,23 @@ } } }, + "items": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/items/-/items-2.1.1.tgz", + "integrity": "sha1-i9FtnIOxlSneWuoyGsqtp4NkoZg=" + }, + "joi": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-9.2.0.tgz", + "integrity": "sha1-M4WseQGSEwy+Iw6ALsAskhW7/to=", + "requires": { + "hoek": "4.2.0", + "isemail": "2.2.1", + "items": "2.1.1", + "moment": "2.19.1", + "topo": "2.0.2" + } + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -810,6 +858,11 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.26.tgz", + "integrity": "sha512-IIG0FXHB/XpUZ7vGbktoc2EGsF+fLHJ1tU+vaqoKkVRBwH2FDxLTmkGkSp0XHRp6Y3KGZPIldH1YW8lOluGYrA==" + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -893,6 +946,11 @@ "lodash._isiterateecall": "3.0.9" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -927,6 +985,11 @@ "resolved": "https://registry.npmjs.org/logger/-/logger-0.0.1.tgz", "integrity": "sha1-ywgXH4pvb2dLhJna31C+1L77csQ=" }, + "lolex": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.3.tgz", + "integrity": "sha512-BdHq78SeI+6PAUtl4atDuCt7L6E4fab3mSRtqxm4ywaXe4uP7jZ0TTcFNuU20syUjxZc2l7jFqKVMJ+AX0LnpQ==" + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -1023,17 +1086,63 @@ } } }, + "moment": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz", + "integrity": "sha1-VtoaLRy/AdOLfhr8McELz6GSkWc=" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nise": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.2.0.tgz", + "integrity": "sha512-q9jXh3UNsMV28KeqI43ILz5+c3l+RiNW8mhurEwCKckuHQbL+hTJIKKTiUlCPKlgQ/OukFvSnKB/Jk3+sFbkGA==", + "requires": { + "formatio": "1.2.0", + "just-extend": "1.1.26", + "lolex": "1.6.0", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + }, + "dependencies": { + "lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "node-expat": { + "version": "2.3.16", + "resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.16.tgz", + "integrity": "sha512-e3HyQI0lk5CXyYQ4RsDYGiWdY5LJxNMlNCzo4/gwqY8lhYIeTf5VwGirGDa1EPrcZROmOR37wHuFVnoHmOWnOw==", + "requires": { + "bindings": "1.3.0", + "nan": "2.7.0" + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -2792,8 +2901,12 @@ "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "randomstring": { "version": "1.1.5", @@ -2861,6 +2974,11 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==" + }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", @@ -2889,6 +3007,35 @@ "integrity": "sha1-xUmCuZbHbvDB5rWfvcWCX1txMRM=", "dev": true }, + "sinon": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.0.2.tgz", + "integrity": "sha512-4mUsjHfjrHyPFGDTtNJl0q8cv4VOJGvQykI1r3fnn05ys0sQL9M1Y+DyyGNWLD2PMcoyqjJ/nFDm4K54V1eQOg==", + "requires": { + "diff": "3.2.0", + "formatio": "1.2.0", + "lodash.get": "4.4.2", + "lolex": "2.1.3", + "nise": "1.2.0", + "supports-color": "4.5.0", + "type-detect": "4.0.3" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "sntp": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", @@ -2959,6 +3106,19 @@ "has-flag": "1.0.0" } }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=" + }, + "topo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", + "integrity": "sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI=", + "requires": { + "hoek": "4.2.0" + } + }, "tough-cookie": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", @@ -3071,8 +3231,7 @@ "type-detect": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", - "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", - "dev": true + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=" }, "typeis": { "version": "1.1.1", @@ -3163,6 +3322,16 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "xml2json": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.11.0.tgz", + "integrity": "sha1-HVTx2GjbvQSJK4RdfLrZTFKOFuQ=", + "requires": { + "hoek": "4.2.0", + "joi": "9.2.0", + "node-expat": "2.3.16" + } + }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", diff --git a/package.json b/package.json index d898234..38ea65c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "typescript": "^2.5.2" }, "dependencies": { + "@types/sinon": "^2.3.7", "accepts": "^1.3.4", + "bytes": "^3.0.0", "content-type": "^1.0.4", "cookie": "^0.3.1", "cookie-signature": "^1.0.6", @@ -52,9 +54,13 @@ "logger": "0.0.1", "mime-types": "^2.1.17", "path-to-regexp": "^2.0.0", + "qs": "^6.5.1", + "querystring": "^0.2.0", "randomstring": "^1.1.5", + "sinon": "^4.0.2", "statuses": "^1.3.1", "typeis": "^1.1.1", - "utils-merge": "^1.0.1" + "utils-merge": "^1.0.1", + "xml2json": "^0.11.0" } } diff --git a/src/lib/http/HttpRequest.ts b/src/lib/http/HttpRequest.ts index 250f9ad..97211f4 100644 --- a/src/lib/http/HttpRequest.ts +++ b/src/lib/http/HttpRequest.ts @@ -4,8 +4,9 @@ import * as fresh from "fresh"; 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 INext from "./../types/INext"; -import { mergeParams } from "./../utils/utils"; +import { mergeParams, normalizeType } from "./../utils/utils"; /** * A incoming request created when the event is APIGatewayEvent. @@ -13,6 +14,7 @@ import { mergeParams } from "./../utils/utils"; export default class HttpRequest implements IHttpRequest { public body: { [name: string]: any }|string; + public files: IHttpUploadedFile[]; public basePath: string; public originalBasePath: string; public next: INext; @@ -116,8 +118,24 @@ export default class HttpRequest implements IHttpRequest { } } - public is(types: string|string[]): boolean { - return typeof this.accepts(types) !== "boolean"; + public is(contentTypes: string|string[]): boolean { + let result = false; + const contentTypesArr: string[] = typeof contentTypes === "string" ? [contentTypes] : contentTypes; + const contentType = this.header("content-type"); + + if (!contentType) { + result = true; + } else { + for (const allowContentType of contentTypesArr) { + const normalizedType = normalizeType(allowContentType); + if (contentType.includes(normalizedType)) { + result = true; + break; + } + } + } + + return result; } public fresh(response: IHttpResponse): boolean { diff --git a/src/lib/http/HttpResponse.ts b/src/lib/http/HttpResponse.ts index c9a1a16..249ca0f 100644 --- a/src/lib/http/HttpResponse.ts +++ b/src/lib/http/HttpResponse.ts @@ -3,7 +3,6 @@ import { parse, serialize } from "cookie"; import { sign } from "cookie-signature"; import * as encodeUrl from "encodeurl"; import * as escapeHtml from "escape-html"; -import { lookup } from "mime-types"; import * as statuses from "statuses"; import configuration from "./../configuration/configuration"; import HttpError from "./../exceptions/HttpError"; @@ -13,13 +12,7 @@ import IHttpRequest from "./../types/http/IHttpRequest"; import IHttpResponse from "./../types/http/IHttpResponse"; import IApp from "./../types/IApp"; import INext from "./../types/INext"; -import { merge, setCharset, stringify } from "./../utils/utils"; - -const normalizeType = (type: string): string => { - return type.indexOf("/") === -1 - ? lookup(type) - : type; -}; +import { merge, normalizeType, setCharset, stringify } from "./../utils/utils"; /** * This class represents an HTTP response, with the helpers to be sent. diff --git a/src/lib/http/HttpUploadedFile.ts b/src/lib/http/HttpUploadedFile.ts new file mode 100644 index 0000000..4ab922b --- /dev/null +++ b/src/lib/http/HttpUploadedFile.ts @@ -0,0 +1,46 @@ +import IHttpUploadedFile from "./../types/http/IHttpUploadedFile"; + +/** + * This class represents an uploaded file. + */ +export default class HttpUploadedFile implements IHttpUploadedFile { + + private _contentType: string; + + private _length: number; + + private _fileName: string; + + private _content: string; + + private _headers: {[name: string]: string}; + + constructor(contentType: string, length: number, fileName: string, content: string, headers: {[name: string]: string}) { + this._contentType = contentType; + this._length = length; + this._fileName = fileName; + this._content = content; + this._headers = headers; + } + + get contentType(): string { + return this._contentType; + } + + get length(): number { + return this._length; + } + + get fileName(): string { + return this._fileName; + } + + get content(): string { + return this._content; + } + + get headers(): {[name: string]: string} { + return this._headers; + } + +} diff --git a/src/lib/http/bodyParsers/JsonParser.ts b/src/lib/http/bodyParsers/JsonParser.ts index e013779..5577543 100644 --- a/src/lib/http/bodyParsers/JsonParser.ts +++ b/src/lib/http/bodyParsers/JsonParser.ts @@ -1,5 +1,6 @@ import IBodyParser from "./../../types/http/IBodyParser"; import IHttpHandler from "./../../types/http/IHttpHandler"; +import IHttpRequest from "./../../types/http/IHttpRequest"; import parserHelper from "./parserHelper"; /** @@ -8,9 +9,12 @@ import parserHelper from "./parserHelper"; export default class JsonParser implements IBodyParser { public create(reviver?: (key: string, value: string) => any): IHttpHandler { - return parserHelper((initialBody: string) => { - return JSON.parse(initialBody, reviver); - }); + return parserHelper( + (initialBody: string, req: IHttpRequest): void => { + req.body = JSON.parse(initialBody, reviver); + }, + ["application/json"] + ); } } diff --git a/src/lib/http/bodyParsers/MultiPart.ts b/src/lib/http/bodyParsers/MultiPart.ts new file mode 100644 index 0000000..c1faebc --- /dev/null +++ b/src/lib/http/bodyParsers/MultiPart.ts @@ -0,0 +1,55 @@ +const CONTENT_DISPOSITION = "content-disposition"; +const CONTENT_TYPE = "content-type"; +const CONTENT_LENGHT = "content-length"; + +/** + * A part of a multipart form. + */ +export default class MultiPart { + + public headers: {[name: string]: {[name: string]: string}}; + + public endOfHeader: boolean; + + public value: string; + + constructor() { + this.headers = {}; + this.endOfHeader = false; + } + + public isFile(): boolean { + return this.headers[CONTENT_DISPOSITION] != null && this.headers[CONTENT_DISPOSITION].filename != null; + } + + public getContentType(): string { + return this.headers[CONTENT_TYPE] ? this.headers[CONTENT_TYPE].$ : null; + } + + public getLength(): number { + return this.headers[CONTENT_LENGHT] && this.headers[CONTENT_LENGHT].$ ? parseInt(this.headers[CONTENT_LENGHT].$, 10) : null; + } + + public getFileName(): string { + return this.headers[CONTENT_DISPOSITION] ? this.headers[CONTENT_DISPOSITION].filename : null; + } + + public getName(): string { + return this.headers[CONTENT_DISPOSITION] ? this.headers[CONTENT_DISPOSITION].name : null; + } + + public getContent(): string { + return this.value; + } + + public getHeaders(): {[name: string]: string} { + const result: {[name: string]: string} = {}; + + Object.keys(this.headers).forEach((k) => { + result[k] = this.headers[k].$; + }); + + return result; + } + +} diff --git a/src/lib/http/bodyParsers/MultipartParser.ts b/src/lib/http/bodyParsers/MultipartParser.ts index f3db1cb..3bd1259 100644 --- a/src/lib/http/bodyParsers/MultipartParser.ts +++ b/src/lib/http/bodyParsers/MultipartParser.ts @@ -1,15 +1,123 @@ import IBodyParser from "./../../types/http/IBodyParser"; import IHttpHandler from "./../../types/http/IHttpHandler"; +import IHttpRequest from "./../../types/http/IHttpRequest"; +import HttpUploadedFile from "./../HttpUploadedFile"; +import MultiPart from "./MultiPart"; +import parserHelper from "./parserHelper"; + +const getBoundary = (header: string): string => { + const items: string[] = header ? header.split(";") : null; + let result: string = null; + if (items) { + for (let item of items) { + item = item.trim(); + if (item.indexOf("boundary") >= 0) { + const k = item.split("="); + result = k[1].trim(); + } + } + } + return result; +}; + +const getHeaderValue = (headerValue: string): {[name: string]: string} => { + const headerParts: string[] = headerValue.split(";"); + const result: {[name: string]: string} = {}; + + headerParts.forEach((headerPart: string) => { + const headerPartParts: string[] = headerPart.trim().split("="); + + if (headerPartParts.length === 1) { + result.$ = headerPartParts[0]; + } else if (headerPartParts.length > 1) { + const key = headerPartParts[0].trim().toLowerCase(); + + let value = headerPart.trim().substring(key.length + 1); + if (value[0] === "\"") { + value = value.substring(1); + } + if (value[value.length - 1] === "\"") { + value = value.substring(0, value.length - 1); + } + result[key] = value; + } + }); + + return result; +}; + +const getParts = (body: string, boundary: string): MultiPart[] => { + const parts: MultiPart[] = []; + const lines: string[] = body.split(/\n|\r\n/); + + let part: MultiPart; + for (const line of lines) { + if (line === "--" + boundary) { + if (part) { + parts.push(part); + } + part = new MultiPart(); + } else if (part) { + if (line === "") { + part.endOfHeader = true; + } else if (line === "--" + boundary + "--") { + parts.push(part); + break; + } else if (!part.endOfHeader) { + const headerParts: string[] = line.split(":"); + const headerKey: string = headerParts[0].toLowerCase(); + const headerValue: string = line.substring(headerKey.length + 1).trim(); + + part.headers[headerKey] = getHeaderValue(headerValue); + } else if (part.value) { + part.value += "\n" + line; + } else { + part.value = line; + } + } + } + + return parts; +}; + +const formParse = (initialBody: string, req: IHttpRequest): void => { + const contentType = req.header("content-type"); + const boundary = contentType ? getBoundary(contentType) : null; + + if (!boundary) { + throw new Error("No boundary found. Can not parse the body."); + } + + const parts: MultiPart[] = getParts(initialBody, boundary); + req.body = {}; + req.files = []; + + parts.forEach((part) => { + if (part.isFile()) { + req.files.push(new HttpUploadedFile(part.getContentType(), part.getLength(), part.getFileName(), part.getContent(), part.getHeaders())); + } else { + req.body[part.getName()] = part.value; + } + }); +}; /** * A layer that set the request body depending of its type. */ export default class MultipartParser implements IBodyParser { - public create(): IHttpHandler { - return (req, res, next) => { + public create(options?: {[name: string]: any}): IHttpHandler { + const opts = options || {}; + + // initialize options + const contentType = opts.type || "multipart/form-data"; - }; + return parserHelper( + (initialBody: string, req: IHttpRequest): void => { + formParse(initialBody, req); + }, + [contentType] + ); } } diff --git a/src/lib/http/bodyParsers/UrlEncodedParser.ts b/src/lib/http/bodyParsers/UrlEncodedParser.ts index 5ef101b..3980ce3 100644 --- a/src/lib/http/bodyParsers/UrlEncodedParser.ts +++ b/src/lib/http/bodyParsers/UrlEncodedParser.ts @@ -1,15 +1,109 @@ +import * as qs from "qs"; +import * as querystring from "querystring"; +import HttpError from "./../../exceptions/HttpError"; import IBodyParser from "./../../types/http/IBodyParser"; import IHttpHandler from "./../../types/http/IHttpHandler"; +import IHttpRequest from "./../../types/http/IHttpRequest"; +import parserHelper from "./parserHelper"; + +const parameterCount = (body: string, limit: number): number => { + let count = 0; + let index = 0; + + while (index !== -1) { + count += 1; + index += 1; + + if (count === limit) { + return undefined; + } + + index = body.indexOf("&", index); + } + + return count; +}; + +const extendedParser = (options: {[name: string]: any}): (body: string) => {[name: string]: string} => { + let parameterLimit = options.parameterLimit !== undefined + ? options.parameterLimit + : 1000; + const parse = qs.parse; + + if (isNaN(parameterLimit) || parameterLimit < 1) { + throw new TypeError("Option parameterLimit must be a positive number."); + } + + if (isFinite(parameterLimit)) { + parameterLimit = Number.MAX_SAFE_INTEGER; + } + + return (body: string): {[name: string]: string} => { + const paramCount = parameterCount(body, parameterLimit); + + if (paramCount === undefined) { + throw new HttpError("Too many parameters.", 413); + } + + const arrayLimit = Math.max(100, paramCount); + + return parse(body, { + allowPrototypes: true, + arrayLimit, + depth: Infinity, + parameterLimit + }); + }; +}; + +const simpleParser = (options: {[name: string]: any}): (body: string) => {[name: string]: string} => { + let parameterLimit = options.parameterLimit !== undefined + ? options.parameterLimit + : 1000; + const parse = querystring.parse; + + if (isNaN(parameterLimit) || parameterLimit < 1) { + throw new TypeError("Option parameterLimit must be a positive number."); + } + + if (isFinite(parameterLimit)) { + parameterLimit = Number.MAX_SAFE_INTEGER; + } + + return (body: string): {[name: string]: string} => { + const paramCount = parameterCount(body, parameterLimit); + + if (paramCount === undefined) { + throw new HttpError("Too many parameters.", 413); + } + + return parse(body, undefined, undefined, {maxKeys: parameterLimit}); + }; +}; /** * A layer that set the request body depending of its type. */ export default class UrlEncodedParser implements IBodyParser { - public create(): IHttpHandler { - return (req, res, next) => { + public create(options?: {[name: string]: any}): IHttpHandler { + const opts = options || {}; + + // initialize options + const extended = opts.extended !== false; + const contentType = opts.type || "application/x-www-form-urlencoded"; + + // create the appropriate query parser + const queryParse = extended + ? extendedParser(opts) + : simpleParser(opts); - }; + return parserHelper( + (initialBody: string, req: IHttpRequest): void => { + req.body = queryParse(initialBody); + }, + [contentType] + ); } } diff --git a/src/lib/http/bodyParsers/XmlParser.ts b/src/lib/http/bodyParsers/XmlParser.ts index aec2c2d..c2114f8 100644 --- a/src/lib/http/bodyParsers/XmlParser.ts +++ b/src/lib/http/bodyParsers/XmlParser.ts @@ -1,60 +1,27 @@ +import * as xml2json from "xml2json"; import IBodyParser from "./../../types/http/IBodyParser"; import IHttpHandler from "./../../types/http/IHttpHandler"; +import IHttpRequest from "./../../types/http/IHttpRequest"; import parserHelper from "./parserHelper"; -// Changes String to XML -const xmlToJsonFromString = (initialBody: string, contentType: string): { [name: string]: any } => { - const parser = new DOMParser(); - const xml = parser.parseFromString(initialBody, contentType || "text/xml"); - - return xmlToJson(xml); -} - -// Changes XML to JSON -const xmlToJson = (xml): { [name: string]: any } => { - // Create the return object - var obj = {}; - - if (xml.nodeType == 1) { // element - // do attributes - if (xml.attributes.length > 0) { - obj["@attributes"] = {}; - for (var j = 0; j < xml.attributes.length; j++) { - var attribute = xml.attributes.item(j); - obj["@attributes"][attribute.nodeName] = attribute.nodeValue; - } - } - } else if (xml.nodeType == 3) { // text - obj = xml.nodeValue; - } - - // do children - if (xml.hasChildNodes()) { - for(var i = 0; i < xml.childNodes.length; i++) { - var item = xml.childNodes.item(i); - var nodeName = item.nodeName; - if (typeof(obj[nodeName]) == "undefined") { - obj[nodeName] = xmlToJson(item); - } else { - if (typeof(obj[nodeName].push) == "undefined") { - var old = obj[nodeName]; - obj[nodeName] = []; - obj[nodeName].push(old); - } - obj[nodeName].push(xmlToJson(item)); - } - } - } - return obj; -}; - /** * A layer that set the request body depending of its type. */ export default class XmlParser implements IBodyParser { public create(): IHttpHandler { - return parserHelper(xmlToJsonFromString); + return parserHelper( + (body: string, req: IHttpRequest): void => { + const result: {[name: string]: string} = JSON.parse(xml2json.toJson(body)); + + if (Object.keys(result).length === 0) { + throw new Error("XML can not be parsed."); + } + + req.body = result; + }, + ["text/xml"] + ); } } diff --git a/src/lib/http/bodyParsers/parserHelper.ts b/src/lib/http/bodyParsers/parserHelper.ts index 8919955..8b8d625 100644 --- a/src/lib/http/bodyParsers/parserHelper.ts +++ b/src/lib/http/bodyParsers/parserHelper.ts @@ -1,20 +1,35 @@ +import HttpError from "./../../exceptions/HttpError"; import IHttpHandler from "./../../types/http/IHttpHandler"; import IHttpRequest from "./../../types/http/IHttpRequest"; import IHttpResponse from "./../../types/http/IHttpResponse"; -import HttpError from "./../../exceptions/HttpError"; import INext from "./../../types/INext"; -const parserHelper = (func: (body: string, contentType?: string) => { [name: string]: any }|string): IHttpHandler => { +/** + * Abtraction of the parser conditions and fallback mechanism. + * + * @param {IHttpRequest} req + * @param {IHttpResponse} res + * @param {INext} next + * @return {[type]} + */ +const parserHelper = (func: (body: string, req: IHttpRequest) => void, allowContentTypes?: string[]): IHttpHandler => { return (req: IHttpRequest, res: IHttpResponse, next: INext) => { let error; - if(req.body) { - const contentType = req.header("content-type"); - try { - req.body = func(req.event.body, contentType); - } catch(e) { - if(contentType) { - error = new HttpError("Body can not be parsed.", 400); + if (req.body && !req.context._previouslyBodyParsed) { + if (!allowContentTypes || req.is(allowContentTypes)) { + const contentType = req.header("content-type"); + try { + func(req.event.body, req); + + req.context._previouslyBodyParsed = true; + } catch (e) { + console.log("400 " + req.method + " " + req.path + ": Body can not be parsed: " + req.body, e); + if (e instanceof HttpError) { + error = e; + } else if (contentType) { + error = new HttpError("Body can not be parsed.", 400); + } } } } diff --git a/src/lib/types/http/IHttpRequest.ts b/src/lib/types/http/IHttpRequest.ts index 62e4e60..46a0bc1 100644 --- a/src/lib/types/http/IHttpRequest.ts +++ b/src/lib/types/http/IHttpRequest.ts @@ -2,6 +2,7 @@ import { APIGatewayEvent } from "aws-lambda"; import INext from "./../INext"; import IHttpResponse from "./IHttpResponse"; import IHttpRoute from "./IHttpRoute"; +import IHttpUploadedFile from "./IHttpUploadedFile"; /** * A incoming request created when the event is APIGatewayEvent. @@ -38,6 +39,11 @@ export default interface IHttpRequest { */ body: object|string; + /** + * The files uplodad in the request. + */ + files: IHttpUploadedFile[]; + /** * The parsed path of the request. */ diff --git a/src/lib/types/http/IHttpUploadedFile.ts b/src/lib/types/http/IHttpUploadedFile.ts new file mode 100644 index 0000000..ec6a486 --- /dev/null +++ b/src/lib/types/http/IHttpUploadedFile.ts @@ -0,0 +1,16 @@ +/** + * This class represents an uploaded file. + */ +export default interface IHttpUploadedFile { + + readonly contentType: string; + + readonly length: number; + + readonly fileName: string; + + readonly content: string; + + readonly headers: {[name: string]: string}; + +} diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts index 33ffc92..d11a673 100644 --- a/src/lib/utils/utils.ts +++ b/src/lib/utils/utils.ts @@ -1,5 +1,6 @@ import { APIGatewayEvent } from "aws-lambda"; import { format, parse } from "content-type"; +import { lookup } from "mime-types"; /** * Utils functions shared between multiple classes. @@ -113,3 +114,9 @@ export function stringify(value: {}, replacer: (string[]|number[]), spaces: stri return json; } + +export function normalizeType(type: string): string { + return type.indexOf("/") === -1 + ? lookup(type) + : type; +} diff --git a/test/http/HttpRequest.spec.ts b/test/http/HttpRequest.spec.ts index e188ebd..df5dbee 100644 --- a/test/http/HttpRequest.spec.ts +++ b/test/http/HttpRequest.spec.ts @@ -23,6 +23,7 @@ describe('HttpRequest', () => { header2: 'HEADER VALU 2', 'X-Forwarded-Proto': 'https', 'Host': 'localhost', + 'Content-Type': 'application/json,text/html', 'Accept': 'application/json,text/html', 'Accept-Encoding': 'gzip, deflate', 'Accept-Charset': 'UTF-8, ISO-8859-1', diff --git a/test/http/bodyParsers/JsonParser.spec.ts b/test/http/bodyParsers/JsonParser.spec.ts new file mode 100644 index 0000000..415fc08 --- /dev/null +++ b/test/http/bodyParsers/JsonParser.spec.ts @@ -0,0 +1,85 @@ +import * as Chai from "chai"; +import { spy, SinonSpy } from "sinon"; +import JsonParser from "./../../../src/lib/http/bodyParsers/JsonParser"; +import HttpRequest from "./../../../src/lib/http/HttpRequest"; +import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; +import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; +import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; + +const mainEvent: any = { + body: "{\"param1\": \"value1\"}", + headers: { + "Content-Type": "application/json" + }, + httpMethod: "POST", + isBase64Encoded: true, + path: "/blog", + resource: "API" +}; + +/** + * Test for JsonParser. + */ +describe("JsonParser", () => { + const res: IHttpResponse = {}; + let next: SinonSpy; + let event: any; + const handler: IHttpHandler = (new JsonParser()).create(); + + beforeEach(() => { + event = Object.assign({}, mainEvent); + event.headers = Object.assign({}, mainEvent.headers); + next = spy(); + }); + + it("should call 'next' with a 400 error if the body can not be parsed and header contentType is 'application/json'.", () => { + event.body = "errorBody"; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.not.undefined; + Chai.expect(next.args[0][0].statusCode).to.be.equal(400); + }); + + it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { + event.body = "errorBody"; + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.undefined; + }); + + it("should set the body with the parsed body as an object if header contentType is 'application/json'.", () => { + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal({param1: "value1"}); + }); + + it("should set the body with the parsed body as an object if header contentType is undefined.", () => { + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal({param1: "value1"}); + }); + + it("should NOT set the body if header contentType is 'text/html'.", () => { + event.headers["Content-Type"] = "text/html"; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.equal(event.body); + }); +}); diff --git a/test/http/bodyParsers/MultipartParser.spec.ts b/test/http/bodyParsers/MultipartParser.spec.ts new file mode 100644 index 0000000..8f8b2ef --- /dev/null +++ b/test/http/bodyParsers/MultipartParser.spec.ts @@ -0,0 +1,86 @@ +import * as Chai from "chai"; +import { spy, SinonSpy } from "sinon"; +import MultipartParser from "./../../../src/lib/http/bodyParsers/MultipartParser"; +import HttpUploadedFile from "./../../../src/lib/http/HttpUploadedFile"; +import HttpRequest from "./../../../src/lib/http/HttpRequest"; +import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; +import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; +import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; +import IHttpUploadedFile from "./../../../src/lib/types/http/IHttpUploadedFile"; + +const mainEvent: any = { + body: "------WebKitFormBoundaryvef1fLxmoUdYZWXp\n" + + "Content-Disposition: form-data; name=\"text\"\n" + + "\n" + + "text default\n" + + "------WebKitFormBoundaryvef1fLxmoUdYZWXp\n" + + "Content-Disposition: form-data; name=\"file\"; filename=\"A.txt\"\n" + + "Content-Type: text/plain\n" + + "Content-Length: 17\n" + + "Other-Header: Other\n" + + "\n" + + "file text default\n" + + "------WebKitFormBoundaryvef1fLxmoUdYZWXp--", + headers: { + "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryvef1fLxmoUdYZWXp" + }, + httpMethod: "POST", + isBase64Encoded: true, + path: "/blog", + resource: "API" +}; + +/** + * Test for MultipartParser. + */ +describe("MultipartParser", () => { + const res: IHttpResponse = {}; + let next: SinonSpy; + let event: any; + const handler: IHttpHandler = (new MultipartParser()).create(); + + beforeEach(() => { + event = Object.assign({}, mainEvent); + event.headers = Object.assign({}, mainEvent.headers); + next = spy(); + }); + + it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { + event.body = "errorBody"; + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.undefined; + }); + + it("should set the body with the parsed body as an object if header contentType is 'multipart/form-data'.", () => { + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + const expectedFiles: IHttpUploadedFile[] = []; + expectedFiles.push(new HttpUploadedFile("text/plain", 17, "A.txt", "file text default", { + "content-disposition": "form-data", + "content-type": "text/plain", + "content-length": "17", + "other-header": "Other" + })); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal({text: "text default"}); + Chai.expect(req.files).to.be.deep.equal(expectedFiles); + }); + + it("should NOT set the body if header contentType is 'text/html'.", () => { + event.headers["Content-Type"] = "text/html"; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.equal(event.body); + }); +}); diff --git a/test/http/bodyParsers/UrlEncodedParser.spec.ts b/test/http/bodyParsers/UrlEncodedParser.spec.ts new file mode 100644 index 0000000..eae3618 --- /dev/null +++ b/test/http/bodyParsers/UrlEncodedParser.spec.ts @@ -0,0 +1,74 @@ +import * as Chai from "chai"; +import { spy, SinonSpy } from "sinon"; +import UrlEncodedParser from "./../../../src/lib/http/bodyParsers/UrlEncodedParser"; +import HttpRequest from "./../../../src/lib/http/HttpRequest"; +import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; +import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; +import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; + +const mainEvent: any = { + body: "param1=Value1¶m2=value2", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + httpMethod: "POST", + isBase64Encoded: true, + path: "/blog", + resource: "API" +}; + +/** + * Test for UrlEncodedParser. + */ +describe("UrlEncodedParser", () => { + const res: IHttpResponse = {}; + let next: SinonSpy; + let event: any; + const handler: IHttpHandler = (new UrlEncodedParser()).create(); + + beforeEach(() => { + event = Object.assign({}, mainEvent); + event.headers = Object.assign({}, mainEvent.headers); + next = spy(); + }); + + it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { + event.body = "errorBody"; + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.undefined; + }); + + it("should set the body with the parsed body as an object if header contentType is 'application/x-www-form-urlencoded'.", () => { + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal({param1: "Value1", param2: "value2"}); + }); + + it("should set the body with the parsed body as an object if header contentType is undefined.", () => { + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal({param1: "Value1", param2: "value2"}); + }); + + it("should NOT set the body if header contentType is 'text/html'.", () => { + event.headers["Content-Type"] = "text/html"; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.equal(event.body); + }); +}); diff --git a/test/http/bodyParsers/XmlParser.spec.ts b/test/http/bodyParsers/XmlParser.spec.ts new file mode 100644 index 0000000..30a3658 --- /dev/null +++ b/test/http/bodyParsers/XmlParser.spec.ts @@ -0,0 +1,93 @@ +import * as Chai from "chai"; +import { spy, SinonSpy } from "sinon"; +import XmlParser from "./../../../src/lib/http/bodyParsers/XmlParser"; +import HttpRequest from "./../../../src/lib/http/HttpRequest"; +import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; +import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; +import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; + +const mainEvent: any = { + body: "value1", + headers: { + "Content-Type": "text/xml" + }, + httpMethod: "POST", + isBase64Encoded: true, + path: "/blog", + resource: "API" +}; + +/** + * Test for XmlParser. + */ +describe("XmlParser", () => { + const expectedBody = { + root: { + tag1: { + "$t": "value1", + "attribute1": "valueAttribute1" + } + } + }; + const res: IHttpResponse = {}; + let next: SinonSpy; + let event: any; + const handler: IHttpHandler = (new XmlParser()).create(); + + beforeEach(() => { + event = Object.assign({}, mainEvent); + event.headers = Object.assign({}, mainEvent.headers); + next = spy(); + }); + + it("should call 'next' with a 400 error if the body can not be parsed and header contentType is 'text/xml'.", () => { + event.body = "errorBody"; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.not.undefined; + Chai.expect(next.args[0][0].statusCode).to.be.equal(400); + }); + + it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { + event.body = "errorBody"; + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.undefined; + }); + + it("should set the body with the parsed body as an object if header contentType is 'text/xml'.", () => { + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal(expectedBody); + }); + + it("should set the body with the parsed body as an object if header contentType is undefined.", () => { + event.headers["Content-Type"] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.deep.equal(expectedBody); + }); + + it("should NOT set the body if header contentType is 'text/html'.", () => { + event.headers["Content-Type"] = "text/html"; + const req: IHttpRequest = new HttpRequest(event); + + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(req.body).to.be.equal(event.body); + }); +}); diff --git a/test/http/bodyParsers/parserHelper.spec.ts b/test/http/bodyParsers/parserHelper.spec.ts new file mode 100644 index 0000000..e0b8b42 --- /dev/null +++ b/test/http/bodyParsers/parserHelper.spec.ts @@ -0,0 +1,139 @@ +import * as Chai from "chai"; +import { spy, SinonSpy } from "sinon"; +import parserHelper from "./../../../src/lib/http/bodyParsers/parserHelper"; +import HttpRequest from "./../../../src/lib/http/HttpRequest"; +import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; +import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; +import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; + +const mainEvent: any = { + body: "body", + headers: { + "Content-Type": "application/json" + }, + httpMethod: "POST", + isBase64Encoded: true, + path: "/blog", + resource: "API" +}; + +/** + * Test for parserHelper. + */ +describe("parserHelper", () => { + const body: { [name: string]: any } = { + "param1": "value1" + }; + const res: IHttpResponse = {}; + let next: SinonSpy; + let event: any; + + beforeEach(() => { + event = Object.assign({}, mainEvent); + event.headers = Object.assign({}, mainEvent.headers); + next = spy(); + }); + + it("should call 'next' WITHOUT an error if the body has been previously parsed.", () => { + const parser: SinonSpy = spy(() => { + return body; + }); + + const req: IHttpRequest = new HttpRequest(event); + + const handler: IHttpHandler = parserHelper(parser); + + handler(req, res, next); + handler(req, res, next); + + Chai.expect(parser.calledOnce).to.be.true; + Chai.expect(next.calledTwice).to.be.true; + Chai.expect(next.args[1][0]).to.be.undefined; + }); + + it("should call 'next' WITHOUT an error if the body does not exist.", () => { + event.body = undefined; + const req: IHttpRequest = new HttpRequest(event); + + const parser: SinonSpy = spy(); + + const handler: IHttpHandler = parserHelper(parser); + handler(req, res, next); + + Chai.expect(parser.called).to.be.false; + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.undefined; + }); + + it("should call 'next' with a 400 error if the body can not be parsed and header contentType is NOT undefined.", () => { + const req: IHttpRequest = new HttpRequest(event); + + const handler: IHttpHandler = parserHelper(() => { + throw new Error; + }); + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.not.undefined; + Chai.expect(next.args[0][0].statusCode).to.be.equal(400); + }); + + it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { + event.headers['Content-Type'] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + const handler: IHttpHandler = parserHelper(() => { + throw new Error; + }); + handler(req, res, next); + + Chai.expect(next.called).to.be.true; + Chai.expect(next.args[0][0]).to.be.undefined; + }); + + it("should execute the parser function and set the request body with the returned value if the header contentType is the given one in params.", () => { + const req: IHttpRequest = new HttpRequest(event); + + const parser: SinonSpy = spy(); + + const handler: IHttpHandler = parserHelper(parser, ["application/json"]); + handler(req, res, next); + + Chai.expect(parser.called).to.be.true; + }); + + it("should execute the parser function and set the request body with the returned value if no contentType is given in params.", () => { + const req: IHttpRequest = new HttpRequest(event); + + const parser: SinonSpy = spy(); + + const handler: IHttpHandler = parserHelper(parser); + handler(req, res, next); + + Chai.expect(parser.called).to.be.true; + }); + + it("should execute the parser function and set the request body with the returned value if the header contentType is undefined.", () => { + event.headers['Content-Type'] = undefined; + const req: IHttpRequest = new HttpRequest(event); + + const parser: SinonSpy = spy(); + + const handler: IHttpHandler = parserHelper(parser, ["application/json"]); + handler(req, res, next); + + Chai.expect(parser.called).to.be.true; + }); + + it("should call 'next' WITHOUT execute the parser function otherwise.", () => { + const req: IHttpRequest = new HttpRequest(event); + + const parser: SinonSpy = spy(); + + const handler: IHttpHandler = parserHelper(parser, ["text/html"]); + handler(req, res, next); + + Chai.expect(parser.called).to.be.false; + Chai.expect(next.called); + }); +}); From 1eddc552e71087a8cfb9b403720d55a92e29d90a Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 20:50:12 +0200 Subject: [PATCH 4/9] Added parser class. --- src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.ts b/src/index.ts index 260220b..6787677 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,3 +26,9 @@ 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 IBodyParser } from "./lib/types/http/IBodyParser"; +export { default as JsonParser } from "./lib/http/bodyParsers/JsonParser"; +export { default as MultiPartParser } from "./lib/http/bodyParsers/MultiPartParser"; +export { default as UrlEncodedParser } from "./lib/http/bodyParsers/UrlEncodedParser"; +export { default as XmlParser } from "./lib/http/bodyParsers/XmlParser"; From e789df86ace5b217114caf4c9cd8cfcd1efe33c0 Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 20:50:22 +0200 Subject: [PATCH 5/9] Changed use method to the same that in router. --- src/lib/App.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/App.ts b/src/lib/App.ts index 97ea263..6473466 100644 --- a/src/lib/App.ts +++ b/src/lib/App.ts @@ -74,7 +74,7 @@ export default class App implements IApp { } } - public use(path?: string, ...handler: IHttpHandler[]): IApp { + public use(handler: IHttpHandler|IHttpHandler[], path?: string): IApp { this._router.use(handler, path); return this; From db5ce66ba88b7d74d32006dbe3908d995e8ac926 Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 20:50:38 +0200 Subject: [PATCH 6/9] Changed use method to the same that in router. --- src/lib/types/IApp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/types/IApp.ts b/src/lib/types/IApp.ts index d18bdb5..e6c111b 100644 --- a/src/lib/types/IApp.ts +++ b/src/lib/types/IApp.ts @@ -68,10 +68,10 @@ export default interface IApp { * functions even if they could respond. * * @param {string} path - * @param {IHttpHandler[]} ...handler + * @param {IHttpHandler[]} handlers * @return {IRouter} */ - use(path?: string, ...handler: IHttpHandler[]): IApp; + use(handler: IHttpHandler|IHttpHandler[], path?: string): IApp; /** * Mount router in the given path. If no path is given, the default path From 0da230190256b75aafe8f824ce7ea56aaca569c2 Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 20:50:48 +0200 Subject: [PATCH 7/9] LINT name fixed. --- src/lib/http/bodyParsers/MultipartParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/http/bodyParsers/MultipartParser.ts b/src/lib/http/bodyParsers/MultipartParser.ts index 3bd1259..dabbd9e 100644 --- a/src/lib/http/bodyParsers/MultipartParser.ts +++ b/src/lib/http/bodyParsers/MultipartParser.ts @@ -104,7 +104,7 @@ const formParse = (initialBody: string, req: IHttpRequest): void => { /** * A layer that set the request body depending of its type. */ -export default class MultipartParser implements IBodyParser { +export default class MultiPartParser implements IBodyParser { public create(options?: {[name: string]: any}): IHttpHandler { const opts = options || {}; From 488cbbc3dd1497bc806ac34676787f07802d1bff Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 20:53:26 +0200 Subject: [PATCH 8/9] Added lambda types to pro dependencies. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38ea65c..4e2c470 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "author": "Rogelio Orts", "license": "MIT", "devDependencies": { - "@types/aws-lambda": "0.0.16", "@types/chai": "^4.0.4", "@types/mocha": "^2.2.43", "@types/node": "^8.0.28", @@ -42,6 +41,7 @@ "typescript": "^2.5.2" }, "dependencies": { + "@types/aws-lambda": "0.0.16", "@types/sinon": "^2.3.7", "accepts": "^1.3.4", "bytes": "^3.0.0", From 76d22f592d9ed8f2c00aac9f73b41083d62b068d Mon Sep 17 00:00:00 2001 From: rogelio-o Date: Sat, 28 Oct 2017 21:29:14 +0200 Subject: [PATCH 9/9] Renamed multipart. --- package-lock.json | 3 +-- src/index.ts | 2 +- src/lib/http/bodyParsers/MultipartParser.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84c1c47..c6b1b58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,7 @@ "@types/aws-lambda": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-0.0.16.tgz", - "integrity": "sha512-sk0LAN9NJGdmlcvUvtzsm6azYLqZArMiRnjC4TaHghczJjEcryIaEeuG1C/OQDBMZDvSLn/TTHKhY+s7jJXwyg==", - "dev": true + "integrity": "sha512-sk0LAN9NJGdmlcvUvtzsm6azYLqZArMiRnjC4TaHghczJjEcryIaEeuG1C/OQDBMZDvSLn/TTHKhY+s7jJXwyg==" }, "@types/chai": { "version": "4.0.4", diff --git a/src/index.ts b/src/index.ts index 6787677..67ab341 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,6 @@ export { default as IHttpRouterExecutor } from "./lib/types/http/IHttpRouterExec export { default as IBodyParser } from "./lib/types/http/IBodyParser"; export { default as JsonParser } from "./lib/http/bodyParsers/JsonParser"; -export { default as MultiPartParser } from "./lib/http/bodyParsers/MultiPartParser"; +export { default as MultipartParser } from "./lib/http/bodyParsers/MultipartParser"; export { default as UrlEncodedParser } from "./lib/http/bodyParsers/UrlEncodedParser"; export { default as XmlParser } from "./lib/http/bodyParsers/XmlParser"; diff --git a/src/lib/http/bodyParsers/MultipartParser.ts b/src/lib/http/bodyParsers/MultipartParser.ts index dabbd9e..3bd1259 100644 --- a/src/lib/http/bodyParsers/MultipartParser.ts +++ b/src/lib/http/bodyParsers/MultipartParser.ts @@ -104,7 +104,7 @@ const formParse = (initialBody: string, req: IHttpRequest): void => { /** * A layer that set the request body depending of its type. */ -export default class MultiPartParser implements IBodyParser { +export default class MultipartParser implements IBodyParser { public create(options?: {[name: string]: any}): IHttpHandler { const opts = options || {};