forked from coreh/hookshot
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
246 lines (203 loc) · 6.64 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
'use strict';
var bodyParser = require('body-parser'),
normalize = require('path').normalize,
spawn = require('child_process').spawn,
crypto = require('crypto'),
express = require('express');
module.exports = GitHooked;
/**
* Create a express app with github webhook parsing and actions
*
* @param {String} This is the git reference being listened to
* @param {String|Function} This is the action which will be invoked when the reference is called. Can be either a function that will be executed or a string which will be called from a shell
* @api public
*/
function GitHooked(ref, action, options) {
// default ref to hook and action to reference if only single args sent
// This means that all webhooks will invoke the action
if (arguments.length === 1) {
action = ref;
ref = 'hook';
}
// Defaults options to empty object
options = options || {};
// Check if options is an associative array
if ( options.toString() !== '[object Object]' ) {
throw new Error('GitHooked: Options argument supplied, but type is not an Object');
}
// create express instance
var githooked = express();
// Setup optional middleware
setupMiddleware(githooked, options);
// default bodyparser.json limit to 1mb
options.json = options.json || { limit: '1mb' };
githooked.use(bodyParser.urlencoded({ extended: false, limit: options.json.limit || '1mb' }));
// Setup secret signature validation
if ( options.secret ) {
// Throw error if secret is not a string
if ( typeof options.secret !== 'string' ) {
throw new Error('GitHooked: secret provided but it is not a string');
}
options.json.verify = function(req, res, buf) {
signatureValidation(githooked, options, req, res, buf);
};
}
// json parsing middleware limit is increased to 1mb, otherwise large PRs/pushes will
// fail due to maiximum content-length exceeded
githooked.use(bodyParser.json(options.json));
// Bind Ref, Action, and Options to Express Server
githooked.ghRef = ref;
githooked.ghAction = action;
githooked.ghOptions = options;
// main POST handler
githooked.post('/', function (req, res ) {
var payload = req.body;
// Check if ping event, send 200 if so
if (req.headers['x-github-event'] === 'ping') {
res.sendStatus(200);
return;
}
// Check if Payload was sent
if (!payload) {
githooked.emit('error', 'no payload');
return res.sendStatus(400);
}
// Check if payload has a ref, this is required in order to run contexted scripts
if (!payload.ref || typeof payload.ref !== 'string') {
githooked.emit('error', 'invalid ref');
return res.sendStatus(400);
}
// Created hook event
if (payload.created) {
githooked.emit('create', payload);
}
// Deleted hook event
else if (payload.deleted) {
githooked.emit('delete', payload);
}
// Else, default to a push hook event
else {
githooked.emit('push', payload);
}
// Always emit a global 'hook' event so people can watch all requests
githooked.emit('hook', payload);
// Emit a ref type specfic event
githooked.emit(payload.ref, payload);
res.status(202);
res.send('Accepted\n');
});
if (typeof action === 'string') {
var shell = process.env.SHELL,
args = ['-c', action],
opts = { stdio: 'inherit' };
// Windows specfic env checks
if (shell && isCygwin()) {
shell = cygpath(shell);
} else if (isWin()) {
shell = process.env.ComSpec;
args = ['/s', '/c', '"' + action + '"'];
opts.windowsVerbatimArguments = true;
}
githooked.on(ref, function() {
// Send emit spawn event w/ instance
githooked.emit('spawn', spawn(shell, args, opts));
});
}
else if (typeof action === 'function') {
githooked.on(ref, action);
}
// Development Error middleware
if ( process.env.NODE_ENV === 'development' ) {
githooked.use(function(err, req, res, next) {
console.log(err.stack);
next(err);
});
}
// Default Error middlware
githooked.use(function(err, req, res, next) { // jshint ignore:line
githooked.emit('error', err);
if ( !res.headersSent ) {
res.status(500);
res.end(err.message);
}
});
return githooked;
}
function signatureValidation(githooked, options, req, res, buf) {
// Use crypto.timingSafeEqual when available to avoid timing attacks
// See https://codahale.com/a-lesson-in-timing-attacks/
var compareStrings = function(a, b) { return a === b };
var compareSecure = function(a, b) {
return a.length === b.length && crypto.timingSafeEqual(new Buffer(a), new Buffer(b));
}
var validateSecret = crypto.timingSafeEqual ? compareSecure : compareStrings;
var providedSignature = req.headers['x-hub-signature'];
// Throw an error if secret was provided but no X-Hub-Signature header present
if ( !providedSignature ) {
res.sendStatus(401);
githooked.emit('error', 'no provider signature');
throw new Error('no provider signature');
}
var ourSignature = 'sha1=' + crypto.createHmac('sha1', options.secret).update(buf.toString()).digest('hex');
// Validate Signatures
if ( ! validateSecret(providedSignature, ourSignature) ) {
res.sendStatus(401);
githooked.emit('error', 'signature validation failed');
throw new Error('signature validation failed');
}
}
/**
* Setup middleware if options configured properly
*
* @return {Undefined}
* @api private
*/
function setupMiddleware(githooked, options) {
// Optional middleware pass
if ( options.middleware ) {
// If middleware = function, convert to array
if ( typeof options.middleware === 'function' ) {
options.middleware = [options.middleware];
}
// Add middleware
options.middleware.forEach(function(fn) {
githooked.use(fn);
});
}
}
/**
* Returns `true` if node is currently running on Windows, `false` otherwise.
*
* @return {Boolean}
* @api private
*/
function isWin () {
return 'win32' === process.platform;
}
/**
* Returns `true` if node is currently running from within a "cygwin" environment.
* Returns `false` otherwise.
*
* @return {Boolean}
* @api private
*/
function isCygwin () {
// TODO: implement a more reliable check here...
return isWin() && /cygwin/i.test(process.env.HOME);
}
/**
* Convert a Unix-style Cygwin path (i.e. "/bin/bash") to a Windows-style path
* (i.e. "C:\cygwin\bin\bash").
*
* @param {String} path
* @return {String}
* @api private
*/
function cygpath (path) {
path = normalize(path);
if (path[0] === '\\') {
// TODO: implement better cygwin root detection...
path = 'C:\\cygwin' + path;
}
return path;
}