Skip to content

Commit

Permalink
Merge pull request #10 from totem/develop
Browse files Browse the repository at this point in the history
0.5.0 : Support for Github hook signature
  • Loading branch information
sukrit007 committed Feb 17, 2015
2 parents 48ef63e + d59dd2c commit d707a55
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 40 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@

## 0.4.0

+ OSS Implementation of image factory
+ OSS Implementation of image factory

## 0.5.0

+ Support for Github hook signature
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ might be removed in future releases.
| ETCD_HOST | Etcd server host. | 172.17.42.1 |
| ETCD_PORT | Etcd server port. | 4001 |
| ETCD_TOTEM_BASE | Base path for totem configurations | /totem |
| HOOK_POST_URL | URL to be used for post build notification | |
| HOOK_SECRET | Secret used for github post hook and post build notification |changeit|


## Prerequisites (Development)
Expand Down
49 changes: 29 additions & 20 deletions lib/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var winston = require('winston'),
Datastore = require('./datastore'),
JobRunner = require('./job'),
corsResponse = require('./restify/cors'),
usingSignedRequest = require('./restify/authorize-signature'),
events = require('events'),
util = require('util'),
zSchema = require('z-schema'),
Expand Down Expand Up @@ -133,22 +134,30 @@ Factory.prototype.initApi = function initApi() {
}
});

this.useDefaults = function() {
return [
restify.acceptParser(this.server.acceptable),
restify.queryParser(),
restify.bodyParser(),
corsResponse()
];
};

// Ensures the server only responds to types we can process
this.server.use(restify.acceptParser(this.server.acceptable));

// Parses URI query params
this.server.use(restify.queryParser());

// Parsed body based on content-type
this.server.use(restify.bodyParser());

// Enables CORS support on responses
this.server.use(corsResponse());

this.useSignedGithubRequest = function() {
var bodyParser = restify.bodyParser();
return [
restify.acceptParser(this.server.acceptable),
restify.queryParser(),
bodyParser[0], // First element contains middleware for parsing raw text,
usingSignedRequest(_this.config.hook.secret, 'X-Hub-Signature'),
// Second Elevement contains middleware for parsing JSON
bodyParser[1],
corsResponse()
];
};

// Server static schemas
this.server.get('_schema/:name', function (req, res, next) {
this.server.get('_schema/:name', this.useDefaults(), function (req, res, next) {
var schemaPath = path.resolve(SCHEMA_DIR, req.params.name + '.json');
try {
var schema = fs.createReadStream(schemaPath);
Expand All @@ -160,18 +169,18 @@ Factory.prototype.initApi = function initApi() {
}
});

this.server.get(/\/_jsonary\/?.*/, restify.serveStatic({ directory: './static' }));
this.server.get(/\/_jsonary\/?.*/, this.useDefaults(), restify.serveStatic({ directory: './static' }));

// GET Root
this.server.get('/', function (req, res, next) {
this.server.get('/', this.useDefaults(), function (req, res, next) {
res.header('Link', '</_schema/ROOT>; rel="describedBy"');
res.send({});
return next();
});

// GET /job
// Get a list of all jobs
this.server.get('/job', function (req, res, next) {
this.server.get('/job', this.useDefaults(), function (req, res, next) {
var jobs = _this.jobs.get();
async.map(
Object.keys(jobs),
Expand All @@ -190,7 +199,7 @@ Factory.prototype.initApi = function initApi() {

// GET /job/:id
// Get the job specified by *id*
this.server.get('/job/:id', function (req, res, next) {
this.server.get('/job/:id', this.useDefaults(), function (req, res, next) {
var job = _this.jobs.get(req.params.id);
if (job) {
res.header('Content-Type', 'application/vnd.sh.melt.cdp.if.job.v1+json');
Expand All @@ -204,7 +213,7 @@ Factory.prototype.initApi = function initApi() {

// GET /job/:id/log
// Get the build log for the job specified by *id*
this.server.get('/job/:id/log', function (req, res, next) {
this.server.get('/job/:id/log', this.useDefaults(), function (req, res, next) {
var job = _this.jobs.get(req.params.id);

if (job) {
Expand All @@ -223,7 +232,7 @@ Factory.prototype.initApi = function initApi() {

// POST /job
// Create a new job
this.server.post('/job', function (req, res, next) {
this.server.post('/job', this.useDefaults(), function (req, res, next) {

winston.debug('POST /job: ' + util.inspect(req.body));

Expand Down Expand Up @@ -257,7 +266,7 @@ Factory.prototype.initApi = function initApi() {

// Github POST hook
// Create a new job
this.server.post('/hooks/github', function (req, res, next) {
this.server.post('/hooks/github', this.useSignedGithubRequest(), function (req, res, next) {

winston.debug('POST /hooks/github: ' + util.inspect(req.body));

Expand Down
42 changes: 42 additions & 0 deletions lib/restify/authorize-signature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*jshint maxcomplexity:15 */

/*!
* Docker Image Factory - Authenticates request using request signature.
* Signature is calculated using SHA HMAC (using hexdigest) of the payload
* using pre-configured secret
*
*/

'use strict';

var crypto = require('crypto'),
restify = require('restify');

module.exports = usingSignedRequest;

/**
* Returns middleware that authorizes the request payload using signature
* specified in the header.
*
* @param secret Secret to be used for computing SHA HMAC of the payload.
* @param header Header name of the request which contains signature.
* @returns {authorize}
*/
function usingSignedRequest(secret, header) {
header = header || 'X-Hook-Signature';

return function authorize(req, res, next) {
var hmac = crypto.createHmac('sha1', secret);
hmac.update(req.body);
var calculatedSignature = 'sha1=' + hmac.digest('hex');
var actualSignature = req.header(header);
if (actualSignature !== calculatedSignature) {
return (next(
new restify.errors.InvalidCredentialsError(
'Mismatch in computed signature and the passed signature of the request payload.')));
} else {
return (next());
}
};

}
2 changes: 1 addition & 1 deletion npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "image-factory",
"version": "0.4.0",
"version": "0.5.0",
"description": "Docker Image Factory",
"keywords": [
"docker",
Expand Down
68 changes: 51 additions & 17 deletions test/integration/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,29 @@ var MINIMUM_BUILD_REQUEST = {
repo: "fake-repo"
};

var GIT_HOOK_BUILD_REQUEST = {
ref: "fake-owner/fake-repo/fake-branch",
after: "1234",
repository: {
name: "fake-repo",
owner: {
name: "fake-owner"
}
},
deleted: false
};
var GIT_HOOK_PUSH_REQUEST_STR = '{ \
"ref": "fake-owner/fake-repo/fake-branch", \
"after": "1234", \
"repository": { \
"name": "fake-repo", \
"owner": { \
"name": "fake-owner" \
} \
}, \
"deleted": false \
}';

var GIT_HOOK_DELETE_REQUEST_STR = '{ \
"ref": "fake-owner/fake-repo/fake-branch", \
"after": "1234", \
"repository": { \
"name": "fake-repo", \
"owner": { \
"name": "fake-owner" \
} \
}, \
"deleted": true \
}';

var FULL_BUILD_REQUEST = {
owner: "fake-owner",
Expand Down Expand Up @@ -149,8 +161,11 @@ describe('Image Factory - REST API', function () {
request.post(
BASE_URL + '/hooks/github',
{
body: JSON.stringify(GIT_HOOK_BUILD_REQUEST),
headers: { 'Content-Type': 'application/json'}
body: GIT_HOOK_PUSH_REQUEST_STR,
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha1=75486e24e49d01aa87cff3951314993ce649cc8a'
}
},
function (err, response, body) {
expect(err).to.not.exist;
Expand All @@ -167,13 +182,14 @@ describe('Image Factory - REST API', function () {
});

it('should return No Content when POST /hooks/github with deleted flag as true', function (done) {
var payload = _.clone(GIT_HOOK_BUILD_REQUEST);
payload.deleted = true;
request.post(
BASE_URL + '/hooks/github',
{
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json'}
body: GIT_HOOK_DELETE_REQUEST_STR,
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha1=b1b6f5e7379d550cfaaf597d90f26d832d999f3f'
}
},
function (err, response, body) {
expect(err).to.not.exist;
Expand All @@ -183,6 +199,24 @@ describe('Image Factory - REST API', function () {
);
});

it('should return Unauthorized response when invalid signature is passed', function (done) {
request.post(
BASE_URL + '/hooks/github',
{
body: GIT_HOOK_PUSH_REQUEST_STR,
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha1=invalid'
}
},
function (err, response, body) {
expect(err).to.not.exist;
expect(response.statusCode).to.equal(401);
done();
}
);
});

});

describe('GET /job/{id}', function () {
Expand Down

0 comments on commit d707a55

Please sign in to comment.