Http messages

http-server/index.ts at develop · adonisjs/http-server
express/request.js at master · expressjs/express

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.

headers

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 delimiter

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.

Response