abstruse
Version:
Abstruse CI
480 lines (442 loc) • 15.8 kB
text/typescript
import * as express from 'express';
import * as crypto from 'crypto';
import { getConfigAsync, saveConfigAsync } from './setup';
import {
pingGitHubRepository,
pingBitbucketRepository,
synchronizeBitbucketPullRequest,
pingGitLabRepository,
synchronizeGitLabPullRequest,
pingGogsRepository,
createGitHubPullRequest,
createGogsPullRequest,
synchronizeGitHubPullRequest,
synchronizeGogsPullRequest
} from './db/repository';
import { startBuild } from './process-manager';
import { writeJsonFile } from './fs';
export let webhooks = express.Router();
webhooks.post('/github', (req: express.Request, res: express.Response) => {
getConfigAsync()
.then(config => {
let headers = req.headers;
let payload = req.body;
let sig = headers['x-hub-signature'] as string;
let ev = headers['x-github-event'] as string;
let id = headers['x-github-delivery'] as string;
if (!sig) {
return res.status(400).json({ error: 'No X-Hub-Signature found on request' });
}
if (!ev) {
return res.status(400).json({ error: 'No X-Github-Event found on request' });
}
if (!id) {
return res.status(400).json({ error: 'No X-Github-Delivery found on request' });
}
if (!verifyGithubWebhook(sig, payload, config.secret)) {
return res.status(400).json({ error: 'X-Hub-Signature does not match blob signature' });
}
if (req.secure) {
config.url = 'https://' + req.headers.host;
} else {
config.url = 'http://' + req.headers.host;
}
switch (ev) {
case 'ping':
saveConfigAsync(config)
.then(() => pingGitHubRepository(payload))
.then(repo => res.status(200).json({ msg: 'ok' }))
.catch(err => res.status(400).json(err));
break;
case 'push':
saveConfigAsync(config)
.then(() => pingGitHubRepository(payload))
.then(repo => {
let buildData = {
data: payload,
start_time: new Date(),
repositories_id: repo.id
};
return startBuild(buildData);
})
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'pull_request':
switch (payload.action) {
case 'opened':
saveConfigAsync(config)
.then(() => createGitHubPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'closed':
// should kill all jobs related to this PR?
res.status(200).json({ msg: 'ok' });
break;
case 'reopened':
saveConfigAsync(config)
.then(() => synchronizeGitHubPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'assigned':
res.status(200).json({ msg: 'ok' });
break;
case 'unassigned':
res.status(200).json({ msg: 'ok' });
break;
case 'review_requested':
res.status(200).json({ msg: 'ok' });
break;
case 'review_request_removed':
res.status(200).json({ msg: 'ok' });
break;
case 'labeled':
res.status(200).json({ msg: 'ok' });
break;
case 'unlabeled':
res.status(200).json({ msg: 'ok' });
break;
case 'edited':
res.status(200).json({ msg: 'ok' });
break;
case 'synchronize':
saveConfigAsync(config)
.then(() => synchronizeGitHubPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
}
break;
default:
res.status(200).json({ msg: 'ok' });
break;
}
});
});
webhooks.post('/bitbucket', (req: express.Request, res: express.Response) => {
getConfigAsync()
.then(config => {
let headers = req.headers;
let payload = req.body;
let sig = headers['x-request-uuid'] as string;
let ev = headers['x-event-key'] as string;
let id = headers['x-hook-uuid'] as string;
if (!sig) {
return res.status(400).json({ error: 'No X-Request-UUID found on request' });
}
if (!ev) {
return res.status(400).json({ error: 'No X-Event-Key found on request' });
}
if (!id) {
return res.status(400).json({ error: 'No X-Hook-UUID found on request' });
}
if (req.secure) {
config.url = 'https://' + req.headers.host;
} else {
config.url = 'http://' + req.headers.host;
}
switch (ev) {
case 'repo:push':
saveConfigAsync(config)
.then(() => pingBitbucketRepository(payload))
.then(repo => {
let buildData = {
data: payload,
start_time: new Date(),
repositories_id: repo.id
};
return startBuild(buildData);
})
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'repo:commit_status_created':
res.status(200).json({ msg: 'ok' });
break;
case 'repo:commit_status_updated':
res.status(200).json({ msg: 'ok' });
break;
case 'repo:commit_comment_created':
res.status(200).json({ msg: 'ok' });
break;
case 'repo:fork':
res.status(200).json({ msg: 'ok' });
break;
case 'issue:comment_created':
res.status(200).json({ msg: 'ok' });
break;
case 'issue:created':
res.status(200).json({ msg: 'ok' });
break;
case 'issue:updated':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:unapproved':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:approved':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:comment_created':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:comment_updated':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:comment_deleted':
res.status(200).json({ msg: 'ok' });
break;
case 'repo:transfer':
res.status(200).json({ msg: 'ok' });
break;
case 'repo:updated':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:created':
case 'pullrequest:updated':
saveConfigAsync(config)
.then(() => synchronizeBitbucketPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'pullrequest:fulfilled':
res.status(200).json({ msg: 'ok' });
break;
case 'pullrequest:rejected':
res.status(200).json({ msg: 'ok' });
break;
default:
break;
}
});
});
webhooks.post('/gitlab', (req: express.Request, res: express.Response) => {
getConfigAsync()
.then(config => {
let headers = req.headers;
let payload = req.body;
let ev = headers['x-gitlab-event'] as string;
let sig = headers['x-gitlab-token'] as string;
if (!sig) {
return res.status(400).json({ error: 'No X-GitLab-Token found on request' });
}
if (!ev) {
return res.status(400).json({ error: 'No X-GitLab-Event found on request' });
}
if (sig !== config.secret) {
return res.status(400).json({ error: 'X-GitLab-Token does not match' });
}
if (req.secure) {
config.url = 'https://' + req.headers.host;
} else {
config.url = 'http://' + req.headers.host;
}
switch (ev) {
case 'Push Hook':
saveConfigAsync(config)
.then(() => pingGitLabRepository(payload))
.then(repo => {
let buildData = {
data: payload,
start_time: new Date(),
repositories_id: repo.id
};
return startBuild(buildData);
})
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'Wiki Page Hook':
res.status(200).json({ msg: 'ok' });
break;
case 'Build Hook':
res.status(200).json({ msg: 'ok' });
break;
case 'Note Hook':
res.status(200).json({ msg: 'ok' });
break;
case 'Issue Hook':
res.status(200).json({ msg: 'ok' });
break;
case 'Merge Request Hook':
saveConfigAsync(config)
.then(() => synchronizeGitLabPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'Tag Push Hook':
res.status(200).json({ msg: 'ok' });
break;
case 'Pipeline Hook':
res.status(200).json({ msg: 'ok' });
break;
default:
break;
}
});
});
webhooks.post('/gogs', (req: express.Request, res: express.Response) => {
getConfigAsync()
.then(config => {
let headers = req.headers;
let payload = req.body;
let ev = headers['x-gogs-event'] as string;
let sig = headers['x-gogs-signature'] as string;
let id = headers['x-gogs-delivery'] as string;
if (!sig) {
return res.status(400).json({ error: 'No X-Gogs-Signature found on request' });
}
if (!ev) {
return res.status(400).json({ error: 'No X-Gogs-Event found on request' });
}
if (!id) {
return res.status(400).json({ error: 'No X-Gogs-Delivery found on request' });
}
if (!verifyGogsWebhook(sig, payload, config.secret)) {
return res.status(400).json({ error: 'X-Gogs-Signature does not match blob signature' });
}
if (req.secure) {
config.url = 'https://' + req.headers.host;
} else {
config.url = 'http://' + req.headers.host;
}
switch (ev) {
case 'push':
saveConfigAsync(config)
.then(() => pingGogsRepository(payload))
.then(repo => {
let buildData = {
data: payload,
start_time: new Date(),
repositories_id: repo.id
};
return startBuild(buildData);
})
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'create':
res.status(200).json({ msg: 'ok' });
break;
case 'delete':
res.status(200).json({ msg: 'ok' });
break;
case 'fork':
res.status(200).json({ msg: 'ok' });
break;
case 'issues':
res.status(200).json({ msg: 'ok' });
break;
case 'issue_comment':
res.status(200).json({ msg: 'ok' });
break;
case 'release':
res.status(200).json({ msg: 'ok' });
break;
case 'pull_request':
switch (payload.action) {
case 'opened':
saveConfigAsync(config)
.then(() => createGogsPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'closed':
res.status(200).json({ msg: 'ok' });
break;
case 'reopened':
saveConfigAsync(config)
.then(() => synchronizeGogsPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
case 'label_updated':
res.status(200).json({ msg: 'ok' });
break;
case 'milestoned':
res.status(200).json({ msg: 'ok' });
break;
case 'assigned':
res.status(200).json({ msg: 'ok' });
break;
case 'unassigned':
res.status(200).json({ msg: 'ok' });
break;
case 'demilestoned':
res.status(200).json({ msg: 'ok' });
break;
case 'label_cleared':
res.status(200).json({ msg: 'ok' });
break;
case 'edited':
res.status(200).json({ msg: 'ok' });
break;
case 'synchronized':
saveConfigAsync(config)
.then(() => synchronizeGogsPullRequest(payload))
.then(build => startBuild(build))
.then(buildData => res.status(200).json({ msg: 'ok', data: buildData }))
.catch(err => {
console.error(err);
res.status(400).json({ error: err });
});
break;
default:
break;
}
break;
default:
break;
}
});
});
function verifyGithubWebhook(signature: string, payload: any, secret: string): boolean {
let sig = `sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(payload)).digest('hex')}`;
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(sig));
}
function verifyGogsWebhook(signature: string, payload: any, secret: string): boolean {
let sig = `${crypto.createHmac('sha256', secret).update(JSON.stringify(payload, null, 2)).digest('hex')}`;
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(sig));
}