1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const crypto = require('crypto');
|
16 | const path = require('path');
|
17 | const fse = require('fs-extra');
|
18 | const { Probot, Server } = require('probot');
|
19 | const pino = require('pino');
|
20 | const { expressify, wrap } = require('@adobe/openwhisk-action-utils');
|
21 | const { logger } = require('@adobe/openwhisk-action-logger');
|
22 | const { getPrivateKey } = require('@probot/get-private-key');
|
23 | const { resolveAppFunction } = require('probot/lib/helpers/resolve-app-function');
|
24 | const hbs = require('hbs');
|
25 | const PinoInterface = require('./pino-interface.js');
|
26 |
|
27 | const ERROR = {
|
28 | statusCode: 500,
|
29 | headers: {
|
30 | 'Cache-Control': 'no-store, private, must-revalidate',
|
31 | },
|
32 | body: 'Internal Server Error.',
|
33 | };
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | function validatePayload(secret, payload = '', signature) {
|
43 | if (!signature) {
|
44 | throw Error('signature required');
|
45 | }
|
46 | if (!secret) {
|
47 | throw Error('secret required');
|
48 | }
|
49 | const sig = signature.split('=');
|
50 | if (sig.length !== 2) {
|
51 | throw Error('invalid signature format.');
|
52 | }
|
53 | const signed = crypto.createHmac(sig[0], secret).update(payload, 'utf-8').digest();
|
54 | if (!crypto.timingSafeEqual(signed, Buffer.from(sig[1], 'hex'))) {
|
55 | throw Error('signature not valid.');
|
56 | }
|
57 | }
|
58 |
|
59 | module.exports = class OpenWhiskWrapper {
|
60 | constructor() {
|
61 | this._viewsDirectory = [];
|
62 | this._apps = [];
|
63 | this._appId = null;
|
64 | this._secret = null;
|
65 | this._privateKey = null;
|
66 | this._githubToken = null;
|
67 | this._webhookPath = '/';
|
68 | }
|
69 |
|
70 | async resolveApps() {
|
71 | return Promise.all(this._apps.map(async (app, idx, apps) => {
|
72 | if (typeof app === 'string') {
|
73 |
|
74 | apps[idx] = await resolveAppFunction(app);
|
75 | }
|
76 | }));
|
77 | }
|
78 |
|
79 | withApp(app) {
|
80 | this._apps.push(app);
|
81 | return this;
|
82 | }
|
83 |
|
84 | withViewsDirectory(value) {
|
85 | this._viewsDirectory.push(path.resolve(process.cwd(), value));
|
86 | return this;
|
87 | }
|
88 |
|
89 | withAppId(appId) {
|
90 | this._appId = appId;
|
91 | return this;
|
92 | }
|
93 |
|
94 | withWebhookSecret(secret) {
|
95 | this._secret = secret;
|
96 | return this;
|
97 | }
|
98 |
|
99 | withGithubPrivateKey(key) {
|
100 | this._privateKey = key;
|
101 | return this;
|
102 | }
|
103 |
|
104 | withGithubToken(token) {
|
105 | this._githubToken = token;
|
106 | return this;
|
107 | }
|
108 |
|
109 | withWebHookPath(value) {
|
110 | this._webhookPath = value;
|
111 | return this;
|
112 | }
|
113 |
|
114 | |
115 |
|
116 |
|
117 |
|
118 |
|
119 | async createProbotServer(params) {
|
120 |
|
121 | this._appId = params.GH_APP_ID;
|
122 | this._secret = params.GH_APP_WEBHOOK_SECRET;
|
123 | this._privateKey = params.GH_APP_PRIVATE_KEY;
|
124 | await logger.init(params);
|
125 | return this.initProbot(params);
|
126 | }
|
127 |
|
128 | async initProbot(params) {
|
129 | const log = pino({
|
130 | level: process.env.LOG_LEVEL || 'info',
|
131 | name: 'probot',
|
132 | serializers: {
|
133 | err: pino.stdSerializers.err,
|
134 | req: pino.stdSerializers.req,
|
135 | res: PinoInterface.resSerializer,
|
136 | },
|
137 | }, new PinoInterface());
|
138 |
|
139 | const options = {
|
140 | id: this._appId,
|
141 | secret: this._secret,
|
142 | privateKey: this._privateKey,
|
143 | catchErrors: false,
|
144 | githubToken: this._githubToken,
|
145 | log,
|
146 | };
|
147 |
|
148 | const server = new Server({
|
149 | log: options.log,
|
150 | webhookPath: this._webhookPath,
|
151 |
|
152 | Probot: Probot.defaults(options),
|
153 | });
|
154 |
|
155 | if (this._viewsDirectory.length === 0) {
|
156 | this.withViewsDirectory('./views');
|
157 | }
|
158 |
|
159 | server.expressApp.set('views', this._viewsDirectory);
|
160 | log.debug('Set view directory to %s', server.expressApp.get('views'));
|
161 | const hbsEngine = hbs.create();
|
162 | hbsEngine.localsAsTemplateData(server.expressApp);
|
163 | server.expressApp.engine('hbs', hbsEngine.__express);
|
164 |
|
165 | try {
|
166 | server.expressApp.locals.pkgJson = await fse.readJson(path.join(process.cwd(), 'package.json'));
|
167 | } catch (e) {
|
168 | log.info('unable to load package.json %s', e);
|
169 | }
|
170 |
|
171 | await server.load((app, opts) => {
|
172 | this._apps.forEach((handler) => {
|
173 | handler({ app, getRouter: opts.getRouter }, params, options);
|
174 | });
|
175 | });
|
176 |
|
177 | return server;
|
178 | }
|
179 |
|
180 | create() {
|
181 | const run = async (params) => {
|
182 | const {
|
183 | __ow_method: method,
|
184 | __ow_headers: headers,
|
185 | __ow_body: body,
|
186 | __ow_logger: log,
|
187 | } = params;
|
188 |
|
189 |
|
190 | if (!this._appId) {
|
191 | this._appId = params.GH_APP_ID;
|
192 | }
|
193 | if (!this._secret) {
|
194 | this._secret = params.GH_APP_WEBHOOK_SECRET;
|
195 | }
|
196 | if (!this._privateKey) {
|
197 | this._privateKey = params.GH_APP_PRIVATE_KEY || getPrivateKey();
|
198 | }
|
199 |
|
200 | await this.resolveApps();
|
201 |
|
202 |
|
203 | let { event, eventId, payload } = params;
|
204 | const { signature } = params;
|
205 |
|
206 | let delegateRequest = true;
|
207 | if (event && eventId && signature && payload) {
|
208 |
|
209 | try {
|
210 | validatePayload(this._secret, payload, signature);
|
211 | payload = JSON.parse(payload);
|
212 | } catch (e) {
|
213 | log.error(`Error validating payload: ${e.message}`);
|
214 | return ERROR;
|
215 | }
|
216 | log.debug('payload signature valid.');
|
217 | delegateRequest = false;
|
218 | } else if (method === 'post' && headers) {
|
219 |
|
220 | if (headers['content-type'] === 'application/json') {
|
221 | payload = JSON.parse(Buffer.from(body, 'base64').toString('utf8'));
|
222 | }
|
223 | event = headers['x-github-event'];
|
224 | eventId = headers['x-github-delivery'];
|
225 | }
|
226 |
|
227 | if (eventId && payload && payload.action) {
|
228 | log.info(`Received event ${eventId} ${event}${payload.action ? (`.${payload.action}`) : ''}`);
|
229 | }
|
230 |
|
231 | let probotServer;
|
232 | try {
|
233 | log.debug('intializing probot...');
|
234 | probotServer = await this.initProbot(params);
|
235 | } catch (e) {
|
236 | log.error(`Error while loading probot: ${e.stack || e}`);
|
237 | return ERROR;
|
238 | }
|
239 |
|
240 | try {
|
241 | let result = {
|
242 | statusCode: 200,
|
243 | headers: {},
|
244 | body: 'ok\n',
|
245 | };
|
246 |
|
247 | if (delegateRequest) {
|
248 | result = await expressify(probotServer.expressApp)(params);
|
249 | } else {
|
250 |
|
251 | await probotServer.probotApp.receive({
|
252 | name: event,
|
253 | payload,
|
254 | });
|
255 | }
|
256 |
|
257 |
|
258 | if (!result.headers['cache-control']) {
|
259 | result.headers['cache-control'] = 'no-store, private, must-revalidate';
|
260 | }
|
261 |
|
262 | return result;
|
263 | } catch (err) {
|
264 | log.error(err);
|
265 | return ERROR;
|
266 | }
|
267 | };
|
268 |
|
269 | return wrap(run).with(logger);
|
270 | }
|
271 | };
|