@doodad-js/http
Version:
HTTP server (alpha)
1,508 lines (1,247 loc) • 105 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: NodeJs_Server_Http.js - HTTP Server tools for NodeJs
// 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")
//! INJECT("import {default as nodeFs} from 'fs';")
//! INJECT("import {default as nodeZlib} from 'zlib';")
//! INJECT("import {default as nodeHttp} from 'http';")
//! INJECT("import {default as nodeCrypto} from 'crypto';")
//! INJECT("import {default as nodeCluster} from 'cluster';")
//! ELSE()
"use strict";
const nodeFs = require('fs'),
nodeZlib = require('zlib'),
nodeHttp = require('http'),
nodeCrypto = require('crypto'),
nodeCluster = require('cluster');
//! END_IF()
exports.add = function add(modules) {
modules = (modules || {});
modules['Doodad.NodeJs.Server.Http'] = {
version: /*! REPLACE_BY(TO_SOURCE(VERSION(MANIFEST("name")))) */ null /*! END_REPLACE()*/,
dependencies: [
'Doodad.Server.Http',
],
create: function create(root, /*optional*/_options, _shared) {
const doodad = root.Doodad,
types = doodad.Types,
tools = doodad.Tools,
files = tools.Files,
namespaces = doodad.Namespaces,
mime = tools.Mime,
//locale = tools.Locale,
mixIns = doodad.MixIns,
//interfaces = doodad.Interfaces,
//extenders = doodad.Extenders,
io = doodad.IO,
//ioInterfaces = io.Interfaces,
ioMixIns = io.MixIns,
nodejs = doodad.NodeJs,
cluster = nodejs.Cluster,
nodejsIO = nodejs.IO,
nodejsIOInterfaces = nodejsIO.Interfaces,
server = doodad.Server,
//serverInterfaces = server.Interfaces,
ipc = server.Ipc,
http = server.Http,
//httpInterfaces = http.Interfaces,
httpMixIns = http.MixIns,
nodejsServer = nodejs.Server,
nodejsHttp = nodejsServer.Http,
minifiers = io.Minifiers,
templates = doodad.Templates,
templatesHtml = templates.Html,
dates = tools.Dates,
moment = dates.Moment; // Optional
const modulePath = files.parsePath(module.filename).set({file: null});
const __Internal__ = {
};
tools.complete(_shared.Natives, {
windowJSON: global.JSON,
globalBuffer: global.Buffer,
globalProcess: global.process,
//mathFloor: global.Math.floor,
mathRound: global.Math.round,
mathMax: global.Math.max,
});
// TODO:
// 1) (todo) Setup page: IPs, Ports, Base URLs, Fall-back Pages (html status), Max number of processes, Storage Manager location
// 3) (working on) Static files : Base URL (done), file(done)/folder(done), alias (done), Verbs (done), in/out process option (todo), mime type (auto or custom) (done), charset (done), metadata (if text/html) (todo)
// 4) (todo) Dynamic files : Base URL (done), Page class (done), Verbs (done), in/out process option, mime type ('text/html' or custom) (done), charset (done), metadata (if text/html) (todo)
// 5) (todo) Session and Shared Data Storage: Storage Manager Server, Storage Type Class (Pipes (Streams), RAM, Files, DB, ...), Data passed with JSON
// 6) (todo) User/Password/Permissions
nodejsHttp.REGISTER(doodad.EXPANDABLE(http.Response.$extend(
mixIns.NodeEvents,
{
$TYPE_NAME: 'Response',
$TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('NodeJsResponse')), true) */,
__endRacer: doodad.PRIVATE(null),
nodeJsStream: doodad.PROTECTED(null),
nodeJsStreamOnError: doodad.NODE_EVENT('error', function nodeJsStreamOnError(context, err) {
err.trapped = true;
if (!this.ended) {
this.__endRacer.resolve(this.end(true));
};
}),
nodeJsStreamOnClose: doodad.NODE_EVENT('close', function nodeJsStreamOnClose(context) {
// Response stream has been closed
if (!this.ended) {
this.__endRacer.resolve(this.end(!!this.nodeJsStream.finished));
};
}),
nodeJsStreamOnFinish: doodad.NODE_EVENT('finish', function nodeJsStreamOnFinish(context) {
// Response stream has been closed
if (!this.ended) {
this.__endRacer.resolve(this.end(true));
};
}),
create: doodad.OVERRIDE(function create(request, nodeJsStream) {
types.setAttribute(this, 'message', nodeHttp.STATUS_CODES[this.status]);
this._super(request);
const Promise = types.getPromise();
this.__endRacer = Promise.createRacer();
this.nodeJsStream = nodeJsStream;
this.nodeJsStreamOnError.attach(nodeJsStream);
this.nodeJsStreamOnClose.attachOnce(nodeJsStream);
this.nodeJsStreamOnFinish.attachOnce(nodeJsStream);
}),
destroy: doodad.OVERRIDE(function destroy() {
if (!types.DESTROYED(this.nodeJsStream)) {
this.nodeJsStream.end();
};
this.nodeJsStreamOnError.clear();
this.nodeJsStreamOnClose.clear();
this.nodeJsStreamOnFinish.clear();
const racer = this.__endRacer;
if (racer && !racer.isSolved()) {
this.__endRacer.reject(new types.ScriptInterruptedError("Response object is about to be destroyed."));
};
types.DESTROY(this.stream);
this._super();
}),
end: doodad.PUBLIC(doodad.NON_REENTRANT(doodad.ASYNC(function end(forceDisconnect) {
// NOTE: MUST ALWAYS REJECTS
if (this.ended) {
throw new server.EndOfRequest();
};
const Promise = types.getPromise();
return Promise.try(function tryEnd() {
types.setAttribute(this, 'ended', true); // blocks additional operations...
this.__ending = true; // ...but some operations are still allowed
if (!forceDisconnect) {
if (this.status !== types.HttpStatus.OK) {
const ev = new doodad.Event({promise: Promise.resolve()});
this.onStatus(ev);
if (ev.prevent) {
return ev.data.promise;
};
};
};
return undefined;
}, this)
.finally(function() {
let promise = null;
const stream = this.stream,
destroyed = stream && types.DESTROYED(stream),
buffered = stream && !destroyed && stream._implements(ioMixIns.BufferedStreamBase);
if (forceDisconnect || destroyed || this.nodeJsStream.finished) {
types.DESTROY(this.nodeJsStream);
} else {
if (!this.trailersSent) {
this.sendTrailers();
};
promise = Promise.create(function(resolve, reject) {
this.nodeJsStream.once('destroy', resolve);
this.nodeJsStream.once('close', resolve);
this.nodeJsStream.once('finish', resolve);
this.nodeJsStream.once('error', reject);
if (buffered) {
stream.flushAsync({purge: true})
.then(function(dummy) {
if (stream.canWrite()) {
stream.write(io.EOF);
return stream.flushAsync({purge: true});
} else {
this.nodeJsStream.end();
};
return undefined;
}, null, this)
.catch(reject);
} else if (stream && stream.canWrite()) {
return stream.writeAsync(io.EOF);
} else {
this.nodeJsStream.end();
};
return undefined;
}, this);
};
this.__ending = false; // now blocks any operation
return promise;
}, this)
.then(function(dummy) {
if (!this.request.ended) {
return this.request.end(forceDisconnect);
};
return undefined;
}, null, this)
.then(function(dummy) {
throw new server.EndOfRequest();
});
}))),
setStatus: doodad.OVERRIDE(function setStatus(status, /*optional*/message) {
status = status || types.HttpStatus.OK;
message = message || nodeHttp.STATUS_CODES[status];
this._super(status, message);
}),
sendHeaders: doodad.PUBLIC(function sendHeaders() {
if (this.ended && !this.__ending) {
throw new server.EndOfRequest();
};
if (this.headersSent) {
throw new types.NotAvailable("Can't respond with a new status or new headers because the headers have already been sent to the client.");
};
if (this.nodeJsStream.headersSent) {
// NOTE: Should not happen
throw new types.NotAvailable("Can't send the headers and the status because Node.js has already sent headers to the client.");
};
this.onSendHeaders(new doodad.Event());
const response = this.nodeJsStream;
response.statusCode = this.status;
response.statusMessage = this.message;
tools.forEach(this.headers, function(value, name) {
if (value) {
response.setHeader(name, value);
} else {
response.removeHeader(name);
};
});
types.setAttribute(this, 'headersSent', true);
}),
__streamOnWrite: doodad.PROTECTED(function __streamOnWrite(ev) {
if ((!this.ended || this.__ending) && !this.headersSent) {
this.sendHeaders();
};
}),
__streamOnError: doodad.PROTECTED(function __streamOnError(ev) {
ev.preventDefault(); // error handled
if (!this.ended) {
this.__endRacer.resolve(this.end(true));
};
}),
getStream: doodad.OVERRIDE(doodad.NON_REENTRANT(function getStream(/*optional*/options) {
// NOTE: "getStream" is NON_REENTRANT
if (this.ended && !this.__ending) {
throw new server.EndOfRequest();
};
const Promise = types.getPromise();
options = tools.nullObject(options);
const status = options.status,
message = options.message,
headers = options.headers;
if (status || headers) {
if (this.headersSent) {
throw new types.NotAvailable("Can't set a new status or new headers because the headers have been sent to the client.");
};
if (headers) {
this.addHeaders(headers);
};
if (status) {
this.setStatus(status, message);
};
};
if (options.contentType) {
this.setContentType(options.contentType, {encoding: options.encoding});
} else if (options.encoding) {
this.setContentType(this.contentType || 'text/plain', {encoding: options.encoding});
};
const currentStream = this.stream;
if (currentStream) {
return currentStream;
};
if (!this.contentType) {
throw new types.Error("'Content-Type' has not been set.");
};
const responseStream = new nodejsIO.BinaryOutputStream(this.nodeJsStream);
this.request.onSanitize.attachOnce(null, function() {
types.DESTROY(responseStream);
});
responseStream.onWrite.attachOnce(this, this.__streamOnWrite, 10);
const ev = new doodad.Event({
stream: Promise.resolve(responseStream),
options: options,
});
this.onGetStream(ev);
// NOTE: "ev.data.stream" can be overriden, and it can be a Promise that returns a stream, or the stream itself.
return this.__endRacer.race(Promise.resolve(ev.data.stream)
.then(function(responseStream) {
if (types.isNothing(responseStream)) {
throw new http.StreamAborted();
};
root.DD_ASSERT && root.DD_ASSERT(types._implements(responseStream, ioMixIns.OutputStreamBase), "Invalid response stream.");
let headers = null;
tools.forEach(this.__pipes, function(pipe) {
const pipeHeaders = pipe.options.headers;
if (pipeHeaders) {
if (headers) {
tools.extend(headers, pipeHeaders);
} else {
headers = tools.nullObject(pipeHeaders);
};
};
pipe.options.pipeOptions = tools.nullObject(pipe.options.pipeOptions);
// <PRB> No longer works since Node 8.2.1 : Sometimes it generates incomplete files.
//if (!types._implements(pipe.stream, io.Stream) && types._implements(responseStream, io.Stream)) {
// const iwritable = responseStream.getInterface(nodejsIOInterfaces.IWritable);
// pipe.stream.pipe(iwritable, pipe.options.pipeOptions);
//} else {
// pipe.stream.pipe(responseStream, pipe.options.pipeOptions);
//};
//responseStream = pipe.stream;
const pipeStream = pipe.stream;
const isNodeStream = !types._implements(pipeStream, ioMixIns.StreamBase);
const sourceStream = (isNodeStream ? new nodejsIO.BinaryInputOutputStream(pipeStream) : pipeStream);
sourceStream.pipe(responseStream, pipe.options.pipeOptions);
if (isNodeStream) {
this.request.onSanitize.attachOnce(null, function() {
types.DESTROY(sourceStream);
});
};
responseStream = sourceStream;
}, this);
this.__pipes = null; // disables "addPipe".
if (headers) {
this.onSendHeaders.attachOnce(this, function(ev) {
// Re-add pipe headers
this.addHeaders(headers);
});
};
const encoding = this.contentType.params.charset;
if (types._implements(responseStream, io.Stream)) {
if (encoding && !types._implements(responseStream, ioMixIns.TextOutputStream)) {
const textStream = new io.TextDecoderStream({encoding: encoding});
this.request.onSanitize.attachOnce(null, function() {
types.DESTROY(textStream);
});
textStream.pipe(responseStream);
responseStream = textStream;
};
} else {
if (encoding) {
if (!nodejsIO.TextInputStream.$isValidEncoding(encoding)) {
throw new types.Error("Invalid encoding.");
};
responseStream = new nodejsIO.TextOutputStream(responseStream, {encoding: encoding});
} else {
responseStream = new nodejsIO.BinaryOutputStream(responseStream);
};
this.request.onSanitize.attachOnce(null, function() {
types.DESTROY(responseStream);
});
};
responseStream.onError.attach(this, this.__streamOnError, 10);
this.stream = responseStream;
this.request.setFullfilled(true);
return responseStream;
}, null, this)
.catch(function(err) {
types.DESTROY(responseStream);
throw err;
}, this));
})),
sendTrailers: doodad.PROTECTED(function sendTrailers(/*optional*/trailers) {
if (this.ended && !this.__ending) {
throw new server.EndOfRequest();
};
if (this.trailersSent) {
throw new types.NotAvailable("Trailers have already been sent and the request will be closed.");
};
if (!this.headersSent) {
this.sendHeaders(); // must write headers before
};
if (trailers) {
this.addTrailers(trailers);
};
trailers = this.trailers;
if (!types.isEmpty(trailers)) {
this.nodeJsStream.addTrailers(trailers);
};
types.setAttribute(this, 'trailersSent', true);
}),
clear: doodad.OVERRIDE(function clear() {
if (this.ended) {
throw new server.EndOfRequest();
};
if (!this.headersSent) {
this.clearHeaders();
};
if (!this.trailersSent) {
this.clearTrailers();
};
if (this.stream) {
this.stream.clear();
};
}),
respondWithStatus: doodad.OVERRIDE(function respondWithStatus(status, /*optional*/message, /*optional*/headers, /*optional*/data) {
// NOTE: Must always throws an error.
if (this.ended) {
throw new server.EndOfRequest();
};
if (this.headersSent) {
throw new types.NotAvailable("Can't respond with a new status or new headers because the headers have already been sent to the client.");
};
this.addHeaders(headers);
types.setAttributes(this, {
status: status,
message: message || nodeHttp.STATUS_CODES[status],
statusData: data,
});
this.request.setFullfilled(true);
return this.request.end();
}),
respondWithError: doodad.OVERRIDE(function respondWithError(ex) {
// NOTE: Must always throw an error.
if (this.ended) {
throw new server.EndOfRequest();
};
if (ex.critical) {
throw ex;
} else {
ex.trapped = true;
if (!ex.bubble) {
this.clear();
this.request.setFullfilled(true);
if (!this.nodeJsStream) {
// Too late !
return this.end();
} else if (this.headersSent) {
// Too late !
return this.request.end();
} else {
return this.respondWithStatus(types.HttpStatus.InternalError, null, null, ex);
};
};
};
return undefined;
}),
sendFile: doodad.PUBLIC(doodad.ASYNC(function sendFile(path) {
const Promise = types.getPromise();
if (this.ended) {
throw new server.EndOfRequest();
};
if (!(path instanceof files.Path)) {
path = files.Path.parse(path);
};
return Promise.create(function tryStat(resolve, reject) {
nodeFs.stat(path.toApiString(), doodad.Callback(this, function getStatsCallback(err, stats) {
if (err) {
reject(err);
} else {
resolve(stats);
};
}));
}, this)
.then(function parseStats(stats) {
if (!stats.isFile()) {
throw new types.HttpError(types.HttpStatus.NotFound);
};
const contentTypes = this.request.getAcceptables(mime.getTypes(path.file) || ['application/octet-stream']);
if (!contentTypes.length) {
throw new types.HttpError(types.HttpStatus.UnsupportedMediaType);
};
this.setContentType(contentTypes[0]);
this.addHeaders({
'Last-Modified': http.toRFC1123Date(stats.mtime), // ex.: Fri, 10 Jul 2015 03:16:55 GMT
'Content-Length': stats.size,
});
if (!this.getHeader('Content-Disposition')) {
this.addHeader('Content-Disposition', 'attachment; filename="' + path.file.replace(/"/g, '\\"') + '"');
};
if (this.request.verb !== 'HEAD') {
return this.getStream();
};
return undefined;
}, null, this)
.then(function(outputStream) {
if (outputStream) {
const inputStream = nodeFs.createReadStream(path.toApiString());
this.request.onSanitize.attachOnce(null, function() {
types.DESTROY(inputStream);
});
const iwritable = outputStream.getInterface(nodejsIOInterfaces.IWritable);
inputStream.pipe(iwritable);
return outputStream.onEOF.promise();
};
return undefined;
}, null, this)
.catch(function cacthError(err) {
if (err.code === 'ENOENT') {
throw new types.HttpError(types.HttpStatus.NotFound);
} else if (err.code === 'EPERM') {
throw new types.HttpError(types.HttpStatus.Forbidden);
} else {
throw err;
};
}, this);
})),
})));
nodejsHttp.REGISTER(doodad.EXPANDABLE(http.Request.$extend(
mixIns.NodeEvents,
{
$TYPE_NAME: 'Request',
$TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('NodeJsRequest')), true) */,
__endRacer: doodad.PRIVATE(null),
nodeJsStream: doodad.PROTECTED(null),
startTime: doodad.PROTECTED(null),
$__setAbortedWhenEnded: doodad.PROTECTED(false),
__aborted: doodad.PROTECTED(false),
$__time: doodad.PROTECTED(doodad.TYPE(null)),
$__totalHour: doodad.PROTECTED(doodad.TYPE(0)),
$__perSecond: doodad.PROTECTED(doodad.TYPE(0.0)),
$__perMinute: doodad.PROTECTED(doodad.TYPE(0.0)),
$__perHour: doodad.PROTECTED(doodad.TYPE(0.0)),
$__oldPerSecond: doodad.PROTECTED(doodad.TYPE(null)),
$__noActivityStart: doodad.PROTECTED(doodad.TYPE(null)),
$__noActivityTimeout: doodad.PROTECTED(doodad.TYPE(null)),
$__statsUpdated: doodad.PROTECTED(doodad.TYPE(false)),
$getStats: doodad.OVERRIDE(function $getStats() {
const stats = this._super();
return tools.extend(stats, {
perSecond: this.$__perSecond,
perMinute: this.$__perMinute,
perHour: this.$__perHour,
});
}),
$clearStats: doodad.OVERRIDE(function $clearStats() {
this._super();
this.$__time = null;
this.$__totalHour = 0;
this.$__perSecond = 0.0;
this.$__perMinute = 0.0;
this.$__perHour = 0.0;
const oldPerSecond = this.$__oldPerSecond;
if (oldPerSecond) {
oldPerSecond.length = 0;
} else {
this.$__oldPerSecond = [];
};
const noActivityTimeout = this.$__noActivityTimeout;
if (noActivityTimeout) {
noActivityTimeout.cancel();
this.$__noActivityTimeout = null;
};
this.$__noActivityStart = null;
this.$__statsUpdated = false;
}),
$compileStats: doodad.PROTECTED(doodad.TYPE(function $compileStats() {
const oldPerSecond = this.$__oldPerSecond;
let perSecond = 0.0;
const time = this.$__time;
let seconds = 0.0;
if (time) {
const diff = _shared.Natives.globalProcess.hrtime(time);
seconds = diff[0] + (diff[1] / 1e9);
if (seconds > 0.0) {
perSecond = this.$__totalHour / seconds;
};
if (seconds > 86400.0) {
this.$__time = null;
this.$__totalHour = 0;
};
};
oldPerSecond.push(perSecond);
let count = oldPerSecond.length;
if (count > 60) {
oldPerSecond.shift();
count--;
};
if (count > 1) {
const max = count - 1; // last appended is already included in "perScecond".
for (let i = 0; i < max; i++) {
perSecond += oldPerSecond[i];
};
perSecond /= count;
};
this.$__perSecond = perSecond;
const perMinute = (seconds >= 60.0 ? perSecond * 60.0 : 0.0);
this.$__perMinute = perMinute;
this.$__perHour = (seconds >= 3600.0 ? perMinute * 60.0 : 0.0);
})),
$watchNoActivity: doodad.PROTECTED(doodad.TYPE(function $watchNoActivity() {
this.$__noActivityStart = _shared.Natives.globalProcess.hrtime();
this.$__noActivityTimeout = tools.callAsync(function noActivityTimer() {
this.$__noActivityTimeout = null;
if (this.$__statsUpdated) {
this.$__statsUpdated = false;
this.$watchNoActivity();
} else {
const oldPerSecond = this.$__oldPerSecond;
const diff = _shared.Natives.globalProcess.hrtime(this.$__noActivityStart);
let seconds = diff[0] + (diff[1] / 1e9);
seconds = _shared.Natives.mathRound(seconds);
let count = oldPerSecond.length;
if (count > 0) {
let totalHour = this.$__totalHour;
while ((seconds > 0) && (count > 0)) {
totalHour -= _shared.Natives.mathMax(_shared.Natives.mathRound(oldPerSecond.shift()), 1);
if (totalHour < 0) {
totalHour = 0;
break;
};
seconds--;
count--;
};
this.$__totalHour = totalHour;
};
this.$compileStats();
if (this.$__perSecond > 0.0) {
this.$watchNoActivity();
} else {
this.$__time = null;
this.$__totalHour = 0;
oldPerSecond.length = 0;
};
};
}, 1000, this, null, true);
})),
nodeJsStreamOnError: doodad.NODE_EVENT('error', function nodeJsStreamOnError(context, err) {
err.trapped = true;
if (!this.ended) {
this.__endRacer.resolve(this.end(true));
};
}),
nodeJsStreamOnClose: doodad.NODE_EVENT('close', function nodeJsStreamOnClose(context) {
if (this.ended) {
const type = types.getType(this);
if (type.$__setAbortedWhenEnded) {
this.__aborted = true;
};
} else {
this.__endRacer.resolve(this.end(!!this.nodeJsStream.aborted));
};
}),
$create: doodad.OVERRIDE(function $create(/*paramarray*/...args) {
this._super(...args);
// <PRB> Before Node.js version 10.2.1, the 'close' event was normally not emitted when the request was ended.
this.$__setAbortedWhenEnded = (tools.Version.compare("10.2.1", process.versions.node) < 0);
}),
create: doodad.OVERRIDE(function create(server, nodeJsRequest, nodeJsResponse) {
const Promise = types.getPromise();
this.startTime = _shared.Natives.globalProcess.hrtime();
this.nodeJsStream = nodeJsRequest;
this.nodeJsStreamOnError.attach(nodeJsRequest);
this.nodeJsStreamOnClose.attachOnce(nodeJsRequest);
this._super(server, nodeJsRequest.method, nodeJsRequest.url, nodeJsRequest.headers, [nodeJsResponse]);
this.__endRacer = Promise.createRacer();
////////////////
// STATISTICS //
////////////////
const type = types.getType(this);
type.$__totalHour++;
if (type.$__time) {
type.$compileStats();
} else {
type.$__time = _shared.Natives.globalProcess.hrtime();
};
type.$__statsUpdated = true;
if (!type.$__noActivityTimeout) {
type.$watchNoActivity();
};
}),
destroy: doodad.OVERRIDE(function destroy() {
this.nodeJsStreamOnError.clear();
this.nodeJsStreamOnClose.clear();
types.DESTROY(this.stream);
const racer = this.__endRacer;
if (racer && !racer.isSolved()) {
this.__endRacer.reject(new types.ScriptInterruptedError("Request object is about to be destroyed."));
};
this._super();
}),
createResponse: doodad.OVERRIDE(function createResponse(nodeJsRequest) {
return new nodejsHttp.Response(this, nodeJsRequest);
}),
proceed: doodad.OVERRIDE(function proceed(handlersOptions, /*optional*/options) {
return this.__endRacer.race(this._super(handlersOptions, options));
}),
end: doodad.OVERRIDE(function end(/*optional*/forceDisconnect) {
// NOTE: MUST ALWAYS REJECTS
const Promise = types.getPromise();
if (this.ended) {
throw new server.EndOfRequest();
};
function wait() {
if (!forceDisconnect) {
const queue = this.__waitQueue;
if (queue.length) {
this.__waitQueue = [];
return this.__endRacer.race(Promise.all(queue))
.then(wait, null, this);
};
};
return undefined;
};
return Promise.try(function tryEndRequest() {
types.setAttribute(this, 'ended', true); // blocks additional operations...
this.__ending = true; // ...but some operations are still allowed
if (!this.response.ended) {
return this.response.end(forceDisconnect)
.catch(server.EndOfRequest, function () { });
};
return undefined;
}, this)
.then(function(dummy) {
this.__ending = false; // now blocks any operation
const stream = this.stream,
destroyed = stream && types.DESTROYED(stream),
buffered = stream && !destroyed && stream._implements(ioMixIns.BufferedStreamBase);
if (forceDisconnect || destroyed) {
types.DESTROY(this.nodeJsStream);
this.sanitize(); // should prevents blocking on "wait" if everything is cleaned correctly on sanitize.
} else {
if (buffered) {
return stream.flushAsync({purge: true});
};
};
return undefined;
}, null, this)
.then(wait, null, this)
.catch(this.catchError, this)
.nodeify(function(err, dummy) {
const type = types.getType(this);
if (this.__aborted || forceDisconnect || types.DESTROYED(this.response)) {
type.$__aborted++;
} else {
const status = this.response.status;
if (types.HttpStatus.isInformative(status) || types.HttpStatus.isSuccessful(status)) {
type.$__successful++;
} else if (types.HttpStatus.isRedirect(status)) {
type.$__redirected++;
} else { // if (types.HttpStatus.isError(status))
const failed = type.$__failed;
if (types.has(failed, status)) {
failed[status]++;
} else {
failed[status] = 1;
};
};
};
this.onEnd();
if (err) {
throw err;
};
throw new server.EndOfRequest();
}, this);
}),
__streamOnError: doodad.PROTECTED(function __streamOnError(ev) {
ev.preventDefault(); // error handled
if (!this.ended) {
this.__endRacer.resolve(this.end(true));
};
}),
getStream: doodad.OVERRIDE(doodad.NON_REENTRANT(function getStream(/*optional*/options) {
// NOTE: "getStream" is NON_REENTRANT
const Promise = types.getPromise();
if (this.ended && !this.__ending) {
throw new server.EndOfRequest();
};
options = tools.nullObject(this.__streamOptions, options);
const currentStream = this.stream;
if (currentStream) {
return currentStream;
};
const acceptContentEncodings = (this.__contentEncodings.length ? this.__contentEncodings : ['identity']);
const contentEncoding = (this.getHeader('Content-Encoding') || 'identity').toLowerCase(); // case-insensitive
if (acceptContentEncodings.indexOf(contentEncoding) < 0) {
return this.response.respondWithStatus(types.HttpStatus.UnsupportedMediaType);
};
const requestStream = new nodejsIO.BinaryInputStream(this.nodeJsStream);
this.onSanitize.attachOnce(null, function() {
types.DESTROY(requestStream);
});
const ev = new doodad.Event({
stream: Promise.resolve(requestStream),
options: options,
});
this.onGetStream(ev);
// NOTE: "ev.data.stream" can be overriden, and it can be a Promise that returns a stream, or the stream itself.
return this.__endRacer.race(Promise.resolve(ev.data.stream)
.then(function(requestStream) {
if (types.isNothing(requestStream)) {
throw new http.StreamAborted();
};
root.DD_ASSERT && root.DD_ASSERT(types._implements(requestStream, ioMixIns.InputStreamBase), "Invalid request stream.");
let accept = options.accept; // content-types expected by the page
if (types.isString(accept)) {
accept = [http.parseAcceptHeader(accept)];
};
if (!accept || !accept.length) {
return this.response.respondWithStatus(types.HttpStatus.UnsupportedMediaType);
};
const requestType = this.contentType;
if (!requestType) {
return this.response.respondWithStatus(types.HttpStatus.UnsupportedMediaType);
};
let requestEncoding = null;
if (types.has(requestType.params, 'charset')) {
// Encoding of the request body
requestEncoding = requestType.params.charset;
};
const contentTypes = tools.filter(accept, function(type) {
return (((type.name === requestType.name) || (type.name === '*/*') || ((type.type === requestType.type) && (type.subtype === '*'))) && (type.weight > 0.0));
});
if (types.isEmpty(contentTypes)) {
return this.response.respondWithStatus(types.HttpStatus.UnsupportedMediaType);
};
if (!requestEncoding) {
requestEncoding = options.encoding; // default encoding
};
requestStream.onError.attach(this, this.__streamOnError, 10);
tools.forEach(this.__pipes, function forEachPipe(pipe) {
pipe.options.pipeOptions = tools.nullObject(pipe.options.pipeOptions);
//if (!types._implements(requestStream, io.Stream) && types._implements(pipe.stream, io.Stream)) {
// const iwritable = pipe.stream.getInterface(nodejsIOInterfaces.IWritable);
// requestStream.pipe(iwritable, pipe.options.pipeOptions);
//} else {
// requestStream.pipe(pipe.stream, pipe.options.pipeOptions);
//};
//requestStream = pipe.stream;
const pipeStream = pipe.stream;
const isNodeStream = !types._implements(pipeStream, ioMixIns.StreamBase);
const destStream = (isNodeStream ? new nodejsIO.BinaryOutputStream(pipeStream) : pipeStream);
//destStream.onError.attachOnce(this, this.onError);
requestStream.pipe(destStream, pipe.options.pipeOptions);
const sourceStream = (isNodeStream ? new nodejsIO.BinaryInputStream(pipeStream) : pipeStream);
if (isNodeStream) {
this.onSanitize.attachOnce(null, function() {
types.DESTROY(destStream);
types.DESTROY(sourceStream);
});
};
requestStream = sourceStream;
}, this);
this.__pipes = null; // disables "addPipe".
if (types._implements(requestStream, io.Stream)) {
if (requestEncoding && !types._implements(requestStream, [ioMixIns.TextInputStream, ioMixIns.ObjectTransformableOut])) {
const textStream = new io.TextDecoderStream({encoding: requestEncoding});
//textStream.onError.attachOnce(this, this.onError);
//this.onSanitize.attachOnce(null, function() {
// types.DESTROY(textStream);
//});
requestStream.pipe(textStream);
requestStream = textStream;
};
} else {
if (requestEncoding) {
if (!nodejsIO.TextInputStream.$isValidEncoding(requestEncoding)) {
return this.response.respondWithStatus(types.HttpStatus.UnsupportedMediaType);
};
requestStream = new nodejsIO.TextInputStream(requestStream, {encoding: requestEncoding});
} else {
requestStream = new nodejsIO.BinaryInputStream(requestStream);
};
//requestStream.onError.attachOnce(this, this.onError);
//this.onSanitize.attachOnce(null, function() {
// types.DESTROY(requestStream);
//});
};
this.stream = requestStream;
this.setFullfilled(true);
return requestStream;
}, null, this)
.catch(function(err) {
types.DESTROY(requestStream);
throw err;
}, this));
})),
getTime: doodad.PUBLIC(function getTime() {
if (this.ended && !this.__ending) {
throw new server.EndOfRequest();
};
const time = _shared.Natives.globalProcess.hrtime(this.startTime);
return (time[0] * 1000) + (time[1] / 1e6);
}),
getSource: doodad.PUBLIC(function getSource() {
// TODO: Add more informations
if (this.ended && !this.__ending) {
throw new server.EndOfRequest();
};
return tools.nullObject({
address: this.nodeJsStream.socket.remoteAddress,
});
}),
})));
nodejsHttp.REGISTER(http.Server.$extend(
mixIns.NodeEvents,
{
$TYPE_NAME: 'Server',
$TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('NodeJsServer')), true) */,
__nodeServer: doodad.PROTECTED(doodad.READ_ONLY()),
__address: doodad.PROTECTED(doodad.READ_ONLY()),
__listening: doodad.PROTECTED(false),
onNodeRequest: doodad.NODE_EVENT('request', function onNodeRequest(context, nodeRequest, nodeResponse) {
//const Promise = types.getPromise();
if (this.__listening) {
if (this.options.validHosts) {
const host = nodeRequest.headers['host'];
if (tools.indexOf(this.options.validHosts, host) < 0) {
nodeResponse.writeHead(types.HttpStatus.BadRequest);
nodeResponse.end();
return;
};
};
try {
const request = new nodejsHttp.Request(this, nodeRequest, nodeResponse);
request.onError.attach(this, function(ev) {
this.onError(ev);
});
request.response.onError.attach(this, function(ev) {
this.onError(ev);
});
const ev = new doodad.Event({
request: request,
});
this.onNewRequest(ev);
if (!ev.prevent) {
request.proceed(this.handlersOptions)
.catch(request.catchError)
.then(function endRequest() {
if (!types.DESTROYED(request) && !request.ended) {
if (request.isFullfilled()) {
return request.end();
} else {
request.response.clear();
return request.response.respondWithStatus(types.HttpStatus.NotFound);
};
};
return undefined;
})
.catch(request.catchError)
.nodeify(function requestCleanup(err, result) {
types.DESTROY(request);
types.DESTROY(nodeRequest);
types.DESTROY(nodeResponse);
if (err) {
throw err;
};
})
.catch(tools.catchAndExit); // fatal error
};
} catch(ex) {
// <PRB> On error, we must return something to the browser or otherwise it will repeat the request if we just drop the connection !!!
nodeResponse.statusCode = types.HttpStatus.InternalError;
nodeResponse.end(function() {
types.DESTROY(nodeRequest);
types.DESTROY(nodeResponse);
});
throw ex;
};
};
}),
//onNodeConnect: doodad.NODE_EVENT('connect', function onNodeConnect(context) {
//}),
onNodeListening: doodad.NODE_EVENT('listening', function onNodeListening(context) {
types.setAttribute(this, '__address', this.__nodeServer.address());
tools.log(tools.LogLevels.Info, "HTTP server listening on port '~port~', address '~address~'.", this.__address);
tools.log(tools.LogLevels.Warning, "IMPORTANT: It is an experimental and not finished software. Don't use it on production, or do it at your own risks. Please report bugs and suggestions to 'doodadjs [at] gmail <dot> com'.");
}),
onNodeError: doodad.NODE_EVENT('error', function onNodeError(context, ex) {
this.onError(new doodad.ErrorEvent(ex));
}),
onNodeClose: doodad.NODE_EVENT('close', function onNodeClose(context) {
const server = this.__nodeServer;
this.onNodeListening.detach(server);
this.onNodeError.detach(server);
this.onNodeClose.detach(server);
//this.onNodeConnect.detach(server);
tools.log(tools.LogLevels.Info, "Listening socket closed (address '~address~', port '~port~').", this.__address);
types.setAttribute(this, '__nodeServer', null);
}),
isListening: doodad.OVERRIDE(function isListening() {
return this.__listening;
}),
listen: doodad.OVERRIDE(function listen(/*optional*/options) {
if (!this.__listening) {
this.__listening = true;
options = tools.nullObject(options);
const protocol = options.protocol || 'http';
let factory;
if ((protocol === 'http') || (protocol === 'https')) {
/* eslint global-require: "off", import/no-dynamic-require: "off" */
factory = require(protocol);
} else {
throw new doodad.Error("Invalid protocol : '~0~'.", [protocol]);
};
let server;
if (protocol === 'https') {
// TODO: Implement other available options
// TODO: Ask for private key's passphrase from the terminal if encrypted and decrypt the key.
const opts = tools.nullObject();
if (options.pfxFile) {
opts.pfx = nodeFs.readFileSync(types.toString(options.pfxFile));
} else if (options.rawPfx) {
opts.pfx = options.rawPfx;
} else {
if (options.keyFile) {
opts.key = nodeFs.readFileSync(types.toString(options.keyFile));
} else if (options.rawKey) {
opts.key = options.rawKey;
} else {
throw new types.Error("Missing private key file.");
};
if (options.certFile) {
opts.cert = nodeFs.readFileSync(types.toString(options.certFile));
} else if (options.rawCert) {
opts.cert = options.rawCert;
} else {
throw new types.Error("Missing certificate file.");
};
};
if (!opts.pfx && !opts.key && !opts.cert) {
throw new types.Error("Missing private key and certificate files.");
};
server = factory.createServer(opts);
} else {
server = factory.createServer();
};
if (types.has(options, 'timeout')) {
server.setTimeout(options.timeout);
} else {
// Default of 2 minutes is too limited...
server.setTimeout(5 * 60 * 1000); // 5 minutes
};
this.onNodeRequest.attach(server);
this.onNodeListening.attach(server);
this.onNodeError.attach(server);
this.onNodeClose.attach(server);
//if (options.acceptConnect) {
// this.onNodeConnect.attach(server);
//};
//server.on('connection');
//server.on('checkContinue');
//server.on('upgrade');
//server.on('clientError');
types.setAttribute(this, '__nodeServer', server);
const target = options.target || '127.0.0.1';
const type = options.type || 'tcp'; // 'tcp', 'unix', 'handle'
if (type === 'tcp') { // TCP/IP Socket
const port = options.port || (protocol === 'https' ? 443 : 80);
const queueLength = options.queueLength;
if (root.DD_ASSERT) {
root.DD_ASSERT(types.isString(target), "Invalid target.");
root.DD_ASSERT(types.isInteger(port) && (port >= 0) && (port <= 65535), "Invalid port.");
root.DD_ASSERT(types.isNothing(queueLength) || (types.isInteger(queueLength) && (queueLength > 0)), "Invalid queue length.");
};
server.listen(port, target, queueLength);
} else if (type === 'unix') { // Unix Socket
root.DD_ASSERT && root.DD_ASSERT(types.isString(target), "Invalid target.");
server.listen(target);
} else if (type === 'handle') { // System Handle
root.DD_ASSERT && root.DD_ASSERT(types.isObject(target) && (('_handle' in target) || ('fd' in target)), "Invalid target.");
server.listen(target);
} else {
throw new doodad.Error("Invalid target type option : '~0~'.", [type]);
};
types.setAttribute(this, 'protocol', protocol);
this.onListen(new doodad.Event());
};
}),
stopListening: doodad.OVERRIDE(function stopListening() {
if (this.__listening) {
this.__listening = false;
this.onStopListening(new doodad.Event());
this.__nodeServer.close();
types.setAttributes(this, {
__nodeServer: null,
__address: null,
});
};
}),
}));
nodejsHttp.REGISTER(doodad.BASE(templatesHtml.PageTemplate.$extend(
{
$TYPE_NAME: 'FolderPageTemplate',
$TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('FolderPageTemplate')), true) */,
path: doodad.PROTECTED(null),
$readDir: doodad.PUBLIC(doodad.ASYNC(function $readDir(handler, path) {
return files.readdir(path, {async: true})
.then(function sortFiles(filesList) {
filesList = filesList
.filter(function(file) {
if (file.isFolder) {
return true;
};
if (!handler.options.mimeTypes) {
return true;
};
const mimeTypes = mime.getTypes(file.name) || ['application/octet-stream'];
file.mimeTypes = mimeTypes.filter(function(type) {
return tools.some(handler.options.mimeTypes, function(mimeType) {
return mimeType.name === type;
});
});
return (file.mimeTypes.length > 0);
})
.sort(function(file1, file2) {
const n1 = file1.name.toUpperCase(),
n2 = file2.name.toUpperCase();
if ((!file1.isFolder && file2.isFolder)) {
return 1;
} else if (file1.isFolder && !file2.isFolder) {
return -1;
} else if (n1 > n2) {
return 1;
} else if (n1 < n2) {
return -1;
} else {
return 0;
};
});
//console.log(require('util').inspect(filesList));
return filesList;
}, null, this);
})),
create: doodad.OVERRIDE(function create(request, cacheHandler, path) {
this._super(request, cacheHandler);
this.path = path;
if (cacheHandler) {
const state = request.getHandlerState(cacheHandler);
state.onNewCached.attachOnce(this, function(handler, cached) {
files.watch(this.path.toApiString(), function() {
cached.invalidate();
});
});
};
}),
readDir: doodad.PUBLIC(doodad.ASYNC(function readDir() {
return types.getType(this).$readDir(this.request.currentHandler, this.path);
})),
})));
nodejsHttp.REGISTER(http.FileSystemPage.$extend(
{
$TYPE_NAME: 'FileSystemPage',
$TYPE_UUID: '' /*! INJECT('+' + TO_SOURCE(UUID('FileSystemPage')), true) */,
$applyGlobalHandlerStates: doodad.OVERRIDE(function $applyGlobalHandlerStates(server) {
this._super(server);
const resType = this.DD_FULL_NAME;
server.applyGlobalHandlerState(nodejsHttp.CacheHandler, {
generateKey: doodad.OVERRIDE(function generateKey(request, handler, keyObj) {
this._super(request, handler, keyObj);
const res = !request.url.file && request.url.getArg('res', true);
if (res) {
keyObj.url.path = null;
keyObj.url.file = null;
keyObj.resType = resType;
keyObj.res = res;
};
}),
});
}),
$prepare: doodad.OVERRIDE(function $prepare(options) {
types.getDefault(options, 'depth', Infinity);
options = this._super(options);
let val;
options.defaultEncoding = options.defaultEncoding || 'utf-8';
val = files.parsePath(options.path);
const stats = nodeFs.statSync(val.toApiString());
options.isFolder = !stats.isFile();
if (options.isFolder) {
val = val.pushFile();
};
options.path = val;
options.showFolders = types.toBoolean(options.showFolders);
if (options.showFolders) {
val = options.folderTemplate;
if (types.isNothing(val)) {
val = modulePath.combine('./res/templates/Folder.ddt');
} else if (!(val instanceof files.Path)) {
val = files.Path.parse(val);
};
root.DD_ASSERT && root.DD_ASSERT((val instanceof files.Path), "Invalid folder template.");
options.folderTemplate = val;
};
return options;
}),
getSystemPath: doodad.OVERRIDE(function getSystemPath(request, targetUrl) {
let path = null;
if (targetUrl) {
if (this.options.isFolder) {
if (targetUrl.args.has('res')) {
path = this.options.folderTemplate.set({file: ''}).combine('./public/' + targetUrl.args.get('res', true));
} else if (targetUrl.isRelative) {
path = this.options.path.combine(targetUrl.set({domain: null}));
} else {
const handlerState = request.getHandlerState(this);
const handlerUrl = handlerState.url.pushFile();
const relativeUrl = targetUrl.relative(handlerUrl);
path = this.options.path.combine(relativeUrl);
};
} else if (!targetUrl.isRelative && !targetUrl.args.has('res')) {
path = this.options.path;
};
};
return path;
}),
__getFileUrl: doodad.PROTECTED(function getFileUrl(request) {
const state = request.getHandlerState(this);
const urlRemaining = state.matcherResult.urlRemaining;
const url = (urlRemaining && (urlRemaining.path.length || urlRemaining.file) ? urlRemaining : request.url);
return url;
}),
createStream: doodad.OVERRIDE(function createStream(request, /*optional*/options) {
let url = types.get(options, 'url', null);
if (!url) {
url = this.__getFileUrl(request);
};
const path = this.getSystemPath(request, url);
if (!path) {
return null;
};
const nodeStream = nodeFs.createReadStream(path.toApiString());
const inputStream = new nodejsIO.BinaryInputStream(nodeStream);
request.onSanitize.attachOnce(null, function() {
types.DESTROY(inputStream);
types.DESTROY(nodeStream);
});
return inputStream;
}),
addHeaders: doodad.PROTECTED(doodad.ASYNC(function addHeaders(request) {
const Promise = types.getPromise();
const url = this.__getFileUrl(request);
const path = this.getSystemPath(request, url);
if (!path) {
return null;
};
const stat = function(path) {
return Promise.create(function tryStat(resolve, reject) {
const pathStr = path.toApiString();
nodeFs.stat(pathStr, doodad.Callback(this, function getStatsCallback(err, stats) {
if (err) {
if (err.code === 'ENOENT') {
resolve(null);
} else {
reject(err);
};
} else {
if (stats.isFile()) {
if (path.file) {