westtrade/cleantalk

View on GitHub
src/CleantalkRequest.js

Summary

Maintainability
B
6 hrs
Test Coverage
'use strict';

/**
* @Author: Popov Gennadiy <dio>
* @Date:   2016-12-17T18:19:23+03:00
* @Email:  me@westtrade.tk
* @Last modified by:   dio
* @Last modified time: 2017-03-30T13:35:54+03:00
*/

const zlib = require('zlib');

const {IncomingMessage} = require('http');
const assert = require('assert');
const url = require('url');
const qs = require('querystring');

const {getFromPath} = require('./util');

const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1);

const QUERYSTRING_BODY_TYPE = 'querystring';
const JSON_BODY_TYPE = 'json';

const defaultOptions = {
    sendHeaders: true,
    excludeHeaders: [],
    bodyType: QUERYSTRING_BODY_TYPE,
};

const ALLOWED_BODY_TYPES = [QUERYSTRING_BODY_TYPE, JSON_BODY_TYPE];

const privateKey = Symbol('Private properties');
const parseBody = Symbol('Parse headers method');


/**
 * CleantalkRequest - helper class
 */
class CleantalkRequest {

 /**
  * constructor - Description
  *
  * @param {object} Options         Description
  * @param {object}   Options.data    Predefined data
  * @param {object}   Options.options Parser options
  * Explanation:
  * Options.options.sendHeaders - automatic parse and send headers of request
  * Options.options.excludeHeaders - List of excluded headers
  * Options.options.bodyType - Body parser type - maybe json or querystring
  * Options.options.language - Language is alias for
  *
  * @param {object}   Options.aliases List of aliases
  * Explanation:
  * List of aliases in dot notaion e.g.
  * {
  *     'sender_email': 'body.sender_email' - for post data in request body
  *     'sender_nickname': 'query.sender_email' - for get data in http query like http://.../?sender_email=
  * }
  *
  * @param {IncomingMessage}
  *
  */
    constructor({data, options, aliases, request} = {}) {
        if (request) {
            assert(request instanceof IncomingMessage, 'Request argument must be instance of http.ClientRequest.');
        }

        data = data || {
            submit_time: 3,
        };

        if (options) {
            assert.equal(typeof options, 'object', 'Options must be an object.');
        }

        options = options || {};
        if (options) {
            assert.equal(typeof options, 'object', 'Options must be an object.');
            options = Object.assign({}, defaultOptions, options);
        }

        const {sendHeaders, excludeHeaders, bodyType,} = options;

        aliases = aliases || {};
        if (aliases) {
            assert.equal(typeof aliases, 'object', 'Aliases must be an object.');
        }

        assert(ALLOWED_BODY_TYPES.includes(bodyType), `options.bodyType must be in list of allowed types ${ALLOWED_BODY_TYPES.join(', ')}`);

        if (!this.all_headers && sendHeaders) {

            this.all_headers = Object.keys(request.headers).reduce((headers, originalHeader) => {

                const header = capitalizeFirstLetter(originalHeader);
                if (excludeHeaders.includes(originalHeader)) {
                    return headers;
                }

                headers[header] = request.headers[originalHeader];
                return headers;
            }, {});
        }

        let {sender_info, post_info} = data;

        if (!sender_info) {
            sender_info = sender_info || {};
            sender_info['remote_addr'] = request.headers['x-forwarded-for'] ||
                request.connection.remoteAddress ||
                request.socket.remoteAddress ||
                request.connection.socket.remoteAddress;

            sender_info['remote_addr'] = sender_info['remote_addr'].replace(/^.*:/, '');
            data.sender_info = sender_info;
        }

        if (!post_info) {
            post_info = post_info || {};
            post_info['REFFERRER'] = request.headers.referer;
            post_info['USER_AGENT'] = request.headers['user-agent'] || '';
            if (request.headers['x-ucbrowser-ua']) {  //special case of UC Browser
                post_info['USER_AGENT'] = request.headers['x-ucbrowser-ua'];
            }

            data.post_info = post_info;
        }

        data['x_forwarded_for'] = request.headers['x_forwarded_for'] || '';
        data['x_real_ip'] = request.headers['X_REAL_IP'] || '';
        data['sender_ip'] = sender_info['remote_addr'];


        let {query: queryRaw} = url.parse(request.url);

        const ready = this[parseBody](request, bodyType)
            .then((body = {}) => {
                let query = qs.parse(queryRaw);
                const inputData = {body, query};
                data = Object.keys(aliases).reduce((data, key) => {

                    if (({}).hasOwnProperty.call(data, key)) {
                        return data;
                    }

                    let aliasPath = aliases[key];
                    let value = getFromPath(inputData, aliasPath);

                    if (value) {
                        data[key] = value;
                    }
                    return data;

                }, data);

                Object.assign(this, data);

                return this;
            });

        this[privateKey] = {
            ready,
        };
    }

    get ready() {
        return this[privateKey].ready;
    }

    [parseBody](request, bodyType = QUERYSTRING_BODY_TYPE) {

        return new Promise((resolve, reject) => {

            if (request.method === 'GET') {
                return resolve({});
            }

            let stream = request;

            let encoding = (request.headers['content-encoding'] || '').toLowerCase();

            switch (encoding) {
            case 'deflate':
                stream = stream.pipe(zlib.createInflate());
                break;
            case 'gzip':
                stream = stream.pipe(zlib.createInflate());
                break;
            }

            let body = '';

            stream.on('data', (chunk) => { body += chunk.toString('utf-8'); });
            stream.on('error', reject);
            stream.on('end', () => {

                let data = {};

                switch (bodyType) {
                case QUERYSTRING_BODY_TYPE:
                    data = qs.parse(body);
                    break;
                case JSON_BODY_TYPE:
                    data = JSON.parse(body);
                    break;
                default:
                    reject(new Error('Wrong body type'));
                }

                resolve(data);
            });
        });
    }
}

module.exports = CleantalkRequest;