Node: Express: proxy request

Date: 2017-02-01
const express = require("express");
const request = require("request");
const app = express();

app.use((req, res, next) => {
    let data = '';
    req.setEncoding('utf8');
    req.on('data', (chunk) => data += chunk);
    req.on('end', () => {
        req.rawBody = data;
        next();
    });
});

app.get('/', (_req, res) => res.send('Proxy requests via a POST call to /ajax'));

const port = 4202;
app.listen(port, () => console.log(`App listening on port ${port}!`))
app.post('/ajax', (req, res) => ajaxRequest(req, res));

function formDataUrlEncoded(data) {
    var str = [];
    if (!data) return;
    Object.keys(data).forEach(key =>str.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key])));
    return str.join("&");
}
function formData(data) {
    var str = [];
    if (!data) return;
    Object.keys(data).forEach(key => str.push(key + "=" + data[key]));
    return str.join("&");
}

function ajaxRequest(req, res) {    
    let settings = {};
    if (req.rawBody) {
        settings = JSON.parse(req.rawBody);
    } else {
        throw "the body is empty.";
    }
    return new Promise((resolve, reject) => {
        if (!settings.url) throw "the url-property must be specified.";
        settings.method = settings.method || "GET";
        settings.headers = settings.headers || [];
        settings.responseType = settings.responseType || "text";
        settings.body = settings.body || settings.data || {};
        settings.proxy = ''; // config.settings.proxyUrl
        // Check valid data format
        let isFormData = false;
        let isFormDataUrlEncoded = false;
        //var isXml = false;
        let isJson = false;
        let contentTypeSet = false;
        if (settings.contentType) {
            contentTypeSet = true;
            isFormData = settings.contentType.includes('form-data');
            isFormDataUrlEncoded = settings.contentType.includes('form-urlencoded ');
            //isXml = settings.contentType.includes('xml');
            isJson = settings.contentType.includes('json');
        }
        // Validate data and try to fix errors.
        let postData = null;
        if (settings.method.toLowerCase() !== 'get') {
            if (isFormData) {
                postData = formData(settings.data);
            }
            else if (isFormDataUrlEncoded) {
                postData = formDataUrlEncoded(settings.data);
            }
            else if (isJson) {
                postData = settings.data;
                if ('string' !== typeof settings.data) {
                    postData = JSON.stringify(settings.data);
                }
            }
            else if (!contentTypeSet) {
                // default to JSON
                settings.contentType = 'application/json';
                // if (stringHelper.isValidJson(settings.data)) {
                //     postData = settings.data;
                // } else if (stringHelper.canConvertToJsonString(settings.data)) {
                //     postData = JSON.stringify(settings.data);
                // } else {
                //     throw "mismatch between contenttype and data property.";
                // }
            }
        }
        settings.body = postData;        
        let result = request(settings, (error) => {
            console.info("completed", settings.url);
            if (error) {
                console.error("error_a", error);
                reject(new Error(error));
            } else {
                resolve();
            }
        });
        result.on('response', response => {
            result.pipe(res);
            response.on('end', resolve);
        });
        result.on('error', err => {
            console.error("error_b", err);
            reject(err);
        });
    })
    .catch((err) => res.status(400).send(String(err)));
}
var config = rfr("config");
router.route('/data/stream/:requestUrl?')
    .all((req, res) => {
        var settings = {
            method: req.method,
            url: req.params.requestUrl,
            headers: req.headers,
            proxy: config.settings.proxyUrl || ''
        };
        var webRequest = request(settings);
        req.pipe(webRequest);
        webRequest.pipe(res);
        var headersSend = false;
        webRequest.on('data', function() {
            if (!headersSend) {
                headersSend = true;
                try {
                    res.writeHead(webRequest.response.statusCode, webRequest.response.headers);
                } catch (e) {
                    // can not write headers, ignore!
                    log.info("can not write headers when they are already send");
                }
            }
        });
        // webRequest.on('end', function (response) {
        // });
    });
service.proxyURL = function(url) {
    let proxyUrl = window.location.origin + '/api/data/stream/';
    if (proxyUrl.indexOf('://') > -1) {

        if (url.indexOf(proxyUrl) !== -1) {
            // url already is a NAPI streaming url
            return url;
        }

        let baseUrl = '';
        if (url.indexOf('://') !== -1) {
            // url is full url
        } else if (url[0] === '/') {
            // url is relative to root
            baseUrl = window.location.origin;
        } else {
            baseUrl = window.location.href + '/';
            // url is relative to current url
        }
        url = baseUrl + url;

        let encodedUrl = encodeURIComponent(url);
        return proxyUrl + encodedUrl;
    }
    return url;
};
var xhrService = {};
var Promise = require("bluebird");
var rfr = require("rfr");
var log = rfr("services/logService").getLogger();
var request = require("request");
var config = rfr("services/configService").getConfig();
var stringHelper = rfr("helpers/stringHelper");

// global
var XMLHttpRequest = require('xhr2');


xhrService.xhr = function () {
    return new XMLHttpRequest();
};

var formDataUrlEncoded = function (data) {
    var str = [];
    if (data) {
        Object.keys(data).forEach(function (key) {
            str.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
        });
    }
    return str.join("&");
};

var formData = function (data) {
    var str = [];
    if (data) {
        Object.keys(data).forEach(function (key) {
            str.push(key + "=" + data[key]);
        });
    }
    return str.join("&");
};

var authErrorHandler = null;
var onAuthError = function (xhr, event) {
    if (authErrorHandler) { authErrorHandler(xhr, event); }
};

xhrService.setAuthErrorHandler = function (handler) {
    authErrorHandler = handler;
};


var timeoutHandler = null;
var onTimeout = function (xhr, event) {
    if (timeoutHandler) { timeoutHandler(xhr, event); }
};

xhrService.setTimeoutHandler = function (handler) {
    timeoutHandler = handler;
};

var networkErrorHandler = null;
var onNetworkError = function (xhr, event) {
    if (networkErrorHandler) { networkErrorHandler(xhr, event); }
};

xhrService.setNetworkErrorHandler = function (handler) {
    networkErrorHandler = handler;
};

var networkSuccessHandler = null;
var onNetworkSuccess = function (xhr, event) {
    if (networkSuccessHandler) { networkSuccessHandler(xhr, event); }
};

xhrService.setNetworkSuccessHandler = function (handler) {
    networkSuccessHandler = handler;
};

xhrService.request = function (settings) {
    settings = settings || {};

    return new Promise(function (resolve, reject) {


        var method = (settings.method || "GET").toUpperCase();
        var url = settings.url || "";
        var data = settings.data;
        var contentType = settings.contentType || "x-www-form-urlencoded";
        var headers = settings.headers || [];
        var responseType = settings.responseType || "text";
        var postData = null;

        if (method === "POST" || method === "PUT")
        {
            if (contentType === "x-www-form-urlencoded") {
                postData = formDataUrlEncoded(data);
            } else if (typeof data !== "string") {
                postData = JSON.stringify(data);
            } else {
                postData = data;
            }
        } else {
            url += "?" + formDataUrlEncoded(data);
        }

        var xhr = xhrService.xhr();
        if (!xhr) {

            reject("Error: could not create a XMLHttpRequest");
            return;
        }

        xhr.open(method, url, true);

        if (headers) {
            Object.keys(headers).forEach(function (key) {
                xhr.setRequestHeader(key, headers[key]);
            });
        }

        //xhr.timeout = 30000; // 30 seconds

        xhr.addEventListener("error", function (event) {
            onNetworkError(xhr, event);

            reject("Error: network error");
        });
        xhr.addEventListener("timeout", function (event) {
            onTimeout(xhr, event);

            reject("Error: timeout error");
        });

        xhr.responseType = responseType;
        xhr.addEventListener("load", function (event) {
            log.info("Request completed", url, xhr.status);

            if (xhr.status != 200 && xhr.status != 201 && xhr.status != 304) {
                reject(xhr);
            }
            else {
                //onNetworkSuccess(xhr, event);

                resolve(xhr, xhr.response);
            }
        }, false);

        // log.info({
        //     method: method,
        //     url: url,
        //     headers: headers,
        //     data: postdata
        // });

        xhr.send(postData);
    });
};

xhrService.requestProxyStream = function (req, resp, settings) {
    return new Promise(function (resolve, reject) {
        settings.proxy = config.settings.proxyUrl || '';
        var result = request(settings);
        req.pipe(result);
        result.pipe(resp);
        result.on('response', function (response) {
            response.on('end', resolve);
        });
        result.on('error', reject);
    });
};

xhrService.requestProxy = function (settings) {
    settings = settings || {};

    return new Promise(function (resolve, reject) {

        /*
        *  In contrast to the xhrService.request function
        *  this function doesn't use the xhr2 package.
        *  This function uses the request package instead.
        *
        *  The request object does not have te
        *  properties "response" or "responseText".
        *  Instead it has a property namend "body".
        *
        *  Also when sending data there is nog "data" property
        *  on the settingsobject. Instad a "body" property is used.
        *
        *  The property contains the UNCOMPRESSED data for the request.
        *  To keep the data valid I used the onData event to return the
        *  COMPRESSED data instead.
        *
        */

        // Make sure some properties are set or set default values.
        if (!settings.url) throw 'the url-property must be spedificed. ';
        settings.method = settings.method || "GET";
        settings.headers = settings.headers || [];
        settings.responseType = settings.responseType || "text";
        settings.body = settings.body || settings.data || {};
        settings.proxy = config.settings.proxyUrl || '';

        // Check valid data format
        var isFormData = false;
        var isFormDataUrlEncoded = false;
        //var isXml = false;
        var isJson = false;
        var contentTypeSet = false;

        if (settings.contentType) {
            contentTypeSet = true;
            isFormData = -1 !== settings.contentType.indexOf('form-data');
            isFormDataUrlEncoded = -1 !== settings.contentType.indexOf('form-urlencoded ');
            //isXml = -1 !== settings.contentType.indexOf('xml');
            isJson = -1 !== settings.contentType.indexOf('json');
        }

        // Validate data and try to fix errors.
        var postData = null;
        if (settings.method.toLowerCase() !== 'get') {
            if (isFormData) {
                postData = formData(settings.data);
            }
            else if (isFormDataUrlEncoded) {
                postData = formDataUrlEncoded(settings.data);
            }
            // else if (isXml) {
            //
            // }
            else if (isJson) {
                postData = settings.data;
                if ('string' !== typeof settings.data) {
                    postData = JSON.stringify(settings.data);
                }
            }
            else if (!contentTypeSet) {
                // default to JSON
                settings.contentType = 'application/json';
                if (stringHelper.isValidJson(settings.data)) {
                    postData = settings.data;
                }
                else if (stringHelper.canConvertToJsonString(settings.data)) {
                    postData = JSON.stringify(settings.data);
                }
                else {
                    throw 'mismatch between contenttype and data property.';
                }
            }
        }
        settings.body = postData;
        
        // Do the request
        request(settings, function (error, response, body) {
            log.info("RequestProxy completed", settings.url);
            // response and body are unused
            if (error) {
                reject(new Error(error));
            }
        })
        .on('response', function (response) {

            // Unmodified http.IncomingMessage object
            response.on('data', function (data) {
                // Compressed data as it is received
                if (typeof response.rawData == 'undefined') {
                    response.rawData = data; // create new property
                }
                else {
                    if (data instanceof Buffer) {
                        response.rawData = Buffer.concat([response.rawData, data]);
                    }
                    else {
                        response.rawData += data;
                    }

                }
            })
            .on('end', function () {
                resolve(response);
            });

        });//.pipe(fs.createWriteStream('doodle.png'));


    });
};

xhrService.requestProxy2 = function (req, res, settings) {
    settings = settings || {};

    return new Promise(function (resolve, reject) {

        /*
        *  In contrast to the xhrService.request function
        *  this function doesn't use the xhr2 package.
        *  This function uses the request package instead.
        *
        *  The request object does not have te
        *  properties "response" or "responseText".
        *  Instead it has a property namend "body".
        *
        *  Also when sending data there is nog "data" property
        *  on the settingsobject. Instad a "body" property is used.
        *
        *  The property contains the UNCOMPRESSED data for the request.
        *  To keep the data valid I used the onData event to return the
        *  COMPRESSED data instead.
        *
        */

        // Make sure some properties are set or set default values.
        if (!settings.url) throw 'the url-property must be spedificed. ';
        settings.method = settings.method || "GET";
        settings.headers = settings.headers || [];
        settings.responseType = settings.responseType || "text";
        settings.body = settings.body || settings.data || {};
        settings.proxy = config.settings.proxyUrl || '';

        // Check valid data format
        var isFormData = false;
        var isFormDataUrlEncoded = false;
        //var isXml = false;
        var isJson = false;
        var contentTypeSet = false;

        if (settings.contentType) {
            contentTypeSet = true;
            isFormData = -1 !== settings.contentType.indexOf('form-data');
            isFormDataUrlEncoded = -1 !== settings.contentType.indexOf('form-urlencoded ');
            //isXml = -1 !== settings.contentType.indexOf('xml');
            isJson = -1 !== settings.contentType.indexOf('json');
        }

        // Validate data and try to fix errors.
        var postData = null;
        if (settings.method.toLowerCase() !== 'get') {
            if (isFormData) {
                postData = formData(settings.data);
            }
            else if (isFormDataUrlEncoded) {
                postData = formDataUrlEncoded(settings.data);
            }
            // else if (isXml) {
            //
            // }
            else if (isJson) {
                postData = settings.data;
                if ('string' !== typeof settings.data) {
                    postData = JSON.stringify(settings.data);
                }
            }
            else if (!contentTypeSet) {
                // default to JSON
                settings.contentType = 'application/json';
                if (stringHelper.isValidJson(settings.data)) {
                    postData = settings.data;
                }
                else if (stringHelper.canConvertToJsonString(settings.data)) {
                    postData = JSON.stringify(settings.data);
                }
                else {
                    throw 'mismatch between contenttype and data property.';
                }
            }
        }
        settings.body = postData;
        
        // Do the request
        var result = request(settings, function (error, response, body) {
            log.info("RequestProxy_1 completed", settings.url);
            // response and body are unused
            if (error) {
                log.error("RequestProxy_1 error_1", error);
                reject(new Error(error));
            }
        });

        var myTimeout = setTimeout(function() {
            //res.status(500).send({ error: 'something blew up' });
            result = request(settings, function (error, response, body) {
                log.error("RequestProxy_2 completed", settings.url);
                // response and body are unused
                if (error) {
                    log.error("RequestProxy_1 error_2", error);
                    reject(new Error(error));
                }
            });

            result.on('response', function (response) {
                result.pipe(res);
                response.on('end', resolve);
            });
            result.on('error', function(err) {
                log.error("RequestProxy_2 error_2", err);
                reject(err);
            });
        }, 3000);

        result.on('response', function (response) {
            clearTimeout(myTimeout);
            result.pipe(res);
            response.on('end', resolve);
        });
        result.on('error', function(err) {
            log.error("RequestProxy_1 error_2", err);
            reject(err);
        });
    });
};


module.exports = xhrService;
// Packages to use
var Promise = require("bluebird");
var rfr = require("rfr");
var log = rfr("services/logService").getLogger();
var xhrService = rfr("services/xhrService");

var dataService = {};

dataService.delegateRequest = function (req, res) {

    var requestType = req.params.requestType;
    var asyncResult = null;//Promise.reject('invalid type: ' + requestType);
    switch (requestType) {
        case 'ajax':
            var settings = {};
            if (req.rawBody) {
                settings = JSON.parse(req.rawBody);
            }
            asyncResult = dataService.ajax(req, res, settings);
            break;
        case 'url':
            asyncResult = dataService.url(req);
            break;
        case 'stream':
            asyncResult = dataService.stream(req, res);
            break;
        case 'local':
            asyncResult = dataService.local(req);
            break;
        default:
            //default code block
            asyncResult = Promise.reject('invalid type: ' + requestType);
            log.info("delegateRequest(): request type unknown ", requestType);
    }
    return asyncResult;
};

dataService.ajax = (req, res, settings) => {
    return new Promise(function (resolve, reject) {
        // all ajax request use the proxy if proxyUrl is set in the config;
        /*
        var request = xhrService.requestProxy(settings);        
        request.then((xhr) => {
            var result = {
                xhr: xhr,
                contentBuffer: ''
            };

            if (xhr.rawData && xhr.rawData instanceof Buffer) {
                result.contentBuffer = xhr.rawData;
            } else {
                var arrayBuffer = xhr.body;
                var byteArray = new Uint8Array(arrayBuffer);
                result.contentBuffer = new Buffer(byteArray, 'binary');
            }
            resolve(result);
        })
        .catch((xhr) => {
            log.error(xhr);
            reject(xhr);
        });
        */
        return xhrService.requestProxy2(req, res, settings);
    });
};

dataService.url = (request) => {
    return new Promise(function (resolve, reject) {
        var settings = setupSettingsObjectFromRequest(request);
        dataService.ajax(settings)
            .then((result) => {
                resolve(result);
            })
            .catch((xhr) => {
                reject(xhr);
            });
    });
};

dataService.stream = (req, resp) => {
    return new Promise(function (resolve, reject) {
        var settings = setupSettingsObjectFromRequest(req);
        return xhrService.requestProxyStream(req, resp, settings);
    });
};

dataService.local = (settings) => {
    return new Promise(function (resolve, reject) {
        reject('not implemented yet');
    });
};

module.exports = dataService;

/* Define some helper functions for this service */
function setupSettingsObjectFromRequest(request) {
    // setup settings from current request
    var settings = {};
    settings.headers = request.headers || {};

    var url = request.params.requestUrl || throwError('requestUrlNotSet');
    settings.headers.host = url;
    settings.method = request.method;
    settings.url = url;
    settings.responseType = "arraybuffer";

    if (request.rawBody) {
        var body = JSON.parse(request.rawBody) || {};
        settings.data = body;
    }

    // add some default properties
    settings.crossDomain = true;
    settings.headers['cache-control'] = 'no-cache';

    return settings;
}

function throwError(errorMessage) {
    throw new Error(errorMessage);
}
5980cookie-checkNode: Express: proxy request