From 4e72d53b941a47ee9b868a670904f31ed36717c2 Mon Sep 17 00:00:00 2001 From: Leonardo Crauss Daronco Date: Tue, 11 Nov 2014 17:36:01 -0200 Subject: [PATCH] Webhooks: sign callback calls with a checksum Simple signature done initially in a way similar to how bbb-web does it. --- labs/bbb-callback/callback_emitter.coffee | 19 +++++-- labs/bbb-callback/utils.coffee | 62 +++++++++++++++++++++++ labs/bbb-callback/web_server.coffee | 57 ++------------------- 3 files changed, 79 insertions(+), 59 deletions(-) create mode 100644 labs/bbb-callback/utils.coffee diff --git a/labs/bbb-callback/callback_emitter.coffee b/labs/bbb-callback/callback_emitter.coffee index 501568beb4..41d02116a0 100644 --- a/labs/bbb-callback/callback_emitter.coffee +++ b/labs/bbb-callback/callback_emitter.coffee @@ -1,12 +1,15 @@ EventEmitter = require('events').EventEmitter request = require("request") +config = require("./config") +Utils = require("./utils") + # Class that emits a callback. Will try several times until the callback is # properly emitted and stop when successful (or after a given number of tries). # Emits "success" on success and "error" when gave up trying to emit the callback. module.exports = class CallbackEmitter extends EventEmitter - constructor: (@url, @message) -> + constructor: (@callbackURL, @message) -> start: -> @_scheduleNext 0 @@ -23,13 +26,19 @@ module.exports = class CallbackEmitter extends EventEmitter , timeout) _emitMessage: (callback) -> - # TODO: the external meeting ID is not on redis yet - # message.meetingID = rep.externalMeetingID + # basic data structure + data = + timestamp: new Date().getTime() + event: @message + + # add a checksum to the post data + checksum = Utils.checksum("#{@callbackURL}#{JSON.stringify(data)}#{config.bbb.sharedSecret}") + data.checksum = checksum requestOptions = - uri: @url + uri: @callbackURL method: "POST" - json: @message + json: data request requestOptions, (error, response, body) -> if error? diff --git a/labs/bbb-callback/utils.coffee b/labs/bbb-callback/utils.coffee new file mode 100644 index 0000000000..f0160b6761 --- /dev/null +++ b/labs/bbb-callback/utils.coffee @@ -0,0 +1,62 @@ +sha1 = require("sha1") +url = require("url") + +config = require("./config") + +Utils = exports + +# Calculates the checksum given a url `fullUrl` and a `salt`, as calculate by bbb-web. +Utils.checksumAPI = (fullUrl, salt) -> + query = Utils.queryFromUrl(fullUrl) + method = Utils.methodFromUrl(fullUrl) + Utils.checksum(method + query + salt) + +# Calculates the checksum for a string. +# Just a wrapper for the method that actually does it. +Utils.checksum = (string) -> + sha1(string) + +# Get the query of an API call from the url object (from url.parse()) +# Example: +# +# * `fullUrl` = `http://bigbluebutton.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` +# * returns: `name=Demo+Meeting&meetingID=Demo` +Utils.queryFromUrl = (fullUrl) -> + + # Returns the query without the checksum. + # We can't use url.parse() because it would change the encoding + # and the checksum wouldn't match. We need the url exactly as + # the client sent us. + query = fullUrl.replace(/&checksum=[^&]*/, '') + query = query.replace(/checksum=[^&]*&/, '') + query = query.replace(/checksum=[^&]*$/, '') + matched = query.match(/\?(.*)/) + if matched? + matched[1] + else + '' + +# Get the method name of an API call from the url object (from url.parse()) +# Example: +# +# * `fullUrl` = `http://mconf.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` +# * returns: `create` +Utils.methodFromUrl = (fullUrl) -> + urlObj = url.parse(fullUrl, true) + urlObj.pathname.substr (config.bbb.apiPath + "/").length + +# Returns the IP address of the client that made a request `req`. +# If can not determine the IP, returns `127.0.0.1`. +Utils.ipFromRequest = (req) -> + + # the first ip in the list if the ip of the client + # the others are proxys between him and us + if req.headers?["x-forwarded-for"]? + ips = req.headers["x-forwarded-for"].split(",") + ipAddress = ips[0]?.trim() + + # fallbacks + ipAddress ||= req.headers?["x-real-ip"] # when behind nginx + ipAddress ||= req.connection?.remoteAddress + ipAddress ||= "127.0.0.1" + ipAddress diff --git a/labs/bbb-callback/web_server.coffee b/labs/bbb-callback/web_server.coffee index bd947ed04d..11eaf22324 100644 --- a/labs/bbb-callback/web_server.coffee +++ b/labs/bbb-callback/web_server.coffee @@ -1,9 +1,9 @@ express = require("express") -sha1 = require("sha1") url = require("url") config = require("./config") Hook = require("./hook") +Utils = require("./utils") # Web server that listens for API calls and process them. module.exports = class WebServer @@ -77,48 +77,13 @@ module.exports = class WebServer urlObj = url.parse(req.url, true) checksum = urlObj.query["checksum"] - if checksum is @_checksum(req.url, config.bbb.sharedSecret) + if checksum is Utils.checksumAPI(req.url, config.bbb.sharedSecret) next() else console.log "checksum check failed, sending a checksumError response" res.setHeader("Content-Type", "text/xml") res.send cleanupXML(config.api.responses.checksumError) - # Calculates the checksum given a url `fullUrl` and a `salt`. - _checksum: (fullUrl, salt) -> - query = @_queryFromUrl(fullUrl) - method = @_methodFromUrl(fullUrl) - sha1(method + query + salt) - - # Get the query of an API call from the url object (from url.parse()) - # Example: - # - # * `fullUrl` = `http://bigbluebutton.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` - # * returns: `name=Demo+Meeting&meetingID=Demo` - _queryFromUrl: (fullUrl) -> - - # Returns the query without the checksum. - # We can't use url.parse() because it would change the encoding - # and the checksum wouldn't match. We need the url exactly as - # the client sent us. - query = fullUrl.replace(/&checksum=[^&]*/, '') - query = query.replace(/checksum=[^&]*&/, '') - query = query.replace(/checksum=[^&]*$/, '') - matched = query.match(/\?(.*)/) - if matched? - matched[1] - else - '' - - # Get the method name of an API call from the url object (from url.parse()) - # Example: - # - # * `fullUrl` = `http://mconf.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` - # * returns: `create` - _methodFromUrl: (fullUrl) -> - urlObj = url.parse(fullUrl, true) - urlObj.pathname.substr (config.bbb.apiPath + "/").length - respondWithXML = (res, msg) -> res.setHeader("Content-Type", "text/xml") res.send cleanupXML(msg) @@ -126,23 +91,7 @@ respondWithXML = (res, msg) -> # Returns a simple string with a description of the client that made # the request. It includes the IP address and the user agent. clientDataSimple = (req) -> - "ip " + ipFromRequest(req) + ", using " + req.headers["user-agent"] - -# Returns the IP address of the client that made a request `req`. -# If can not determine the IP, returns `127.0.0.1`. -ipFromRequest = (req) -> - - # the first ip in the list if the ip of the client - # the others are proxys between him and us - if req.headers?["x-forwarded-for"]? - ips = req.headers["x-forwarded-for"].split(",") - ipAddress = ips[0]?.trim() - - # fallbacks - ipAddress ||= req.headers?["x-real-ip"] # when behind nginx - ipAddress ||= req.connection?.remoteAddress - ipAddress ||= "127.0.0.1" - ipAddress + "ip " + Utils.ipFromRequest(req) + ", using " + req.headers["user-agent"] # Cleans up a string with an XML in it removing spaces and new lines from between the tags. cleanupXML = (string) ->