bigbluebutton-Github/bbb-webhooks/callback_emitter.js
prlanzarin 440fe5c57f bbb-webhooks: add a timeout configuration to be used on POST requests
Added the new config requestTimeout (and env var REQUEST_TIMEOUT). It is piped to the request module POST operation to avoid clogging the queue when there are no responses to hooks or the HTTPS socket connection cant be established
2020-06-24 18:03:55 +00:00

143 lines
4.5 KiB
JavaScript

const _ = require('lodash');
const request = require("request");
const url = require('url');
const EventEmitter = require('events').EventEmitter;
const config = require('config');
const Logger = require("./logger.js");
const Utils = require("./utils.js");
// Use to perform a callback. Will try several times until the callback is
// properly emitted and stop when successful (or after a given number of tries).
// Used to emit a single callback. Destroy it and create a new class for a new callback.
// Emits "success" on success, "failure" on error and "stopped" when gave up trying
// to perform the callback.
module.exports = class CallbackEmitter extends EventEmitter {
constructor(callbackURL, message, permanent) {
super();
this.callbackURL = callbackURL;
this.message = message;
this.nextInterval = 0;
this.timestamp = 0;
this.permanent = permanent;
}
start() {
this.timestamp = new Date().getTime();
this.nextInterval = 0;
this._scheduleNext(0);
}
_scheduleNext(timeout) {
setTimeout( () => {
this._emitMessage((error, result) => {
if ((error == null) && result) {
this.emit("success");
} else {
this.emit("failure", error);
// get the next interval we have to wait and schedule a new try
const interval = config.get("hooks.retryIntervals")[this.nextInterval];
if (interval != null) {
Logger.warn(`[Emitter] trying the callback again in ${interval/1000.0} secs`);
this.nextInterval++;
this._scheduleNext(interval);
// no intervals anymore, time to give up
} else {
this.nextInterval = config.get("hooks.permanentIntervalReset"); // Reset interval to permanent hooks
if(this.permanent){
this._scheduleNext(this.nextInterval);
}
else {
return this.emit("stopped");
}
}
}
});
}
, timeout);
}
_emitMessage(callback) {
let data, requestOptions;
const serverDomain = config.get("bbb.serverDomain");
const sharedSecret = config.get("bbb.sharedSecret");
const bearerAuth = config.get("bbb.auth2_0");
const timeout = config.get('hooks.requestTimeout');
// data to be sent
// note: keep keys in alphabetical order
data = {
event: "[" + this.message + "]",
timestamp: this.timestamp,
domain: serverDomain
};
if (bearerAuth) {
const callbackURL = this.callbackURL;
requestOptions = {
followRedirect: true,
maxRedirects: 10,
uri: callbackURL,
method: "POST",
form: data,
auth: {
bearer: sharedSecret
},
timeout
};
}
else {
// calculate the checksum
const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`);
// get the final callback URL, including the checksum
const urlObj = url.parse(this.callbackURL, true);
let callbackURL = this.callbackURL;
callbackURL += _.isEmpty(urlObj.search) ? "?" : "&";
callbackURL += `checksum=${checksum}`;
requestOptions = {
followRedirect: true,
maxRedirects: 10,
uri: callbackURL,
method: "POST",
form: data,
timeout
};
}
const responseFailed = (response) => {
var statusCode = (response != null ? response.statusCode : undefined)
// consider 401 as success, because the callback worked but was denied by the recipient
return !((statusCode >= 200 && statusCode < 300) || statusCode == 401)
};
request(requestOptions, function(error, response, body) {
if ((error != null) || responseFailed(response)) {
Logger.warn(`[Emitter] error in the callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)}`, "error:", error, "status:", response != null ? response.statusCode : undefined);
callback(error, false);
} else {
Logger.info(`[Emitter] successful callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)}`);
callback(null, true);
}
});
}
};
// A simple string that identifies the event
var simplifiedEvent = function(event) {
if (event.event != null) {
event = event.event
}
try {
const eventJs = JSON.parse(event);
return `event: { name: ${(eventJs.data != null ? eventJs.data.id : undefined)}, timestamp: ${(eventJs.data.event != null ? eventJs.data.event.ts : undefined)} }`;
} catch (e) {
return `event: ${event}`;
}
};