-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathwebhook.js
More file actions
138 lines (114 loc) · 4.22 KB
/
webhook.js
File metadata and controls
138 lines (114 loc) · 4.22 KB
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
#!/usr/bin/env node
// crypto is expected to be installed globally
const { execSync } = require('child_process');
const { createHmac, timingSafeEqual } = require('crypto');
const http = require('http');
const pm2config = require('./ecosystem.config.js');
const secrets = require('./ofl-secrets.json');
const pm2AppConfig = pm2config.apps.find((app) => app.name === 'ofl');
const deploymentConfig = {
env: pm2AppConfig.env,
action: '',
webhookPort: 40_010,
webhookPath: '/',
webhookSecret: secrets.OFL_WEBHOOK_SECRET,
};
startServer()
.then(() => console.log('Exited'))
.catch((error) => console.error('Exited with error', error));
/**
* @returns {Promise} Promise that resolves/rejects when the server process terminates.
*/
function startServer() {
const port = deploymentConfig.webhookPort;
console.log(`Starting webhook listener on port ${port}`);
return new Promise((resolve, reject) => {
http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.write('Received');
response.end();
if (request.method !== 'POST') {
return;
}
const bodyChunks = [];
let totalSize = 0;
request.on('data', (data) => {
bodyChunks.push(data);
totalSize += data.length;
if (totalSize > 1e6) { // 1MB limit
request.destroy(new Error('Request body too large'));
}
}).on('end', () => {
const body = Buffer.concat(bodyChunks);
processRequest(request.url, body, request.headers);
});
}).on('close', resolve).on('error', reject).listen(port);
});
}
/**
* Handle a received request from the server and check if it is valid. If so,
* call {@link redeploy} to update the corresponding app.
*
* @param {string} url The absolute path the request was received at.
* @param {Buffer} body The raw body from GitHub.
* @param {Record<string, string>} headers Headers of the request.
*/
function processRequest(url, body, headers) {
console.log(`Received webhook request at ${url}`);
if (deploymentConfig.webhookPath !== url) {
return;
}
const hmac = createHmac('sha256', deploymentConfig.webhookSecret);
hmac.update(body);
const xub = 'X-Hub-Signature-256';
const received = Buffer.from(headers[xub] || headers[xub.toLowerCase()] || '');
const digest = hmac.digest('hex');
const expected = Buffer.from(`sha256=${digest}`);
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
const expectedString = expected.toString('utf-8');
const receivedString = received.toString('utf-8');
console.error(`Wrong secret: Expected '${expectedString}', received '${receivedString}'`);
return;
}
console.info('Secret test passed');
const eventName = headers['X-GitHub-Event'] || headers['x-github-event'];
if (eventName !== 'push') {
console.log(`Wrong event name: Expected 'push', received '${eventName}'`);
return;
}
const json = JSON.parse(body.toString('utf-8'));
if (json.ref !== 'refs/heads/master') {
console.log(`Wrong branch: Expected 'refs/heads/master', received '${json.ref}'`);
return;
}
console.log(`Redeploy because of commit ${json.head_commit.id}: '${json.head_commit.message}'`);
redeploy(json);
}
/**
* Calls redeploy bash script and notify admin via email if script fails.
* @param {object} webhookPayload The data delivered by GitHub via the webhook.
*/
function redeploy(webhookPayload) {
try {
execSync('./redeploy.sh', {
cwd: '/home/flo',
env: { ...process.env, ...deploymentConfig.env },
encoding: 'utf-8',
stdio: 'pipe',
});
console.log('Successfully deployed.');
}
catch (error) {
console.log(`Redeploy process failed with exit code ${error.status}.`);
console.log('Notify admin via email about failed deployment...');
const subject = 'OFL Deployment failed';
let body = new Date().toISOString();
body += `\n\nsubprocess status: ${error.status}, signal: ${error.signal}`;
body += `\n\nsubprocess stdout:\n${error.stdout}`;
body += `\n\nsubprocess stderr:\n${error.stderr}`;
body += `\n\nwebhook payload: ${JSON.stringify(webhookPayload, null, 2)}`;
execSync(`mail -s "${subject}" root`, {
input: body,
});
}
}