core
Version:
Core Libs
851 lines (754 loc) • 22.5 kB
JavaScript
/**
* @overview Response
* @copyright Dreamlab Onet.pl sp. z. o o 2012
*/
var http = require('http');
var Message = require('./Message.js').Message;
var Assertions = require('../common/Assertions.js').Assertions;
var Types = require('../common/Types.js').Types;
var BinaryData = require('../data/BinaryData.js').BinaryData;
var Request = require('../http/Request.js').Request;
var Event = require('../event/Event.js').Event;
var EmptyLogger = require('../logger/EmptyLogger.js').EmptyLogger;
/**
* Http Response
* @class Response
* @classdesc Object that represents http response
* @extends Message
*
* @requires http
* @requires Message
* @requires Assertions
* @requires Types
* @requires BinaryData
* @requires Request
* @requires Event
*
* @param {Object|Buffer|http.ServerResponse} params
* @return {Response}
*/
var Response = function (params) {
Message.call(this);
this._pipelined = false;
this._pipelinedSize = 0;
this._sentHeaders = false;
this._statusCode = null;
this._cookieKey = 'set-cookie';
this._httpServerResponse = null;
this._reCharset = /charset=([A-Za-z\-0-9]+)/i;
this._reStatusLine = /([0-9]{1,}\.[0-9]{1,}).*([0-9]{3,})/;
this._logger = new EmptyLogger();
this.buffers = [];
this.dataLength = 0;
if (Types.isInstanceOf(params, http.ServerResponse)) {
this._httpServerResponse = params;
} else if (Buffer.isBuffer(params)) {
this.init(this._parseBuffer(params));
} else {
this.init(params);
}
};
Response.prototype = Object.create(Message.prototype);
Response.prototype.setLogger = function (logger) {
this._logger = logger;
return this;
};
Response.prototype.getLogger = function () {
return this._logger;
};
Response.prototype.hasLogger = function () {
return this._logger !== null;
};
/**
* Set cookie
* @method
* @param {String} cookie
* @return {Response}
*/
Response.prototype.setCookie = function (cookie) {
var cookies = this.headers.getHeader("set-cookie");
if (!cookies) {
cookies = [];
} else if (!Types.isArray(cookies)) {
cookies = [cookies];
}
cookies.push(cookie);
this.headers.setHeader("set-cookie", cookies, true);
return this;
};
/**
* Get cookie
* @method
* @param {String} cookieName
* @return {String|null}
*/
Response.prototype.getCookie = function (cookieName) {
var cookies = this.headers.getHeader("set-cookie"), i, l;
if (!cookies) {
return null;
} else if (!Types.isArray(cookies)) {
cookies = [cookies];
}
i = 0, l = cookies.length;
for (; i < l; i++) {
if (cookies[i].indexOf(cookieName + "=") == 0) {
return cookies[i];
}
}
return null;
};
/**
* Remove cookie
* @method
* @param {String} cookieName
* @return {null|Boolean}
*/
Response.prototype.removeCookie = function (cookieName) {
var cookies = this.headers.getHeader("set-cookie"), i, l;
if (!cookies) {
return null;
} else if (!Types.isArray(cookies)) {
cookies = [cookies];
}
i = 0, l = cookies.length;
for (; i < l; i++) {
if (cookies[i].indexOf(cookieName + "=") == 0) {
break;
}
}
if (i == l) {
return false;
}
cookies.splice(i, 1);
this.headers.setHeader("set-cookie", cookies, true);
return true;
};
/**
* Sets http status code
* @method
* @throws Response.Exception.WRONG_STATUS_CODE
* @param {Number} code
* @return {Response}
*/
Response.prototype.setStatusCode = function (code) {
Assertions.guard(code, [Assertions.isNumber, Assertions.isNull], Response.Exception.WRONG_STATUS_CODE);
this._statusCode = code;
return this;
};
/**
* Returns http status code
* @method
* @return {Number}
*/
Response.prototype.getStatusCode = function () {
return this._statusCode;
};
/**
* Sets pipelined flag. Response [will/will not] be piped without buffering
* @method
* @param {Boolean} pipelined
* @return {Response}
*/
Response.prototype.setPipelined = function (pipelined) {
this._pipelined = pipelined;
};
/**
* Returns pipelined flag. Response [will/will not] be piped without buffering
* @method
* @return {Boolean}
*/
Response.prototype.getPipelined = function () {
return this._pipelined;
};
/**
* Returns response size when request was in pipelined mode
* @method
* @return {Number}
*/
Response.prototype.getPipelinedSize = function () {
return this._pipelinedSize;
};
/**
* Sets response body (reply data)
* @method
* @private
* @param {BinaryData|Buffer|String} body
* @return {Response}
*/
Response.prototype._setBody = function (body) {
if (Types.isInstanceOf(body, BinaryData)) {
this._body = body;
} else {
var encoding, characterEncoding = null;
if (Types.isInstanceOf(body, Buffer)) {
if (!this._isBinary()) {
encoding = BinaryData.Encoding.TEXT;
var charset = this.getCharset();
switch (charset) {
case 'iso-8859-2':
characterEncoding = BinaryData.CharacterEncoding.ISO_8859_2;
encoding = BinaryData.Encoding.BINARY;
break;
case 'utf-8':
case 'utf8':
characterEncoding = BinaryData.CharacterEncoding.UTF8
break;
default:
// FIXME: this is not true, content could be text
encoding = BinaryData.Encoding.BINARY;
break;
}
} else {
encoding = BinaryData.Encoding.BINARY;
}
} else {
encoding = BinaryData.Encoding.TEXT;
characterEncoding = BinaryData.CharacterEncoding.UTF8;
}
this._body = new BinaryData(body, encoding, characterEncoding);
}
return this;
};
/**
* Sets response body (reply data)
* @method
* @param {BinaryData} body
* @return {Response}
*/
Response.prototype.setBody = function (body) {
Assertions.isInstanceOf(body, BinaryData);
this._body = body;
return this;
};
/**
* Indicates if response content type matches given contentType parameter
* @method
* @throws Response.Exception.WRONG_CONTENT_TYPE
* @param {String} contentType mime type to match (e.g. text/html, application/*)
* @returns {Boolean}
*/
Response.prototype.isContentType = function (contentType) {
Assertions.isString(contentType, Response.Exception.WRONG_CONTENT_TYPE);
var contentHeader = this.getHeader('content-type');
if (contentHeader) {
var currentContentTypeParts = this.getHeader('content-type').split(";")[0].split("/");
var checkedContentTypeParts = contentType.split(";")[0].split("/");
if (
(checkedContentTypeParts[0] == "*" || checkedContentTypeParts[0] == currentContentTypeParts[0])
&&
(checkedContentTypeParts[1] == "*" || checkedContentTypeParts[1] == currentContentTypeParts[1])
) {
return true;
}
}
return false;
};
/**
* Indicates if response charset matches given charset parameter
* @method
* @throws Response.Exception.WRONG_CHARSET_TYPE
* @param {String} charset charset to match (e.g. uf8)
* @returns {Boolean}
*/
Response.prototype.isCharset = function (charset) {
Assertions.isString(charset, Response.Exception.WRONG_CHARSET_TYPE);
var currentCharset = this.getCharset();
if (!Types.isNull(currentCharset) && currentCharset.toLowerCase() == charset.toLowerCase()) {
return true;
}
return false;
};
/**
* Returns response charset
* @method
* @param {String} charset charset to match (e.g. uf8)
* @returns {String|null}
*/
Response.prototype.getCharset = function () {
var matches = this._reCharset.exec(this.getHeader('content-type'));
if (matches && !Types.isUndefined(matches[1])) {
return matches[1].toLowerCase();
}
return null;
};
/**
* Returns response encoding
* @method
* @returns {String|null}
*/
Response.prototype.getEncoding = function () {
if (this._isBinary()) {
return 'base64';
}
return this.getCharset();
};
/**
* Returns JSON representation of a response
* @method
* @returns {Object}
*/
Response.prototype.toJson = function () {
var body = this.getBody();
if (body) {
this.setHeader('content-length', this.getBody().length(), true);
}
return {
"statusCode": this._statusCode,
"httpVersion": this._httpVersion,
"headers": this.headers.getHeaders(),
"body": body ? this.getBody().toString() : body
};
};
Response.prototype.setHeaders = function (headers, normalize) {
if (this.hasLogger()) {
headers = this._logger.parse(headers);
}
Message.prototype.setHeaders.call(this, headers, normalize);
return this;
};
/**
* Send response data to the client
* @method
* @returns {undefined}
*/
Response.prototype.send = function () {
// 666 - 709
if (!this.getPipelined() && this._httpServerResponse) {
var body = '';
var contentBody = this.getBody();
if (this._statusCode != 304 && this._statusCode != 415) {
if (contentBody instanceof BinaryData) {
// 653 - 680
// mozemy wyslac jako czysty string (nie musimy robic buffera)
// HACKYY'
if (this.getHeader('content-encoding') != 'gzip' &&
contentBody.getEncoding() == BinaryData.Encoding.TEXT &&
contentBody.getCharacterEncoding() == BinaryData.CharacterEncoding.UTF8) {
body = contentBody.toUTF8String();
this.setHeader('content-length', contentBody.length(), true);
} else {
body = contentBody.toBuffer();
this.setHeader('content-length', contentBody.length(), true);
}
}
}
// PRO 20x: 1912-2169
// PRO 20x: 1845-2061 (bez samego access log (po zlozeniu msg)
// PRO 20x: 1455-1623
// 632 - 666
if (!this._statusCode) {
console.log(' -- StatusCode is not SET! Defaulting to 503!');
this._statusCode = 503;
}
if (this.hasLogger()) {
this._logger.append(this);
}
this._httpServerResponse.writeHead(this._statusCode, this.getHeaders());
if (body.length > 0) {
this._httpServerResponse.end(body);
} else {
this._httpServerResponse.end();
}
}
this.dispatchEvent(SendEvent);
};
/**
* Determines if response is binary content type
* @method
* @private
* @returns {Boolean}
*/
Response.prototype._isBinary = function () {
if (this.isContentType('text/*') || this.isContentType('application/javascript') || this.isContentType('application/x-javascript') || this.isContentType('application/xhtml+xml') || this.isContentType('application/json') || this.isContentType('application/xml') || this.isContentType('application/x-web-app-manifest+json')) {
return false;
}
return true;
};
/**
* Determines if response is text content type
* @method
* @private
* @returns {Boolean}
*/
Response.prototype._isText = function (contentType) {
return !this._isBinary(contentType);
};
/**
* Parses http status line
* @method
* @private
* @param {String} statusLine
* @returns {Object} example: {statusCode: 200, httpVersion: 1.1}
*/
Response.prototype._parseStatusLine = function (statusLine) {
var matches = this._reStatusLine.exec(statusLine);
return {statusCode: parseInt(matches[2], 10), httpVersion: matches[1]};
};
/**
* Returns status line as string
* @method
* @private
* @returns {String} example: "HTTP/1.1 200 OK"
*/
Response.prototype._statusLineAsString = function () {
var statusCode = this.getStatusCode();
return 'HTTP/' + this.getHttpVersion() + ' ' + statusCode + ' ' + http.STATUS_CODES[statusCode];
};
/**
* Determines if caching is disabled for this response
* @method
* @returns {Boolean}
*/
Response.prototype.isCachingDisabled = function () {
var pragma = this.getHeader('pragma');
var cacheControl = this.getHeader('cache-control');
var cachingDisabled = false;
cachingDisabled = cachingDisabled || (pragma && pragma.indexOf('no-cache') > -1);
cachingDisabled = cachingDisabled || (cacheControl && cacheControl.indexOf('no-cache') > -1);
cachingDisabled = cachingDisabled || (cacheControl && cacheControl.indexOf('private') > -1);
return cachingDisabled;
};
/**
* Disable caching by setting appropriate headers
* @method
* @returns {Response}
*/
Response.prototype.disableCaching = function () {
this.setHeader('pragma', 'no-cache', true);
this.setHeader('cache-control', 'no-cache', true);
return this;
};
/**
* Disable caching by removing appropriate headers
* @method
* @returns {Response}
*/
Response.prototype.enableCaching = function () {
this.removeHeader('pragma');
this.removeHeader('cache-control');
return this;
};
/**
* Returns response cache time in seconds
* @method
* @returns {Number}
*/
Response.prototype.getCacheTimeInSeconds = function () {
// jezeli jest pragma: no-cache to nigdy nie cachuj!
var pragma = this.getHeader('pragma');
if (pragma && pragma.indexOf('no-cache') > -1) {
return 0;
}
var cacheControl = this.getHeader('cache-control');
if (cacheControl) {
// jezeli cache-control: no-cache to nigdy nie cachuj!
if (cacheControl.indexOf('no-cache') > -1) {
return 0;
}
// jezeli cache-control: private to nigdy nie cachuj bo mamy shared cache
if (cacheControl.indexOf('private') > -1) {
return 0;
}
// poszukaj czy cache-control ma max-age wtedy zacachuj na max-age
var maxAge = cacheControl.match(/max-age=([0-9]+)/i);
if (maxAge && maxAge[1]) {
return parseInt(maxAge[1], 10); // ilosc sekund na ile cachujemy
}
}
// jezeli jest expires to zacachuj na expires
var expires = this.getHeader('expires');
if (expires) {
expires = new Date(expires);
if (!isNaN(expires.getTime())) {
var now = new Date();
var diff = Math.round((expires - now) / 1000);
if (diff > 0) {
return diff;
}
}
}
// jezeli nie ma zadnych naglowkow to nigdy nie cachuj
return 0;
};
/**
* Sets response cache time in seconds
* @method
* @param {Number} time
* @returns {Response}
*/
Response.prototype.setCacheTime = function (time) {
time = parseInt(time, 10);
if (!isNaN(time)) {
this.setHeader('cache-control', 'max-age=' + time, true);
}
return this;
};
/**
* Determines if response body is empty (null)
* @method
* @returns {Boolean}
*/
Response.prototype.isEmptyBody = function () {
return this._body == null;
};
/**
* Determines if response content is expired
* @method
* @returns {Boolean}
*/
Response.prototype.isExpired = function () {
var expires = this.getHeader('expires');
if (expires) {
expires = new Date(expires);
if (!isNaN(expires.getTime())) {
var now = new Date();
if (expires > now) {
return false;
}
}
}
return true;
};
/**
* Determines if caching is disabled for this response
* @method
* @throws Response.Exception.WRONG_REQUEST_OBJECT
* @param {Request} request
* @returns {Boolean}
*/
Response.prototype.isModified = function (request) {
if (!(request instanceof Request)) {
throw Response.Exception.WRONG_REQUEST_OBJECT;
}
var modifiedSince = request.getHeader('if-modified-since');
var lastModified = this.getHeader('last-modified');
// check If-Modified-Since
if (modifiedSince && lastModified) {
modifiedSince = new Date(modifiedSince);
lastModified = new Date(lastModified);
// Ignore invalid dates
if (!isNaN(modifiedSince.getTime())) {
if (lastModified <= modifiedSince) {
return false;
}
}
}
return true;
};
/**
* Parses response serialized in Buffer
* @method
* @private
* @throws Message.Exception.WRONG_BUFFER
* @param {Buffer} buffer
* @returns {Object} JSON representation of response
*/
Response.prototype._parseBuffer = function (buffer) {
var headersEnd = -1;
var bodyStart = -1;
for (var i = 0, l = buffer.length; i < l; i++) {
if (buffer[i] == 13 && buffer[i + 2] == 13 && buffer[i + 1] == 10 && buffer[i + 3] == 10) {
headersEnd = i;
bodyStart = i + 4;
break;
}
}
Assertions.equal(true, headersEnd > -1, Message.Exception.WRONG_BUFFER);
Assertions.equal(true, bodyStart > -1, Message.Exception.WRONG_BUFFER);
var resp = this._parseHead(buffer.slice(0, headersEnd).toString('utf8'));
resp.body = buffer.slice(bodyStart);
return resp;
};
// FIXME: ta funkcja dziala tylko z Response!
/**
* Parses response head (status line + headers) serialized in Buffer
* @method
* @private
* @param {Buffer|String} data
* @returns {Object} JSON representation of response {headers:{'key': 'value'}, statusCode: 200, httpVersion: 1.1}
*/
Response.prototype._parseHead = function (data) {
var headers = data.split("\r\n"), statusLine = this._parseStatusLine(headers.shift());
return {
headers: this._parseHeaders(headers),
statusCode: statusLine.statusCode,
httpVersion: statusLine.httpVersion
};
};
/**
* Parses response head (status line + headers) serialized in Buffer
* @method
* @private
* @param {Buffer|String} headers
* @returns {Object} JSON representation of response
*/
Response.prototype._parseHeaders = function (headers) {
var newHeaders = {}, pos = 0, headerName;
for (var i = 0, l = headers.length; i < l; i++) {
//wiem ze ten kod jest glupi ale split w javascripcie jest jeszcze glupszy
pos = headers[i].indexOf(':');
headerName = headers[i].substring(0, pos).trim().toLowerCase();
if (headerName == 'set-cookie') {
if (!newHeaders.hasOwnProperty(headerName) || !Types.isArray(newHeaders[headerName])) {
newHeaders[headerName] = [];
}
newHeaders[headerName].push(headers[i].substring(pos + 1, headers[i].length).trim());
} else {
newHeaders[headerName] = headers[i].substring(pos + 1, headers[i].length).trim();
}
}
return newHeaders;
};
/**
* Returns response head (status line + headers) as string
* @method
* @private
* @returns {String}
*/
Response.prototype._headAsString = function () {
return this._statusLineAsString() + "\r\n" + this._headersAsString() + "\r\n";
};
/**
* Returns response headers as string
* @method
* @private
* @returns {String}
*/
Response.prototype._headersAsString = function () {
var str = '',
cookie,
headers = this.headers;
for (var key in headers.getHeaders()) {
//if(this._headers.hasOwnProperty(key)){
if (key == 'set-cookie') {
cookie = headers.getRawHeader(key);
for (var items = 0, max = cookie.length; items < max; items++) {
str += key + ':' + cookie[items] + "\r\n";
}
} else {
str += key + ':' + headers.getRawHeader(key).join(',') + "\r\n";
}
//}
}
return str;
};
/**
* Returns response serialized into Buffer
* @method
* @returns {Buffer}
*/
Response.prototype.toBuffer = function () {
this.setHeader('content-length', this.getBody().length(), true);
var headBuffer = new Buffer(this._headAsString());
var bodyBuffer = this.getBody().toBuffer();
var buffer = new Buffer(headBuffer.length + bodyBuffer.length);
headBuffer.copy(buffer);
bodyBuffer.copy(buffer, headBuffer.length);
return buffer;
};
/**
* Returns response serialized into BinaryData
* @method
* @returns {BinaryData}
*/
Response.prototype.toBinaryData = function () {
return new BinaryData(this.toBuffer(), this.getBody().getEncoding(), this.getBody().getCharacterEncoding());
};
/**
* Writes chunk of data to response
* @method
* @fires Response.Event.WRITE
*/
Response.prototype.write = function (buffer) {
this.buffers.push(buffer);
this.dataLength += buffer.length;
this.dispatchEvent(new Event(Response.Event.WRITE, buffer));
};
/**
* Signals the end of response data stream
* @method
* @fires Response.Event.END
*/
Response.prototype.end = function () {
var buffer = new Buffer(this.dataLength);
var bufferPosition = 0;
for (var i = 0, l = this.buffers.length; i < l; i++) {
this.buffers[i].copy(buffer, bufferPosition);
bufferPosition += this.buffers[i].length;
}
this.buffers = [];
this.dataLength = 0;
this._setBody(buffer);
this.dispatchEvent(new Event(Response.Event.END, null));
};
/**
* @static
* @constant
* @namespace
*/
Response.Event = {};
/**
* @static
* @constant
* @type {String}
* @default
*/
Response.Event.SEND = "Response_Event_SEND";
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Event.WRITE = "Response.Event.WRITE";
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Event.END = "Response.Event.END";
/**
* @static
* @constant
* @namespace
*/
Response.Exception = {};
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Exception.WRONG_BODY = "Body has to be a string";
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Exception.WRONG_STATUS_CODE = "Status code has to be a number";
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Exception.WRONG_CONTENT_TYPE = "Content-type must be a string";
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Exception.WRONG_CHARSET_TYPE = "Charset must be a string";
/**
* @constant
* @static
* @type {String}
* @default
*/
Response.Exception.WRONG_REQUEST_OBJECT = "Wrong request object";
// precached send event
var SendEvent = new Event(Response.Event.SEND);
exports.Response = Response;