2017-05-31 18:57:31 +03:00
|
|
|
//
|
2019-10-03 04:41:16 +03:00
|
|
|
// Copyright (c) Microsoft.
|
2017-05-31 18:57:31 +03:00
|
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
|
|
//
|
|
|
|
|
|
|
|
/*eslint no-console: ["error", { allow: ["warn", "log", "dir"] }] */
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
2019-04-06 00:45:34 +03:00
|
|
|
import _= require('lodash');
|
|
|
|
import async = require('async');
|
2019-10-03 04:41:16 +03:00
|
|
|
import { Operations } from '../business/operations';
|
|
|
|
import { Organization } from '../business/organization';
|
2017-05-31 18:57:31 +03:00
|
|
|
const crypto = require('crypto');
|
|
|
|
const secureCompare = require('secure-compare');
|
|
|
|
|
2019-10-03 04:41:16 +03:00
|
|
|
import Tasks from './tasks';
|
2017-05-31 18:57:31 +03:00
|
|
|
|
2019-04-06 00:45:34 +03:00
|
|
|
interface IValidationError extends Error {
|
|
|
|
statusCode?: number;
|
|
|
|
computedHash?: string;
|
|
|
|
}
|
|
|
|
|
2019-10-03 04:41:16 +03:00
|
|
|
export abstract class WebhookProcessor {
|
|
|
|
abstract filter(data: any): boolean;
|
|
|
|
abstract run(operations: Operations, organization: Organization, data: any): Promise<boolean>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface IOrganizationWebhookEvent {
|
|
|
|
body: any;
|
|
|
|
rawBody?: any;
|
|
|
|
properties: IGitHubWebhookProperties;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface IGitHubWebhookProperties {
|
|
|
|
delivery: string;
|
|
|
|
signature: string;
|
|
|
|
event: string;
|
|
|
|
started: string; // Date UTC string
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface IProcessOrganizationWebhookOptions {
|
|
|
|
operations: Operations;
|
|
|
|
organization: Organization;
|
|
|
|
event: IOrganizationWebhookEvent;
|
|
|
|
acknowledgeValidEvent?: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default async function ProcessOrganizationWebhook(options: IProcessOrganizationWebhookOptions): Promise<any> {
|
2017-05-31 18:57:31 +03:00
|
|
|
const operations = options.operations;
|
|
|
|
if (!operations) {
|
2019-10-03 04:41:16 +03:00
|
|
|
throw new Error('No operations instance provided');
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
const organization = options.organization;
|
|
|
|
const event = options.event;
|
|
|
|
if (!organization || !organization.name) {
|
2019-10-03 04:41:16 +03:00
|
|
|
throw new Error('Missing organization instance');
|
|
|
|
}
|
|
|
|
if (!organization.active) {
|
|
|
|
console.log(`inactive or unadopted organization ${organization.name}`);
|
|
|
|
if (options.acknowledgeValidEvent) {
|
|
|
|
options.acknowledgeValidEvent();
|
|
|
|
}
|
|
|
|
return;
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
if (!event) {
|
2019-10-03 04:41:16 +03:00
|
|
|
throw new Error('Missing event');
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
if (!event.body) {
|
2019-10-03 04:41:16 +03:00
|
|
|
throw new Error('Missing event body');
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
const body = event.body;
|
|
|
|
const rawBody = event.rawBody || JSON.stringify(body);
|
|
|
|
const properties = event.properties;
|
|
|
|
if (!properties || !properties.delivery || !properties.signature || !properties.event) {
|
2019-10-03 04:41:16 +03:00
|
|
|
if (options.acknowledgeValidEvent) {
|
|
|
|
options.acknowledgeValidEvent();
|
|
|
|
}
|
|
|
|
throw new Error('Missing event properties - delivery, signature, and/or event');
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
2019-10-03 04:41:16 +03:00
|
|
|
// try {
|
|
|
|
// await verifySignatures(properties.signature, organization.webhookSharedSecrets, rawBody);
|
|
|
|
// } catch (validationError) {
|
2019-04-06 00:45:34 +03:00
|
|
|
// NO LONGER VALIDATING SIG
|
|
|
|
// if (validationError) {
|
|
|
|
// if (operations && operations.insights) {
|
|
|
|
// const possibleOrganization = body && body.organization ? body.organization.login : 'unknown-org';
|
|
|
|
// console.warn(`incorrect hook signature - ${possibleOrganization} organization`);
|
|
|
|
// operations.insights.trackMetric({ name: 'WebhookIncorrectSecrets', value: 1 });
|
|
|
|
// operations.insights.trackEvent({
|
|
|
|
// name: 'WebhookIncorrectSecret',
|
|
|
|
// properties: {
|
|
|
|
// org: possibleOrganization,
|
|
|
|
// delivery: properties.delivery,
|
|
|
|
// event: properties.event,
|
|
|
|
// signature: properties.signature,
|
|
|
|
// approximateTime: properties.started.toISOString(),
|
|
|
|
// computedHash: validationError.computedHash,
|
|
|
|
// },
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
// return callback(validationError);
|
|
|
|
// }
|
2019-10-03 04:41:16 +03:00
|
|
|
//}
|
2019-04-06 00:45:34 +03:00
|
|
|
|
2019-10-03 04:41:16 +03:00
|
|
|
// In a bus scenario, if a short timeout window is used for queue
|
|
|
|
// visibility, a client may want to acknowledge this being a valid
|
|
|
|
// event at this time. After this point however there is no
|
|
|
|
// guarantee of successful execution.
|
|
|
|
if (options.acknowledgeValidEvent) {
|
|
|
|
options.acknowledgeValidEvent();
|
|
|
|
}
|
|
|
|
let interestingEvents = 0;
|
|
|
|
const work = Tasks.filter(task => task.filter(event));
|
|
|
|
if (work.length > 0) {
|
|
|
|
++interestingEvents;
|
|
|
|
console.log(`[* interesting event found: ${event.properties.event} (${work.length} interested tasks)]`);
|
|
|
|
} else {
|
|
|
|
console.log(`[skipping event: ${event.properties.event}]`);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let processor of work) {
|
|
|
|
try {
|
|
|
|
await processor.run(operations, organization, event);
|
|
|
|
} catch (processInitializationError) {
|
|
|
|
console.log('Processor ran into an error with an event:');
|
|
|
|
console.dir(processInitializationError);
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
2019-10-03 04:41:16 +03:00
|
|
|
}
|
|
|
|
return interestingEvents;
|
|
|
|
}
|
2017-05-31 18:57:31 +03:00
|
|
|
|
2019-10-03 04:41:16 +03:00
|
|
|
async function verifySignatures(signature, hookSecrets: string[], rawBody): Promise<void> {
|
2017-05-31 18:57:31 +03:00
|
|
|
// To ease local development and simple scenarios, if no shared secrets are
|
|
|
|
// configured, they are not required.
|
|
|
|
if (!hookSecrets || !hookSecrets.length) {
|
2019-10-03 04:41:16 +03:00
|
|
|
return;
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
if (!signature) {
|
2019-10-03 04:41:16 +03:00
|
|
|
throw new Error('No event signature was provided');
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
const computedSignatures = [];
|
|
|
|
for (let i = 0; i < hookSecrets.length; i++) {
|
|
|
|
const sharedSecret = hookSecrets[i];
|
|
|
|
const sha1 = crypto.createHmac('sha1', sharedSecret);
|
|
|
|
sha1.update(rawBody, 'utf8');
|
|
|
|
const computedHash = 'sha1=' + sha1.digest('hex');
|
|
|
|
if (secureCompare(computedHash, signature)) {
|
2019-10-03 04:41:16 +03:00
|
|
|
return;
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|
|
|
|
computedSignatures.push(computedHash);
|
|
|
|
}
|
2019-04-06 00:45:34 +03:00
|
|
|
const validationError: IValidationError = new Error('The signature could not be verified');
|
2017-05-31 18:57:31 +03:00
|
|
|
validationError.statusCode = 401;
|
|
|
|
validationError.computedHash = computedSignatures.join(', ');
|
2019-10-03 04:41:16 +03:00
|
|
|
throw validationError;
|
2017-05-31 18:57:31 +03:00
|
|
|
}
|