Skip to content
Merged
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
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "node-hook-action",
"version": "1.0.3",
"description": "Sample node express webhook application that trigger shell commands",
"main": "src/makeServer",
"main": "src/WebhookServerLauncher.js",
"type": "module",
"scripts": {
"test": "mocha --unhandled-rejections=strict tests/*.test.js",
"testAsync": "set ENABLE_ASYNC_TESTS=true&& mocha --unhandled-rejections=strict tests/*.test.js"
Expand All @@ -27,19 +28,20 @@
"homepage": "https://github.com/creharmony/node-hook-action#readme",
"dependencies": {
"async": "^3.2.0",
"body-parser": "^1.19.0",
"crypto": "^1.0.1",
"express": "^4.17.1",
"body-parser": "^2.2.0",
"espress": "^0.0.0",
"express": "^5.1.0",
"http-errors": "^2.0.0",
"jsonpath": "^1.0.2",
"moment": "^2.29.1",
"shelljs": "^0.8.4",
"uuid": "^8.3.2"
"shelljs": "^0.10.0",
"uuid": "^13.0.0"
},
"devDependencies": {
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"mocha": "^9.1.4",
"supertest": "^6.0.1"
"chai": "^6.2.0",
"chai-http": "^5.1.2",
"mocha": "^11.7.4",
"supertest": "^7.1.4"
},
"pnpm": {
"overrides": {
Expand Down
1,107 changes: 599 additions & 508 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions src/HookLogger.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require("fs");
const moment = require("moment");
import fs from 'fs';
import moment from 'moment';

class HookLogger {
export default class HookLogger {

constructor(config) {
if (!config || !config.server_config) {
Expand Down Expand Up @@ -56,5 +56,3 @@ class HookLogger {
}

}

module.exports = HookLogger;
120 changes: 63 additions & 57 deletions src/HookServer.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,74 @@
const express = require("express");
const bodyParser = require("body-parser");
const HookService = require('./HookService')
const HookLogger = require('./HookLogger')
const API_MATRIX = require('./routes/ApiRoutes').matrix
import express from "express";
import createError from 'http-errors';
import HookService from './HookService.js';
import HookLogger from './HookLogger.js';
import {matrix as API_MATRIX} from './routes/ApiRoutes.js';

class HookServer {
export default class HookServer {

constructor(config) {
if (!config || !config.server_config) {
throw "missing server config";
constructor(config) {
if (!config || !config.server_config) {
throw "missing server config";
}
this._config = config;
this._port = config.server_config.port;
this._hostname = config.server_config.host;
this._service = new HookService(config);
this._logger = new HookLogger(config);
}
this._config = config;
this._port = config.server_config.port;
this._hostname = config.server_config.host;
this._service = new HookService(config);
this._logger = new HookLogger(config);
}

close() {
console.log('stop');
if (hookServer.listeningServer) {
hookServer.listeningServer.close();
close() {
const hookServer = this;
console.log('stop');
if (hookServer.listeningServer) {
hookServer.listeningServer.close();
}
}
}

/**
* server error handler
*/
onError(err) {
this._logger.error("HookServer error: " + err);
}
/**
* server error handler
*/
onError(err) {
this._logger.error("HookServer error: " + err);
}

start() {
var hookServer = this;
return new Promise((resolve, reject) => {
hookServer.app = express();
hookServer.app.use(express.json());// accept json payload
start() {
const hookServer = this;
return new Promise(resolve => {
hookServer.app = express();
hookServer.app.use(express.json());// accept json payload

var serverPath = hookServer._config.server_config.path;
var listeningUrl = `http://${this._hostname}:${this._port}${serverPath}`;
const serverPath = hookServer._config.server_config.path;
const listeningUrl = `http://${this._hostname}:${this._port}${serverPath}`;

//~ API options
var apiOptions = {
config: hookServer._config,
service: hookServer._service,
logger: hookServer._logger,
listeningUrl
}
//~ API mapping
Object.keys(API_MATRIX).forEach(apiPath => {
var apiEndpoint = serverPath + apiPath;
// DEBUG // hookServer._logger.debug(` * serve route ${apiEndpoint}`);
hookServer.app.use(apiEndpoint, API_MATRIX[apiPath].apiRoutes(apiOptions));
});
//~ API options
const apiOptions = {
config: hookServer._config,
service: hookServer._service,
logger: hookServer._logger,
listeningUrl
};
//~ API mapping
Object.keys(API_MATRIX).forEach(apiPath => {
const apiEndpoint = serverPath + apiPath;
// DEBUG // hookServer._logger.debug(` * serve route ${apiEndpoint}`);
hookServer.app.use(apiEndpoint, API_MATRIX[apiPath](apiOptions));
});

// catch 404 and forward to error handler
hookServer.app.use((req, res, next) => {
// DEBUG // console.info("MDW req",req.path);
next(createError(404));
});

//~ Express listen
hookServer.listeningServer = hookServer.app
.listen(hookServer._port, hookServer._hostname, () => {
hookServer._logger.info(`Listening ${listeningUrl}`);
resolve(hookServer);
})
.on('error', hookServer.onError.bind(this));
});
}
}

module.exports = HookServer;
//~ Express listen
hookServer.listeningServer = hookServer.app
.listen(hookServer._port, hookServer._hostname, () => {
hookServer._logger.info(`Listening ${listeningUrl}`);
resolve(hookServer);
})
.on('error', hookServer.onError.bind(this));
});
}
}
9 changes: 4 additions & 5 deletions src/HookService.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const uuidv4 = require('uuid').v4;
const crypto = require("crypto");
import { v4 as uuidv4 } from 'uuid';
import crypto from "crypto";

class HookService {
export default class HookService {

constructor(config) {
if (!config || !config.server_config) {
Expand Down Expand Up @@ -98,5 +98,4 @@ class HookService {
return arr.find(arrayEntry => (arrayEntry.id && id === arrayEntry.id));
}

}
module.exports = HookService;
}
27 changes: 27 additions & 0 deletions src/WebhookServerLauncher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from "fs";
import HookServer from './HookServer.js';

export class WebhookServerLauncher {
async loadConfiguration() {
const rawConfig = await fs.promises.readFile("./config.json");
return JSON.parse(rawConfig.toString());
}

async initializeServer(config) {
const hookServer = new HookServer(config);
await hookServer.start();
return hookServer;
}

static async launch() {
try {
// DEBUG // console.log("Launching webhook server...");
const launcher = new WebhookServerLauncher();
const config = await launcher.loadConfiguration();
return await launcher.initializeServer(config);
} catch (error) {
console.log(error.stack)
throw new Error(`Failed to launch webhook server: ${error.message}`);
}
}
}
21 changes: 0 additions & 21 deletions src/makeServer.js

This file was deleted.

12 changes: 7 additions & 5 deletions src/routes/ApiRoutes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module.exports.matrix = {
'/actions':require('./hookActions'),
'/':require('./hook'),
'/*':require('./notFound')
};
import actions from './hookActions.js';
import root from './hook.js';

export const matrix = {
'/actions': actions,
'/': root
}
54 changes: 26 additions & 28 deletions src/routes/hook.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const router = require('express').Router();
const shelljs = require("shelljs");
const jp = require("jsonpath");
import express from 'express';
import shelljs from 'shelljs';
import jp from 'jsonpath';
const router = express.Router();

function apiRoutes(options) {
var { config, service, logger, listeningUrl } = options;
export default function apiRoutes(options) {
const {config, service, logger, listeningUrl} = options;

router.post('/', function(req, res) {
var eventType = null;
router.post('/', function(req, res) {
let eventType = null;
// DEBUG // console.log('REQ HEADERS' + JSON.stringify(req.headers));
// DEBUG // console.log('POST BODY', req.body);
if (service.isXGithub(req)) {
Expand Down Expand Up @@ -37,47 +38,47 @@ function apiRoutes(options) {

const actions = config.actions.filter((configAction) => {
if (configAction.headers) {// does headers conditions match the request headers ?
var matchedHeaders = Object.keys(configAction.headers).filter((configActionHeader) => {
return (req.headers[configActionHeader] === configAction.headers[configActionHeader]);
});
if (matchedHeaders.length !== Object.keys(configAction.headers).length) {
const matchedHeaders = Object.keys(configAction.headers).filter((configActionHeader) => {
return (req.headers[configActionHeader] === configAction.headers[configActionHeader]);
});
if (matchedHeaders.length !== Object.keys(configAction.headers).length) {
return false;
}
}
if (configAction.payload) {// does body conditions match the request body ?
var matchedPayloadConditions = Object.keys(configAction.payload).filter((jsonPath) => {
// DEBUG // console.info("jp.paths(req.body, " + jsonPath + ") :" + jp.value(req.body, jsonPath));
return (jp.value(req.body, jsonPath) === configAction.payload[jsonPath]);
});
if (matchedPayloadConditions.length !== Object.keys(configAction.payload).length) {
const matchedPayloadConditions = Object.keys(configAction.payload).filter((jsonPath) => {
// DEBUG // console.info("jp.paths(req.body, " + jsonPath + ") :" + jp.value(req.body, jsonPath));
return (jp.value(req.body, jsonPath) === configAction.payload[jsonPath]);
});
if (matchedPayloadConditions.length !== Object.keys(configAction.payload).length) {
return false;
}
}
var matchedEvents = configAction.events.filter((actionEvent) => actionEvent.event === eventType);
// condition are done, lets play!
const matchedEvents = configAction.events.filter((actionEvent) => actionEvent.event === eventType);
// condition are done, lets play!
return matchedEvents && matchedEvents.length > 0;
});

if (!actions || actions.length < 1) {
logger.error(`${service.getSourceIp(req)} | nothing to be done`);
res.status(304);//https://http.cat/304
res.status(304);
res.end();
return;
}
// DEBUG // console.log('actions : ' + JSON.stringify(actions));
var answered = false;
let answered = false;
actions.forEach((todoAction) => todoAction.events.forEach((actionEvent) => {
if (actionEvent.event !== eventType) {
return;
}
const tpl = eval("`" + actionEvent.action + "`");
if (actionEvent.async && actionEvent.async === true){
var actionId = service.generateUUID();
service.pushAsyncAction(actionId);
const actionId = service.generateUUID();
service.pushAsyncAction(actionId);
shelljs.exec(tpl, {}, function(code, stdout, stderr) {
var wasSuccess = (code === 0);
var actionResult = {actionId, wasSuccess, code, stderr, stdout};
logger.info(`${service.getSourceIp(req)} | ${eventType} | async action (id='${actionId}') '${actionEvent.action}' done - code:'${code}'`);
const wasSuccess = (code === 0);
const actionResult = {actionId, wasSuccess, code, stderr, stdout};
logger.info(`${service.getSourceIp(req)} | ${eventType} | async action (id='${actionId}') '${actionEvent.action}' done - code:'${code}'`);
service.doneAsyncAction(actionId, actionResult);
});
logger.info(`${service.getSourceIp(req)} | ${eventType} | async action (id='${actionId}') '${actionEvent.action}' CREATED`);
Expand All @@ -100,6 +101,3 @@ function apiRoutes(options) {

return router
}

module.exports.apiRoutes = apiRoutes;

Loading