UNPKG

core

Version:

Core Libs

851 lines (754 loc) 22.5 kB
/** * @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;