Skip to content

Commit

Permalink
Add secret support for 'GitHub Webhook'
Browse files Browse the repository at this point in the history
  • Loading branch information
SantiMA10 committed Sep 26, 2020
1 parent 17178f2 commit f3516e3
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 11 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ You can deploy this webhook in different ways

0. Get the source code
```
git clone https://github.com/streamdevs/webhook.git
git clone https://github.com/streamdevs/webhook.**git**
```
1. Change into the source code directory
```
Expand Down Expand Up @@ -102,6 +102,7 @@ We make use of the following environment variables:
| NOTIFY_ISSUES_ASSIGNED_TO | A comma-separated list of GitHub user names. Only issues assigned to these users will be notified or leave it empty to receive all notifications. | No | _empty array_ |
| IGNORE_PR_OPENED_BY | A comma-separated list of GitHub user names. Only PR not opened by these users will be notified or leave it empty to receive all notifications. | No | _empty array_ |
| NOTIFY_CHECK_RUNS_FOR | Comma-separated list of branches to notify Check Runs for. Leave empty to notify for any branch | No | _empty_ _array_ |
| GITHUB_SECRET | Allows you to set a secret in order to verify that the request are from GitHub | No | _empty_ |

### GitHub Configuration

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dev": "nodemon src/index.js"
},
"dependencies": {
"@hapi/boom": "^9.1.0",
"@hapi/hapi": "^19.1.1",
"@hapi/joi": "^17.1.1",
"axios": "^0.19.2",
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Config {
TWITCH_BOT_NAME?: string;
TWITCH_BOT_TOKEN?: string;
TWITCH_BOT_CHANNEL?: string;
GITHUB_SECRET?: string;
port: number | string;
NOTIFY_CHECK_RUNS_FOR: string[];
NOTIFY_ISSUES_ASSIGNED_TO: string[];
Expand Down Expand Up @@ -35,5 +36,6 @@ export const getConfig = (): Config => {
NOTIFY_CHECK_RUNS_FOR: process.env['NOTIFY_CHECK_RUNS_FOR']?.split(',') || [],
NOTIFY_ISSUES_ASSIGNED_TO: process.env['NOTIFY_ISSUES_ASSIGNED_TO']?.split(',') || [],
IGNORE_PR_OPENED_BY: process.env['IGNORE_PR_OPENED_BY']?.split(',') || [],
GITHUB_SECRET: process.env['GITHUB_SECRET'],
};
};
36 changes: 28 additions & 8 deletions src/routes/github/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { gitHubWebhookPayload } from '../../schemas/gitHubWebhookPayload';
import { gitHubWebhookHeaders } from '../../schemas/gitHubWebhookHeaders';
import { StreamLabs } from '../../services/StreamLabs';
import { TwitchChat } from '../../services/TwitchChat';
import { Boom, forbidden } from '@hapi/boom';
import crypto from 'crypto';
import { Request, ResponseObject, ResponseToolkit, ServerRoute } from '@hapi/hapi';
import { Config } from '../../config';

import { reactionBuild } from '../../reactions/github';
import { Request, ResponseObject, ResponseToolkit, ServerRoute } from '@hapi/hapi';
import { RepositoryWebhookPayload } from '../../schemas/github/repository-webhook-payload';
import { gitHubWebhookHeaders } from '../../schemas/gitHubWebhookHeaders';
import { gitHubWebhookPayload } from '../../schemas/gitHubWebhookPayload';
import { StreamLabs } from '../../services/StreamLabs';
import { TwitchChat } from '../../services/TwitchChat';

export const routes = (config: Config): ServerRoute[] => [
{
Expand All @@ -18,11 +19,30 @@ export const routes = (config: Config): ServerRoute[] => [
payload: gitHubWebhookPayload(),
},
},
handler: async (request: Request, h: ResponseToolkit): Promise<ResponseObject> => {
handler: async (request: Request, h: ResponseToolkit): Promise<ResponseObject | Boom> => {
const { payload, headers } = (request as unknown) as {
payload: RepositoryWebhookPayload;
headers: { 'x-github-event': string };
headers: { 'x-github-event': string; 'x-hub-signature': string };
};

if (config.GITHUB_SECRET) {
if (!headers['x-hub-signature']) {
console.error("missing 'x-hub-signature' header");
return forbidden();
}

const hmac = crypto.createHmac('sha1', config.GITHUB_SECRET);
const digest = Buffer.from(
'sha1=' + hmac.update(JSON.stringify(payload)).digest('hex'),
'utf8',
);
const checksum = Buffer.from(headers['x-hub-signature'], 'utf8');
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
console.error('unable to verify request signature');
return forbidden();
}
}

const event = headers['x-github-event'];

const streamlabs = new StreamLabs({ token: config.STREAMLABS_TOKEN || '' }, request);
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/gitHubWebhookHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { object, Schema, string } from '@hapi/joi';

export function gitHubWebhookHeaders(): Schema {
return object({ 'x-github-event': string().required() }).unknown();
return object({ 'x-github-event': string().required(), 'x-hub-signature': string() }).unknown();
}
79 changes: 79 additions & 0 deletions test/routes/github/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,83 @@ describe('POST /github', () => {
expect(statusCode).toBe(200);
expect(result).toEqual({ message: `Ignoring event: 'project'` });
});

describe("with 'GITHUB_SECRET' configured", () => {
it("rejects requests without 'X-Hub-Signature' header", async () => {
const subject = await initServer({ ...getConfig(), GITHUB_SECRET: 'patatas' });
const request = {
method: 'POST',
url: '/github',
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'project',
},
payload: {
hook: { events: ['created'] },
sender: {
login: 'user',
},
repository: {
full_name: 'org/repo',
},
},
};

const { statusCode } = await subject.inject(request);

expect(statusCode).toEqual(403);
});

it("rejects requests with invalid 'X-Hub-Signature' header", async () => {
const subject = await initServer({ ...getConfig(), GITHUB_SECRET: 'patatas' });
const request = {
method: 'POST',
url: '/github',
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'project',
'X-Hub-Signature': 'patatas',
},
payload: {
hook: { events: ['created'] },
sender: {
login: 'user',
},
repository: {
full_name: 'org/repo',
},
},
};

const { statusCode } = await subject.inject(request);

expect(statusCode).toEqual(403);
});

it("accept requests with valid 'X-Hub-Signature' header", async () => {
const subject = await initServer({ ...getConfig(), GITHUB_SECRET: 'patatas' });
const request = {
method: 'POST',
url: '/github',
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'project',
'X-Hub-Signature': 'sha1=7027fb0d07cb42f7c273aa2258f54f6626ca3f3c',
},
payload: {
hook: { events: ['created'] },
sender: {
login: 'user',
},
repository: {
full_name: 'org/repo',
},
},
};

const { statusCode } = await subject.inject(request);

expect(statusCode).toEqual(200);
});
});
});
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@
dependencies:
"@hapi/hoek" "9.x.x"

"@hapi/[email protected]", "@hapi/boom@^9.0.0":
"@hapi/[email protected]", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.0.tgz#0d9517657a56ff1e0b42d0aca9da1b37706fec56"
integrity sha512-4nZmpp4tXbm162LaZT45P7F7sgiem8dwAh2vHWT6XX24dozNjGMg6BvKCRvtCUcmcXqeMIUqWN8Rc5X8yKuROQ==
Expand Down

0 comments on commit f3516e3

Please sign in to comment.