@doodad-js/http
Version:
HTTP server (alpha)
1,614 lines (1,352 loc) • 102 kB
JavaScript
//! 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