diff --git a/README.md b/README.md index d77f325..73cee80 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ var nginx = new Nginx(); var nginx = new Nginx({ sudo: true, pidFile: '/run/nginx.pid', - confDir: '/usr/local/etc/nginx/conf.d/' + confDir: '/usr/local/etc/nginx/conf.d' // directory containing .conf files }); ``` The configuration is automatically loaded depending on the operating system. The default configuration lives in /conf. Feel free to contribute. @@ -38,15 +38,72 @@ nginx.start(); nginx.stop(); nginx.reload(); -// callback +/** callback example */ nginx.start(function (err) { }); -// promises +/** promises example */ nginx.stop().then(function () { // success }, function (err) { // errror }); ``` + +### Methods for managing vhosts +Programmatically ```create, read, update & delete``` vhosts. This will require your Nginx configuration file to require a http section where their is "an ```include directive``` for all .conf files from a certain folder". (We represent this in our library as ```confDir```); [https://kcode.de/wordpress/2033-nginx-configuration-with-includes](More information.) + +#### Create +``` +nginx.create({ + id: '', // unique identifier + raw: '', // raw server block + template: 'default', // path or string representing template + values: '' // values to be filled in by template +}) +.then(nginx.reload) // reload nginx +.then(function (response) { + // success +}) +.catch(function (err) { + // error +}); +``` + +An example would be +``` +nginx.create({ + id: 'test', + template: 'default', + values: { + cname: 'example.com', + port: 80, + proxy_pass: 'http://localhost:8080' + } +}) +``` +### Read +``` +nginx.read('unique identifier'); +``` + +### Update +``` +nginx.update('unique identifier', { + /** + * same options as create + */ + template: 'default', + values: { + cname: 'example.com', + port: 80, + proxy_pass: 'http://localhost:8080' + } +}); +``` + +#### Delete +``` +nginx.delete('unique identifier') +``` diff --git a/conf/default.darwin.js b/conf/default.darwin.js index 1972c64..762fa89 100644 --- a/conf/default.darwin.js +++ b/conf/default.darwin.js @@ -5,5 +5,5 @@ module.exports = { sudo: false, pidFile: '/usr/local/var/run/nginx.pid', - confDir: '/usr/local/etc/nginx/servers/' + confDir: '/usr/local/etc/nginx/servers' }; diff --git a/conf/default.js b/conf/default.js index 90259dd..42af669 100644 --- a/conf/default.js +++ b/conf/default.js @@ -6,5 +6,6 @@ module.exports = { online: false, sudo: true, pidFile: '/run/nginx.pid', - confDir: '/usr/local/etc/nginx/conf.d/' + confDir: '/usr/local/etc/nginx/conf.d', + identifier: 'nginxo_', }; diff --git a/lib/Lifecycle.js b/lib/Lifecycle.js index b9bd540..bdfe702 100644 --- a/lib/Lifecycle.js +++ b/lib/Lifecycle.js @@ -54,7 +54,7 @@ Lifecycle.prototype.end = function() { } Lifecycle.prototype.reload = function(cb) { - return utils.zombieCommand.apply(this, 'nginx -s reload', cb); + return utils.zombieCommand.apply(this, ['nginx -s reload', cb]); } Lifecycle.prototype.start = function(cb) { diff --git a/lib/Nginx.js b/lib/Nginx.js index 5bb1c53..fa77f30 100644 --- a/lib/Nginx.js +++ b/lib/Nginx.js @@ -7,7 +7,7 @@ const util = require('util'), utils = require('./utils'), config = require('./config'), - Lifecycle = require('./Lifecycle'); + Server = require('./Server'); function Nginx(opts) { var self = this; @@ -33,4 +33,7 @@ function Nginx(opts) { Nginx.super_.call(this); } -util.inherits(Nginx, Lifecycle); +util.inherits(Nginx, Server); + +module.exports = Nginx; +Nginx.utils = utils; diff --git a/lib/Server.js b/lib/Server.js new file mode 100644 index 0000000..e6c3585 --- /dev/null +++ b/lib/Server.js @@ -0,0 +1,140 @@ +/** + * Nginx Lifecycle + * @author Muhammad Dadu + */ + +const + util = require('util'), + utils = require('./utils'), + path = require('path'), + fs = require('fs'), + Lifecycle = require('./Lifecycle'), + Promise = require('promise'); + +function Server() { + Server.super_.call(this); +} +util.inherits(Server, Lifecycle); + +/** + * list conf's + * @param {Function} cb callback + * @return {Promise} promise + */ +Server.prototype.list = function (cb) { + var globPattern = this.confDir + '/**/' + this.identifier + '*.conf', + self = this; + return new Promise(function (resolve, reject) { + (!cb || typeof cb !== 'function') && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); + var files = utils.getGlobbedFiles(globPattern), + ids = []; + files.forEach(function (file) { + ids.push(file.replace(self.confDir + '/' + self.identifier, '').replace('.conf', '')); + }); + cb(null, ids); + }); +} + +/** + * Create a new vhost + * @param {Object} opts + * @param {String} opts.id unique identifier + * @param {String} opts.raw [text to save] + * @param {String} opts.template [template name or path] + * @param {Object} opts.values [values for the template] + * @param {Function} cb callback + * @return {Promise} promise + */ +Server.prototype.create = function (opts, cb) { + var self = this; + return new Promise(function (resolve, reject) { + (!cb || typeof cb !== 'function') && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); + !opts.id && (opts.id = String((new Date()).getTime())); + !opts.template && (opts.template = 'default'); + var filePath = path.join(self.confDir, self.identifier + opts.id + '.conf'); + // verify nonexistance + if (fs.existsSync(filePath)) { return cb('vhost with id `' + opts.id + '` exists'); } + if (!opts.raw && opts.values && !opts.template) { return cb('Missing attributes'); } + if (opts.raw) { + return fs.writeFile(filePath, opts.raw, function (err) { + if (err) { return cb(err); } + return cb(null, opts); + }); + } + utils.render(opts.template, opts.values).then(function (html) { + return fs.writeFile(filePath, html, 'utf8', function (err) { + if (err) { return cb(err); } + return cb(null, opts); + }); + }, cb); + }); +} + +/** + * Read a vhost + * @param {String} id unique identifier + * @param {Function} cb callback + * @return {Promise} promise + */ +Server.prototype.read = function (id, cb) { + var self = this; + return new Promise(function (resolve, reject) { + (!cb || typeof cb !== 'function') && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); + if (!id) { return cb('id is missing'); } + var filePath = path.join(self.confDir, self.identifier + id + '.conf'); + // verify existance + if (!fs.existsSync(filePath)) { return cb('vhost with id `' + id + '` does not exist'); } + fs.readFile(filePath, function (err, file) { + if (err) { return cb(err); } + cb(null, file); + }); + }); +} + +/** + * Update a vhost + * @param {String} id unique identifier + * @param {Object} opts + * @param {String} opts.raw [text to save] + * @param {String} opts.template [template name or path] + * @param {Object} opts.values [values for the template] + * @param {Function} cb callback + * @return {Promise} promise + */ +Server.prototype.update = function (id, opts, cb) { + var self = this; + return new Promise(function (resolve, reject) { + (!cb || typeof cb !== 'function') && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); + if (!id) { return cb('id is missing'); } + var filePath = path.join(self.confDir, self.identifier + id + '.conf'); + // verify existance + if (!fs.existsSync(filePath)) { return cb('vhost with id `' + id + '` does not exist'); } + opts.id = id; + self.delete(id).then(self.create(opts)).then(function (a) { + cb(null, a); + }, cb); + }); +} + +/** + * Delete a vhost + * @param {String} id unique identifier + * @param {Function} cb callback + * @return {Promise} promise + */ +Server.prototype.delete = function (id, cb) { + var self = this; + return new Promise(function (resolve, reject) { + (!cb || typeof cb !== 'function') && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); + if (!id) { return cb('id is missing'); } + var filePath = path.join(self.confDir, self.identifier + id + '.conf'); + // verify existance + if (!fs.existsSync(filePath)) { return cb('vhost with id `' + id + '` does not exist'); } + fs.unlink(filePath, function (err) { + if (err) { return cb(err); } + cb(null); + }); + }); +} + +module.exports = Server; diff --git a/lib/utils.js b/lib/utils.js index 1e5cbe3..7a132d0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,10 +7,15 @@ const _ = require('lodash'), glob = require('glob'), exec = require('child_process').exec, - Promise = require('promise'); + path = require('path'), + fs = require('fs'), + Promise = require('promise'), + Handlebars = require('handlebars'); var utils = module.exports; +const TEMPLATES_DIR = path.join(__dirname, '../templates'); + /** * Get files by glob patterns */ @@ -33,10 +38,64 @@ utils.getGlobbedFiles = function getGlobbedFiles(globPatterns) { utils.zombieCommand = function zombieCommand(command, cb) { var sudo = this.sudo || ''; return new Promise(function (resolve, reject) { - !cb && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); + (!cb || typeof cb !== 'function') && (cb = function (a, b) { if (a) { return reject(a); } resolve(b); }); exec(sudo + command, function (err, stdout, stderr) { if (err || stderr) { return cb(err || stderr); } cb(null, stdout.trim()); }); }); -}; \ No newline at end of file +}; + +// register helpers +Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) { + switch (operator){ + case "==": + return (v1==v2)?options.fn(this):options.inverse(this); + case "!=": + return (v1!=v2)?options.fn(this):options.inverse(this); + case "===": + return (v1===v2)?options.fn(this):options.inverse(this); + case "!==": + return (v1!==v2)?options.fn(this):options.inverse(this); + case "&&": + return (v1&&v2)?options.fn(this):options.inverse(this); + case "||": + return (v1||v2)?options.fn(this):options.inverse(this); + case "<": + return (v1": + return (v1>v2)?options.fn(this):options.inverse(this); + case ">=": + return (v1>=v2)?options.fn(this):options.inverse(this); + default: + return eval(""+v1+operator+v2)?options.fn(this):options.inverse(this) + } +}); + +utils.render = function (template, context) { + return new Promise(function (resolve, reject) { + var templatePath = null; + (function () { + var toLook = [template, + path.join(TEMPLATES_DIR, template), + path.join(TEMPLATES_DIR, template + '.ejs'), + path.join(TEMPLATES_DIR, template + '.conf'), + path.join(TEMPLATES_DIR, template + '.conf.ejs')]; + + for (var i = 0, l = toLook.length; i < l; ++i) { + if (fs.existsSync(toLook[i])) { + templatePath = toLook[i]; + return; + } + } + })(); + if (!templatePath) { return reject('Template not found'); } + // compile template + var tmplate = Handlebars.compile(String(fs.readFileSync(templatePath))); + // generate html + var html = tmplate(context); + resolve(html); + }); +} diff --git a/package.json b/package.json index be93c11..690a5e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nginx-o", - "version": "0.0.1", + "version": "0.0.2", "description": "Programmatically manipulate a running Nginx instance", "main": "lib/Nginx.js", "scripts": { @@ -13,6 +13,7 @@ "license": "ISC", "dependencies": { "glob": "^7.0.3", + "handlebars": "^4.0.5", "lodash": "^4.6.1", "procinfo": "^0.1.4", "promise": "^7.1.1" diff --git a/templates/default.conf.ejs b/templates/default.conf.ejs new file mode 100644 index 0000000..0ef191c --- /dev/null +++ b/templates/default.conf.ejs @@ -0,0 +1,18 @@ +# clout service +server { + listen *:{{port}}; + + server_name {{cname}}; + + # access_log /var/log/nginx/{{cname}}.access.log; + # error_log /var/log/nginx/{{cname}}.error.log; + + location / { + proxy_pass {{proxy_pass}}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} \ No newline at end of file