commit dc8187c9e823b9a323e2eae0f0ed9c9cbca2309f Author: Matthieu Lalonde Date: Sat Apr 16 15:55:12 2011 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d6ac60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +var/db/* +temp* +*.tmproj +.makefiles* +configs/*.js +build/* +*.node +.DS_Store +._* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b8c3074 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,10 @@ +[submodule "modules/node-hash"] + path = modules/node-hash + url = git://github.com/Marak/node_hash.git +[submodule "modules/node-ldapsearch"] + path = modules/node-ldapsearch + url = git://github.com/xSmurf/node-ldapsearch.git +[submodule "modules/ltx"] + [ath = modules/node-ltx + url = git://github.com/astro/ltx.git + diff --git a/configs.js b/configs.js new file mode 100644 index 0000000..a7715a0 --- /dev/null +++ b/configs.js @@ -0,0 +1,65 @@ +var sys = require("sys"), +events = require("events"), +fs = require("fs"); + +var Configs = this; + +var load = function (debug) { + events.EventEmitter.call(this); + + var self = this; + Configs.debug = debug || false; + + dir = __dirname + "/configs/"; + fs.readdir(dir, function (err, files) { + if (err) { + sys.puts(("[ warn ] unable to load config directory: " + dir).magenta); + return; + } + + for (var k = 0, l = files.length; k < l; ++k) { + if (!(/\.js$/.exec(files[k]))) { + continue; + } + + var filePath = dir + files[k]; + 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) { + Configs[key] = configFile[key]; + + if (Object.keys(configFile).length > 1) { + if (key === "init" && typeof Configs.init === "function") { + Configs.init.call(Configs, Configs); + + delete Configs.init; + } + } else { + if (typeof Configs[key].init === "function") { + Configs[key].init.call(Configs, Configs[key]); + + delete Configs[key].init; + } + } + }); + + sys.puts(("[ config ] ./" + fileKey).magenta); + } + + Configs.loaded = true; + self.emit("config:loaded"); + }); +}; + +sys.inherits(load, events.EventEmitter); + +exports.load = load; diff --git a/configs/freenode.js.default b/configs/freenode.js.default new file mode 100644 index 0000000..d9a7768 --- /dev/null +++ b/configs/freenode.js.default @@ -0,0 +1,27 @@ +var path = require("path"); + +exports.Config = { + version: "0.0.1", + + responderDir: "responders", + + color: "green", + + colors: { + commands: "red", + modules: "green", + configs: "magenta", + success: "green", + failure: "red" + }, + + init: function(self) + { + 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 new file mode 100644 index 0000000..a0724bb --- /dev/null +++ b/handlers/freenode.js @@ -0,0 +1,198 @@ +var debug = true; + +var sys = require("sys"), + util = require("util"), + events = require("events"), + colors = require("colors"), + path = require("path"), + hash = require("../deps/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"); + +var FreeNode = function() { + var self = this; + this.uptime = (new Date().getTime()); + this.loaders = []; + this.ConfigsLoader = null; + this.RespondersLoader = null; + + process.title = "FreeNode"; + + events.EventEmitter.call(this); + + process.chdir(path.dirname(__dirname)); + + var onInitedListerner = function() { + self.removeListener("loaded", onInitedListerner); + self.onInited(); + }; + + this.addListener("loaded", onInitedListerner); + + this.addDependencies(); + + return this; +}; + + +sys.inherits(FreeNode, events.EventEmitter); +exports.FreeNode = FreeNode; + + +/** +* init: Processes the loader chain by starting each loader function +* and waiting for the return object's loaded event before processing the next one. +*/ +FreeNode.prototype.init = function(reload) { + var self = this; + + this.reload = false; + if (typeof reload !== "undefined") { + this.reload = reload; + } + + if (this.loaders.length > 0) { + var setup = function(params) { + return function() { + var success = params.success; + + if (typeof success[2] === "undefined") { + var obj = success[0](); + } else { + var obj = success[2]; + } + + if (typeof params.failure !== "undefined") { + var failure = params.failure; + var cbFailure = function(error) { + obj.removeListener(failure[1], cbFailure); + failure[0](error); + }; + } + + var cbSuccess = function() { + obj.removeListener(success[1], cbSuccess); + if (typeof failure !== "undefined") { + obj.removeListener(failure[1], cbFailure); + } + + self.loaders.shift(); + self.init(self.reload); + }; + + if (typeof params.failure !== "undefined") { + obj.addListener(failure[1], cbFailure); + } + + obj.addListener(success[1], cbSuccess); + + if (typeof success[2] !== "undefined") { + success[0](); + } + }; + }(this.loaders[0]); + + setup(); + } else if (this.reload === false) { + this.emit("loaded"); + } else if (this.reload === true) { + this.emit("reloaded"); + } + + return this; +}; + +/** +* reinit: Prepares and initiates a processs resource reload +* (triggered by SIGHUP) +*/ +FreeNode.prototype.reinit = function(cbReturn) { + var self = this; + + this.addDependencies(true); + + var onReloaded = function() { + self.removeListener("reloaded", onReloaded); + console.log(("\nFreeNode Reload Completed!\n\n")[Configs.color]); + }; + + this.addListener("reloaded", onReloaded); + + if (typeof cbReturn === "function") { + var cbFunc = function(cb) { + return function() { + self.removeListener("reloaded", cbFunc); + cbReturn(); + }; + }(cbReturn); + + this.addListener("reloaded", cbFunc); + } + + this.init(true); +}; + +FreeNode.prototype.onInited = function() { + +}; + +/** +* addDependencies: loads a list of the process' dependencies in the loading chain. +* Some dependencies are skipped if this is called in reload mode +*/ +FreeNode.prototype.addDependencies = function(reload) { + var self = this; + + if (typeof reload === "undefined") { + var reload = false; + } + + this.addDependency( + // Loader + [function() { + console.log(("Loading Configuration Files")[FreeNodeConfig.colors.configs]); + self.ConfigsLoader = new Configs.load(debug); + return self.ConfigsLoader; + }, "config:loaded"] + ); + + if (reload === true) { + // We also want to wait for the MUC to bind + this.addDependency( + // Loader + [function() { + console.log(("Removing responders for reload")[Configs.colors.responders]); + + Responders.unload.apply(self.Responders, [self]); + }, "responders:deloaded", this] + ); + } + + this.addDependency( + // Loader + [function() { + console.log(("Loading Responders")[Configs.colors.responders]); + + Responders.load.apply(self, [self]); + }, "responders:loaded", this] + ); +}; + +/** +* addDependency: adds a dependency data to the loading chain +* +* success[0] = The object loader function, should return the object on which to bind the loaded event +* success[1] = The "loaded" event which the object will trigger when it's done loading +* success[2] = Optionally, you can specify an object on which to bind the event before calling the loader function +* This is useful in cases where the loading function triggers an the event on a different object that already exists +* Optional: +* failure[0] = Callback function to bind to the object on failure +* failure[1] = Error event name on which to bind the callback +*/ +FreeNode.prototype.addDependency = function(success, failure) { + this.loaders[this.loaders.length] = {success:success, failure:failure}; +}; + diff --git a/handlers/httpd.js b/handlers/httpd.js new file mode 100644 index 0000000..e69de29 diff --git a/main.js b/main.js new file mode 100644 index 0000000..6e075fb --- /dev/null +++ b/main.js @@ -0,0 +1,14 @@ +var FreeNodeHandler = require("./libs/freenode").FreeNode, + Configs = require("./configs"); + +// Instanciate our main object +var FreeNode = new FreeNodeHandler(); + +FreeNode.init(); + +// Process SIGHUP by calling reinit +process.on("SIGHUP", function() { + console.log(("\n\n**** Reloading FreeNode! ****\n")[Configs.color]); + + FreeNode.reinit(); +}); diff --git a/modules/date.js b/modules/date.js new file mode 100644 index 0000000..b1bc5bc --- /dev/null +++ b/modules/date.js @@ -0,0 +1,133 @@ +/* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan + * MIT license + * + * Includes enhancements by Scott Trenda + * and Kris Kowal + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + * + * http://blog.stevenlevithan.com/archives/date-time-format + * + */ + +var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; +}(); + +// Some common format strings +dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + httpHeaders: "UTC:ddd, dd mmm yyyy HH:MM:ss \"GMT\"", + mediumFull: "ddd mmm dd HH:MM", + mediumFile: "ddd_mmm_dd-HH-MM", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" +}; + +// Internationalization strings +dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] +}; + +exports.dateFormat = dateFormat; + +// For convenience... +Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); +}; diff --git a/modules/recurseDir.js b/modules/recurseDir.js new file mode 100644 index 0000000..0113ba1 --- /dev/null +++ b/modules/recurseDir.js @@ -0,0 +1,115 @@ +var sys = require("sys"), + util = require("util"), + events = require("events"), + colors = require("colors"), + fs = require("fs"); + +/** + * read a directory (recursively deep) + * data[] = an object for each element in the directory + * .name = item's name (file or folder name) + * .stat = item's stat (.stat.isDirectory() == true IF a folder) + * .children = another data[] for the children + * filter = an object with various filter settings: + * .depth = max directory recursion depth to travel + * (0 or missing means: infinite) + * (1 means: only the folder passed in) + * .hidden = true means: process hidden files and folders (defaults to false) + * .callback = callback function: callback(name, path, filter) -- returns truthy to keep the file + * + * + * @param path = path to directory to read (".", ".\apps") + * @param callback = function to callback to: callback(err, data) + * @param [filter] = (optional) filter object + * + * Shamelessly snagged from: + * http://utahjs.com/2010/09/16/nodejs-events-and-recursion-readdir/ + */ +exports.recurseDir = function(path, callback, filter) { + if (filter) { + // process filter. are we too deep yet? + if (!filter.depthAt) filter.depthAt = 1; // initialize what depth we are at + if (filter.depth && filter.depth < filter.depthAt) { + callback(undefined, []); // we are too deep. return "nothing found" + return; + } + } + + // queue up a "readdir" file system call (and return) + fs.readdir(path, function(err, files) { + if (err) { + callback(err); + return; + } + var doHidden = false; // true means: process hidden files and folders + if (filter && filter.hidden) { + doHidden = true; // filter requests to process hidden files and folders + } + var count = 0; // count the number of "stat" calls queued up + var countFolders = 0; // count the number of "folders" calls queued up + var data = []; // the data to return + + // iterate over each file in the dir + files.forEach(function (name) { + // ignore files that start with a "." UNLESS requested to process hidden files and folders + if (doHidden || name.indexOf(".") !== 0) { + // queue up a "stat" file system call for every file (and return) + count += 1; + fs.stat(path + "/" + name, function(err, stat) { + if (err) { + callback(err); + return; + } + var processFile = true; + if (filter && filter.callback) { + processFile = filter.callback(name, stat, filter); + } + if (processFile) { + var obj = {}; + obj.name = name; + obj.filepath = path + "/" + name; + obj.stat = stat; + data.push(obj); + if (stat.isDirectory()) { + countFolders += 1; + // perform "recurseDir" on each child folder (which queues up a readdir and returns) + (function(obj2) { + // obj2 = the "obj" object + exports.recurseDir(path + "/" + name, function(err, data2) { + if (err) { + callback(err); + return; + } + // entire child folder info is in "data2" (1 fewer child folders to wait to be processed) + countFolders -= 1; + obj2.children = data2; + if (countFolders <= 0) { + // sub-folders found. This was the last sub-folder to processes. + callback(undefined, data); // callback w/ data + } else { + // more children folders to be processed. do nothing here. + } + }); + })(obj); + } + } + // 1 more file has been processed (or skipped) + count -= 1; + if (count <= 0) { + // all files have been processed. + if (countFolders <= 0) { + // no sub-folders were found. DONE. no sub-folders found + callback(undefined, data); // callback w/ data + } else { + // children folders were found. do nothing here (we are waiting for the children to callback) + } + } + }); + } + }); + if (count <= 0) { // if no "stat" calls started, then this was an empty folder + callback(undefined, []); // callback w/ empty + } + }); +}; + diff --git a/responders.js b/responders.js new file mode 100644 index 0000000..315f88a --- /dev/null +++ b/responders.js @@ -0,0 +1,141 @@ +var sys = require("sys"), + util = require("util"), + events = require("events"), + colors = require("colors"), + recurseDir = require("./deps/recurseDir").recurseDir, + Configs = require("./configs"); + +var load = function (Foulinks) { + var self = this; + + if (typeof this.Responders === "undefined") { + this.Responders = {}; + } + + var loadResponder = function(filePath, responderBase) { + var fileKey = filePath.replace(/^(.*)\/|\.js$/g, ""); + + if (typeof responderBase[fileKey] === "undefined") { + responderBase[fileKey] = new Object(); + } + + try { + var responderFile = require(filePath).Responder; + responderFile.forEach(function (func) { + func.apply(responderBase[fileKey], [Foulinks]); + + responderBase[fileKey].responderFile = filePath; + + if (typeof responderBase[fileKey].init === "function") { + responderBase[fileKey].init(); + } + }); + + 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") { + delete process.mainModule.moduleCache[filePath]; + } + + delete responderBase[fileKey]; + + sys.puts(("[ responders ] ERROR Loading ." + + filePath.replace(__dirname+"/"+Configs.responderDir, "").replace(/(\.js)$/, "") + + ": " + err.toString() + "\n" + err.stack.toString())[Configs.colors.failure]); + } + }; + + var loadResponders = function(data) { + for (fileIndex in data) { + if (data[fileIndex].stat.isDirectory()) { + loadResponders(data[fileIndex].children); + } else { + var baseStart = data[fileIndex].filepath.indexOf(Configs.responderDir) + Configs.responderDir.length + 1; + var basePath = data[fileIndex].filepath.substr(baseStart); + var responderBase = self.Responders; + var baseSections = basePath.split("/"); + + for (index in baseSections) { + if (index >= baseSections.length - 1) { + continue; + } + + if (typeof responderBase[baseSections[index]] === "undefined") { + responderBase[baseSections[index]] = {}; + } + + responderBase = responderBase[baseSections[index]]; + } + + responderBase.isDir = true; + + loadResponder(data[fileIndex].filepath, responderBase); + } + } + }; + + var validFile = function(name, stat, filter) { + if (stat.isDirectory() === true) { + return true; + } else { + if (/\.js$/.test(name)) { + return true; + } + } + + return false; + }; + + recurseDir(__dirname + "/" + Configs.responderDir, function(err, data) { + if (typeof err === "undefined") { + loadResponders(data); + + self.emit("responders:loaded"); + } else { + self.emit("responders:error"); + } + }, {callback: validFile}); +}; + +exports.load = load; + +var unload = function(Foulinks, recurse) { + // We went too far! + if (typeof this.responderFile !== "undefined") { + return; + } + + for (fileKey in this) { + if (typeof this[fileKey] === "object") { + // This is a responder and we should remove it + if (typeof this[fileKey].responderFile !== "undefined") { + var filePath = this[fileKey].responderFile; + if (typeof this[fileKey].deinit === "function") { + this[fileKey].deinit(); + + 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]); + // Sub responders follow... maybe + } else { + unload.apply(this[fileKey], [Foulinks, true]); + } + } + } + + if (recurse !== true) { + Foulinks.emit("responders:deloaded"); + } +}; + +exports.unload = unload; + diff --git a/responders/dialplan/inbound-lookup.js b/responders/dialplan/inbound-lookup.js new file mode 100644 index 0000000..e69de29 diff --git a/responders/directory/basic.js b/responders/directory/basic.js new file mode 100644 index 0000000..e69de29