core
Version:
Core Libs
585 lines (503 loc) • 14.4 kB
JavaScript
/**
* @overview Loader
* @copyright Dreamlab Onet.pl sp. z. o o 2012
*/
var http = require('http'),
https = require('https'),
util = require('util');
var Request = require('./Request.js').Request,
Response = require('./Response.js').Response,
Event = require('../event/Event.js').Event,
ErrorEvent = require('../event/ErrorEvent.js').ErrorEvent,
EventDispatcher = require('../event/EventDispatcher.js').EventDispatcher,
Types = require('../common/Types.js');
/**
* Http Loader
* @class Loader
* @classdesc Processes given requests and returns the result
* @extends EventDispatcher
* @param {Request} request request to process
*/
var Loader = function (request) {
EventDispatcher.call(this);
this._buffer = null;
this._request = null;
this._timeout = 8000;
this._timer = null;
this._followRedirects = true;
this._maxRedirectsCount = 10;
this._currentRedirectsCount = 0;
this._dataBuffering = true;
this._agent = false;
this._errorEmited = false;
this._requestStart = null;
this._requestLength = null;
this._logStatus = true;
this.setRequest(request);
};
Loader.prototype = Object.create(EventDispatcher.prototype);
Loader.prototype.setLogStatus = function (value) {
this._logStatus = !!value;
return this;
}
/**
* enables/disables data buffering
* @method
* @param {Boolean} buffering
* @returns {Loader}
*/
Loader.prototype.setDataBuffering = function (buffering) {
this._dataBuffering = buffering;
return this;
};
/**
* callback responsible for gathering response chunks
* @method
* @private
* @param {Server.HttpResponse} serverResponse
* @fires Loader.Event.HEADERS
* @fires Loader.Event.PROGRESS
*/
Loader.prototype._handleServerResponse = function (serverResponse) {
var dataLength = 0,
buffers = [],
self = this,
response = new Response();
response.setStatusCode(serverResponse.statusCode);
response.setHeaders(serverResponse.headers);
response.setHttpVersion(serverResponse.httpVersion);
//console.log(">>>>> LOADER headers");
this.dispatchEvent(new Event(Loader.Event.HEADERS, {
"statusCode": serverResponse.statusCode,
"headers": serverResponse.headers,
"httpVersion": serverResponse.httpVersion,
"body": null,
"response": response,
"stream": serverResponse
}));
serverResponse.addListener('data', function (buffer) {
//console.log(">>>>> LOADER data");
if (self._dataBuffering) {
buffers.push(buffer);
dataLength += buffer.length;
}
response.write(buffer);
self.dispatchEvent(new Event(Loader.Event.PROGRESS, buffer));
});
serverResponse.addListener('end', function () {
//console.log(">>>>> LOADER end");
self._buffer = new Buffer(dataLength);
var bufferPosition = 0;
for (var i = 0, l = buffers.length; i < l; i++) {
buffers[i].copy(self._buffer, bufferPosition);
bufferPosition += buffers[i].length;
}
response.end();
self._onEnd(serverResponse);
});
};
/**
* sets timer which is responsible for timeout detection
* @method
* @private
* @param {Server.HttpRequest} request
* @fires Loader.Event.ERROR
*/
Loader.prototype._setTimer = function (request) {
var self = this;
this._timer = setTimeout(function () {
request.removeAllListeners('response');
request.abort();
self._log();
if (!self._errorEmited) {
self.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, -3, 'Http response timeout'));
self._errorEmited = true;
}
}, this.getTimeout());
};
/**
* clears timer handle which is responsible for timeout detection
* @method
* @private
*/
Loader.prototype._clearTimer = function () {
clearTimeout(this._timer);
};
/**
* clears internal chunks array
* @method
* @private
*/
Loader.prototype._clearChunks = function () {
this._buffer = [];
};
/**
* check for 3xx following
* @method
* @private
* @param {Server.HttpResponse} response
* @fires Loader.Event.ERROR
* @returns {Boolean}
*/
Loader.prototype._checkForRedirect = function (response) {
var requestMethod = this._request.getMethod();
if (response.statusCode >= 300 && response.statusCode < 400 &&
response.headers.hasOwnProperty('location') && this._followRedirects &&
requestMethod != Request.PUT && requestMethod != Request.POST) {
response.removeAllListeners('data');
response.removeAllListeners('end');
if (this._currentRedirectsCount >= this._maxRedirectsCount) {
if (!this._errorEmited) {
this.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, -4, 'maxRedirectsCount reached, stopping'));
this._errorEmited = true;
}
return true;
}
this._currentRedirectsCount++;
var host = this._request.getHost();
var connectionHost = this._request.getConnectionHost();
this._request.setUrl(response.headers.location);
/* redirect with no host */
if (!this._request.getHost() && host) {
this._request.setHost(host);
}
if (!this._request.getConnectionHost() && connectionHost) {
this._request.setConnectionHost(connectionHost);
}
this._clearChunks();
this.load();
return true;
}
return false;
};
Loader.prototype._log = function (res, type) {
var req = this.getRequest();
var code = 0;
var len = 0;
if (res) {
code = res.statusCode;
len = this._buffer.length;
}
var upstream = req.getHeader('host') || '-';
var gateway = req.getConnectionHost() || '-';
var tt = Date.now() - this._requestStart;
var rel = this._requestLength || 0;
type = type || 'http';
if (this._logStatus) {
console.info('Loader/Monitoring: upstream=%s code=%s tt=%s len=%s method=%s gateway=%s type=%s reqlen=%s', upstream, code, tt, len, req.getMethod(), gateway, type, rel);
} else {
console.log('Loader/Monitoring: upstream=%s code=%s tt=%s len=%s method=%s gateway=%s type=%s reqlen=%s', upstream, code, tt, len, req.getMethod(), gateway, type, rel);
}
};
/**
* called on request end, when whole data has been retreived from server
* @method
* @private
* @param {http.IncomingMessage} response
* @fires Loader.Event.ERROR
* @fires Loader.Event.LOADED
*/
Loader.prototype._onEnd = function (response) {
var requestMethod = this._request.getMethod();
this._log(response);
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.hasOwnProperty('location') && this._followRedirects && requestMethod != Request.PUT && requestMethod != Request.POST) {
if (this._currentRedirectsCount >= this._maxRedirectsCount) {
if (!this._errorEmited) {
this.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, -4, 'maxRedirectsCount reached, stopping'));
this._errorEmited = true;
}
return;
}
this._currentRedirectsCount++;
this._request.setUrl(response.headers.location);
this._clearChunks();
this.load();
return;
}
var data = this._parseResponse(response);
if (data == false) {
//tutaj zakladamy ze _parseResponse obsluzyl blad
return;
}
this.dispatchEvent(new Event(Loader.Event.LOADED, data));
};
/**
* creates Response from node http.IncomingMessage and received data chunks
* @method
* @private
* @param {http.IncomingMessage} response
*/
Loader.prototype._parseResponse = function (response) {
return new Response({
"statusCode": response.statusCode,
"headers": response.headers,
"httpVersion": response.httpVersion,
"body": this._buffer
});
};
/**
* creates node http.request compatible object based on given request
* @method
* @private
* @param {Request} request
* @throws Loader.Exception.INCOMPLETE_REQUEST
*/
Loader.prototype._createRequest = function (request) {
var ua = request.getHeader('user-agent');
if (!ua) {
request.setHeader('user-agent', 'nodejs ' + process.env.OPAL_IDENTITY || '', true);
}
var host = request.getConnectionHost() || request.getHost();
//FIXME to pasuje rozwiazac w klasie Request gdy klonuje inny request
host = host.replace(':'+request.getPort(), '');
var port = request.getPort();
var path = request.getPath();
var method = request.getMethod();
var headers = request.getHeaders();
if (!host || !port || !path || !method || !headers) {
this._log();
throw Loader.Exception.INCOMPLETE_REQUEST;
}
var retValue = {
host: host,
port: +port,
path: path + request.getQueryString(),
method: method,
headers: headers
};
if (!this._agent) {
retValue.agent = false;
}
return retValue;
};
/**
* sets timeout in miliseconds
* @method
* @param {Number} ms
* @returns {Loader}
*/
Loader.prototype.setTimeout = function (ms) {
this._timeout = ms;
return this;
};
/**
* gets timeout in miliseconds
* @method
* @returns {Number}
*/
Loader.prototype.getTimeout = function () {
return this._timeout;
};
/** sets followRedirects
* @method
* @param {Boolean} follow
* @throws Loader.Exception.FOLLOW_REDIRECTS_TYPE_ERROR
* @returns {Loader}
*/
Loader.prototype.setFollowRedirects = function (follow) {
if (typeof follow == 'boolean') {
this._followRedirects = follow;
} else {
throw Loader.Exception.FOLLOW_REDIRECTS_TYPE_ERROR;
}
return this;
};
/**
* gets followRedirects
* @method
* @returns {Boolean}
*/
Loader.prototype.getFollowRedirects = function () {
return this._followRedirects;
};
/**
* sets maxRedirects after which processing request will be aborted
* @method
* @param {Number} count
* @returns {Loader}
*/
Loader.prototype.setMaxRedirectsCount = function (count) {
this._maxRedirectsCount = count;
return this;
};
/**
* gets maxRedirects count
* @method
* @returns {Number}
*/
Loader.prototype.getMaxRedirectsCount = function () {
return this._maxRedirectsCount;
};
/**
* sets request object to process,
* request to process is a copy from a original request
* @method
* @param {Request} request
* @throws Loader.Exception.WRONG_REQUEST_INSTANCE
*/
Loader.prototype.setRequest = function (request) {
if (request && !(request instanceof Request)) {
throw Loader.Exception.WRONG_REQUEST_INSTANCE;
} else if (request) {
this._request = new Request(request);
this._currentRedirectCount = 0;
this._clearChunks();
}
};
/**
* returns request (a copy of it) which is going to be processed
*/
Loader.prototype.getRequest = function () {
return this._request;
};
/**
* aborts a request, clears timer, dispatches ABORTED event
* @method
* @fires Loader.Event.ABORTED
*/
Loader.prototype.abort = function () {
if (this._clientRequest) {
this._clientRequest.abort();
}
this._clearTimer();
this.dispatchEvent(new Event(Loader.Event.ABORTED, 'Aborted'));
};
/**
* set agent value
* @param {Boolean} value
*/
Loader.prototype.setAgent = function (value) {
if (!Types.isBoolean(value))
throw new Error(Loader.Exception.AGENT_VALUE);
this._agent = value;
};
/**
* makes request to the backend system based on the request object
* @method
* @param {Request} request
* @fires Loader.Event.ERROR
* @throws Loader.Exception.REQUEST_NOT_SET
*/
Loader.prototype.load = function (request) {
this.setRequest(request);
this._errorEmited = false;
if (this._request == null) {
throw Loader.Exception.REQUEST_NOT_SET;
}
var that = this;
this._requestStart = Date.now();
var options = this._createRequest(this._request);
var httpClient = http;
if(this._request.getProto() == 'https:') {
httpClient = https;
}
this._clientRequest = httpClient.request(options, function (serverResponse) {
that._clearTimer();
if (that._checkForRedirect(serverResponse)) {
return;
} else {
that._handleServerResponse(serverResponse);
}
});
this._clientRequest.addListener('error', function (e) {
options.agent = null;
console.error('LOADER ERROR NODE REQUEST: ' + util.inspect(e, true, 20) + ' Options: ' + util.inspect(options, true, 20));
that._clearTimer();
that._log();
if (!that._errorEmited) {
that.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, e, -1, 'Http request error: ' + e.message));
that._errorEmited = true;
}
});
// jezeli sa dane np POST to tez je wpisuje
if (this._request.getBody().length() > 0) {
var buf = this._request.getBody().toBuffer();
this._requestLength = buf.length;
this._clientRequest.write(buf);
}
this._clientRequest.end();
this._setTimer(this._clientRequest);
};
/**
* @static
* @namespace
* @constant
*/
Loader.Event = {};
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Event.HEADERS = "Loader_HEADERS";
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Event.PROGRESS = "Loader_PROGRESS";
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Event.LOADED = "Loader_LOADED";
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Event.ABORTED = "Loader_ABORTED";
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Event.ERROR = "Loader_ERROR";
/**
* @static
* @namespace
* @constant
*/
Loader.Exception = {};
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Exception.INCOMPLETE_REQUEST = 'Given request misses some key values';
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Exception.REQUEST_NOT_SET = 'Request has not been set';
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Exception.WRONG_REQUEST_INSTANCE = 'Given request is not a instance of Request';
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Exception.FOLLOW_REDIRECTS_TYPE_ERROR = 'setFollowRedirects requires an argument to be a boolean';
/**
* @static
* @constant
* @type {String}
* @default
*/
Loader.Exception.AGENT_VALUE = 'Agent param value must be a type of Boolean';
exports.Loader = Loader;