UNPKG

@doodad-js/http

Version:
1,614 lines (1,352 loc) 102 kB
//! BEGIN_MODULE() //! REPLACE_BY("// Copyright 2015-2018 Claude Petit, licensed under Apache License version 2.0\n", true) // doodad-js - Object-oriented programming framework // File: Server_Http.js - Server tools // Project home: https://github.com/doodadjs/ // Author: Claude Petit, Quebec city // Contact: doodadjs [at] gmail.com // Note: I'm still in alpha-beta stage, so expect to find some bugs or incomplete parts ! // License: Apache V2 // // Copyright 2015-2018 Claude Petit // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! END_REPLACE() //! IF_SET("mjs") //! ELSE() "use strict"; //! END_IF() exports.add = function add(modules) { modules = (modules || {}); modules['Doodad.Server.Http'] = { version: /*! REPLACE_BY(TO_SOURCE(VERSION(MANIFEST("name")))) */ null /*! END_REPLACE()*/, namespaces: ['Interfaces', 'MixIns'], create: function create(root, /*optional*/_options, _shared) { const doodad = root.Doodad, types = doodad.Types, tools = doodad.Tools, locale = tools.Locale, files = tools.Files, dates = tools.Dates, namespaces = doodad.Namespaces, //mime = tools.Mime, //interfaces = doodad.Interfaces, mixIns = doodad.MixIns, extenders = doodad.Extenders, //widgets = doodad.Widgets, io = doodad.IO, server = doodad.Server, serverMixIns = server.MixIns, http = server.Http, //httpInterfaces = http.Interfaces, httpMixIns = http.MixIns, ioJson = io.Json, //ioXml = io.Xml, moment = dates.Moment, // optional unicode = tools.Unicode; const __Internal__ = { }; tools.complete(_shared.Natives, { windowRegExp: global.RegExp, }); /* RFC 7230 token = 1*tchar tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA ; any VCHAR, except delimiters A string of text is parsed as a single value if it is quoted using double-quote marks. quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text obs-text = %x80-FF The backslash octet ("\") can be used as a single-octet quoting mechanism within quoted-string and comment constructs. Recipients that process the value of a quoted-string MUST handle a quoted-pair as if it were replaced by the octet following the backslash. quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) OWS = *( SP / HTAB ) ; optional whitespace RWS = 1*( SP / HTAB ) ; required whitespace BWS = OWS ; "bad" whitespace */ /* RFC 7231 Accept = #( media-range [ accept-params ] ) media-range = ( "* / *" / ( type "/" "*" ) / ( type "/" subtype ) ) *( OWS ";" OWS parameter ) accept-params = weight *( accept-ext ) accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] weight = OWS ";" OWS "q=" qvalue qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) */ __Internal__.isObsText = function isObsText(chrAscii) { //return ((chrAscii >= 0x80) && (chrAscii <= 0xFF)); return (chrAscii >= 0x80); }; __Internal__.getNextTokenOrString = function getNextTokenOrString(value, /*byref*/pos, /*optional*/token, /*optional byref*/delimiters) { const delims = delimiters && delimiters[0]; let i = pos[0], quoted = false, str = '', quotePair = false, endOfToken = false; if (delimiters) { // No delimiter encountered delimiters[0] = null; }; let chr = unicode.nextChar(value, i); while (chr) { const prevI = i, chrAscii = chr.codePoint; if (quoted) { // QUOTED STRING if (chrAscii === 0x5C) { // '\\' quotePair = true; } else if (quotePair) { quotePair = false; if ( (chrAscii === 0x09) || // '\t' ((chrAscii >= 0x20) && (chrAscii <= 0x7E)) || // US ASCII Visible Chars __Internal__.isObsText(chrAscii) ) { str += chr.chr; } else { // Invalid string str = null; break; }; } else if (chrAscii === 0x22) { // '"' quoted = false; } else if ( (chrAscii === 0x09) || // '\t' (chrAscii === 0x20) || // ' ' (chrAscii === 0x21) || // '!' ((chrAscii >= 0x23) && (chrAscii <= 0x5B)) || ((chrAscii >= 0x5D) && (chrAscii <= 0x7E)) || __Internal__.isObsText(chrAscii) ) { str += chr.chr; } else { // Invalid string str = null; break; }; } else { // TOKEN if ((chrAscii === 0x09) || (chrAscii === 0x20)) { // OWS // Skip spaces if (str) { endOfToken = true; }; } else if (chrAscii === 0x22) { // '"' if (endOfToken || str || token) { // Invalid token str = null; break; } else { quoted = true; }; } else if ((chrAscii >= 0x20) && (chrAscii <= 0x7E)) { // US ASCII Visible Chars, excepted delimiters if (delims && (delims.indexOf(chr.chr) >= 0)) { // Delimiter encountered. End of token. if (delimiters) { delimiters[0] = chr.chr; }; i = chr.index + chr.size; break; } else if (endOfToken) { i = prevI; break; } else { str += chr.chr; }; } else { // Invalid token str = null; break; }; }; i = chr.index + chr.size; chr = chr.nextChar(); }; if (quoted) { // Unterminated quoted string str = null; }; pos[0] = i; return str; }; http.ADD('parseAcceptHeader', function parseAcceptHeader(value) { const result = tools.nullObject(), pos = [], delimiters = []; let i = 0, media, token, str, qvalue, acceptExts; while (i < value.length) { qvalue = 1.0; acceptExts = tools.nullObject(); pos[0] = i; // by ref delimiters[0] = ";,"; // by ref media = __Internal__.getNextTokenOrString(value, pos, true, delimiters); i = pos[0]; if (!media) { // Invalid token return null; }; if (delimiters[0] !== ',') { newExt: while (i < value.length) { pos[0] = i; // by ref delimiters[0] = "="; // by ref token = __Internal__.getNextTokenOrString(value, pos, true, delimiters); i = pos[0]; if (!token) { // Invalid token return null; }; if (delimiters[0] !== "=") { // Invalid token return null; }; token = token.toLowerCase(); // param names are case-insensitive if (token === 'q') { pos[0] = i; // by ref delimiters[0] = ";,"; // by ref qvalue = __Internal__.getNextTokenOrString(value, pos, false, delimiters); i = pos[0]; qvalue = types.toFloat(qvalue, 3); if ((qvalue < 0.0) || (qvalue > 1.0)) { // Invalid "qvalue" return null; }; } else { pos[0] = i; // by ref delimiters[0] = ";,"; // by ref str = __Internal__.getNextTokenOrString(value, pos, false, delimiters); i = pos[0]; if (str === null) { // Invalid token or quoted string return null; }; acceptExts[token] = str; }; if (delimiters[0] === ',') { break newExt; }; }; }; media = media.toLowerCase(); // medias are case-insensitive token = tools.split(media, '/', 2); const type = token[0] || '*', subtype = (token.length > 1) && token[1] || '*'; result[media] = types.freezeObject(tools.nullObject({ name: media, type: type, subtype: subtype, weight: qvalue, exts: types.freezeObject(acceptExts), })); }; return types.values(result).sort(function(media1, media2) { if (media1.weight > media2.weight) { return -1; } else if (media1.weight < media2.weight) { return 1; } else { return 0; }; }); }); http.ADD('parseAcceptEncodingHeader', function parseAcceptEncodingHeader(value) { if (!value) { return []; }; const result = tools.nullObject(), pos = [], delimiters = []; let i = 0, encoding, token, str, qvalue, acceptExts; while (i < value.length) { qvalue = 1.0; acceptExts = tools.nullObject(); pos[0] = i; // by ref delimiters[0] = ";,"; // by ref encoding = __Internal__.getNextTokenOrString(value, pos, true, delimiters); i = pos[0]; if (!encoding) { // Invalid token return null; }; if (delimiters[0] !== ',') { newExt: while (i < value.length) { pos[0] = i; // by ref delimiters[0] = "="; // by ref token = __Internal__.getNextTokenOrString(value, pos, true, delimiters); i = pos[0]; if (!token) { // Invalid token return null; }; if (delimiters[0] !== "=") { // Invalid token return null; }; token = token.toLowerCase(); // param names are case-insensitive if (token === 'q') { pos[0] = i; // by ref delimiters[0] = ";,"; // by ref qvalue = __Internal__.getNextTokenOrString(value, pos, false, delimiters); i = pos[0]; qvalue = types.toFloat(qvalue, 3); if ((qvalue < 0.0) || (qvalue > 1.0)) { // Invalid "qvalue" return null; }; } else { pos[0] = i; // by ref delimiters[0] = ";,"; // by ref str = __Internal__.getNextTokenOrString(value, pos, false, delimiters); i = pos[0]; if (str === null) { // Invalid token or quoted string return null; }; acceptExts[token] = str; }; if (delimiters[0] === ',') { break newExt; }; }; }; encoding = encoding.toLowerCase(); // codings are case-insensitive // NOTE: 'identity' means 'no encoding' if ((qvalue > 0.0) || (encoding === 'identity')) { // NOTE: 'identity' with 'weight' at 0.0 forces an encoding result[encoding] = types.freezeObject(tools.nullObject({ name: encoding, weight: qvalue, exts: types.freezeObject(acceptExts), })); }; }; // Server MUST accept 'identity' unless explicitly not acceptable (weight at 0.0) if (result.identity) { // Client EXPLICITLY reject 'identity' if (result.identity.weight <= 0.0) { delete result.identity; }; } else { // Client DID NOT reject 'identity' result.identity = types.freezeObject(tools.nullObject({ name: 'identity', weight: -1.0, // exceptional weight to make it very low priority exts: types.freezeObject(tools.nullObject()), })); }; return types.values(result) .sort(function(encoding1, encoding2) { if (encoding1.weight > encoding2.weight) { return -1; } else if (encoding1.weight < encoding2.weight) { return 1; } else { return 0; }; }); }); http.ADD('parseContentTypeHeader', function parseContentTypeHeader(contentType) { if (!contentType) { return null; }; const pos = []; let delimiters = []; pos[0] = 0; // byref delimiters = [';']; // byref let media = __Internal__.getNextTokenOrString(contentType, pos, true, delimiters); if (!media) { // Invalid token return null; }; media = media.toLowerCase(); // content-types are case-insensitive const tmp = tools.split(media, '/', 2); const type = tmp[0], subtype = tmp[1]; if (!type || !subtype) { // Invalid media return null; }; const params = tools.nullObject(); if (delimiters[0] === ';') { while (pos[0] < contentType.length) { delimiters = ['=']; // byref let name = __Internal__.getNextTokenOrString(contentType, pos, true, delimiters); if (!name) { // Invalid token return null; }; name = name.toLowerCase(); // param names are case-insensitive delimiters = [';']; // byref const value = __Internal__.getNextTokenOrString(contentType, pos, false, delimiters); params[name] = value || ''; }; }; const weight = types.toFloat(types.get(params, 'q', 1.0)); return types.freezeObject(tools.nullObject({ name: media, type: type, subtype: subtype, params: types.freezeObject(params), weight: weight, customData: tools.nullObject(), // Allows to store custom fields even if object is frozen. toString: function toString() { return this.name + tools.reduce(this.params, function(result, value, key) { if (!types.isNothing(value)) { result += "; " + types.toString(key) + "=" + types.toString(value); }; return result; }, ""); }, clone: function clone() { const params = tools.nullObject(this.params); const customData = tools.nullObject(this.customData); const newType = tools.nullObject(this); newType.params = types.freezeObject(params); newType.customData = customData; return types.freezeObject(newType); }, set: function set(attrs) { const params = tools.nullObject(this.params, types.get(attrs, 'params')); const customData = tools.nullObject(this.customData, types.get(attrs, 'customData')); const newType = tools.nullObject(this, attrs); newType.params = types.freezeObject(params); newType.customData = customData; return types.freezeObject(newType); }, })); }); http.ADD('parseContentDispositionHeader', function parseContentDispositionHeader(contentDisposition) { if (!contentDisposition) { return null; }; const pos = []; let delimiters = []; pos[0] = 0; // byref delimiters = [';=']; // byref let media = __Internal__.getNextTokenOrString(contentDisposition, pos, true, delimiters); if (media === null) { return null; }; const params = tools.nullObject(); if (delimiters[0] === ';') { media = media.toLowerCase(); // content-dispositions are case-insensitive } else { delimiters = [';']; // byref const value = __Internal__.getNextTokenOrString(contentDisposition, pos, false, delimiters); params[media] = value || ''; media = ''; }; while (pos[0] < contentDisposition.length) { delimiters = ['=']; // byref let name = __Internal__.getNextTokenOrString(contentDisposition, pos, true, delimiters); if (!name) { // Invalid token return null; }; name = name.toLowerCase(); // param names are case-insensitive delimiters = [';']; // byref const value = __Internal__.getNextTokenOrString(contentDisposition, pos, false, delimiters); params[name] = value || ''; }; return types.freezeObject(tools.nullObject({ name: media, params: types.freezeObject(params), toString: function toString() { return this.name + tools.reduce(this.params, function(result, value, key) { if (!types.isNothing(value)) { result += "; " + types.toString(key) + "=" + types.toString(value); }; return result; }, ""); }, })); }); http.ADD('compareMimeTypes', function compareMimeTypes(mimeType1, mimeType2) { if (mimeType1.name === mimeType2.name) { return 40; } else if ((mimeType1.type === mimeType2.type) && ((mimeType1.subtype === '*') || (mimeType2.subtype === '*'))) { return 30; } else if (((mimeType1.type === '*') || (mimeType2.type === '*')) && (mimeType1.subtype === mimeType2.subtype)) { return 20; } else if (((mimeType1.type === '*') && (mimeType1.subtype === '*')) || ((mimeType2.type === '*') && (mimeType2.subtype === '*'))) { return 10; } else { return 0; }; }); http.ADD('toRFC1123Date', function(date) { // ex.: Fri, 10 Jul 2015 03:16:55 GMT if (moment && moment.isMoment(date)) { date = date.toDate(); }; return dates.strftime('%a, %d %b %Y %H:%M:%S GMT', date, __Internal__.enUSLocale, true); }); httpMixIns.REGISTER(doodad.MIX_IN(doodad.Class.$extend( mixIns.Events, mixIns.Creatable, { $TYPE_NAME: 'Headers', $TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('HeadersMixIn')), true) */, headers: doodad.PROTECTED(null), contentType: doodad.PUBLIC(doodad.READ_ONLY(null)), contentDisposition: doodad.PUBLIC(doodad.READ_ONLY(null)), __varyingHeaders: doodad.PROTECTED(null), onHeadersChanged: doodad.EVENT(false), create: doodad.OVERRIDE(function create(/*paramarray*/...args) { this.headers = tools.nullObject(); this._super(...args); }), getHeader: doodad.PUBLIC(function getHeader(name) { const fixed = tools.title(name, '-'); return this.headers[fixed]; }), getHeaders: doodad.PUBLIC(function getHeaders(/*optional*/names) { if (names) { if (!types.isArray(names)) { names = [names]; }; const headers = {}; tools.forEach(names, function(name) { const fixed = tools.title(name, '-'); headers[name] = this.headers[fixed]; if (name !== fixed) { headers[fixed] = this.headers[fixed]; }; }); return headers; } else { return tools.extend({}, this.headers); }; }), addHeader: doodad.PUBLIC(function addHeader(name, value) { const responseHeaders = this.headers; const fixed = tools.title(tools.trim(name), '-'); value = (types.isNothing(value) ? '' : tools.trim(types.toString(value))); if (fixed === 'Content-Type') { this.setContentType(value); } else if (fixed === 'Content-Disposition') { this.setContentDisposition(value); } else if (fixed === 'Vary') { this.setVary(value); } else { if (value) { responseHeaders[fixed] = value; } else { delete responseHeaders[fixed]; }; this.onHeadersChanged(new doodad.Event({headers: [fixed]})); }; }), addHeaders: doodad.PUBLIC(function addHeaders(headers) { const responseHeaders = this.headers; const changed = tools.nullObject(); tools.forEach(headers, function(value, name) { const fixed = tools.title(tools.trim(name), '-'); value = (types.isNothing(value) ? '' : tools.trim(types.toString(value))); if (fixed === 'Content-Type') { this.setContentType(value); } else if (fixed === 'Content-Disposition') { this.setContentDisposition(value); } else if (fixed === 'Vary') { this.setVary(value); } else { if (value) { responseHeaders[fixed] = value; } else { delete responseHeaders[fixed]; }; changed[fixed] = null; }; }, this); const changedKeys = types.keys(changed); if (changedKeys.length) { this.onHeadersChanged(new doodad.Event({headers: changedKeys})); }; }), clearHeaders: doodad.PUBLIC(function clearHeaders(/*optional*/names) { let changedHeaders; if (names) { if (!types.isArray(names)) { names = [names]; }; changedHeaders = []; for (let i = 0; i < names.length; i++) { const fixed = tools.title(tools.trim(names[i]), '-'); if (fixed in this.headers) { changedHeaders.push(fixed); if (fixed === 'Content-Type') { types.setAttribute(this, 'contentType', null); } else if (fixed === 'Content-Disposition') { types.setAttribute(this, 'contentDisposition', null); } else if (fixed === 'Vary') { this.__varyingHeaders = null; }; delete this.headers[fixed]; }; }; } else { changedHeaders = types.keys(this.headers); types.setAttributes(this, { headers: tools.nullObject(), contentType: null, }); }; if (changedHeaders.length) { this.onHeadersChanged(new doodad.Event({headers: changedHeaders})); }; }), setContentType: doodad.PUBLIC(function setContentType(contentType, /*optional*/options) { options = tools.nullObject(options); if (types.isString(contentType)) { contentType = http.parseContentTypeHeader(contentType); }; const encoding = options.encoding; if (encoding) { contentType = contentType.set({params: {charset: encoding}}); }; types.setAttribute(this, 'contentType', contentType); this.headers['Content-Type'] = contentType.toString(); this.onHeadersChanged(new doodad.Event({headers: ['Content-Type']})); return this.contentType; }), setContentDisposition: doodad.PUBLIC(function setContentDisposition(contentDisposition) { if (types.isString(contentDisposition)) { contentDisposition = http.parseContentDispositionHeader(contentDisposition); }; types.setAttribute(this, 'contentDisposition', contentDisposition); this.headers['Content-Disposition'] = (contentDisposition && contentDisposition.toString() || ""); this.onHeadersChanged(new doodad.Event({headers: ['Content-Disposition']})); return this.contentDisposition; }), setVary: doodad.PUBLIC(function setVary(names) { if (!this.__varyingHeaders) { this.__varyingHeaders = tools.nullObject(); }; tools.forEach(names.split(','), function(name) { const fixed = tools.title(tools.trim(name), '-'); this.__varyingHeaders[fixed] = true; }, this); const vary = tools.reduce(this.__varyingHeaders, function(result, dummy, name) { return result + ', ' + name; }, ""); this.headers['Vary'] = vary.slice(2); this.onHeadersChanged(new doodad.Event({headers: ['Vary']})); return vary; }), storeHeaders: doodad.PUBLIC(function storeHeaders(storeObj, /*optional*/names) { storeObj.addHeaders(this.getHeaders(names)); }), }))); http.REGISTER(doodad.BASE(doodad.Object.$extend( httpMixIns.Headers, //mixIns.Events, // serverMixIns.Response, { $TYPE_NAME: 'Response', $TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('ResponseBase')), true) */, onGetStream: doodad.EVENT(false), onError: doodad.ERROR_EVENT(), onStatus: doodad.EVENT(false), onSendHeaders: doodad.EVENT(false), __ending: doodad.PROTECTED(false), ended: doodad.PUBLIC(doodad.PERSISTENT(doodad.READ_ONLY(false))), request: doodad.PUBLIC(doodad.READ_ONLY(null)), status: doodad.PUBLIC(doodad.READ_ONLY(types.HttpStatus.OK)), message: doodad.PUBLIC(doodad.READ_ONLY('OK')), statusData: doodad.PUBLIC(doodad.READ_ONLY(null)), trailers: doodad.PROTECTED(null), headersSent: doodad.PUBLIC(doodad.READ_ONLY(false)), trailersSent: doodad.PUBLIC(doodad.READ_ONLY(false)), __pipes: doodad.PROTECTED(null), stream: doodad.PROTECTED(null), getStream: doodad.PUBLIC(doodad.ASYNC(doodad.MUST_OVERRIDE())), // function(/*optional*/options) clear: doodad.PUBLIC(doodad.MUST_OVERRIDE()), // function clear() respondWithStatus: doodad.PUBLIC(doodad.ASYNC(doodad.MUST_OVERRIDE())), // function respondWithStatus(/*optional*/status, /*optional*/message, /*optional*/headers, /*optional*/data) respondWithError: doodad.PUBLIC(doodad.ASYNC(doodad.MUST_OVERRIDE())), // function respondWithError(ex) // TODO: Validate reset: doodad.PUBLIC(function reset() { if (!this.ended) { if (this.__handlersStates) { const handlers = this.request.getHandlers().filter(function(handler) { return !types.isFunction(handler); }); this.clearEvents(handlers); }; types.setAttributes(this, { headers: tools.nullObject(), trailers: tools.nullObject(), __pipes: [], stream: null, }); }; }), create: doodad.OVERRIDE(function create(request) { this._super(); types.setAttribute(this, 'request', request); this.reset(); }), setContentType: doodad.OVERRIDE(function setContentType(contentType, /*optional*/options) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; if (this.headersSent) { throw new types.NotAvailable("Can't add new headers because headers have been sent to the client."); }; contentType = this.request.getAcceptables(contentType, options)[0]; if (!contentType) { throw new types.HttpError(types.HttpStatus.NotAcceptable); }; return this._super(contentType, options); }), addHeader: doodad.OVERRIDE(function addHeader(name, value) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; if (this.headersSent) { throw new types.NotAvailable("Can't add new headers because headers have been sent to the client."); }; this._super(name, value); this.request.setFullfilled(true); }), addHeaders: doodad.OVERRIDE(function addHeaders(headers) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; if (this.headersSent) { throw new types.NotAvailable("Can't add new headers because headers have been sent to the client."); }; this._super(headers); this.request.setFullfilled(true); }), clearHeaders: doodad.OVERRIDE(function clearHeaders(/*optional*/names) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; if (this.headersSent) { throw new types.NotAvailable("Can't clear headers because they have been sent to the client."); }; this._super(names); }), addTrailer: doodad.PUBLIC(function addTrailer(name, value) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; if (this.trailersSent) { throw new types.NotAvailable("Can't add new trailers because trailers have been sent and the request has ended."); }; const responseTrailers = this.trailers; const fixed = tools.title(tools.trim(name), '-'); value = (types.isNothing(value) ? '' : tools.trim(types.toString(value))); if (value) { responseTrailers[fixed] = value; } else { delete responseTrailers[fixed]; }; this.onHeadersChanged(new doodad.Event({headers: [fixed], areTrailers: true})); this.request.setFullfilled(true); }), addTrailers: doodad.PUBLIC(function addTrailers(trailers) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; if (this.trailersSent) { throw new types.NotAvailable("Can't add new trailers because trailers have been sent and the request has ended."); }; const responseTrailers = this.trailers; const changed = tools.nullObject(); tools.forEach(trailers, function(value, name) { const fixed = tools.title(tools.trim(name), '-'); value = (types.isNothing(value) ? '' : tools.trim(types.toString(value))); if (value) { responseTrailers[fixed] = value; } else { delete responseTrailers[fixed]; }; changed[fixed] = null; }); const changedKeys = types.keys(changed); if (changedKeys.length) { this.onHeadersChanged(new doodad.Event({headers: changedKeys, areTrailers: true})); }; this.request.setFullfilled(true); }), clearTrailers: doodad.PUBLIC(function clearTrailers(/*optional*/names) { if (this.ended && !this.__ending) { throw new server.EndOfRequest(); }; let changedTrailers; if (names) { if (!types.isArray(names)) { names = [names]; }; changedTrailers = []; for (let i = 0; i < names.length; i++) { const fixed = tools.title(tools.trim(names[i]), '-'); if (fixed in this.trailers) { changedTrailers.push(fixed); delete this.trailers[fixed]; }; }; } else { changedTrailers = types.keys(this.tailers); types.setAttributes(this, { trailers: tools.nullObject(), }); }; if (changedTrailers.length) { this.onHeadersChanged(new doodad.Event({headers: changedTrailers, areTrailers: true})); }; }), setStatus: doodad.PUBLIC(function setStatus(status, /*optional*/message) { if (this.ended) { throw new server.EndOfRequest(); }; if (this.headersSent) { throw new types.NotAvailable("Can't respond with a new status because the headers have already been sent to the client."); }; types.setAttributes(this, { status: status || types.HttpStatus.OK, message: message || null, }); if (status) { this.request.setFullfilled(true); }; }), addPipe: doodad.PUBLIC(function addPipe(stream, /*optional*/options) { if (this.ended) { throw new server.EndOfRequest(); }; if (!this.__pipes) { throw new types.NotAvailable("'addPipe' is not available because pipes have already been proceed."); }; options = tools.nullObject(options); const headers = options.headers; if (headers) { this.addHeaders(headers); }; // TODO: Assert on "stream" // NOTE: Pipes are made at "getStream". const pipe = {stream: stream, options: options}; if (options.unshift) { this.__pipes.unshift(pipe); } else { this.__pipes.push(pipe); }; }), clearPipes: doodad.PUBLIC(function clearPipes() { if (this.ended) { throw new server.EndOfRequest(); }; if (!this.__pipes) { throw new types.NotAvailable("'clearPipes' is not available because pipes have already been proceed."); }; this.__pipes = []; }), hasStream: doodad.PUBLIC(function hasStream() { return !!this.stream; }), hasContent: doodad.PUBLIC(function hasContent() { return this.hasStream() || !types.isNothing(this.status) || !types.isEmpty(this.headers) || !types.isEmpty(this.trailers); }), }))); http.REGISTER(doodad.EXPANDABLE(doodad.Object.$extend( mixIns.RawEvents, { $TYPE_NAME: 'HandlerState', $TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('HandlerState')), true) */, parent: doodad.PUBLIC(doodad.READ_ONLY(null)), matcherResult: doodad.PUBLIC(doodad.READ_ONLY(null)), mimeTypes: doodad.PUBLIC(doodad.READ_ONLY(null)), url: doodad.PUBLIC(doodad.READ_ONLY(null)), mustDestroy: doodad.PUBLIC(doodad.READ_ONLY(false)), }))); http.REGISTER(doodad.BASE(doodad.Object.$extend( httpMixIns.Headers, serverMixIns.Request, { $TYPE_NAME: 'Request', $TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('RequestBase')), true) */, onGetStream: doodad.EVENT(false), __ending: doodad.PROTECTED(false), ended: doodad.PUBLIC(doodad.PERSISTENT(doodad.READ_ONLY(false))), response: doodad.PUBLIC(doodad.READ_ONLY(null)), verb: doodad.PUBLIC(doodad.READ_ONLY(null)), url: doodad.PUBLIC(doodad.READ_ONLY(null)), data: doodad.PUBLIC(doodad.READ_ONLY(null)), clientCrashed: doodad.PUBLIC(doodad.READ_ONLY(false)), clientCrashRecovery: doodad.PUBLIC(doodad.READ_ONLY(false)), contentType: doodad.PUBLIC(doodad.READ_ONLY(null)), createResponse: doodad.PROTECTED(doodad.MUST_OVERRIDE()), // function createResponse(/*paramarray*/) stream: doodad.PROTECTED(null), __streamOptions: doodad.PROTECTED(null), getStream: doodad.PUBLIC(doodad.ASYNC(doodad.MUST_OVERRIDE())), // function getStream(/*optional*/options) __pipes: doodad.PROTECTED(null), __waitQueue: doodad.PROTECTED(null), // before 'close' __redirectsCount: doodad.PROTECTED(0), __parsedAccept: doodad.PROTECTED(null), id: doodad.PUBLIC(doodad.READ_ONLY(null)), // ease debugging __handlersStates: doodad.PROTECTED(null), currentHandler: doodad.PUBLIC(doodad.READ_ONLY(null)), __contentEncodings: doodad.PROTECTED(null), __fullfilled: doodad.PROTECTED(false), $__actives: doodad.PROTECTED(doodad.TYPE(0)), $__active_requests: doodad.PROTECTED(doodad.TYPE( new types.Set() )), $__total: doodad.PROTECTED(doodad.TYPE(0)), $__successful: doodad.PROTECTED(doodad.TYPE(0)), $__redirected: doodad.PROTECTED(doodad.TYPE(0)), $__failed: doodad.PROTECTED(doodad.TYPE(null)), $__aborted: doodad.PROTECTED(doodad.TYPE(0)), $getStats: doodad.PUBLIC(doodad.TYPE(function $getStats() { return tools.nullObject({ actives: this.$__actives, total: this.$__total, successful: this.$__successful, redirected: this.$__redirected, failed: this.$__failed, aborted: this.$__aborted, }); })), $getActives: doodad.PUBLIC(doodad.TYPE(function $getActives() { const actives = []; this.$__active_requests.forEach(function(request) { actives[actives.length] = request.url.toString(); }); return actives; })), $clearStats: doodad.PUBLIC(doodad.TYPE(function $clearStats() { this.$__total = 0; this.$__successful = 0; this.$__redirected = 0; this.$__failed = tools.nullObject(); this.$__aborted = 0; })), $create: doodad.OVERRIDE(function $create() { this._super(); this.$clearStats(); }), // TODO: Validate reset: doodad.PUBLIC(function reset() { if (!this.ended) { if (this.__handlersStates) { const handlers = this.getHandlers().filter(function(handler) { return !types.isFunction(handler); }); this.response.clearEvents(handlers); this.clearEvents(handlers); tools.forEach(this.__handlersStates, function(state, handler) { if (state.mustDestroy) { types.DESTROY(handler); types.DESTROY(state); }; }); }; types.setAttributes(this, { __pipes: [], __streamOptions: tools.nullObject(), __waitQueue: [], __handlersStates: new types.Map(), stream: null, __fullfilled: false, __contentEncodings: [], }); this.onSanitize.stackSize = 60; }; }), create: doodad.OVERRIDE(function create(server, verb, url, headers, /*optional*/responseArgs) { const type = types.getType(this); if (type.$__total >= types.getSafeIntegerBounds().max) { type.$clearStats(); }; type.$__total++; type.$__actives++; if (type.$__active_requests) { type.$__active_requests.add(this); }; try { if (types.isString(url)) { url = files.Url.parse(url); }; if (root.DD_ASSERT) { root.DD_ASSERT && root.DD_ASSERT(types._implements(server, httpMixIns.Server), "Invalid server."); root.DD_ASSERT(types.isString(verb), "Invalid verb."); root.DD_ASSERT(types._instanceof(url, files.Url), "Invalid URL."); root.DD_ASSERT(types.isObject(headers), "Invalid headers."); }; this._super(); types.setAttributes(this, { server: server, verb: verb.toUpperCase(), data: tools.nullObject(), id: tools.generateUUID(), }); this.addHeaders(headers); let host = this.getHeader('Host'); if (host) { host = files.Url.parse(server.protocol + '://' + host + '/'); }; url = files.Url.parse(url); if (host) { url = host.combine(url); }; this.__redirectsCount = types.toInteger(url.args.get('redirects', true)); if (!types.isFinite(this.__redirectsCount) || (this.__redirectsCount < 0)) { this.__redirectsCount = 0; }; const clientCrashed = types.toBoolean(url.args.get('crashReport', false)); const clientCrashRecovery = types.toBoolean(url.args.get('crashRecovery', false)); //throw new types.Error("allo"); // To simulate an error on 'create' url = url.removeArgs(['redirects', 'crashReport', 'crashRecovery']); this.reset(); types.setAttributes(this, { url: url, clientCrashed: clientCrashed, clientCrashRecovery: (clientCrashRecovery && !clientCrashed), __parsedAccept: http.parseAcceptHeader(this.getHeader('Accept') || '*/*'), response: this.createResponse.apply(this, responseArgs || []), }); } catch(ex) { type.$__actives--; const failed = type.$__failed; const status = types.HttpStatus.InternalError; if (types.has(failed, status)) { failed[status]++; } else { failed[status] = 1; }; throw ex; }; }), destroy: doodad.OVERRIDE(function destroy() { this.sanitize(); tools.forEach(this.__handlersStates, function(state, handler) { if (state.mustDestroy) { types.DESTROY(handler); types.DESTROY(state); }; }); types.DESTROY(this.response); const type = types.getType(this); type.$__actives--; if (type.$__active_requests) { type.$__active_requests.delete(this); }; this._super(); }), hasHandler: doodad.PUBLIC(function hasHandler(handler) { const handlers = this.__handlersStates.keys(); return tools.some(handlers, function someHandler(hndlr) { return (types.isJsFunction(hndlr) ? (hndlr === handler) : types.isLike(hndlr, handler)); }); }), getHandlers: doodad.PUBLIC(function getHandlers(/*optional*/handler) { const handlers = this.__handlersStates.keys(); if (handler) { return tools.filter(handlers, function someHandler(hndlr) { return (types.isJsFunction(hndlr) ? (hndlr === handler) : types.isLike(hndlr, handler)); }); } else { return types.toArray(handlers); }; }), getHandlerState: doodad.PUBLIC(function getHandlerState(/*optional*/handler) { let state = null; if (types.isNothing(handler)) { handler = this.currentHandler; }; if (types.isJsFunction(handler) || types._implements(handler, httpMixIns.Handler)) { const states = this.__handlersStates; state = states.get(handler); if (!state) { this.applyHandlerState(handler); state = states.get(handler); }; }; return state; }), applyHandlerState: doodad.PUBLIC(function applyHandlerState(/*optional*/handler, /*optional*/stateProto) { if (types.isNothing(handler)) { handler = this.currentHandler; } else if (types.isString(handler)) { handler = namespaces.get(handler); }; root.DD_ASSERT && root.DD_ASSERT(types.isJsFunction(handler) || types._implements(handler, httpMixIns.Handler), "Invalid handler."); const handlerType = types.getType(handler) || handler; let hndlrs; if (types.isType(handler)) { hndlrs = tools.filter(this.__handlersStates.keys(), function(hndlr) { return !types.isType(hndlr) && types.isLike(hndlr, handler); }); } else { hndlrs = [handler]; }; let globalState = null; const states = this.__handlersStates; tools.forEach(hndlrs, function(hndlr) { let state = states.get(hndlr); if (!state) { if (!globalState) { globalState = this.server.getGlobalHandlerState(handlerType); }; state = new globalState(); states.set(hndlr, state); }; if (stateProto) { state.extend(stateProto).create(); }; }, this); }), getAcceptables: doodad.PUBLIC(function getAcceptables(/*optional*/contentTypes, /*optional*/options) { // Get negociated mime types between the handler and the client. Defaults to the "Accept" header. options = tools.nullObject(options); const handlerState = options.handler && this.getHandlerState(options.handler); const handlerTypes = handlerState && handlerState.mimeTypes; const acceptableTypes = this.__parsedAccept; const allowedTypes = handlerTypes || acceptableTypes; const hasHandlerTypes = !!handlerTypes; const hasAcceptableTypes = !!acceptableTypes; const discardWilcards = hasHandlerTypes && hasAcceptableTypes && !tools.some(acceptableTypes, function(mimeType) { return (mimeType.type === '*') && (mimeType.subtype === '*'); }); if (!contentTypes) { return allowedTypes; }; if (!types.isArray(contentTypes)) { contentTypes = [contentTypes]; }; const acceptedTypes = []; if (allowedTypes) { for (let i = 0; i < contentTypes.length; i++) { let contentType = contentTypes[i]; if (types.isString(contentType)) { contentType = http.parseContentTypeHeader(contentType); }; if (contentType.weight > 0.0) { const result = tools.reduce(allowedTypes, function(result, handlerType, index) { if (!discardWilcards || !((handlerType.type === '*') && (handlerType.subtype === '*'))) { const score = http.compareMimeTypes(handlerType, contentType); if (score > result.score) { result.score = score; result.mimeType = handlerType; result.index = index; }; }; return result; }, {mimeType: null, score: 0, index: -1}); if (result.mimeType) { // Get parameters from the allowed mime types (typicaly 'charset') const newParams = tools.complete({}, result.mimeType.params, contentType.params); const newContentType = contentType.set({weight: result.mimeType.weight, params: newParams}); newContentType.customData.index = result.index; // for "sort" acceptedTypes.push(newContentType); }; }; }; }; acceptedTypes.sort(function(type1, type2) { if (type1.weight > type2.weight) { return -1; } else if (type1.weight < type2.weight) { return 1; } else if (type1.customData.index > type2.customData.index) { return 1; } else if (type1.customData.index < type2.customData.index) { return -1; } else { return 0; }; }); return acceptedTypes; }), redirectClient: doodad.PUBLIC(doodad.ASYNC(function redirectClient(url, /*optional*/isPermanent) { // NOTE: Must always throw an error. if (this.ended) { throw new server.EndOfRequest(); }; const maxRedirects = this.server.options.maxRedirects || 5; if (this.response.headersSent) { throw new types.NotAvailable("Unable to redirect because HTTP headers are already sent."); } else if (this.__redirectsCount >= maxRedirects) { return this.end(); } else { //this.response.clear(); this.__redirectsCount++; url = this.url.set({file: null}).combine(url); const status = (isPermanent ? types.HttpStatus.MovedPermanently : types.HttpStatus.TemporaryRedirect); return this.response.respondWithStatus(status, null, { 'Location': url.toString({ args: { redirects: this.__redirectsCount, }, }), }); }; })), redirectServer: doodad.PUBLIC(doodad.ASYNC(function redirectServer(url, /*optional*/options) { // NOTE: Must always throw an error. if (this.ended) { throw new server.EndOfRequest(); }; options = tools.nullObject(options); const maxRedirects = this.server.options.maxRedirects || 5; if (this.response.headersSent) { throw new types.NotAvailable("Unable to redirect because HTTP headers are already sent."); } else if (this.__redirectsCount >= maxRedirects) { return this.end(); } else { this.response.clear(); this.__redirectsCount++; url = this.url.set({ file: null }).combine(url); //.setArgs({redirects: this.__redirectsCount}); types.setAttribute(this, 'url', url); const verb = options.verb; if (verb) { types.setAttribute(this, 'verb', verb); }; const data = options.data; if (data) { tools.extend(this.data, data); }; this.reset(); this.response.reset(); // NOTE: See "Request.catchError" throw new http.ProceedNewHandlers(this.server.handlersOptions); }; })), addPipe: doodad.PUBLIC(function addPipe(stream, /*optional*/options) { if (this.ended) { throw new server.EndOfRequest(); }; if (!this.__pipes) { throw new types.NotAvailable("'addPipe' is not available because pipes have already been proceed."); }; // TODO: Assert on "stream" // NOTE: Don't immediatly do pipes to not start the transfer. Pipes and transfer are made at "getStream". options = tools.nullObject(options); const pipe = {stream: stream, options: options}; if (options.unshift) { this.__pipes.unshift(pipe); } else { this.__pipes.push(pipe); }; }), clearPipes: doodad.PUBLIC(function clearPipes() { if (this.ended) { throw new server.EndOfRequest(); }; if (!this.__pipes) { throw new types.NotAvailable("'clearPipes' is not available because pipes have already been proceed."); }; this.__pipes = []; }), setStreamOptions: doodad.PUBLIC(function setStreamOptions(options) { if (this.ended) { throw new server.EndOfRequest(); }; const accept = types.get(this.__streamOptions, 'accept') || []; tools.extend(this.__streamOptions, options); if (types.get(options, 'accept')) { let newAccept = options.accept; if (!types.isArray(newAccept)) { newAccept = [newAccept]; }; this.__streamOptions.accept = tools.append(accept, newAccept.map(function(value) { return (types.isString(value) ? http.parseAcceptHeader(value)[0] : value); })); }; }), hasStream: doodad.PUBLIC(function hasStream() { return !!this.stream; }), isFullfilled: doodad.PUBLIC(function isFullfilled() { return this.__fullfilled; }), setFullfilled: doodad.PUBLIC(function setFullfilled(fullfilled) { this.__fullfilled = !!fullfilled; }), resolve: doodad.PUBLIC(doodad.ASYNC(function resolve(url, type) { if (this.ended) { throw new server.EndOfRequest(); }; if (types.isString(type)) { const tmp = namespaces.get(type); if (!tmp) { throw new types.ValueError("Unknown type : '~0~'.", [type]); }; type = tmp; }; if (!types._implements(type, httpMixIns.Handler)) { throw new types.ValueError("Invalid handler : '~0~'.", [types.getTypeName(type) || '<unknown>']); }; return this.proceed(this.server.handlersOptions, {resolve: url, handlerType: type}) .then(function(resolved) { if (resolved && type) { resolved = resolved.filter(obj => types.isLike(obj.handler, type)); }; if (resolved && resolved.length) { return resolved; }; return null; // not found }); })), proceed: doodad.PUBLIC(doodad.ASYNC(function proceed(handlersOptions, /*optional*/options) { const Promise = types.getPromise(); if (this.ended) { throw new server.EndOfRequest(); }; if (!types.isArray(handlersOptions)) { handlersOptions = [handlersOptions]; }; const requestedUrl = this.url; const urlToResolve = types.get(options, 'resolve', null); const handlerType = types.get(options, 'handlerType', null); const runHandler = function _runHandler(handlerOptions, resolved) { handlerOptions = tools.nullObject(handlerOptions); let handler = handlerOptions.handler; const acceptedMimeTypes = this.getAcceptables(handlerOptions.mimeTypes || ['*/*']); if (acceptedMimeTypes && acceptedMimeTypes.length) { const parentState = handlerOptions.parent && this.getHandlerState(handlerOptions.parent); const matcherResult = handlerOptions.matcherResult; const stateUrl = matcherResult && (parentState && parentState.url ? parentState.url.combine(matcherResult.url, {isRelative: true}) : requestedUrl.set({url: matcherResult.url})); let mustDestroy = false; if (types.isType(handler)) { // TODO: Reuse objects on "redirectServer" handler = handler.$createInstance(handlerOptions); mustDestroy = true; }; //console.log(types.getTypeName(handler) + ": " + this.url.toString() + " " + this.id); const handlerState = this.getHandlerState(handler); const stateValues = { parent: handlerOptions.parent || null, matcherResult: matc