Http messages
Intro
First of all I want to get my http request and parse it, inside a Request
there is some important field:
- body
- header
- method
- cookies
- url
- uploaded files
In the Response
we’ll need:
- status code
- body
- headers
Between the response and the request we have fields in common and that will be the field in the Message
class:
- body
- headers
All the headers will be stored in lower cases, based on RFC 2616 the headers name are case insensitive.
abstract class Message {
private body: string;
private headers: Map<string, string> = new Map<string, string>();
private protocol: string = '1.1';
constructor() {
this.body = '';
}
getBody() {
return this.body;
}
setBody(body: string) {
this.body = body;
}
getHeaders(): Map<string, string> {
return this.headers;
}
hasHeader(name: string) {
return this.headers.has(name.toLowerCase());
}
addHeader(name: string, value: string) {
this.headers.set(name.toLowerCase(), value);
}
removeHeader(name: string) {
this.headers.delete(name.toLowerCase());
}
getHeader(name: string) {
return this.headers.get(name.toLowerCase());
}
getProtocol() {
this.protocol;
}
}
export { Message }
Request
The Request
will extends the Message
, and we will add the fields:
- method
- cookies
- uri
- bodyParsed
- files
To fill theses attributes we will use the class IncomingMessage
.
Method
The method is eady to get, it will be on the method
attribute.
this.method = this.request.method ?? '';
Cookies
To loads the cookies
private loadCookies(req: IncomingMessage) {
let cookies = this.getHeader('Cookie');
if (cookies === undefined) {
cookies = this.getHeader('cookie');
}
if (cookies !== undefined) {
cookies.split(';')
.map(v => v.split('='))
.map(cookie => this.addCookie(
decodeURIComponent(cookie[0].trim()),
decodeURIComponent(cookie[1].trim())
));
}
}
Uri
For that part we will need to create a Uri
class, that will parse an Uri.
Body
The body of a request can be a lot of things, so we will need to do different parsing based of the content type.
But first we’ll need to retreive the raw body, to do so I changed the requestListener
method inside the Server
class
requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {
let request = new Request(req);
let self = this;
var body = "";
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function () {
request.updateBody(body);
self.router.handle(request, res);
});
}
Then inside the Request
class I made a method to load and parse the body.
public updateBody(body: string) {
this.setBody(body);
this.loadBody();
}
private loadBody() {
let contentType = this.getHeader('Content-Type');
if (contentType !== undefined && this.getBody() !== null) {
switch (contentType) {
case 'application/x-www-form-urlencoded':
this.processFormUrlEncoded();
break;
case 'application/json':
this.processJson();
break;
default:
//Test multipart/form-data
let regex = new RegExp('multipart/form-data; boundary=(?<boundary>.*)$');
let result = regex.exec(contentType);
if (result) {
let boundary = result.groups?.boundary;
if (boundary) {
this.processMultipartFormData(boundary);
}
}
}
}
}
Form url encoded
key1=value1&key2=value2
Multipart Form Data
For this type of data the Content-Type
header look like this.
multipart/form-data; boundary=--------------------------451354242906847142002334
And the body of the request like this
----------------------------451354242906847142002334
Content-Disposition: form-data; name="key1"
value1
----------------------------451354242906847142002334
Content-Disposition: form-data; name="key2"
value2
----------------------------451354242906847142002334--
As mentioned in rfc1341 from w3.org each part will be delimited by the boundary with a --
at the start of the line. This delimiter will at the start of each part, and for the last part the ending delimiter will the boundary between --
at the start and at the end of the line.
Here is the example from the w3.org documentation
From my understanding each parameter will have an “header” with Content-Disposition
that define the name of the parameter, and if it’s a file it will define the original filename.
In the first part I’ll split the body to get the part of each parameters. Then in the second I’ll parse the content of each body part, in this part I’ll retreive the content and the headers inside two variables. In the third part I’ll parse the Content-Disposition
header to get the name of the parameter, and whether it’s a file or not. And in the fourth and last part I’ll add the parameter to the bodyParsed attribute if it’s not a file. If the parameter is a File for now I’ll do nothing, and will change this later when I’ll process the files.
private processMultipartFormData(boundary: string) {
let lines = this.getBody().split('\r\n');
let started = false;
let parts: string[][] = []
//Part 1 - Getting each part of the body
let currentPart: string[] = [];
for (let line of lines) {
if (line === '--' + boundary + '--') {
if (started) {
parts.push(currentPart);
currentPart = [];
}
} else if (line === '--' + boundary) {
if (started) {
parts.push(currentPart);
currentPart = [];
}
started = true;
} else {
if (started) {
currentPart.push(line);
}
}
}
//Part 2 - Parsing each body part
for (let part of parts) {
let headers: Map<string, string> = new Map<string, string>();
let isContent = false;
let content: string[] = [];
for (let line of part) {
if (!isContent && line === '') {
isContent = true;
} else {
if (isContent) {
content.push(line);
} else {
let headerParts = line.split(':');
headers.set(headerParts[0].toLowerCase().trim(), headerParts[1]);
}
}
}
let contentValue = content.join('\r\n')
// Part 3 - parsing the Content-Disposition "header"
let dispositions: Map<string, string> = new Map<string, string>();
let dispositionParts = headers.get('content-disposition')?.split(';');
if (dispositionParts !== undefined) {
for (let param of dispositionParts) {
let regexResult = /(?<name>[^=]*)="(?<value>.*)"$/.exec(param.trim())
if (regexResult?.groups?.name !== undefined && regexResult?.groups?.value !== undefined) {
dispositions.set(regexResult?.groups?.name, regexResult?.groups?.value)
}
}
// Part 4 - Interpreting the "headers" to add the parameters
let name = dispositions.get('name');
if (name !== undefined) {
let filename = dispositions.get('filename');
if (filename === undefined) {
this.bodyParsed.set(name, contentValue);
} else {
// @todo add an uploaded file
}
}
}
}
}
JSON body
To parse the JSON body I’ll use the JSON.parse() method.
private processJson() {
let parameters = JSON.parse(this.getBody());
for (let name in parameters) {
this.bodyParsed.set(name, parameters[name])
}
}
Files
To manages the uploaded files we will first create a File
class and a UploadedFile
class.