diff --git a/.gitignore b/.gitignore index 422b1ca..378874c 100644 --- a/.gitignore +++ b/.gitignore @@ -134,7 +134,6 @@ storage/* !storage/logs/.gitkeep !storage/.gitkeep -resources/actions/* -resources/schemas/* -!resources/actions/.gitkeep -!resources/schemas/.gitkeep +packages/custom +!packages/custom/.gitkeep +!packages/custom/package-custom.json diff --git a/README_XMPP.md b/README_XMPP.md index 9ac36d2..8cbff42 100644 --- a/README_XMPP.md +++ b/README_XMPP.md @@ -22,5 +22,18 @@ curl -X POST https:///api/flow/v1/ -d '{"message":""}' ``` +``` +curl -X POST https:///api/flow/v1//token + -H 'Content-Type: application/json' + -d '{"message":""}' +``` + ### PHP ### Nodejs + +## Messenger + +https://gajim.org/ +https://pidgin.im/ +https://profanity-im.github.io/ +https://dino.im/ diff --git a/resources/actions/.gitkeep b/custom/.gitkeep similarity index 100% rename from resources/actions/.gitkeep rename to custom/.gitkeep diff --git a/custom/actions/Pingdom.js b/custom/actions/Pingdom.js new file mode 100644 index 0000000..71ef190 --- /dev/null +++ b/custom/actions/Pingdom.js @@ -0,0 +1,35 @@ +import Action from './../../packages/server/actions/action.js' +import XmppHelper from './../helpers/Xmpp.js' + +/** + * Getting Http State + * + * @author Björn Hase + * @license http://opensource.org/licenses/MIT The MIT License + * @link https://git.node001.net/HerrHase/super-hog.git + * + */ + +class Pingdom extends Action { + + /** + * + * + */ + async run() { + + const xmppHelper = new XmppHelper( + process.env.XMPP_SERVICE, + process.env.XMPP_DOMAIN, + process.env.XMPP_USERNAME, + process.env.XMPP_PASSWORD + ) + + const message = this.data.check_params.full_url + ' / ' + this.data.current_state + await xmppHelper.sendToRoom(this.flow, process.env.XMPP_ROOM, message) + + } + +} + +export default Pingdom diff --git a/custom/actions/Xmpp.js b/custom/actions/Xmpp.js new file mode 100644 index 0000000..20452df --- /dev/null +++ b/custom/actions/Xmpp.js @@ -0,0 +1,32 @@ +import Action from './../../packages/server/actions/action.js' +import XmppHelper from './../helpers/Xmpp.js' + +/** + * Xmpp Message + * + * @author Björn Hase + * @license http://opensource.org/licenses/MIT The MIT License + * @link https://git.node001.net/HerrHase/super-hog.git + * + */ + +class Xmpp extends Action { + + /** + * + * + */ + async run() { + const xmppHelper = new XmppHelper( + process.env.XMPP_SERVICE, + process.env.XMPP_DOMAIN, + process.env.XMPP_USERNAME, + process.env.XMPP_PASSWORD + ) + + await xmppHelper.sendToRoom(this.flow, process.env.XMPP_ROOM, this.data.message) + } + +} + +export default Xmpp diff --git a/custom/helpers/Xmpp.js b/custom/helpers/Xmpp.js new file mode 100644 index 0000000..2e95e2f --- /dev/null +++ b/custom/helpers/Xmpp.js @@ -0,0 +1,80 @@ +import { client, xml } from '@xmpp/client' +import debug from '@xmpp/debug' +import DOMPurify from 'isomorphic-dompurify' + +import Action from './../../packages/server/actions/action.js' +import logger from './../../packages/server/helper/logger.js' + +/** + * + * + * @author Björn Hase + * @license http://opensource.org/licenses/MIT The MIT License + * @link https://git.node001.net/HerrHase/super-hog.git + * + */ + +class XmppHelper { + + /** + * + * + */ + constructor(service, domain, username, password) { + this.service = service + this.domain = domain + this.username = username + this.password = password + } + + /** + * + * + */ + async sendToRoom(flow, room, message) { + + // cleaning data + message = DOMPurify.sanitize(message) + + const xmpp = client({ + service: this.service, + domain: this.domain, + username: this.username, + password: this.password + }) + + if (process.env.APP_DEBUG) { + debug(xmpp, true) + } + + // handle if client has errors + xmpp.on('error', (error) => { + logger(flow.uuid).error('xmpp / error ' + error) + }) + + // handle if client goes online + xmpp.once('online', async (address) => { + + // join group + await xmpp.send(xml("presence", { to: room + '/' + this.username }, xml("x", { xmlns: 'http://jabber.org/protocol/muc' }))) + + // sends a message to the room + const message = xml( + 'message', { + type: 'groupchat', + to: room + }, + xml('body', {}, message) + ) + + await xmpp.send(message) + logger(flow.uuid).info('xmpp / message send') + + await xmpp.disconnect() + }) + + await xmpp.start() + } +} + +export default XmppHelper diff --git a/custom/package-custom.json b/custom/package-custom.json new file mode 100644 index 0000000..94c035e --- /dev/null +++ b/custom/package-custom.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "custom", + "type": "module" +} diff --git a/custom/package.json b/custom/package.json new file mode 100644 index 0000000..b71053a --- /dev/null +++ b/custom/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "name": "custom", + "type": "module", + "dependencies": { + "@xmpp/client": "^0.14.0" + }, + "devDependencies": { + "@xmpp/debug": "^0.14.0" + } +} diff --git a/examples/actions/Xmpp.js b/examples/actions/Xmpp.js new file mode 100644 index 0000000..20452df --- /dev/null +++ b/examples/actions/Xmpp.js @@ -0,0 +1,32 @@ +import Action from './../../packages/server/actions/action.js' +import XmppHelper from './../helpers/Xmpp.js' + +/** + * Xmpp Message + * + * @author Björn Hase + * @license http://opensource.org/licenses/MIT The MIT License + * @link https://git.node001.net/HerrHase/super-hog.git + * + */ + +class Xmpp extends Action { + + /** + * + * + */ + async run() { + const xmppHelper = new XmppHelper( + process.env.XMPP_SERVICE, + process.env.XMPP_DOMAIN, + process.env.XMPP_USERNAME, + process.env.XMPP_PASSWORD + ) + + await xmppHelper.sendToRoom(this.flow, process.env.XMPP_ROOM, this.data.message) + } + +} + +export default Xmpp diff --git a/examples/helpers/Xmpp.js b/examples/helpers/Xmpp.js new file mode 100644 index 0000000..2e95e2f --- /dev/null +++ b/examples/helpers/Xmpp.js @@ -0,0 +1,80 @@ +import { client, xml } from '@xmpp/client' +import debug from '@xmpp/debug' +import DOMPurify from 'isomorphic-dompurify' + +import Action from './../../packages/server/actions/action.js' +import logger from './../../packages/server/helper/logger.js' + +/** + * + * + * @author Björn Hase + * @license http://opensource.org/licenses/MIT The MIT License + * @link https://git.node001.net/HerrHase/super-hog.git + * + */ + +class XmppHelper { + + /** + * + * + */ + constructor(service, domain, username, password) { + this.service = service + this.domain = domain + this.username = username + this.password = password + } + + /** + * + * + */ + async sendToRoom(flow, room, message) { + + // cleaning data + message = DOMPurify.sanitize(message) + + const xmpp = client({ + service: this.service, + domain: this.domain, + username: this.username, + password: this.password + }) + + if (process.env.APP_DEBUG) { + debug(xmpp, true) + } + + // handle if client has errors + xmpp.on('error', (error) => { + logger(flow.uuid).error('xmpp / error ' + error) + }) + + // handle if client goes online + xmpp.once('online', async (address) => { + + // join group + await xmpp.send(xml("presence", { to: room + '/' + this.username }, xml("x", { xmlns: 'http://jabber.org/protocol/muc' }))) + + // sends a message to the room + const message = xml( + 'message', { + type: 'groupchat', + to: room + }, + xml('body', {}, message) + ) + + await xmpp.send(message) + logger(flow.uuid).info('xmpp / message send') + + await xmpp.disconnect() + }) + + await xmpp.start() + } +} + +export default XmppHelper diff --git a/package-lock.json b/package-lock.json index 2fb61e3..5fb1535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,25 @@ { "name": "signpost", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "signpost", + "version": "0.1.0", "workspaces": [ - "packages/*" + "packages/*", + "custom" ] }, + "custom": { + "dependencies": { + "@xmpp/client": "^0.14.0" + }, + "devDependencies": { + "@xmpp/debug": "^0.14.0" + } + }, "node_modules/@acemir/cssom": { "version": "0.9.31", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", @@ -1312,6 +1323,10 @@ "node": ">=20" } }, + "node_modules/custom": { + "resolved": "custom", + "link": true + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -2662,7 +2677,6 @@ "dependencies": { "@fastify/rate-limit": "^10.3.0", "@inquirer/prompts": "^8.3.0", - "@xmpp/client": "^0.14.0", "better-sqlite3": "^12.6.2", "chalk": "^5.6.2", "dayjs": "^1.11.19", @@ -2672,9 +2686,6 @@ "pino": "^10.3.1", "pino-pretty": "^13.1.3", "uuid": "^13.0.0" - }, - "devDependencies": { - "@xmpp/debug": "^0.14.0" } } } diff --git a/package.json b/package.json index 3c3e9e3..cb0c03c 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { "name": "signpost", - "version": "0.1.0", + "version": "0.2.0", "private": true, "workspaces": [ - "packages/*" - ] + "packages/*", + "custom" + ], + "scripts": { + "custom": "cp custom/package-custom.json custom/package.json" + } } diff --git a/packages/server/_config.js b/packages/server/_config.js index bf134e7..7ffde96 100644 --- a/packages/server/_config.js +++ b/packages/server/_config.js @@ -6,6 +6,7 @@ import dotenv from 'dotenv' * * */ + function config() { // getting dir diff --git a/packages/server/cli/addFlow.js b/packages/server/cli/addFlow.js index 6ff5a5a..63bf798 100644 --- a/packages/server/cli/addFlow.js +++ b/packages/server/cli/addFlow.js @@ -54,5 +54,6 @@ const id = flowStore.create(flow, token) if (id) { console.log(chalk.green('done')) console.log(chalk.blue('uuid: ' + flow.uuid)) - console.log(chalk.blue('token: ' + token)) + console.log(chalk.blue('bearer: ' + token)) + console.log(chalk.blue('bearer: ' + encodeURIComponent(token))) } diff --git a/packages/server/cli/indexFlow.js b/packages/server/cli/indexFlow.js new file mode 100644 index 0000000..d6f891d --- /dev/null +++ b/packages/server/cli/indexFlow.js @@ -0,0 +1,27 @@ +import { input, password } from '@inquirer/prompts' +import { v4 as uuidv4 } from 'uuid' +import path from 'path' +import chalk from 'chalk' +import crypto from 'node:crypto' + +import runMigrationSqlite from './../db/migration.js' +import getOrCreateSqlite from './../db/sqlite.js' + +import TokenHelper from './../helper/token.js' + +// getting flow +import FlowStore from './../store/flow.js' + +// get config +import config from './../_config.js' +config() + +// getting db and flowStore +const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true }) +const flowStore = new FlowStore(mainDB) + +const results = flowStore.find() + +for (const result of results) { + console.log(chalk.green(result.uuid + ' / Action: ' + result.action + ' / Schema: ' + result.schema)) +} diff --git a/packages/server/cli/removeFlow.js b/packages/server/cli/removeFlow.js index 5c0ee61..372993e 100644 --- a/packages/server/cli/removeFlow.js +++ b/packages/server/cli/removeFlow.js @@ -1,10 +1,19 @@ import { input, password } from '@inquirer/prompts' -import { v4 as uuidv4 } from 'uuid'; -import { getOrCreateSqlite } from '@nano/sqlite' +import { v4 as uuidv4 } from 'uuid' import path from 'path' +import chalk from 'chalk' +import crypto from 'node:crypto' +import runMigrationSqlite from './../db/migration.js' +import getOrCreateSqlite from './../db/sqlite.js' + +// getting flow import FlowStore from './../store/flow.js' +// get config +import config from './../_config.js' +config() + // getting db and flowStore const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true }) const flowStore = new FlowStore(mainDB) @@ -21,4 +30,4 @@ const uuid = await input({ } }) -flowStore.deleteByUuid(uuid) +flowStore.removeByUuid(uuid) diff --git a/packages/server/handler/bearer.js b/packages/server/handler/bearer.js new file mode 100644 index 0000000..2d37dac --- /dev/null +++ b/packages/server/handler/bearer.js @@ -0,0 +1,44 @@ +import DOMPurify from 'isomorphic-dompurify' +import TokenHelper from './../helper/token.js' +import logger from './../helper/logger.js' + +/** + * handle token + * + * @author Björn Hase + * @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3 + * @link https://git.node001.net/HerrHase/signpost.git + * + */ + +async function bearerHandler(request, response) { + + if (!request.headers.authorization) { + return response + .code(403) + .send() + } + + let token = DOMPurify.sanitize(request.headers.authorization) + token = token.match(/^Bearer ([A-Za-z0-9._~+/-]+=*)$/) + + // check if token exists + if (!token || !token[1]) { + logger(response.locals.flow.uuid).error('token not found in header') + + return response + .code(403) + .send() + } + + // check if token is same as for the flow + if (!TokenHelper.equal(token[1], response.locals.flow.hash)) { + logger(response.locals.flow.uuid).error('token not equal with hash from flow') + + return response + .code(403) + .send() + } +} + +export default bearerHandler diff --git a/packages/server/handler/token.js b/packages/server/handler/token.js index dca181e..aa46e94 100644 --- a/packages/server/handler/token.js +++ b/packages/server/handler/token.js @@ -12,27 +12,11 @@ import logger from './../helper/logger.js' */ async function tokenHandler(request, response) { - - if (!request.headers.authorization) { - return response - .code(403) - .send() - } - let token = DOMPurify.sanitize(request.headers.authorization) - token = token.match(/^Bearer ([A-Za-z0-9._~+/-]+=*)$/) - - // check if token exists - if (!token[1]) { - logger(response.locals.flow.uuid).error('token not found in header') - - return response - .code(403) - .send() - } + let token = DOMPurify.sanitize(request.params.token) // check if token is same as for the flow - if (!TokenHelper.equal(token[1], response.locals.flow.hash)) { + if (!TokenHelper.equal(token, response.locals.flow.hash)) { logger(response.locals.flow.uuid).error('token not equal with hash from flow') return response diff --git a/packages/server/helper/resolver.js b/packages/server/helper/resolver.js index 003d78a..fdf8c8c 100644 --- a/packages/server/helper/resolver.js +++ b/packages/server/helper/resolver.js @@ -11,7 +11,7 @@ import path from 'path' */ function resolveActionClass(className) { - let classPath = path.join(process.env.APP_BASE_DIR, 'resources/actions/' + className + '.js') + let classPath = path.join(process.env.APP_BASE_DIR, 'custom/actions/' + className + '.js') let result = undefined if (fs.existsSync(classPath)) { @@ -34,7 +34,7 @@ function resolveActionClass(className) { */ function resolveSchema(schemaName) { - let schemaPath = path.join(process.env.APP_BASE_DIR, 'resources/schemas/' + schemaName + '.json') + let schemaPath = path.join(process.env.APP_BASE_DIR, 'custom/schemas/' + schemaName + '.json') let result = undefined if (fs.existsSync(schemaPath)) { diff --git a/packages/server/helper/token.js b/packages/server/helper/token.js index 51fcc28..c3aaa7c 100644 --- a/packages/server/helper/token.js +++ b/packages/server/helper/token.js @@ -20,7 +20,7 @@ const TokenHelper = { */ create(token) { const hmac = createHmac(process.env.APP_HASH_TYPE, process.env.APP_SALT) - const buffer = new Buffer(token) + const buffer = new Buffer.from(token) return hmac.update(buffer).digest('base64') }, diff --git a/packages/server/http/api/flow.js b/packages/server/http/api/flow.js index 1400c51..33c7a98 100644 --- a/packages/server/http/api/flow.js +++ b/packages/server/http/api/flow.js @@ -1,4 +1,5 @@ import tokenHandler from './../../handler/token.js' +import bearerHandler from './../../handler/bearer.js' import flowHandler from './../../handler/flow.js' import logger from './../../helper/logger.js' @@ -14,7 +15,6 @@ import logger from './../../helper/logger.js' export default async function(fastify, options) { fastify.addHook('preHandler', flowHandler) - fastify.addHook('preHandler', tokenHandler) /** * @@ -23,7 +23,37 @@ export default async function(fastify, options) { * @param {object} response * */ - fastify.post('/:uuid(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)', async function (request, response) { + fastify.post('/:uuid(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)', { preHandler: [ bearerHandler ] }, async function (request, response) { + + if (response.locals.schema && !request.validateInput(request.body, response.locals.schema)) { + return response + .code(400) + .send() + } + + // running actions + const action = new response.locals.action.default(response.locals.flow, request.body) + + response + .code(204) + .send() + + // try run from action + try { + await action.run() + } catch(error) { + logger(response.locals.flow.uuid).error('webhook / run / ' + error) + } + }) + + /** + * + * + * @param {object} request + * @param {object} response + * + */ + fastify.post('/:uuid(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)/:token(^[A-Za-z0-9+\/]+={0,2}$)', { preHandler: [ tokenHandler ] }, async function (request, response) { if (response.locals.schema && !request.validateInput(request.body, response.locals.schema)) { return response diff --git a/packages/server/package.json b/packages/server/package.json index 7f9d806..f1ebb93 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,12 +1,11 @@ { "private": true, "name": "server", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "dependencies": { "@fastify/rate-limit": "^10.3.0", "@inquirer/prompts": "^8.3.0", - "@xmpp/client": "^0.14.0", "better-sqlite3": "^12.6.2", "chalk": "^5.6.2", "dayjs": "^1.11.19", @@ -17,12 +16,11 @@ "pino-pretty": "^13.1.3", "uuid": "^13.0.0" }, - "devDependencies": { - "@xmpp/debug": "^0.14.0" - }, "scripts": { "start": "node index.js", "migrate": "node cli/migrate.js", - "addFlow": "node cli/addFlow.js" + "addFlow": "node cli/addFlow.js", + "removeFlow": "node cli/removeFlow.js", + "indexFlow": "node cli/indexFlow.js" } } diff --git a/packages/server/store/flow.js b/packages/server/store/flow.js index 49a7d9d..13aeb8a 100644 --- a/packages/server/store/flow.js +++ b/packages/server/store/flow.js @@ -16,10 +16,10 @@ class FlowStore extends Store { } /** - * + * create hash with current date * */ - create(data, hash) { + create(data) { data.date_created_at = dayjs().toISOString() return super.create(data) } @@ -32,6 +32,23 @@ class FlowStore extends Store { return this._db.prepare('SELECT * FROM ' + this._tableName + ' WHERE uuid = ?') .get(uuid) } + + /** + * + * + */ + removeByUuid(uuid) { + return this._db.prepare('DELETE FROM ' + this._tableName + ' WHERE uuid = ?') + .run(uuid) + } + + /** + * + */ + find() { + return this._db.prepare('SELECT * FROM ' + this._tableName) + .all() + } } export default FlowStore diff --git a/packages/server/store/store.js b/packages/server/store/store.js index 1280bfb..00c84c2 100644 --- a/packages/server/store/store.js +++ b/packages/server/store/store.js @@ -35,7 +35,7 @@ class Store { * */ remove(id) { - this.db + return this._db .prepare('DELETE FROM ' + this._tableName + ' WHERE id = ?') .run(id) } diff --git a/resources/schemas/.gitkeep b/resources/schemas/.gitkeep deleted file mode 100644 index e69de29..0000000