This repository has been archived by the owner on May 17, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 20
/
index.js
355 lines (316 loc) · 12 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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/**
* Copyright 2016 vmarchaud. All rights reserved.
* Use of this source code is governed by a license that
* can be found in the LICENSE file.
*/
var http = require('http');
var crypto = require('crypto');
var pmx = require('pmx');
var pm2 = require('pm2');
var util = require('util');
var spawn = require('child_process').spawn;
var async = require('async');
var vizion = require('vizion');
var ipaddr = require('ipaddr.js');
/**
* Init pmx module
*/
pmx.initModule({}, function (err, conf) {
pm2.connect(function (err2) {
if (err || err2) {
console.error(err || err2);
return process.exit(1);
}
// init the worker only if we can connect to pm2
new Worker(conf).start();
});
});
/**
* Constructor of our worker
*
* @param {object} opts The options
* @returns {Worker} The instance of our worker
* @constructor
*/
var Worker = function (opts) {
if (!(this instanceof Worker)) {
return new Worker(opts);
}
this.opts = opts;
this.port = this.opts.port || 8888;
this.apps = opts.apps;
if (typeof (this.apps) !== 'object') {
this.apps = JSON.parse(this.apps);
}
this.server = http.createServer(this._handleHttp.bind(this));
return this;
};
/**
* Main function for http server
*
* @param req The Request
* @param res The Response
* @private
*/
Worker.prototype._handleHttp = function (req, res) {
var self = this;
// send instant answer since its useless to respond to the webhook
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('OK');
// do something only with post request
if (req.method !== 'POST') {
res.end();
return;
}
// get source ip
req.ip = req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : false) ||
(req.socket ? req.socket.remoteAddress : false) || ((req.connection && req.connection.socket)
? req.connection.socket.remoteAddress : false) || '';
if (req.ip.indexOf('::ffff:') !== -1) {
req.ip = req.ip.replace('::ffff:', '');
}
// get the whole body before processing
req.body = '';
req.on('data', function (data) {
req.body += data;
}).on('end', function () {
self.processRequest(req);
});
res.end();
};
/**
* Main function of the module
*
* @param req The Request of the call
*/
Worker.prototype.processRequest = function (req) {
var targetName = reqToAppName(req);
if (targetName.length === 0) return;
var targetApp = this.apps[targetName];
if (!targetApp) return;
var error = this.checkRequest(targetApp, req);
if (error) {
console.log(error);
return;
}
console.log('[%s] Received valid hook for app %s', new Date().toISOString(), targetName);
var execOptions = {
cwd: targetApp.cwd,
env: process.env,
shell: true
};
var phases = {
resolveCWD: function resolveCWD(cb) {
// if cwd is provided, we expect that it isnt a pm2 app
if (targetApp.cwd) return cb();
// try to get the cwd to execute it correctly
pm2.describe(targetName, function (err, apps) {
if (err || !apps || apps.length === 0) return cb(err || new Error('Application not found'));
// execute the actual command in the cwd of the application
targetApp.cwd = apps[0].pm_cwd ? apps[0].pm_cwd : apps[0].pm2_env.pm_cwd;
return cb();
});
},
pullTheApplication: function pullTheApplication(cb) {
vizion.update({
folder: targetApp.cwd
}, logCallback(cb, '[%s] Successfuly pulled application %s', new Date().toISOString(), targetName));
},
preHook: function preHook(cb) {
if (!targetApp.prehook) return cb();
spawnAsExec(targetApp.prehook, execOptions,
logCallback(cb, '[%s] Prehook command has been successfuly executed for app %s', new Date().toISOString(), targetName));
},
reloadApplication: function reloadApplication(cb) {
if (targetApp.nopm2) return cb();
pm2.gracefulReload(targetName,
logCallback(cb, '[%s] Successfuly reloaded application %s', new Date().toISOString(), targetName));
},
postHook: function postHook(cb) {
if (!targetApp.posthook) return cb();
// execute the actual command in the cwd of the application
spawnAsExec(targetApp.posthook, execOptions,
logCallback(cb, '[%s] Posthook command has been successfuly executed for app %s', new Date().toISOString(), targetName));
}
};
async.series(Object.keys(phases).map(function(k){ return phases[k]; }),
function (err, results) {
if (err) {
console.log('[%s] An error has occuring while processing app %s', new Date().toISOString(), targetName);
if (targetApp.errorhook) spawnAsExec(targetApp.errorhook, execOptions,
logCallback(() => {}, '[%s] Errorhook command has been successfuly executed for app %s', new Date().toISOString(), targetName));
console.error(err);
}
});
};
/**
* Checks if a request is valid for an app.
*
* @param targetApp The app which the request has to be valid
* @param req The request to analyze
* @returns {string|true} True if success or the string of the error if not.
*/
Worker.prototype.checkRequest = function checkRequest(targetApp, req) {
var targetName = reqToAppName(req);
switch (targetApp.service) {
case 'gitlab': {
if (!req.headers['x-gitlab-token']) {
return util.format('[%s] Received invalid request for app %s (no headers found)', new Date().toISOString(), targetName);
}
if (req.headers['x-gitlab-token'] !== targetApp.secret) {
return util.format('[%s] Received invalid request for app %s (not matching secret)', new Date().toISOString(), targetName);
}
break;
}
case 'jenkins': {
// ip must match the secret
if (req.ip.indexOf(targetApp.secret) < 0) {
return util.format('[%s] Received request from %s for app %s but ip configured was %s', new Date().toISOString(), req.ip, targetName, targetApp.secret);
}
var body = JSON.parse(req.body);
if (body.build.status !== 'SUCCESS') {
return util.format('[%s] Received valid hook but with failure build for app %s', new Date().toISOString(), targetName);
}
if (targetApp.branch && body.build.scm.branch.indexOf(targetApp.branch) < 0) {
return util.format('[%s] Received valid hook but with a branch %s than configured for app %s', new Date().toISOString(), body.build.scm.branch, targetName);
}
break;
}
case 'droneci': {
// Authorization header must match configured secret
if (!req.headers['Authorization']) {
return util.format('[%s] Received invalid request for app %s (no headers found)', new Date().toISOString(), targetName);
}
if (req.headers['Authorization'] !== targetApp.secret) {
return util.format('[%s] Received request from %s for app %s but incorrect secret', new Date().toISOString(), req.ip, targetName);
}
var data = JSON.parse(req.body);
if (data.build.status !== 'SUCCESS') {
return util.format('[%s] Received valid hook but with failure build for app %s', new Date().toISOString(), targetName);
}
if (targetApp.branch && data.build.branch.indexOf(targetApp.branch) < 0) {
return util.format('[%s] Received valid hook but with a branch %s than configured for app %s', new Date().toISOString(), data.build.branch, targetName);
}
break;
}
case 'bitbucket': {
var tmp = JSON.parse(req.body);
var ip = targetApp.secret || '104.192.143.0/24';
var configured = ipaddr.parseCIDR(ip);
var source = ipaddr.parse(req.ip);
if (req.ip === req.headers['x-forwarded-for']) {
return util.format('[%s] Received request from %s for app %s but ip %s cannot be trusted (coming from headers)', new Date().toISOString(), req.ip, targetName, ip);
}
if (!source.match(configured)) {
return util.format('[%s] Received request from %s for app %s but ip configured was %s', new Date().toISOString(), req.ip, targetName, ip);
}
if (!tmp.push) {
return util.format("[%s] Received valid hook but without 'push' data for app %s", new Date().toISOString(), targetName);
}
if (targetApp.branch && tmp.push.changes[0] && tmp.push.changes[0].new.name.indexOf(targetApp.branch) < 0) {
return util.format('[%s] Received valid hook but with a branch %s than configured for app %s', new Date().toISOString(), tmp.push.changes[0].new.name, targetName);
}
break;
}
case 'gogs': {
if (!req.headers['x-gogs-event'] || !req.headers['x-gogs-signature']) {
return util.format('[%s] Received invalid request for app %s (no headers found)', new Date().toISOString(), targetName);
}
// compute hash of body with secret, github should send this to verify authenticity
var temp = crypto.createHmac('sha256', targetApp.secret);
temp.update(req.body, 'utf-8');
var hash = temp.digest('hex');
if (hash !== req.headers['x-gogs-signature']) {
return util.format('[%s] Received invalid request for app %s', new Date().toISOString(), targetName);
}
var body = JSON.parse(req.body)
if (targetApp.branch) {
var regex = new RegExp('/refs/heads/' + targetApp.branch)
if (!regex.test(body.ref)) {
return util.format('[%s] Received valid hook but with a branch %s than configured for app %s', new Date().toISOString(), body.ref, targetName);
}
}
break;
}
case 'github' :
default: {
if (!req.headers['x-github-event'] || !req.headers['x-hub-signature']) {
return util.format('[%s] Received invalid request for app %s (no headers found)', new Date().toISOString(), targetName);
}
// compute hash of body with secret, github should send this to verify authenticity
var temp = crypto.createHmac('sha1', targetApp.secret);
temp.update(req.body, 'utf-8');
var hash = temp.digest('hex');
if ('sha1=' + hash !== req.headers['x-hub-signature']) {
return util.format('[%s] Received invalid request for app %s', new Date().toISOString(), targetName);
}
var body = JSON.parse(req.body)
if (targetApp.branch) {
var regex = new RegExp('/refs/heads/' + targetApp.branch)
if (!regex.test(body.ref)) {
return util.format('[%s] Received valid hook but with a branch %s than configured for app %s', new Date().toISOString(), body.ref, targetName);
}
}
break;
}
}
return false;
};
/**
* Lets start our server
*/
Worker.prototype.start = function () {
var self = this;
this.server.listen(this.opts.port, function () {
console.log('Server is ready and listen on port %s', self.port);
});
};
/**
* Executes the callback, but in case of success shows a message.
* Also accepts extra arguments to pass to console.log.
*
* Example:
* logCallback(next, '% worked perfect', appName)
*
* @param {Function} cb The callback to be called
* @param {string} message The message to show if success
* @returns {Function} The callback wrapped
*/
function logCallback(cb, message) {
var wrappedArgs = Array.prototype.slice.call(arguments);
return function (err, data) {
if (err) return cb(err);
wrappedArgs.shift();
console.log.apply(console, wrappedArgs);
cb();
}
}
/**
* Given a request, returns the name of the target App.
*
* Example:
* Call to 34.23.34.54:3000/api-2
* Will return 'api-2'
*
* @param req The request to be analysed
* @returns {string|null} The name of the app, or null if not found.
*/
function reqToAppName(req) {
var targetName = null;
try {
targetName = req.url.split('/').pop();
} catch (e) {}
return targetName || null;
}
/**
* Wraps the node spawn function to work as exec (line, options, callback).
* This avoid the maxBuffer issue, as no buffer will be stored.
*
* @param {string} command The line to execute
* @param {object} options The options to pass to spawn
* @param {function} cb The callback, called with error as first argument
*/
function spawnAsExec(command, options, cb) {
var child = spawn('eval', [command], options);
child.on('close', cb);
}