diff --git a/.jshintrc b/.jshintrc index b258983..662a595 100644 --- a/.jshintrc +++ b/.jshintrc @@ -8,5 +8,5 @@ "undef": true, "unused": true, "maxdepth": 2, - "maxcomplexity": 6 + "maxcomplexity": 7 } diff --git a/.travis.yml b/.travis.yml index 17b8450..39e7663 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,14 @@ language: node_js -sudo: false cache: directories: - node_modules node_js: - - '0.10' - - '0.11' - - '0.12' - - '4' - - '5' + - 8 # stable + - 6 # LTS + - 4 # maintenance -before_install: - - npm i -g npm@^2.0.0 before_script: - npm prune script: @@ -25,8 +20,8 @@ after_success: notifications: webhooks: urls: - - https://git.dvbris.com - - https://git.geraintwhite.co.uk - - https://git.oliverfaircliff.com + - https://git.dvbris.com/?semver + - https://git.geraintwhite.co.uk/?semver + - https://git.oliverfaircliff.com/?semver on_success: always on_failure: never diff --git a/README.md b/README.md index f8f183d..20a2762 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,9 @@ Example: "repo_dir": "/home/git/deploy/repos", "getter": "/home/git/deploy/github-getter/get.sh {repo_dir} {output} {repo} {branch}", "post_receive": "/home/git/deploy/post-receive/bin/post-receive -p {dir}", - "github_secret": "HelloWorld", - "travis_token": "top_secret" + "github_secret": "secret_github_secret", + "travis_token": "secret_travis_token", + "url_secret": "secret_url_secret" } ``` @@ -56,6 +57,16 @@ In order for your listener to receive payloads you need to set up a webhook on G - **Github** - follow the instructions [here](https://developer.github.com/webhooks/creating/) and put the webhook secret in `config.json` as `github_secret` - **Travis** - follow the instructions [here](https://docs.travis-ci.com/user/notifications/#Webhook-notification) and put your user token in `config.json` as `travis_token` +If the service you are using does not sign the payloads or provide authorisation headers, you can use the `url_secret` option and add a `?secret=` to the webhook url. + +### URL Parameters + +- `secret` - verify payload if URL secret in `config.json` matches this +- `branch` - run build if branch in payload matches this (defaults to master if omitted) +- `semver` - run build if branch in payload matches semver (e.g. v1.2.3) + +Example: `https://git.example.com/?semver&secret=pass1234&branch=dev` + ## Documentation diff --git a/build-manager.js b/build-manager.js index db0cdaa..4acd65f 100644 --- a/build-manager.js +++ b/build-manager.js @@ -61,6 +61,10 @@ BuildManager.prototype.error = function (res, code, error) { BuildManager.prototype.hook = function (req, res) { var self = this; + if (req.query && req.query.secret) { + req.headers.url_secret = req.query.secret; + } + // Get payload req.pipe(bl(function (err, data) { if (err) { diff --git a/build.js b/build.js index ad55a90..d5c8092 100644 --- a/build.js +++ b/build.js @@ -1,4 +1,4 @@ -var url = require('url'), +var semver = require('semver'), exec = require('child_process').exec, parser = require('./parser'); @@ -76,8 +76,10 @@ Build.prototype.check_payload = function () { } // Check branch in payload matches branch in URL - var branch = url.parse(self.req.url).pathname.replace(/^\/|\/$/g, '') || 'master'; - if (self.ui.data.branch !== branch) { + if (self.req.query.semver === undefined) { + self.req.query.branch = (self.req.query.branch || 'master'); + } + if (!semver.valid(self.ui.data.branch) && self.ui.data.branch !== self.req.query.branch) { return error(400, 'Branches do not match'); } diff --git a/package.json b/package.json index 789dc4a..50932f8 100644 --- a/package.json +++ b/package.json @@ -18,27 +18,28 @@ "semantic-release": "semantic-release pre && npm publish && semantic-release post" }, "dependencies": { - "ansi-to-html": "^0.3.0", + "ansi-to-html": "^0.6.0", "async-tools": "^1.3.0", - "bl": "~1.0.0", + "bl": "~1.2.0", "jade": "~1.11.0", "logging-tool": "~1.2.2", "minimist": "^1.2.0", "node-static": "^0.7.7", - "socket.io": "~1.4.4", - "string-format": "~0.5.0", - "validate-commit-msg": "^2.0.0" + "semver": "^5.1.0", + "socket.io": "~2.0.1", + "string-format": "~0.5.0" }, "devDependencies": { - "codeclimate-test-reporter": "~0.3.0", + "validate-commit-msg": "^2.3.1", + "codeclimate-test-reporter": "~0.4.0", "cracks": "^3.1.2", - "cz-conventional-changelog": "^1.1.5", - "ghooks": "^1.0.3", + "cz-conventional-changelog": "^2.0.0", + "ghooks": "^2.0.0", "istanbul": "~0.4.2", "jshint": "~2.9.1-rc3", - "semantic-release": "^4.3.5", + "semantic-release": "^8.0.0", "tap-spec": "~4.1.1", - "tape": "~4.4.0", + "tape": "~4.8.0", "through2": "~2.0.0" }, "bugs": { diff --git a/parser.js b/parser.js index 39e0a64..14d0064 100644 --- a/parser.js +++ b/parser.js @@ -9,6 +9,10 @@ var Parser = function (data, headers, config) { this.config = config; this.data = {}; this.payload = {}; + + this.check_url_secret = function () { + return !this.config.url_secret || this.headers.url_secret === this.config.url_secret; + }; }; var GitHub = (function () { @@ -21,11 +25,10 @@ var GitHub = (function () { }; gh_parser.prototype.verify_signature = function () { - var signature = 'sha1=' + crypto.createHmac('sha1', this.config.github_secret) .update(this.body) .digest('hex'); - return this.headers['x-hub-signature'] === signature; + return this.headers['x-hub-signature'] === signature && this.check_url_secret(); }; gh_parser.prototype.extract = function () { @@ -100,7 +103,7 @@ var Travis = (function () { var signature = crypto.createHash('sha256') .update(this.headers['travis-repo-slug'] + this.config.travis_token) .digest('hex'); - return this.headers['authorization'] === signature; + return this.headers['authorization'] === signature && this.check_url_secret(); }; travis_parser.prototype.extract = function () { diff --git a/payload.js b/payload.js index 74fa04e..1bf57e3 100755 --- a/payload.js +++ b/payload.js @@ -13,21 +13,26 @@ function selectRnd() { if (argv.h || argv.help) { console.log('Usage: ' + __filename + ' [options]\n'); - console.log('-h|--help display this help message'); - console.log('-p|--port port to send payload requests to'); - console.log('-t|--type payload type to send (travis | github | error) - default github'); + console.log('-h|--help display this help message'); + console.log('-p|--port port to send payload requests to'); + console.log('-t|--type payload type to send (travis | github | error) - default github'); + console.log('-s|--secret secret to add to URL'); + console.log('--semver match semver branch e.g. v1.2.3'); process.exit(); } var type = (argv.t || argv.type || 'github').toLowerCase(); var port = parseInt(argv.p || argv.port || 6003); +var secret = argv.s || argv.secret; +var semver = argv.semver; var payload = {}; var options = { hostname: 'localhost', port: port, path: '/', + query: {}, method: 'POST' }; @@ -90,10 +95,21 @@ if (type === 'travis') { if (branch !== 'master' && Math.random() > 0.05) { - options.path += branch; + options.query.branch = branch; } +if (secret) { + options.query.secret = secret; +} + +if (semver) { + options.query.semver = true; +} + +options.path += '?' + qs.stringify(options.query); + console.log('Sending payload', payload); +console.log('HTTP options', options); http.request(options, function (res) { res.on('data', function (data) { console.log(data.toString()); diff --git a/server.js b/server.js index ab2170d..e468900 100644 --- a/server.js +++ b/server.js @@ -38,6 +38,9 @@ var Server = function (options, ready) { // Setup server self.app = http.createServer(function (req, res) { + var url_parts = url.parse(req.url, true); + for (var key in url_parts) {req[key] = url_parts[key]; } + if (req.method === 'GET') { self.serve(req, res); } else { @@ -141,38 +144,41 @@ Server.prototype.stop = function () { Server.prototype.serve = function (req, res) { var self = this; - var url_parts = url.parse(req.url, true); - if (url_parts.pathname === '/') { - if (url_parts.query.rebuild !== undefined) { // Rebuild last_payload + if (req.pathname === '/') { + if (req.query.rebuild !== undefined) { // Rebuild last_payload logging.log('Rebuild requested'); - self.build_manager.rerun(res, parseInt(url_parts.query.rebuild)); + self.build_manager.rerun(res, parseInt(req.query.rebuild)); } else { // Send the HTML - var html = self.render(); + var html = self.templates.index(self.status()); res.writeHead(200, {'Content-Type': 'text/html'}); res.end(html); } + } else if (req.pathname === '/status') { + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(JSON.stringify(self.status())); + } else { // Serve static files - logging.log('Serving file: ' + url_parts.pathname); + logging.log('Serving file: ' + req.pathname); fileserver.serve(req, res, function(e) { if (e && (e.status === 404)) { - logging.log('404: ' + url_parts.pathname); + logging.log('404: ' + req.pathname); res.writeHead(404, {'Content-Type': 'text/plain'}); - res.end('404 - File not found: ' + url_parts.pathname); + res.end('404 - File not found: ' + req.pathname); } }); } }; /** - * Generate DOM to send to UI of builds dashboard - * @name Server.render + * Generate data to send to builds dashboardself.templates.index)( + * @name Server.status * @function */ -Server.prototype.render = function () { +Server.prototype.status = function () { var self = this; // Sort builds @@ -187,11 +193,11 @@ Server.prototype.render = function () { self.get_build(self.build_manager.current) : builds.length ? builds[0] : {empty: true, data: {}}; - return self.templates.index({ + return { status: self.build_manager.running ? self.STATUS.RUNNING : self.STATUS.READY, builds: builds, current: current - }); + }; }; /** diff --git a/test/common.js b/test/common.js index f5213bb..3d1d6ee 100644 --- a/test/common.js +++ b/test/common.js @@ -21,6 +21,7 @@ function data () { req.method = self.options.method; req.url = self.options.path; req.headers = self.options.headers; + req.query = self.options.query; build_manager.hook(req, res); req.end(payload); @@ -50,7 +51,8 @@ function data () { path: '/', port: '6003', method: 'POST', - headers: {} + headers: {}, + query: {} }; } diff --git a/test/github.js b/test/github.js index 7c4d221..ac86a79 100644 --- a/test/github.js +++ b/test/github.js @@ -15,7 +15,7 @@ test('BEGIN GITHUB PAYLOAD TESTS', function (t) { t.end(); }); test('pass string as payload', function (t) { var payload = 'asdf'; - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { t.equal(data.err, 'Error: Invalid payload', 'correct server response'); @@ -28,7 +28,7 @@ test('pass string as payload', function (t) { test('pass invalid JSON object', function (t) { var payload = JSON.stringify({ property: 'false' }); - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { t.equal(data.err, 'Error: Invalid data', 'correct server response'); @@ -42,7 +42,7 @@ test('pass valid JSON object but invalid signature', function (t) { t.test('valid secret but invalid payload', function (st) { var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs' }); - options.headers['x-hub-signature'] = gen_sig(config.github_secret, 'asdf'); + options.headers = {'x-hub-signature': gen_sig(config.github_secret, 'asdf')}; request(payload, function (res, data) { st.equal(data.err, 'Error: Cannot verify payload signature', 'correct server response'); @@ -53,7 +53,7 @@ test('pass valid JSON object but invalid signature', function (t) { t.test('valid payload but invalid secret', function (st) { var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs' }); - options.headers['x-hub-signature'] = gen_sig('asdf', payload); + options.headers = {'x-hub-signature': gen_sig('asdf', payload)}; request(payload, function (res, data) { st.equal(data.err, 'Error: Cannot verify payload signature', 'correct server response'); @@ -68,7 +68,7 @@ test('pass valid JSON object and valid signature', function (t) { t.test('valid data but invalid branch ref', function (st) { var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs' }); - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { st.equal(data.err, 'Branches do not match', 'correct server response'); @@ -79,7 +79,7 @@ test('pass valid JSON object and valid signature', function (t) { t.test('valid data and valid branch ref', function (st) { var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { st.equal(data.msg, 'Build queued', 'correct server response'); @@ -94,8 +94,8 @@ test('pass custom branch name', function (t) { t.test('valid branch in path but invalid branch ref', function (st) { var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs' }); - options.path = '/dev'; - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); + options.query = {branch: 'dev'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { st.equal(data.err, 'Branches do not match', 'correct server response'); @@ -106,8 +106,8 @@ test('pass custom branch name', function (t) { t.test('valid branch in path and valid branch ref', function (st) { var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/dev' }); - options.path = '/dev'; - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); + options.query = {branch: 'dev'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { st.equal(data.msg, 'Build queued', 'correct server response'); @@ -116,10 +116,78 @@ test('pass custom branch name', function (t) { }); }); - t.test('trailing slash in path', function (st) { - var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/dev' }); - options.path = '/dev/'; - options.headers['x-hub-signature'] = gen_sig(config.github_secret, payload); +}); + +test('pass semver flag', function (t) { + + t.test('semver flag passed but invalid branch ref', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {semver: true}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + + request(payload, function (res, data) { + st.equal(data.err, 'Branches do not match', 'correct server response'); + st.equal(res.statusCode, 400, 'correct status code'); + st.end(); + }); + }); + + t.test('semver flag passed and valid branch ref', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/v1.2.3' }); + options.query = {semver: true}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + + request(payload, function (res, data) { + st.equal(data.msg, 'Build queued', 'correct server response'); + st.equal(res.statusCode, 202, 'correct status code'); + st.end(); + }); + }); + + t.test('semver flag and branch name passed and valid branch ref', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/v1.2.3' }); + options.query = {semver: true, branch: 'master'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + + request(payload, function (res, data) { + st.equal(data.msg, 'Build queued', 'correct server response'); + st.equal(res.statusCode, 202, 'correct status code'); + st.end(); + }); + }); + + t.test('semver flag and branch name passed and valid branch ref but not semver', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {semver: true, branch: 'master'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + + request(payload, function (res, data) { + st.equal(data.msg, 'Build queued', 'correct server response'); + st.equal(res.statusCode, 202, 'correct status code'); + st.end(); + }); + }); + + t.test('semver flag and branch name passed but invalid branch ref', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {semver: true, branch: 'dev'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + + request(payload, function (res, data) { + st.equal(data.err, 'Branches do not match', 'correct server response'); + st.equal(res.statusCode, 400, 'correct status code'); + st.end(); + }); + }); + +}); + +test('pass url_secret', function (t) { + + t.test('pass url_secret in query string but not in config', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {secret: 'password123'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; request(payload, function (res, data) { st.equal(data.msg, 'Build queued', 'correct server response'); @@ -128,4 +196,43 @@ test('pass custom branch name', function (t) { }); }); + t.test('pass same url_secret in query string as in config', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {secret: 'password123'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + config.url_secret = 'password123'; + + request(payload, function (res, data) { + st.equal(data.msg, 'Build queued', 'correct server response'); + st.equal(res.statusCode, 202, 'correct status code'); + st.end(); + }); + }); + + t.test('url_secret in config but not in query string', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + config.url_secret = 'password123'; + + request(payload, function (res, data) { + st.equal(data.err, 'Error: Cannot verify payload signature', 'correct server response'); + st.equal(res.statusCode, 403, 'correct status code'); + st.end(); + }); + }); + + t.test('different url_secret in query string than in config', function (st) { + var payload = JSON.stringify({ repository: { full_name: 'repo' }, ref: 'refs/heads/master' }); + options.query = {secret: 'bogus'}; + options.headers = {'x-hub-signature': gen_sig(config.github_secret, payload)}; + config.url_secret = 'password123'; + + request(payload, function (res, data) { + st.equal(data.err, 'Error: Cannot verify payload signature', 'correct server response'); + st.equal(res.statusCode, 403, 'correct status code'); + st.end(); + }); + }); + }); diff --git a/test/travis.js b/test/travis.js index 8af30a7..7aec7ee 100644 --- a/test/travis.js +++ b/test/travis.js @@ -123,7 +123,7 @@ test('pass custom branch name', function (t) { var payload = qs.stringify({ payload: JSON.stringify({ branch: 'master' }) }); options.headers['authorization'] = gen_sig(config.travis_token, 'repo'); options.headers['travis-repo-slug'] = 'repo'; - options.path = '/dev'; + options.query.branch = 'dev'; request(payload, function (res, data) { st.equal(data.err, 'Branches do not match', 'correct server response'); @@ -136,20 +136,7 @@ test('pass custom branch name', function (t) { var payload = qs.stringify({ payload: JSON.stringify({ branch: 'dev' }) }); options.headers['authorization'] = gen_sig(config.travis_token, 'repo'); options.headers['travis-repo-slug'] = 'repo'; - options.path = '/dev'; - - request(payload, function (res, data) { - st.equal(data.msg, 'Build queued', 'correct server response'); - st.equal(res.statusCode, 202, 'correct status code'); - st.end(); - }); - }); - - t.test('trailing slash in path', function (st) { - var payload = qs.stringify({ payload: JSON.stringify({ branch: 'dev' }) }); - options.headers['authorization'] = gen_sig(config.travis_token, 'repo'); - options.headers['travis-repo-slug'] = 'repo'; - options.path = '/dev/'; + options.query.branch = 'dev'; request(payload, function (res, data) { st.equal(data.msg, 'Build queued', 'correct server response');