change structure, adding cli, adding alternative webhook

main
HerrHase 1 week ago
parent 664ffa6134
commit d199343535

7
.gitignore vendored

@ -134,7 +134,6 @@ storage/*
!storage/logs/.gitkeep !storage/logs/.gitkeep
!storage/.gitkeep !storage/.gitkeep
resources/actions/* packages/custom
resources/schemas/* !packages/custom/.gitkeep
!resources/actions/.gitkeep !packages/custom/package-custom.json
!resources/schemas/.gitkeep

@ -22,5 +22,18 @@ curl -X POST https://<domain>/api/flow/v1/<uuid>
-d '{"message":"<text>"}' -d '{"message":"<text>"}'
``` ```
```
curl -X POST https://<domain>/api/flow/v1/<uuid>/token
-H 'Content-Type: application/json'
-d '{"message":"<text>"}'
```
### PHP ### PHP
### Nodejs ### Nodejs
## Messenger
https://gajim.org/
https://pidgin.im/
https://profanity-im.github.io/
https://dino.im/

@ -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 <me@herr-hase.wtf>
* @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

@ -0,0 +1,32 @@
import Action from './../../packages/server/actions/action.js'
import XmppHelper from './../helpers/Xmpp.js'
/**
* Xmpp Message
*
* @author Björn Hase <me@herr-hase.wtf>
* @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

@ -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 <me@herr-hase.wtf>
* @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

@ -0,0 +1,5 @@
{
"private": true,
"name": "custom",
"type": "module"
}

@ -0,0 +1,11 @@
{
"private": true,
"name": "custom",
"type": "module",
"dependencies": {
"@xmpp/client": "^0.14.0"
},
"devDependencies": {
"@xmpp/debug": "^0.14.0"
}
}

@ -0,0 +1,32 @@
import Action from './../../packages/server/actions/action.js'
import XmppHelper from './../helpers/Xmpp.js'
/**
* Xmpp Message
*
* @author Björn Hase <me@herr-hase.wtf>
* @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

@ -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 <me@herr-hase.wtf>
* @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

21
package-lock.json generated

@ -1,14 +1,25 @@
{ {
"name": "signpost", "name": "signpost",
"version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "signpost", "name": "signpost",
"version": "0.1.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*",
"custom"
] ]
}, },
"custom": {
"dependencies": {
"@xmpp/client": "^0.14.0"
},
"devDependencies": {
"@xmpp/debug": "^0.14.0"
}
},
"node_modules/@acemir/cssom": { "node_modules/@acemir/cssom": {
"version": "0.9.31", "version": "0.9.31",
"resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
@ -1312,6 +1323,10 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/custom": {
"resolved": "custom",
"link": true
},
"node_modules/data-urls": { "node_modules/data-urls": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
@ -2662,7 +2677,6 @@
"dependencies": { "dependencies": {
"@fastify/rate-limit": "^10.3.0", "@fastify/rate-limit": "^10.3.0",
"@inquirer/prompts": "^8.3.0", "@inquirer/prompts": "^8.3.0",
"@xmpp/client": "^0.14.0",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
@ -2672,9 +2686,6 @@
"pino": "^10.3.1", "pino": "^10.3.1",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"uuid": "^13.0.0" "uuid": "^13.0.0"
},
"devDependencies": {
"@xmpp/debug": "^0.14.0"
} }
} }
} }

@ -1,8 +1,12 @@
{ {
"name": "signpost", "name": "signpost",
"version": "0.1.0", "version": "0.2.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"packages/*" "packages/*",
] "custom"
],
"scripts": {
"custom": "cp custom/package-custom.json custom/package.json"
}
} }

@ -6,6 +6,7 @@ import dotenv from 'dotenv'
* *
* *
*/ */
function config() { function config() {
// getting dir // getting dir

@ -54,5 +54,6 @@ const id = flowStore.create(flow, token)
if (id) { if (id) {
console.log(chalk.green('done')) console.log(chalk.green('done'))
console.log(chalk.blue('uuid: ' + flow.uuid)) 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)))
} }

@ -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))
}

@ -1,10 +1,19 @@
import { input, password } from '@inquirer/prompts' import { input, password } from '@inquirer/prompts'
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { getOrCreateSqlite } from '@nano/sqlite'
import path from 'path' 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' import FlowStore from './../store/flow.js'
// get config
import config from './../_config.js'
config()
// getting db and flowStore // getting db and flowStore
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true }) const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
const flowStore = new FlowStore(mainDB) const flowStore = new FlowStore(mainDB)
@ -21,4 +30,4 @@ const uuid = await input({
} }
}) })
flowStore.deleteByUuid(uuid) flowStore.removeByUuid(uuid)

@ -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 <me@herr-hase.wtf>
* @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

@ -13,26 +13,10 @@ import logger from './../helper/logger.js'
async function tokenHandler(request, response) { async function tokenHandler(request, response) {
if (!request.headers.authorization) { let token = DOMPurify.sanitize(request.params.token)
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()
}
// check if token is same as for the flow // 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') logger(response.locals.flow.uuid).error('token not equal with hash from flow')
return response return response

@ -11,7 +11,7 @@ import path from 'path'
*/ */
function resolveActionClass(className) { 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 let result = undefined
if (fs.existsSync(classPath)) { if (fs.existsSync(classPath)) {
@ -34,7 +34,7 @@ function resolveActionClass(className) {
*/ */
function resolveSchema(schemaName) { 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 let result = undefined
if (fs.existsSync(schemaPath)) { if (fs.existsSync(schemaPath)) {

@ -20,7 +20,7 @@ const TokenHelper = {
*/ */
create(token) { create(token) {
const hmac = createHmac(process.env.APP_HASH_TYPE, process.env.APP_SALT) 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') return hmac.update(buffer).digest('base64')
}, },

@ -1,4 +1,5 @@
import tokenHandler from './../../handler/token.js' import tokenHandler from './../../handler/token.js'
import bearerHandler from './../../handler/bearer.js'
import flowHandler from './../../handler/flow.js' import flowHandler from './../../handler/flow.js'
import logger from './../../helper/logger.js' import logger from './../../helper/logger.js'
@ -14,7 +15,6 @@ import logger from './../../helper/logger.js'
export default async function(fastify, options) { export default async function(fastify, options) {
fastify.addHook('preHandler', flowHandler) fastify.addHook('preHandler', flowHandler)
fastify.addHook('preHandler', tokenHandler)
/** /**
* *
@ -23,7 +23,37 @@ export default async function(fastify, options) {
* @param {object} response * @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)) { if (response.locals.schema && !request.validateInput(request.body, response.locals.schema)) {
return response return response

@ -1,12 +1,11 @@
{ {
"private": true, "private": true,
"name": "server", "name": "server",
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fastify/rate-limit": "^10.3.0", "@fastify/rate-limit": "^10.3.0",
"@inquirer/prompts": "^8.3.0", "@inquirer/prompts": "^8.3.0",
"@xmpp/client": "^0.14.0",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
@ -17,12 +16,11 @@
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"uuid": "^13.0.0" "uuid": "^13.0.0"
}, },
"devDependencies": {
"@xmpp/debug": "^0.14.0"
},
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"migrate": "node cli/migrate.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"
} }
} }

@ -16,10 +16,10 @@ class FlowStore extends Store {
} }
/** /**
* * create hash with current date
* *
*/ */
create(data, hash) { create(data) {
data.date_created_at = dayjs().toISOString() data.date_created_at = dayjs().toISOString()
return super.create(data) return super.create(data)
} }
@ -32,6 +32,23 @@ class FlowStore extends Store {
return this._db.prepare('SELECT * FROM ' + this._tableName + ' WHERE uuid = ?') return this._db.prepare('SELECT * FROM ' + this._tableName + ' WHERE uuid = ?')
.get(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 export default FlowStore

@ -35,7 +35,7 @@ class Store {
* *
*/ */
remove(id) { remove(id) {
this.db return this._db
.prepare('DELETE FROM ' + this._tableName + ' WHERE id = ?') .prepare('DELETE FROM ' + this._tableName + ' WHERE id = ?')
.run(id) .run(id)
} }

Loading…
Cancel
Save