UNPKG

@doodad-js/http

Version:
1,508 lines (1,247 loc) 105 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: 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) {