@adobe/probot-serverless-openwhisk
Version:
Probot Serverless OpenWisk
248 lines (217 loc) • 7.1 kB
JavaScript
/*
* Copyright 2018 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
/* eslint-disable no-underscore-dangle */
const crypto = require('crypto');
const path = require('path');
const fse = require('fs-extra');
const { createProbot } = require('probot');
const { logger } = require('probot/lib/logger');
const logWrapper = require('@adobe/openwhisk-action-builder/src/logging').logger;
const { resolve } = require('probot/lib/resolver');
const { findPrivateKey } = require('probot/lib/private-key');
const expressify = require('@adobe/openwhisk-action-builder/src/expressify');
const hbs = require('hbs');
const ERROR = {
statusCode: 500,
headers: {
'Cache-Control': 'no-store, private, must-revalidate',
},
body: 'Internal Server Error.',
};
/**
* Validate if the payload is valid.
* @param secret the webhook secret
* @param payload the payload body
* @param signature the signature of the POST
* @throws Error if the payload is not valid.
*/
function validatePayload(secret, payload = '', signature) {
if (!signature) {
throw Error('signature required');
}
if (!secret) {
throw Error('secret required');
}
const sig = signature.split('=');
if (sig.length !== 2) {
throw Error('invalid signature format.');
}
const signed = crypto.createHmac(sig[0], secret).update(payload, 'utf-8').digest();
if (!crypto.timingSafeEqual(signed, Buffer.from(sig[1], 'hex'))) {
throw Error('signature not valid.');
}
}
module.exports = class OpenWhiskWrapper {
constructor() {
this._viewsDirectory = [];
this._apps = [];
this._appId = null;
this._secret = null;
this._privateKey = null;
this._githubToken = null;
this._webhookPath = '/';
}
withApp(app) {
if (typeof app === 'string') {
this._apps.push(resolve(app));
} else {
this._apps.push(app);
}
return this;
}
withViewsDirectory(value) {
this._viewsDirectory.push(path.resolve(process.cwd(), value));
return this;
}
withAppId(appId) {
this._appId = appId;
return this;
}
withWebhookSecret(secret) {
this._secret = secret;
return this;
}
withGithubPrivateKey(key) {
this._privateKey = key;
return this;
}
withGithubToken(token) {
this._githubToken = token;
return this;
}
withWebHookPath(value) {
this._webhookPath = value;
return this;
}
async initProbot(params) {
const options = {
id: this._appId,
secret: this._secret,
cert: this._privateKey,
catchErrors: false,
githubToken: this._githubToken,
webhookPath: this._webhookPath,
};
const probot = createProbot(options);
if (this._viewsDirectory.length === 0) {
this.withViewsDirectory('./views');
}
probot.server.set('views', this._viewsDirectory);
probot.logger.debug('Set view directory to %s', probot.server.get('views'));
const hbsEngine = hbs.create();
hbsEngine.localsAsTemplateData(probot.server);
probot.server.engine('hbs', hbsEngine.__express);
// load pkgJson as express local
try {
probot.server.locals.pkgJson = await fse.readJson(path.join(process.cwd(), 'package.json'));
} catch (e) {
probot.logger.info('unable to load package.json %s', e);
}
probot.load((app) => {
this._apps.forEach((handler) => {
handler(app, params, options);
});
});
return probot;
}
create() {
const run = async (params) => {
const {
__ow_method: method,
__ow_headers: headers,
__ow_body: body,
} = params;
// set APP_ID, WEBHOOK_SECRET and PRIVATE_KEY if defined via params
if (!this._appId) {
this._appId = params.GH_APP_ID;
}
if (!this._secret) {
this._secret = params.GH_APP_WEBHOOK_SECRET;
}
if (!this._privateKey) {
this._privateKey = params.GH_APP_PRIVATE_KEY || findPrivateKey();
}
// check if the event is triggered via params.
let { event, eventId, payload } = params;
const { signature } = params;
let delegateRequest = true;
if (event && eventId && signature && payload) {
// validate webhook
try {
validatePayload(this._secret, payload, signature);
payload = JSON.parse(payload);
} catch (e) {
logger.error(`Error validating payload: ${e.message}`);
return ERROR;
}
logger.debug('payload signature valid.');
delegateRequest = false;
} else if (method === 'post' && headers) {
// eslint-disable-next-line no-param-reassign
params.__ow_body = Buffer.from(body, 'base64').toString('utf8');
if (headers['content-type'] === 'application/json') {
payload = JSON.parse(params.__ow_body);
}
event = headers['x-github-event'];
eventId = headers['x-github-delivery'];
}
if (eventId && payload && payload.action) {
logger.info(`Received event ${eventId} ${event}${payload.action ? (`.${payload.action}`) : ''}`);
}
let probot;
try {
logger.debug('intializing probot...');
probot = await this.initProbot(params);
} catch (e) {
logger.error(`Error while loading probot: ${e.stack || e}`);
return ERROR;
}
try {
let result = {
statusCode: 200,
headers: {},
body: 'ok\n',
};
if (delegateRequest) {
result = await expressify(probot.server)(params);
} else {
// let probot handle the event
await probot.receive({
name: event,
payload,
});
}
// set cache control header if not set
if (!result.headers['cache-control']) {
result.headers['cache-control'] = 'no-store, private, must-revalidate';
}
return result;
} catch (err) {
logger.error(err);
return ERROR;
}
};
return async (params) => {
// setup logger if configured
logWrapper.init(logger, params);
// eslint-disable-next-line no-underscore-dangle
logger.debug('>> %s %s"\n', (params.__ow_method || 'get').toUpperCase(), params.__ow_path || '/', params.__ow_headers);
// run actual action
const result = await run(params);
// if remote loggers are configured, wait a little to ensure logs buffers are flushed
if (logger.flush) {
logger.flush(); // don't wait for flush.
}
return result;
};
}
};