diff --git a/.gitignore b/.gitignore index cf3dc51..eb78371 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ build/* .DS_Store ._* -*.*~ \ No newline at end of file +*.*~ +lab-status.js +provider-balance.js \ No newline at end of file diff --git a/configs.js b/configs.js index a7715a0..0cc3094 100644 --- a/configs.js +++ b/configs.js @@ -26,12 +26,12 @@ var load = function (debug) { var fullPath = __dirname + filePath.substr(1); fileName = filePath.replace(/(\.js)$/, ""); fileKey = fileName.replace(/^(.*)\//g, ""); - + /* // Delete module cache if (typeof process.mainModule.moduleCache[fullPath] !== "undefined") { delete process.mainModule.moduleCache[fullPath]; } - + */ var configFile = require(fileName).Config; Object.keys(configFile).forEach(function (key) { diff --git a/configs/directory.js.default b/configs/directory.js.default new file mode 100644 index 0000000..d7ab83d --- /dev/null +++ b/configs/directory.js.default @@ -0,0 +1,7 @@ +exports.Config = {directory: { + dialString: "{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}", + vmEnabled: true, + userContext: "default", + userGroup: "default", + baseExtension: 10000 +}}; \ No newline at end of file diff --git a/configs/freenode.js.default b/configs/freenode.js.default index d9a7768..65daa92 100644 --- a/configs/freenode.js.default +++ b/configs/freenode.js.default @@ -3,25 +3,41 @@ var path = require("path"); exports.Config = { version: "0.0.1", - responderDir: "responders", + inboundDestination: "500 XML default", + gatewayName: "Gateway", + + httpd: { + port: 5780, + host: "pbx.example.tld", + color: "yellow" + }, + + ldap: { + uri: "ldaps://ldap.example.tld/", + users: "ou=people,dc=example,dc=tld?*?sub?", + sipPassword: "AstAccountSecret", + vmPassword: "AstAccountMailbox", + userKey: "uidNumber", + fields: ["uidNumber", "uid", "sn", "givenName", "mobile", "AstAccountSecret", "AstAccountMailbox", "objectClass", "dn"] + }, color: "green", colors: { - commands: "red", - modules: "green", + responders: "red", configs: "magenta", success: "green", failure: "red" }, + responderDir: "responders", + init: function(self) { - if (this.debug === true) { - } - /* + if (this.debug === true) {} + if (("fields" in self.ldap) === true) { self.ldap.users.replace(/(\?\*\?)/, self.ldap.fields.join(",")); - }*/ + } } }; \ No newline at end of file diff --git a/handlers/freenode.js b/handlers/freenode.js index a0724bb..e1ba077 100644 --- a/handlers/freenode.js +++ b/handlers/freenode.js @@ -1,3 +1,9 @@ +/** +* TODO: LDAP query cache for speed and to not hammer the ldap server for nothing 5min cache would be plenty +* TODO: VM Change Password +* TODO: Provider Balance +**/ + var debug = true; var sys = require("sys"), @@ -5,21 +11,23 @@ var sys = require("sys"), events = require("events"), colors = require("colors"), path = require("path"), - hash = require("../deps/node-hash/lib/hash"), + hash = require("../modules/node-hash/lib/hash"), // We load this separately because we might need some things before the configs are loaded FreeNodeConfig = require("../configs/freenode").Config, Configs = require("../configs"), Responders = require("../responders"), - LDAPClient = require("../deps/node-ldapsearch/build/default/ldap.node"); - + LDAPClient = require("../modules/node-ldapsearch/build/default/ldap.node"), + httpServer = require("./httpd").httpServer; + var FreeNode = function() { var self = this; this.uptime = (new Date().getTime()); this.loaders = []; this.ConfigsLoader = null; this.RespondersLoader = null; + this.httpd = null; - process.title = "FreeNode"; + process.title = "FreeNode"; events.EventEmitter.call(this); @@ -136,7 +144,9 @@ FreeNode.prototype.reinit = function(cbReturn) { }; FreeNode.prototype.onInited = function() { + this.httpServer = new httpd().init(); + console.log(util.inspect(this)); }; /** @@ -160,6 +170,8 @@ FreeNode.prototype.addDependencies = function(reload) { ); if (reload === true) { + this.httpd.deinit(); + // We also want to wait for the MUC to bind this.addDependency( // Loader @@ -179,6 +191,23 @@ FreeNode.prototype.addDependencies = function(reload) { Responders.load.apply(self, [self]); }, "responders:loaded", this] ); + + + this.addDependency( + // Loader + [function() { + console.log(("Loading HTTP Server")[Configs.httpd.color]); + self.httpd = new httpServer(self.Responders).init(); + return self.httpd; + }, "httpd:binded"], + + // Failure + [function(error) { + sys.error(("Error binding HTTP Server")[Configs.colors.failure]); + process.exit(1); + }, "httpd:error"] + ); + }; /** diff --git a/handlers/httpd.js b/handlers/httpd.js index e69de29..30d9076 100644 --- a/handlers/httpd.js +++ b/handlers/httpd.js @@ -0,0 +1,154 @@ +var sys = require("sys"), + colors = require("colors"), + http = require("http"), + util = require("util"), + querystring = require("querystring"), + events = require("events"), + hash = require("../modules/node-hash/lib/hash"), + dateFormat = require("../modules/date.js").dateFormat, + url = require("url"), + colors = require("colors"), + Configs = require("../configs.js"); + +var httpServer = function(responders) { + var self = this; + this.httpd = null; + this.responders = responders; + + events.EventEmitter.call(this); +}; + +sys.inherits(httpServer, events.EventEmitter); +exports.httpServer = httpServer; + +httpServer.prototype.init = function() { + var self = this; + + self.httpd = http.createServer(function(request, response) { + self.handlerResponse(request, response); + }) + .addListener("error", function(error) { + console.log("httpd crapped out"); + console.log(error); + + self.emit("httpd:error", self); + }); + + self.httpd.listen(Configs.httpd.port); + + self.emit("httpd:binded", self); + + return self; +}; + +httpServer.prototype.deinit = function() { + var self = this; + + self.httpd.close(); +}; + +httpServer.prototype.getResponder = function(decodedBody) { + var self = this, + responder = null, + handler = null, + selectorLen = 0; + + + try { + if (typeof decodedBody["section"] === "undefined") { + throw "Invalid Request"; + } + + if (typeof self.responders[decodedBody["section"].toLowerCase()] === "undefined") { + throw "Invalid Bidings"; + } else { + var bindings = decodedBody["section"].toLowerCase(); + } + + for (entryName in self.responders[bindings]) { + var entry = self.responders[decodedBody["section"]][entryName]; + + if (typeof entry.selector === "undefined") { + continue; + } else { + for (handlerName in entry.selector) { + if (Object.keys(entry.selector[handlerName]).length > selectorLen) { + var ii = 0; + for (selectorKey in entry.selector[handlerName]) { + var match = false; + switch (typeof entry.selector[handlerName][selectorKey]) { + case "string": + match = (decodedBody[selectorKey] === entry.selector[handlerName][selectorKey]); + break; + + case "boolean": + match = ( + (entry.selector[handlerName][selectorKey] === true && decodedBody[selectorKey].length !== 0) + || (entry.selector[handlerName][selectorKey] === false && decodedBody[selectorKey].length === 0) + ); + break; + + case "object": + case "function": + if (entry.selector[handlerName][selectorKey] instanceof RegExp) { + match = entry.selector[handlerName][selectorKey].test(decodedBody[selectorKey]); + } + break; + + } + + if (match === true) { + if (++ii === Object.keys(entry.selector[handlerName]).length) { + selectorLen = Object.keys(entry.selector[handlerName]).length; + responder = entry; + handler = handlerName; + } + } + } + } + } + } + } + } catch (err) { + console.log("Invalid request... " + err); + } + + console.log(util.inspect(decodedBody)); + if (responder !== null) { + console.log(util.inspect(responder)); + return [responder, handler]; + } else { + return null; + } +}; + +httpServer.prototype.handlerResponse = function(request, response) { + var self = this, + fullBody = ""; + + request.on('data', function(chunk) { + // append the current chunk of data to the fullBody variable + fullBody += chunk.toString(); + }); + + request.on('end', function() { + // parse the received body data + var decodedBody = querystring.parse(fullBody); + var responder = null; + + if ((responder = self.getResponder(decodedBody)) !== null) { + var cbReturn = function(responseData) { + if (typeof responseData !== "undefined" && responseData !== null && responseData.length > 0) { + response.writeHead(200, "OK", {'Content-Type': 'text/xml'}); + response.write(responseData, "ascii"); + } + + response.end(); + }; + + responder[0][responder[1]](cbReturn, decodedBody); + } else { + response.end(); + } + }); +}; diff --git a/main.js b/main.js index 6e075fb..340b934 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -var FreeNodeHandler = require("./libs/freenode").FreeNode, +var FreeNodeHandler = require("./handlers/freenode").FreeNode, Configs = require("./configs"); // Instanciate our main object diff --git a/responders.js b/responders.js index 315f88a..ab66e88 100644 --- a/responders.js +++ b/responders.js @@ -2,7 +2,7 @@ var sys = require("sys"), util = require("util"), events = require("events"), colors = require("colors"), - recurseDir = require("./deps/recurseDir").recurseDir, + recurseDir = require("./modules/recurseDir").recurseDir, Configs = require("./configs"); var load = function (Foulinks) { @@ -34,10 +34,10 @@ var load = function (Foulinks) { sys.puts(("[ responders ] ." + filePath.replace(__dirname+"/"+Configs.responderDir, "").replace(/(\.js)$/, ""))[Configs.colors.responders]); } catch (err) { // Don't keep a cache of failed includes! - if (typeof process.mainModule.moduleCache[filePath] !== "undefined") { + /*if (typeof process.mainModule.moduleCache[filePath] !== "undefined") { delete process.mainModule.moduleCache[filePath]; } - + */ delete responderBase[fileKey]; sys.puts(("[ responders ] ERROR Loading ." + @@ -116,12 +116,12 @@ var unload = function(Foulinks, recurse) { delete this[fileKey]; } - + /* // Delete responder cache if (typeof process.mainModule.moduleCache[filePath] !== "undefined") { delete process.mainModule.moduleCache[filePath]; } - + */ delete this[fileKey]; sys.puts(("[ responders ] removed: " + fileKey)[Configs.colors.responders]); diff --git a/responders/dialplan/follow-me.js b/responders/dialplan/follow-me.js new file mode 100644 index 0000000..b89fe84 --- /dev/null +++ b/responders/dialplan/follow-me.js @@ -0,0 +1,46 @@ +var sys = require("sys"), + ltx = require("../../modules/ltx/lib/index"), + Configs = require("../../configs.js"), + LDAPClient = require("../../modules/node-ldapsearch/build/default/ldap.node"); + +var followMe = function(parent) { + this.selector = { + "follow": {"Call-Direction": "inbound", "variable_direction": "inbound", "Caller-Context": "default", "Caller-Destination-Number": /^[1-6]{1}[0-9]{3,6}$/} + }; + + this.follow = function(cbReturn, decodedBody) { + var searchFilter = "(&("+ Configs.ldap.userKey +"="+ decodedBody["Caller-Destination-Number"] +")(mobile=*))"; + var searchUri = Configs.ldap.uri + Configs.ldap.users + searchFilter; + + LDAPClient.Search(searchUri, function(error, result) { + if (typeof error !== "undefined") { + cbReturn(null); + } else if (typeof result[0] !== "undefined") { + var response = new ltx.Element("document", {"type":"freeswitch/xml"}) + .c("section", {"name":"dialplan", "description": "LDAP Follow Me"}) + .c("context", {"name":"default"}) + .c("extension", {"name":"inbound_ldap_lookup"}) + .c("condition", {"field":"destination_number", "expression":"/^"+ decodedBody["Caller-Destination-Number"] +"$/"}) + .c("action", {"application": "set", "data": "hangup_after_bridge=true"}).up() + .c("action", {"application": "set", "data": "continue_on_fail=true"}).up() + .c("action", {"application": "set", "data": "ignore_early_media=true"}).up() + .c("action", {"application": "set", "data": "call_timeout=15"}).up() + .c("action", {"application": "bridge", "data": "sofia/$${domain}/"+ decodedBody["Caller-Destination-Number"]}).up() + .c("action", {"application": "set", "data": "call_timeout=15"}).up() + .c("action", {"application": "bridge", "data": "sofia/gateway/"+ Configs.gatewayName +"/"+ result[0]["mobile"][0]}).up() + .c("action", {"application": "answer"}).up() + .c("action", {"application": "sleep", "data": "1000"}).up() + .c("action", {"application": "voicemail", "data": "default $${domain} "+ decodedBody["Caller-Destination-Number"]}).up() + .up() + .up() + .up() + .up(); + + + cbReturn(response.toString()); + } + }); + }; +}; + +exports.Responder = [followMe]; diff --git a/responders/dialplan/inbound-lookup.js b/responders/dialplan/inbound-lookup.js index e69de29..6f63cc7 100644 --- a/responders/dialplan/inbound-lookup.js +++ b/responders/dialplan/inbound-lookup.js @@ -0,0 +1,38 @@ +var sys = require("sys"), + ltx = require("../../modules/ltx/lib/index"), + Configs = require("../../configs.js"), + LDAPClient = require("../../modules/node-ldapsearch/build/default/ldap.node"); + +var inboundLookup = function(parent) { + this.selector = { + "lookup": {"Call-Direction": "inbound", "variable_direction": "inbound", "Caller-Context": "public"} + }; + + this.lookup = function(cbReturn, decodedBody) { + var searchFilter = "(mobile="+ decodedBody["Caller-ANI"] +")"; + var searchUri = Configs.ldap.uri + Configs.ldap.users + searchFilter; + + LDAPClient.Search(searchUri, function(error, result) { + if (typeof error !== "undefined") { + cbReturn(null); + } else if (typeof result[0] !== "undefined") { + var response = new ltx.Element("document", {"type":"freeswitch/xml"}) + .c("section", {"name":"dialplan", "description": "Inbound LDAP Lookup"}) + .c("context", {"name":"public"}) + .c("extension", {"name":"inbound_ldap_lookup"}) + .c("condition", {"field":"ani", "expression":"/^"+ decodedBody["Caller-ANI"] +"$/"}) + .c("action", {"application": "set", "data": "effective_caller_id_name="+ result[0]["givenName"][0]}).up() + .c("action", {"application": "export", "data": "alert_info=Bellcore-r5"}).up() + .c("action", {"application": "transfer", "data": Configs.inboundDestination}).up() + .up() + .up() + .up() + .up(); + + cbReturn(response.toString()); + } + }); + }; +}; + +exports.Responder = [inboundLookup]; diff --git a/responders/directory/basic.js b/responders/directory/basic.js deleted file mode 100644 index e69de29..0000000 diff --git a/responders/directory/ldap.js b/responders/directory/ldap.js new file mode 100644 index 0000000..67a031d --- /dev/null +++ b/responders/directory/ldap.js @@ -0,0 +1,135 @@ +var sys = require("sys"), + ltx = require("../../modules/ltx/lib/index"), + Configs = require("../../configs.js"), + LDAPClient = require("../../modules/node-ldapsearch/build/default/ldap.node"); + +var ldap = function(parent) { + this.selector = { + "startup": {"tag_name": false, "Event-Name": "REQUEST_PARAMS"}, + "auth": {"tag_name": "domain", "Event-Name": "REQUEST_PARAMS", "action": "sip_auth", "Event-Calling-Function": "sofia_reg_parse_auth", "user": /^[1-9]{1}[0-9]{3,6}$/}, + "mailbox": {"tag_name": "domain", "Event-Name": "GENERAL", "Event-Calling-Function": /^(resolve_id|voicemail_check_main)$/, "user": /^[1-9]{1}[0-9]{3,6}$/}, + "user": {"tag_name": "domain", "Event-Name": "REQUEST_PARAMS", "action": "user_call", "Event-Calling-Function": "user_outgoing_channel", "user": /^[1-9]{1}[0-9]{3,6}$/} + }; + + this.startup = function(cbReturn, decodedBody) { + var response = new ltx.Element("document", {"type":"freeswitch/xml"}) + .c("section", {"name":"directory", "description":"Dynamic User Directory"}) + .c("domain", {"name":"$${domain}"}) + .c("params") + .c("param", {"name":"dial-string", "value": "{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}"}).up() + .up() + .up() + .up(); + + cbReturn(response.toString()); + }; + + this.auth = function(cbReturn, decodedBody) { + var searchFilter = "("+ Configs.ldap.userKey +"="+ decodedBody["user"] +")"; + var searchUri = Configs.ldap.uri + Configs.ldap.users + searchFilter; + + LDAPClient.Search(searchUri, function(error, result) { + if (typeof error !== "undefined") { + cbReturn(null); + } else if (typeof result[0] !== "undefined") { + var response = new ltx.Element("document", {"type":"freeswitch/xml"}) + .c("section", {"name":"directory", "description":"Dynamic User Directory"}) + .c("domain", {"name":"$${domain}"}) + .c("params") + .c("param", {"name":"dial-string", "value": "{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}"}).up() + .up() + .c("groups") + .c("group", {"name":"default"}) + .c("users") + .c("user", {"id":decodedBody["user"]}) + .c("params") + .c("param", {"name":"password", "value":result[0][Configs.ldap.sipPassword][0]}).up() + .c("param", {"name":"vm-password", "value":result[0][Configs.ldap.vmPassword][0]}).up() + .c("param", {"name":"vm-enabled", "value":"true"}).up() + .up() + .c("variables") + .c("variable", {"name":"toll_allow", "value":""}).up() + .c("variable", {"name":"accountcode", "value":decodedBody["user"]}).up() + .c("variable", {"name":"user_context", "value":"default"}).up() + .up() + .up() + .up() + .up() + .up() + .up() + .up(); + + cbReturn(response.toString()); + } + }); + }; + + this.mailbox = function(cbReturn, decodedBody) { + var searchFilter = "("+ Configs.ldap.userKey +"="+ decodedBody["user"] +")"; + var searchUri = Configs.ldap.uri + Configs.ldap.users + searchFilter; + + LDAPClient.Search(searchUri, function(error, result) { + if (typeof error !== "undefined") { + cbReturn(null); + } else if (typeof result[0] !== "undefined") { + + var response = new ltx.Element("document", {"type":"freeswitch/xml"}) + .c("section", {"name":"directory", "description":"Dynamic User Directory"}) + .c("domain", {"name":"$${domain}"}) + .c("params") + .c("param", {"name":"dial-string", "value": "{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}"}).up() + .up() + .c("groups") + .c("group", {"name":"default"}) + .c("users") + .c("user", {"id":decodedBody["user"]}) + .c("params") + .c("param", {"name":"password", "value":result[0][Configs.ldap.sipPassword][0]}).up() + .c("param", {"name":"vm-password", "value":result[0][Configs.ldap.vmPassword][0]}).up() + .c("param", {"name":"vm-enabled", "value":"true"}).up() + .up() + .up() + .up() + .up() + .up() + .up() + .up(); + + cbReturn(response.toString()); + } + }); + }; + + this.user = function(cbReturn, decodedBody) { + var searchFilter = "("+ Configs.ldap.userKey +"="+ decodedBody["user"] +")"; + var searchUri = Configs.ldap.uri + Configs.ldap.users + searchFilter; + + LDAPClient.Search(searchUri, function(error, result) { + if (typeof error !== "undefined") { + cbReturn(null); + } else if (typeof result[0] !== "undefined") { + + var response = new ltx.Element("document", {"type":"freeswitch/xml"}) + .c("section", {"name":"directory", "description":"Dynamic User Directory"}) + .c("domain", {"name":"$${domain}"}) + .c("params") + .c("param", {"name":"dial-string", "value": "{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}"}).up() + .up() + .c("groups") + .c("group", {"name":"default"}) + .c("users") + .c("user", {"id":decodedBody["user"]}).up() + .up() + .up() + .up() + .up() + .up(); + + cbReturn(response.toString()); + } + }); + }; + +}; + +exports.Responder = [ldap];