import { WsNode } from "./../ws-node"; import * as getBody from "raw-body"; import * as typer from "content-type"; import * as mediaTyper from "media-typer"; import * as cors from "cors"; import * as bodyParser from "body-parser"; import * as cookieParser from "cookie-parser"; import { NodeAPI, NodeDef, NodeMessage } from "node-red"; const onHeaders = require('on-headers'); const isUtf8 = require('is-utf8'); module.exports = (RED: NodeAPI) => { class WsBeginNode extends WsNode { constructor(config: NodeDef) { //Init the base class. super(); //Create the node RED.nodes.createNode(this, config); if (!RED.settings.httpNodeRoot) { this.warn("Cannot create WsBeginNode node when httpNodeRoot set to false"); return; } //Create a url based on the id. const url = "/startflow/" + this.z; if (!url) { this.warn("Missing url"); return; } //Create an empty corsHandler let corsHandler = (req: any, res: any, next: any) => { next(); } //Fill the corshanler if we have a httpNodeCors in the settings if (RED.settings.httpNodeCors) { corsHandler = cors(RED.settings.httpNodeCors); RED.httpNode.options("*", corsHandler); } //Create an empty httpMiddleware let httpMiddleware = (req: any, res: any, next: any) => { next(); } //Fill the httpMiddleware if we have a httpNodeMiddleware in the settings if (RED.settings.httpNodeMiddleware) { if (typeof RED.settings.httpNodeMiddleware === "function") { httpMiddleware = RED.settings.httpNodeMiddleware; } } const maxApiRequestSize = RED.settings.apiMaxLength || '5mb'; const jsonParser = bodyParser.json({ limit: maxApiRequestSize }); const urlencParser = bodyParser.urlencoded({ limit: maxApiRequestSize, extended: true }); //Create an empty metricsHandler let metricsHandler = (req: any, res: any, next: any) => { next(); } //Fill the metricsHandler if the metric collection is enabled if (this.metric()) { metricsHandler = this.metricsHandler; } //Start listening to the url with all the handlers/callbacks RED.httpNode.post( url, cookieParser(), httpMiddleware, corsHandler, metricsHandler, jsonParser, urlencParser, this.multipartParser, this.rawBodyParser, (req: any, res: any) => { this.callback(req, res) }, (err: any, req: any, res: any, next: Function) => { this.errorHandler(err, req, res, next) } ); //Remove the routes this.on("close", () => { RED.httpNode._router.stack.forEach((route: any, i: any, routes: any) => { if (route.route && route.route.path === url && route.route.methods["POST"]) { routes.splice(i, 1); } }); }); } /** * Body parser that is used to parse the request and send it in the output */ private rawBodyParser(req: any, res: any, next: Function) { if (req.skipRawBodyParser) { next(); } // don't parse this if told to skip if (req._body) { return next(); } req.body = ""; req._body = true; let isText = true; let checkUTF = false; if (req.headers['content-type']) { const contentType = typer.parse(req.headers['content-type']) if (contentType.type) { const parsedType = mediaTyper.parse(contentType.type); if (parsedType.type === "text") { isText = true; } else if (parsedType.subtype === "xml" || parsedType.suffix === "xml") { isText = true; } else if (parsedType.type !== "application") { isText = false; } else if ((parsedType.subtype !== "octet-stream") && (parsedType.subtype !== "cbor")) { checkUTF = true; } else { // application/octet-stream or application/cbor isText = false; } } } getBody(req, { length: req.headers['content-length'], encoding: isText ? "utf8" : null }, function (err, buf) { if (err) { return next(err); } if (!isText && checkUTF && isUtf8(buf)) { req.body = buf.toString() } else { req.body = buf; } next(); }); } /** * Standard errorhandler that will log the error and send a 500 error back in the request */ private errorHandler(err: any, req: any, res: any, next: Function) { this.warn(err); res.sendStatus(500); }; /** * This function will be called when a succesfull call is made. It will send the data to the next node and call the sendResponse function */ private callback(req: any, res: any) { const msgid = RED.util.generateId(); //Let the client know the request arrived succesfully this.sendResponse(this.createResponseWrapper(this, res), req.body); const message: any = { _msgid: msgid, payload: { request: req } } this.send(message); }; /** * Empty code for the multipart parser. This code can be extended if needed. */ private multipartParser(req: any, res: any, next: Function) { next(); }; /** * Metric handler that will get the headers from the response and call the default metric function. */ private metricsHandler(req: any, res: any, next: any) { const startAt = process.hrtime(); onHeaders(res, () => { if (res._msgid) { const diff = process.hrtime(startAt); const ms = diff[0] * 1e3 + diff[1] * 1e-6; const metricResponseTime = ms.toFixed(3); const metricContentLength = res.getHeader("content-length"); //assuming that _id has been set for res._metrics in HttpOut node! this.metric("response.time.millis", { _msgid: res._msgid }, metricResponseTime); this.metric("response.content-length.bytes", { _msgid: res._msgid }, metricContentLength); } }); next(); }; /** * Send a 200 ok back to the client */ private sendResponse(res: any, payload: any) { if (res) { const statusCode = 200; if (typeof payload == "object" && !Buffer.isBuffer(payload)) { res._res.status(statusCode).jsonp(payload); } else { if (res._res.get('content-length') == null) { let len; if (payload == null) { len = 0; } else if (Buffer.isBuffer(payload)) { len = payload.length; } else if (typeof payload == "number") { len = Buffer.byteLength("" + payload); } else { len = Buffer.byteLength(payload); } res._res.set('content-length', len); } if (typeof payload === "number") { payload = "" + payload; } res._res.status(statusCode).send(payload); } } else { this.warn("No response object"); } } /** * Creates a response wrapper based on the */ private createResponseWrapper(node: any, res: any) { const wrapper = { _res: res }; const toWrap = ["append", "attachment", "cookie", "clearCookie", "download", "end", "format", "get", "json", "jsonp", "links", "location", "redirect", "render", "send", "sendfile", "sendFile", "sendStatus", "set", "status", "type", "consty"]; toWrap.forEach((f: any) => { //@ts-ignore Element implicitly has an 'any' type because expression of type 'any' can't be used to index type wrapper[f] = function () { this.warn("Deprecated call to msg.res." + f); const result = res[f].apply(res, arguments); if (result === res) { return wrapper; } else { return result; } } }); return wrapper; } } /** Register the node */ RED.nodes.registerType("ws-begin-node", WsBeginNode as any); }