From 7830892675efc3c2bb4f1308fff3ac5b7ff81664 Mon Sep 17 00:00:00 2001 From: Nathan Ahlstrom <4483029+NathanAhlstrom@users.noreply.github.com> Date: Thu, 26 Oct 2017 14:37:52 -0500 Subject: [PATCH 1/5] added file lookup.js to include underscore and deep JSON map lookup capability. --- config.js | 39 +++++++++++++++++++++++++++++++++++++++ lookup.js | 25 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 config.js create mode 100644 lookup.js diff --git a/config.js b/config.js new file mode 100644 index 0000000..d309bf9 --- /dev/null +++ b/config.js @@ -0,0 +1,39 @@ +// +// renamed this config.json file to config.js to be able to introduce comments. +// required the same in nodejs. require('config'); +// only change is adding "module.exports = " to beginning of file. +// +module.exports = { + "serverTitle": "ezproxy webhook listener", + "hostname": "localhost", // remove for os.hostname() + "listenerPort": 9000, + "hooks": { + "push": { + "secretKey": "uerWkvxxb5PRBGSPdeA5mjjhEnjzh68X-ezproxy", + "matches": { + "project.default_branch": "production", + "project.path_with_namespace": "ahlstn1/ezproxy" + }, + "commandBatch": "./shellscript.sh" + }, + +/* + * unused + * + "tag_push": { + + }, + "issue": { + + }, + "note": { + + }, + "merge_request": { + + } +* +*/ + + } +} diff --git a/lookup.js b/lookup.js new file mode 100644 index 0000000..c4956ff --- /dev/null +++ b/lookup.js @@ -0,0 +1,25 @@ +/* + * lookup function for inspection objects for keys down several levels. + * from: + * https://gist.github.com/megawac/6162481#file-underscore-lookup-js + */ + +var _ = require('underscore'); + +_.mixin({ + lookup: function (obj, key) { + var type = typeof key, i = 0, length; + if (type == "string" || type == "number") { + key = ("" + key).replace(/\[(.*?)\]/g, function (m, key) { //handle case where [1] or ['xa'] may occur + return "." + key.replace(/^["']|["']$/g, ""); //strip quotes at the start or end of the key + }).split("."); + } + for (length = key.length; i < length; i++) { + if (_.has(obj, key[i])) obj = obj[key[i]]; + else return void 0; + } + return obj; + } +}); + +module.exports = _; From cb9ec5f4b0ab3ffa9a685fdcdd964873be7ccdf9 Mon Sep 17 00:00:00 2001 From: Nathan Ahlstrom <4483029+NathanAhlstrom@users.noreply.github.com> Date: Thu, 26 Oct 2017 14:53:21 -0500 Subject: [PATCH 2/5] updated config.js to generalize --- config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index d309bf9..89ca120 100644 --- a/config.js +++ b/config.js @@ -4,15 +4,15 @@ // only change is adding "module.exports = " to beginning of file. // module.exports = { - "serverTitle": "ezproxy webhook listener", + "serverTitle": "gitlab webhook listener", "hostname": "localhost", // remove for os.hostname() "listenerPort": 9000, "hooks": { "push": { - "secretKey": "uerWkvxxb5PRBGSPdeA5mjjhEnjzh68X-ezproxy", + "secretKey": "SECRETKEYHERE", "matches": { "project.default_branch": "production", - "project.path_with_namespace": "ahlstn1/ezproxy" + "project.path_with_namespace": "username/reponame" }, "commandBatch": "./shellscript.sh" }, From bba449f12432787d3678bc9e6753314d828e8ca9 Mon Sep 17 00:00:00 2001 From: Nathan Ahlstrom <4483029+NathanAhlstrom@users.noreply.github.com> Date: Thu, 26 Oct 2017 15:59:33 -0500 Subject: [PATCH 3/5] minor update to shellscript.sh to echo current working directory. --- shellscript.sh | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 shellscript.sh diff --git a/shellscript.sh b/shellscript.sh old mode 100644 new mode 100755 index 52931ec..0f5f664 --- a/shellscript.sh +++ b/shellscript.sh @@ -2,4 +2,5 @@ # Sample Bash Script # Change this script to run whatever commands you wish and include it in config.json. echo "Hello from GitLab CE Webhook Server!" +echo $PWD; git status From 9c7641500152487756def0c2555acbb8e961ba48 Mon Sep 17 00:00:00 2001 From: Nathan Ahlstrom <4483029+NathanAhlstrom@users.noreply.github.com> Date: Thu, 26 Oct 2017 16:00:33 -0500 Subject: [PATCH 4/5] minor tweak to .gitignore to skip over VI swp files. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e920c16..b754023 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# temporary VI files +.*.swp + # Logs logs *.log From 3cc574595b741eb709cdf4a942400406ee4ded0c Mon Sep 17 00:00:00 2001 From: Nathan Ahlstrom <4483029+NathanAhlstrom@users.noreply.github.com> Date: Thu, 26 Oct 2017 16:03:20 -0500 Subject: [PATCH 5/5] massive updates to GWServer.js: new functions (fnValidateConfig, fnCheckRequest), using underscore + custom lookup function to do deep JSON lookups - necessary to run matches on project.name = 'repoName'; correctly handle non-POST request by exiting silently. --- GWServer.js | 292 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 197 insertions(+), 95 deletions(-) diff --git a/GWServer.js b/GWServer.js index 4d2aa6b..7fa7394 100644 --- a/GWServer.js +++ b/GWServer.js @@ -12,115 +12,217 @@ * Main Server Script. */ -var config = require("./config.json"), - os = require("os"), - http = require("http"), - proc = require("child_process"), - port = config.listenerPort, - supportedHooks = Object.keys(config.hooks), - fnProcessRequest, - fnVerifyMatches, - server; +var config = require("./config.js"), + _ = require("./lookup.js"), + os = require("os"), + fs = require("fs"), + path = require("path"), + http = require("http"), + proc = require("child_process"), + port = config.listenerPort, + host = (config.hasOwnProperty("hostname") && config.hostname != null) ? config.hostname : os.hostname(), + supportedHooks = Object.keys(config.hooks), + fnProcessRequest, + fnVerifyMatches, + server; + + +/** + * This method walks through the config.hooks to validate correct + * execute permission on the command line scripts. + */ +fnValidateConfig = function(sH) { + for (var i = 0; i < sH.length; i++) { + // read properties from config.js + file = _.lookup(config.hooks, sH[i] +".commandBatch"); + loc = _.lookup(config.hooks, sH[i] + ".commandDir"); + + // gynmastics to create a file path + loc = loc == null ? "" : loc; + file = path.join(loc, file); + + // check if we can execute this file. + try { + fs.accessSync(file, + fs.constants.F_OK | fs.constants.R_OK | + fs.constants.X_OK); + } catch (e) { + console.log("Missing file or execute permissions on file: " + file); + console.log("Recommend running chmod +x " + file); + // exiting hard with a big bad error + process.exit(); + } + } +}; + /** * This method verifies conditions given in config[].matches against requestBody */ fnVerifyMatches = function(requestBody, matchesCollection) { - var matchItem; - - for (matchItem in matchesCollection) - { - if (!requestBody.hasOwnProperty(matchItem)) - return false; - else - { - if (requestBody[matchItem] === matchesCollection[matchItem]) - continue; - else - return false; - } - } - - return true; + var matchItem; + + for (matchItem in matchesCollection) + { + console.log("match: " + matchItem + + " value: " + _.lookup(requestBody, matchItem) + + " compare to: " + matchesCollection[matchItem]); + + if (matchesCollection[matchItem] === _.lookup(requestBody,matchItem) ) + continue; + else + return false; + } + + return true; }; + +fnCheckRequest = function(reqHeaders, type) { + + var token, secretKey; + + if ( reqHeaders.hasOwnProperty('x-gitlab-token') ) { + token = reqHeaders['x-gitlab-token']; + } + else { + return false; + } + + if (reqHeaders.hasOwnProperty('x-gitlab-event') && + supportedHooks.indexOf(type) > -1) + { + secretKey = config.hooks[type]["secretKey"]; + } + else { + return false; + } + + if ( token === secretKey ) { + //console.info("token = %s, secretKey = %s", token, secretKey); + return true; + } + + return false; +} + /** * This method does all the processing and command execution on requestBody. */ fnProcessRequest = function(requestBody) { - var object_kind = requestBody.object_kind, - satisfiesMatches = false, - pipedOutput = [], - errors = [], - commandBatch, - hookConfig, - i; - - hookConfig = config.hooks[object_kind]; - if (typeof hookConfig.matches === "object") // Check if 'matches' map is provided with this hook type. - satisfiesMatches = fnVerifyMatches(requestBody, hookConfig.matches); // Verify matches. - else - satisfiesMatches = true; - - // Run commandBatch only if matches are satisfied. - if (satisfiesMatches) - { - // Beware, this is DANGEROUS. - commandBatch = proc.spawn(hookConfig.commandBatch); - - // Collect Bash output. - commandBatch.stdout.on('data', function(data) { - pipedOutput.push(data); - }); - - // Collect Bash errors. - commandBatch.stderr.on('data', function(data) { - errors.push(data); - }); - - // Listen for end of commandBatch execution. - commandBatch.on('exit', function(status) { - if (status === 0) // Check if execution failed with non-Zero status - console.log(Buffer.concat(pipedOutput).toString()); // All good. - else - console.error('Hook Execution Terminated with status : %s \n', status, Buffer.concat(errors).toString()); - }); - } + var object_kind = requestBody.object_kind, + satisfiesMatches = false, + pipedOutput = [], + errors = [], + commandBatch, + hookConfig, + i; + + // retrieve the configuration for this hook-type. + hookConfig = config.hooks[object_kind]; + + // Check if 'matches' map is provided with this hook type. + if (typeof hookConfig.matches === "object") { + // Verify matches - run comparisons. + satisfiesMatches = fnVerifyMatches(requestBody, hookConfig.matches); + } + else { + // no "matches" map in config.js; skip comparison; satisfies = true + satisfiesMatches = true; + } + + console.info("match %s at %s", satisfiesMatches, new Date()); + + // Run commandBatch only if matches are satisfied. + if (satisfiesMatches) + { + console.info("running %s at %s", + hookConfig.commandBatch, new Date()); + + options = { + "cwd": _.lookup(hookConfig, "commandDir") + }; + + // Beware, this is DANGEROUS. + commandBatch = proc.spawn(hookConfig.commandBatch, + options); + + // Collect command output. + commandBatch.stdout.on('data', function(data) { + pipedOutput.push(data); + }); + + // Collect command errors. + commandBatch.stderr.on('data', function(data) { + errors.push(data); + }); + + // Listen for end of commandBatch execution. + commandBatch.on('exit', function(status) { + if (status === 0) // Check if execution failed with non-Zero status + console.log(Buffer.concat(pipedOutput).toString()); // All good. + else + console.error('Hook Execution Terminated with status : %s \n', status, Buffer.concat(errors).toString()); + }); + } }; server = http.createServer(function(request, response) { - var reqHeaders = request.headers, - reqBody = []; - - request - .on('data', function(chunk) { - reqBody.push(chunk); - }) - .on('end', function() { - reqBody = JSON.parse(Buffer.concat(reqBody).toString()); - - // Check if - // x-gitlab-event header is present in headers AND - // object_kind is one of the supported hooks in config - // then respond accordingly. - if (reqHeaders.hasOwnProperty('x-gitlab-event') && - supportedHooks.indexOf(reqBody.object_kind) > -1) - { - response.statusCode = 200; - fnProcessRequest(reqBody); - } - else - response.statusCode = 400; - - response.end(); - }); + var reqHeaders = request.headers, + reqBody = []; + + //console.log(request.method); + if (request.method == 'GET') { + response.statusCode = 404; + response.end(); + return; + } + + request + .on('data', function(chunk) { + reqBody.push(chunk); + }) + .on('end', function() { + reqBody = JSON.parse(Buffer.concat(reqBody).toString()); + + // + // Check if + // x-gitlab-event header is present in headers + // AND object_kind is one of the supported hooks in config.js + // AND x-gitlab-token matches the secretKey in config.js + // then respond accordingly. + // + if ( fnCheckRequest(reqHeaders, reqBody.object_kind) ) + { + console.info("fnCheckRequest passed"); + response.statusCode = 200; + fnProcessRequest(reqBody); + } + else { + console.info("fnCheckRequest failed"); + response.statusCode = 400; + } + + response.end(); + }); }); -server.listen(port, function() { - console.info("%s started on %s:%d at %s", - config.serverTitle, - os.hostname(), - port, - new Date() - ); + +/* + * function to look through config.js to verify execute permissions + * on all "commandBatch" files. + */ +fnValidateConfig(supportedHooks); + + +/* + * begin nodejs httpd server listening on specified ports/hostnames. + */ +server.listen(port, host, function() { + console.info("%s started on %s:%d at %s", + config.serverTitle, + host, + port, + new Date() + ); });