UNPKG

@nodefony/http-bundle

Version:

Nodefony Framework Bundle HTTP

738 lines (704 loc) 21.4 kB
/* * * HTTP KERNEL * * */ const clientErrorExclude = /SSL alert number 46|SSL alert number 48/; class httpKernel extends nodefony.Service { constructor (container, serverStatics) { super("HTTP KERNEL", container, container.get("notificationsCenter")); this.reader = this.get("reader"); this.serverStatic = serverStatics; this.engineTemplate = this.get("templating"); this.domain = this.kernel.domain; this.httpPort = this.kernel.httpPort; this.httpsPort = this.kernel.httpsPort; this.container.addScope("request"); // listen KERNEL EVENTS this.once("onBoot", async () => { this.firewall = this.get("security"); this.router = this.get("router"); this.sessionService = this.get("sessions"); this.csrfService = this.get("csrf"); this.compileAlias(); this.sockjs = this.get("sockjs"); this.socketio = this.get("socketio"); this.settings = this.getParameters("bundles.http"); this.responseTimeout = { HTTP: this.settings.http.responseTimeout, HTTPS: this.settings.https.responseTimeout, HTTP2: this.settings.https.responseTimeout }; this.closeTimeOutWs = { WS: this.settings.websocket.closeTimeout, WSS: this.settings.websocketSecure.closeTimeout }; this.translation = this.get("translation"); this.cdn = this.setCDN(); this.corsManager = this.get("cors"); this.frameworkBundle = this.kernel.getBundle("framework"); this.monitoringBundle = this.kernel.getBundle("monitoring"); if (this.monitoringBundle) { this.profiler = this.monitoringBundle.settings.profiler; this.forceDebugBarProd = this.monitoringBundle.settings.forceDebugBarProd; } else { this.profiler = { active: false }; this.forceDebugBarProd = false; } }); this.once("onReady", async () => { if (this.monitoringBundle) { this.debugView = this.getTemplate("monitoringBundle::debugBar.html.twig"); } }); this.on("onClientError", (e, socket) => { const exclude = clientErrorExclude.test(e.message); if (!exclude) { this.log(e, "WARNING", "SOCKET CLIENT ERROR"); } socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); }); } // EVENTS DISPATCHER async onError (container, error) { let context = null; try { context = container.get("context"); if (!context || context.sended) { this.log(error, "ERROR"); return Promise.reject(error); } } catch (e) { this.log(error, "ERROR"); return Promise.reject(error); } let httpError = null; try { let result = null; const errorType = nodefony.isError(error); switch (errorType) { case "securityError": case "httpError": httpError = error; break; default: if (context.response && context.response.statusCode === 200) { httpError = new nodefony.httpError(error, 500, container); } else { httpError = new nodefony.httpError(error, null, container); } } if (context.listenerCount("onError")) { return context.handle(error) .then((ret) => { if (context) { if (context.notificationsCenter) { context.removeAllListeners("onError"); } context.logRequest(httpError); } return ret; }) .catch((e) => { if (!(e instanceof nodefony.Resolver)) { this.log(e, "ERROR"); } if (context) { if (context.notificationsCenter) { context.removeAllListeners("onError"); } } return this.onError(container, error); }); // context.logRequest(httpError); // return httpError.resolver.callController(httpError); } if (!httpError.context) { httpError.context = context; httpError.resolve(true); } context = httpError.context; context.resolver = httpError.resolver; context.logRequest(httpError); if (context.method === "WEBSOCKET" && context.response && !context.response.connection) { if (!context.rejected) { context.request.reject(httpError.code ? httpError.code : null, httpError.message); context.rejected = true; } context.fire("onFinish", context); return context; } if (!context.response) { context.fire("onFinish", context); return httpError.resolver; } result = httpError.resolver.callController(httpError); if (!context.requested) { context.fire("onRequest", context, httpError.resolver); this.kernel.fire("onRequest", context, httpError.resolver); } if (context.method === "WEBSOCKET") { if (httpError.code < 3000) { httpError.code += 3000; } setTimeout(() => { context.close(httpError.code, httpError.message); }, 500 /* (context.type === "WEBSOCKET" ? this.closeTimeOutWs.WS : this.closeTimeOutWs.WSS)*/); } return result; } catch (e) { if (error) { this.log(error, "ERROR"); } if (httpError) { httpError.log(e, "ERROR"); } else { this.log(e, "ERROR"); } throw e; } } // HTTP ENTRY POINT onHttpRequest (request, response, type) { if (this.sockjs && request.url && request.url.match(this.sockjs.regPrefix)) { this.log(`HTTP drop to sockj ${request.url}`, "DEBUG"); return; } if (response.headersSent) { return; } response.setHeader("Server", "nodefony"); if (this.kernel.settings.system.servers.statics || this.kernel.settings.system.statics) { return this.serverStatic.handle(request, response) .then((res) => { if (res) { this.fire("onServerRequest", request, response, type); return this.handle(request, response, type); } throw new Error("Bad request"); }) .catch((e) => { if (e) { this.log(e, "ERROR", "STATICS SERVER"); } return e; }); } this.fire("onServerRequest", request, response, type); return this.handle(request, response, type); } // WEBSOCKET ENTRY POINT onWebsocketRequest (request, type) { if (this.sockjs && request.resourceURL.path && request.resourceURL.path.match(this.sockjs.regPrefix) ) { this.log(`websocket drop to sockjs : ${request.resourceURL.path}`, "DEBUG"); // let connection = request.accept(null, request.origin); // connection.drop(1006, 'TCP connection lost before handshake completed.', false); request = null; // connection = null ; return; } if (this.socketio && request.resourceURL.path && this.socketio.checkPath(request.resourceURL.path) ) { this.fire("onServerRequest", request, null, type); this.log(`websocket drop to socket.io : ${request.resourceURL.path}`, "DEBUG"); request = null; return; } this.fire("onServerRequest", request, null, type); return this.handle(request, null, type); } handle (request, response, type) { // SCOPE REQUEST ; let log = null; const container = this.container.enterScope("request"); switch (type) { case "HTTP": case "HTTPS": case "HTTP2": log = clc.cyan.bgBlue(`${request.url}`); this.log(`REQUEST HANDLE ${type} : ${log}`, "DEBUG"); return this.handleHttp(container, request, response, type); case "WEBSOCKET": case "WEBSOCKET SECURE": log = clc.cyan.bgBlue(`${request.resource}`); this.log(`REQUEST HANDLE ${type} : ${log}`, "DEBUG"); return this.handleWebsocket(container, request, type); } } // FRONT CONTROLLER handleFrontController (context, checkFirewall = true) { return new Promise((resolve, reject) => { let controller = null; if (this.firewall && checkFirewall) { context.secure = this.firewall.isSecure(context); } if (context.security) { const res = this.firewall.handleCrossDomain(context); if (context.crossDomain && context.method === "OPTIONS") { if (res === 204) { return resolve(res); } } } // FRONT ROUTER try { const resolver = this.router.resolve(context); if (resolver.resolve && !resolver.exception) { context.resolver = resolver; controller = resolver.newController(context.container, context); if (controller.sessionAutoStart) { context.sessionAutoStart = controller.sessionAutoStart; } context.once("onSessionStart", (session) => { controller.session = session; }); return resolve(controller); } if (resolver.exception) { return reject(resolver.exception); } const error = new nodefony.httpError("Not Found", 404, context.container); return reject(error); } catch (e) { if (e instanceof nodefony.Resolver) { if (e.exception) { controller = e.newController(context.container, context); return reject(e.exception); } return reject(new nodefony.httpError("Not Found", 404, context.container)); } return reject(e); } }); } // HTTP ENTRY POINT handleHttp (container, request, response, type) { return new Promise(async (resolve, reject) => { let context = null; let error = null; try { context = this.createHttpContext(container, request, response, type); } catch (e) { error = e; } try { const ctx = await this.onRequestEnd(context, error); if (ctx instanceof nodefony.Context) { if (ctx.secure || ctx.isControlledAccess) { return resolve(context); } return resolve(await ctx.handle()); } return resolve(context); } catch (e) { return this.onError(container, e) .then((res) => resolve(res)) .catch((e) => reject(e)); } }); } startSession (context) { if (context.sessionAutoStart || context.hasSession()) { return this.sessionService.start(context, context.sessionAutoStart) .then((session) => { if (this.firewall) { this.firewall.getSessionToken(context, session); } return session; }) .catch((e) => { throw e; }); } } onRequestEnd (context, error = null) { return new Promise((resolve, reject) => { // EVENT if (!context) { return reject(new nodefony.Error("Bad context", 500)); } context.once("onRequestEnd", async () => { try { if (error) { throw error; } // ADD HEADERS CONFIG if (this.settings[context.scheme].headers) { context.response.setHeaders(this.settings[context.scheme].headers); } // DOMAIN VALID if (this.kernel.domainCheck) { this.checkValidDomain(context); } // FRONT CONTROLLER const ret = await this.handleFrontController(context) .catch((e) => { throw e; }); if (ret === 204) { return resolve(ret); } // FIREWALL if (context.secure || context.isControlledAccess) { const res = await this.firewall.handleSecurity(context); // CSRF TOKEN if (context.csrf) { const token = await this.csrfService.handle(context); if (token) { this.log("CSRF TOKEN OK", "DEBUG"); } } return resolve(res); } // SESSIONS try { await this.startSession(context); // CSRF TOKEN if (context.csrf) { const token = await this.csrfService.handle(context); if (token) { this.log("CSRF TOKEN OK", "DEBUG"); } } return resolve(context); } catch (e) { return reject(e); } } catch (e) { return reject(e); } }); }); } createHttpContext (container, request, response, type) { let context = null; try { context = new nodefony.context.http(container, request, response, type); } catch (e) { this.log(e, "ERROR"); throw e; } // response events context.response.response.once("finish", () => { if (!context) { return; } if (context.finished) { return; } context.logRequest(); context.fire("onFinish", context); context.finished = true; this.container.leaveScope(container); context.clean(); context = null; request = null; response = null; container = null; }); return context; } // WEBSOCKET ENTRY POINT handleWebsocket (container, request, type) { return new Promise(async (resolve, reject) => { let context = null; let error = null; try { context = this.createWebsocketContext(container, request, type); } catch (e) { error = e; } try { const connection = await this.onConnect(context, error); // FIREWALL if (context.secure || context.isControlledAccess) { return resolve(await this.firewall.handleSecurity(context, connection)); } return resolve(await context.handle()); } catch (e) { return this.onError(container, e) .then((res) => resolve(res)) .catch((e) => reject(e)); } }); } onConnect (context, error = null) { // EVENT return new Promise(async (resolve, reject) => { if (!context) { return reject(new nodefony.Error("Bad context", 500)); } try { if (error) { return reject(error); } // DOMAIN VALID if (this.kernel.domainCheck) { this.checkValidDomain(context); } // FRONT CONTROLLER try { const ret = await this.handleFrontController(context) .catch((e) => { throw e; }); if (ret === 204) { return resolve(ret); } } catch (e) { if (e.code && e.code === 404 || context.resolver) { return reject(e); } this.log(e, "ERROR"); // continue } if (context.secure || context.isControlledAccess) { return resolve(await context.connect()); } // SESSIONS if (!context.sessionStarting && (context.sessionAutoStart || context.hasSession())) { try { const session = await this.sessionService.start(context, context.sessionAutoStart) .catch((error) => reject(error)); if (!(session instanceof nodefony.Session)) { this.log(new Error("SESSION START session storage ERROR"), "WARNING"); } if (this.firewall) { this.firewall.getSessionToken(context, session); } } catch (e) { throw e; } } return resolve(await context.connect()); } catch (e) { return reject(e); } }); } createWebsocketContext (container, request, type) { const context = new nodefony.context.websocket(container, request, type); context.once("onFinish", (context) => { if (!context) { return; } if (context.finished) { return; } this.container.leaveScope(container); context.clean(); context.finished = true; context = null; request = null; container = null; type = null; }); return context; } // TOOLS compileAlias () { let str = ""; if (!this.kernel.domainAlias) { str = `^${this.kernel.domain}$`; this.regAlias = new RegExp(str); return; } try { let alias = []; switch (nodefony.typeOf(this.kernel.domainAlias)) { case "string": alias = this.kernel.domainAlias.split(" "); Array.prototype.unshift.call(alias, `^${this.kernel.domain}$`); for (let i = 0; i < alias.length; i++) { if (i === 0) { str = alias[i]; } else { str += `|${alias[i]}`; } } break; case "object": let first = true; for (const myAlias in this.kernel.domainAlias) { if (first) { first = false; str = this.kernel.domainAlias[myAlias]; } else { str += `|${this.kernel.domainAlias[myAlias]}`; } } break; case "array": str = `^${this.kernel.domain}$`; for (let i = 0; i < this.kernel.domainAlias.length; i++) { str += `|${this.kernel.domainAlias[i]}`; } break; default: throw new Error("Config file bad format for domain alias must be a string "); } if (str) { this.regAlias = new RegExp(str); } else { str = `^${this.kernel.domain}$`; this.regAlias = new RegExp(str); } } catch (e) { throw e; } } isValidDomain (context) { return this.regAlias.test(context.domain); } getEngineTemplate (name) { return nodefony.templatings[name]; } parseViewPattern (pattern) { if (pattern && typeof pattern === "string") { const tab = pattern.split(":"); if (tab.length !== 3) { throw new Error(`Not valid Pattern View bundle:directory:filename ==> ${pattern}`); } return { bundle: tab[0], directory: tab[1] || ".", file: tab[2] }; } throw new Error(`Not valid Pattern View bundle:directory:filename ==> ${pattern}`); } getBundleView (objPattern) { let name = null; try { name = this.kernel.getBundleName(objPattern.bundle); } catch (e) { name = objPattern.bundle; } try { const myBundle = this.kernel.getBundle(name); if (!myBundle) { throw new Error(`Resolver Pattern View Bundle : ${objPattern.bundle} NOT exist`); } return myBundle.getView(objPattern.directory, objPattern.file); } catch (e) { throw e; } } getBundleTemplate (objPattern) { let name = null; try { name = this.kernel.getBundleName(objPattern.bundle); } catch (e) { name = objPattern.bundle; } try { const myBundle = this.kernel.getBundle(name); if (!myBundle) { throw new Error(`Resolver Pattern Template Bundle :${objPattern.bundle} NOT exist`); } return myBundle.getTemplate(objPattern.directory, objPattern.file); } catch (e) { throw e; } } getView (name) { try { return this.getBundleView(this.parseViewPattern(name)); } catch (e) { throw e; } } getTemplate (name) { try { return this.getBundleTemplate(this.parseViewPattern(name)); } catch (e) { throw e; } } extendTemplate (param = {}, context = null) { if (!param) { param = {}; } if (param && typeof param !== "object") { throw new Error(`bad paramaters : ${param} must be an object`); } param.nodefony = { name: nodefony.projectName, version: nodefony.version, projectVersion: nodefony.projectVersion, url: context.request.url, environment: this.kernel.environment, debug: this.kernel.debug, local: context.translation.defaultLocale.substr(0, 2), core: this.kernel.isCore, route: context.resolver.getRoute(), getContext: () => context }; return param; } generateUrl (name, variables, host) { try { return this.router.generatePath.call(this.router, name, variables, host); } catch (e) { throw e; } } setCDN () { return this.kernel.settings.CDN; } getCDN (type, nb) { let wish = 0; if (nb) { try { wish = parseInt(wish, 10); } catch (e) { this.log("CDN CONFIG ERROR : ", "ERROR"); this.log(e, "ERROR"); } } switch (typeof this.cdn) { case "object": if (!this.cdn) { return ""; } if (this.cdn.global) { return this.cdn.global; } if (!type) { const txt = "CDN ERROR getCDN bad argument type "; this.log(txt, "ERROR"); throw new Error(txt); } if (type in this.cdn) { if (this.cdn[type][wish]) { return this.cdn[type][wish]; } } return ""; case "string": return this.cdn || ""; default: const txt = "CDN CONFIG ERROR "; this.log(txt, "ERROR"); throw new Error(txt); } } checkValidDomain (context) { if (context.validDomain) { return 200; } const error = `DOMAIN Unauthorized : ${context.domain}`; throw new nodefony.Error(error, 401); } } module.exports = httpKernel;