Skip to content

Add secret support for 'GitHub Webhook' #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
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