Files
fb-admin/backendsrc/FrontblockAdmin.ts
T

309 lines
8.8 KiB
TypeScript

'use strict'
import * as Logger from 'log4js'
import { Plugin, socketioRPC } from 'frontblock-generic/Plugin';
import { ErrorResponse, SuccessResponse } from 'frontblock-generic/Types';
import express = require('express');
import http = require('http');
import https = require('https');
import fs = require('fs');
import bsock = require('bsock');
import fetch = require("node-fetch");
import { async } from 'q';
const kfs = require("key-file-storage").default('kfs')
type pluginEntry =
{
pluginName:string
}
const pluginList:pluginEntry[] =
[
//{ pluginName: 'apiclient' },
//{ pluginName: 'paymentmanager' },
//{ pluginName: 'wallet' },
{ pluginName: 'htmlsupplier' }
]
type releaseEntry =
{
tagName: string,
staticZipURL: URL,
checksumFileURL: URL
}
Logger.configure({
appenders:
{
"admin": { type: 'stdout' },
//app: { type: 'file', filename: 'application.log' }
},
categories:
{
default: { appenders: [ 'admin' ], level: 'debug' }
}
})
const logger = Logger.getLogger("admin")
type hookRPC =
{
type: 'hook',
generator: (socket) => Function,
unhook:(uid:string)=>Promise<ErrorResponse|SuccessResponse>
}
type unhookRPC =
{
type: 'unhook',
fn: Function
}
type callRPC =
{
type: 'call',
fn: Function
}
export type rpcInfo =
{
owner: string,
name: string,
args: string,
info: hookRPC | unhookRPC | callRPC
}
/**
* FrontblockAdmin
*
* The customer-facing dynamic component of the customer backend
* Supports (un)loading plugins which will be communicated to its library component (See FrontblockLib.ts and the info() RPC)
*
* The list of available plugins is published via the frontblock API and downloaded via gitea-releases
*/
export class FrontblockAdmin{
private plugins: Plugin[]
private hookToUids:{[hookName:string]:string[]} = {}
private express
private httpServer
private io = bsock.createServer()
private wsServer = http.createServer()
constructor(){
if(!('admin.conf' in kfs)){
logger.info('Generating ./kfs/admin.conf')
kfs['admin.conf'] = { httpPort: 8080 }
}
this.initialize()
}
private async initialize(){
const wat = await this.fetchReleaseList('htmlsupplier')
logger.debug(wat)
//await this.downloadPlugins()
//this.plugins.forEach(plugin => plugin.start())
//this.startWebsocket()
//this.startWebserver()
}
private async fetchReleaseList(pluginName:string): Promise<releaseEntry[]>{
const res = await fetch('https://gitea.frontblock.me/api/v1/repos/fb-vendor/' + pluginName + '/releases')
const json = await res.json();
const releaseList:releaseEntry[] = []
json.forEach((release) => {
const tagName = release.tag_name
const staticZipURL = release.assets.filter(asset => asset.name === pluginName + '.zip')[0].browser_download_url
const checksumFileURL = release.assets.filter(asset => asset.name === 'md5sum.txt')[0].browser_download_url
releaseList.push({tagName,staticZipURL,checksumFileURL})
})
return releaseList
}
private async downloadPlugin(release:releaseEntry) {
}
private async verifyPlugin(release:releaseEntry) {
}
private async extractPlugin(release:releaseEntry) {
}
private async installPlugin(release:releaseEntry) {
}
private initApis(socket){
//Declare own functions
const rpcInfos:rpcInfo[] = [
{
owner: 'Admin',
name: 'restartWebserver',
args: 'port',
info:{
type:'call',
fn: (port:number) => { this.restartWebserver(port) }
}
},{
owner: 'Admin',
name: 'info',
args: '',
info:{
type:'call',
fn: () => { return rpcInfos }
}
}
]
//translate RPCs to socket-bound function metadata
this.plugins.forEach(plugin => {
const pluginName = plugin.name
plugin.exportRPCs().forEach(rpc => {
const info = this.rpcToRpcInfo(pluginName, rpc)
rpcInfos.push(info)
})
})
//Hook up all the functions
for(const api of rpcInfos){
switch(api.info.type){
case 'call':
socket.hook(api.name, api.info.fn)
break
case 'hook':
const hook = api.info.generator(socket)
hook.bind(this)
socket.hook(api.name, hook)
break
case 'unhook':
socket.hook(api.name, api.info.fn)
break
}
}
//initialize the lists of open hooks
rpcInfos
.filter(rpc => rpc.info.type === "hook")
.forEach(hook => {
this.hookToUids[hook.name] = []
})
//On close, unhook open hooks
socket.on('close', () => {
logger.info("Client disconnected")
rpcInfos.forEach((rpc) => {
if(this.hookToUids[rpc.name] == null)
return
this.hookToUids[rpc.name].forEach((uid) => {
if(rpc.info.type === "hook"){
logger.info("Closing consumer `"+uid+"` owned by `"+rpc.name+"`")
rpc.info.unhook(uid)
}
})
})
})
}
private startWebserver(){
if(this.httpServer != null || this.express != null){
logger.warn("Webserver is already running")
return
}
let port:number = kfs.FrontblockAdmin.httpPort
this.express = express()
this.express.use(express.static('static'))
this.httpServer = new http.Server(this.express)
this.httpServer.listen(port, () => {
logger.info('Admin panel listening for HTTP on *'+port)
})
}
private stopWebserver(){
if(this.httpServer == null || this.express == null){
logger.warn("Webserver is not running")
return
}
this.httpServer.close()
this.httpServer = null
this.express = null
logger.info("Webserver stopped")
}
private restartWebserver(port:number){
this.stopWebserver()
kfs.FrontblockAdmin = { httpPort: port }
this.startWebserver()
}
private startWebsocket(){
try{
this.io.attach(this.wsServer)
this.io.on('socket', (socket) => {
logger.info("New Websocket connection on port", socket.port)
const handleError = (e: any) => {
logger.info("Websocket closing", String(e))
socket.close()
}
socket.on('error', handleError)
this.initApis(socket)
})
logger.info('Admin websocket listening on *20000')
this.wsServer.listen(20000)
}catch(e){
logger.error(String(e))
}
}
private rpcToRpcInfo(owner:string, rpc:socketioRPC):rpcInfo{
switch(rpc.type){
case 'hook':
let f = this.hookGenerator(rpc)
return {owner: owner, name: rpc.name, args: this.extractArgs(f(null)), info: { type: 'hook', generator: f, unhook: rpc.unhook } }
case 'unhook':
return {owner: owner, name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'unhook', fn: rpc.rpc } }
case 'call':
return {owner: owner, name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'call', fn: rpc.rpc } }
}
}
/**
* Generates RPC hooks which support a callback.
*
* Note callbacks *need* to accept a singular argument and they have to be the last parameter!
*/
hookGenerator = (rpc:socketioRPC) => {
const argsArr = this.extractArgs(rpc.rpc).split(',')
argsArr.pop()
const args = argsArr.join(',')
return eval(`(socket) => async (`+args+`) => {
const res = await rpc.rpc(`+args+(args.length!==0?',':'')+` (x) => {
socket.call(res.uid, x).catch(e => {
logger.debug(String(e))
})
})
if(res.uid != null){
this.hookToUids[rpc.name].push(res.uid)
}
return res
}`)
}
private extractArgs(f:Function):string{
let fn = String(f)
let args = fn.substr(0, fn.indexOf(")"))
args = args.substr(fn.indexOf("(")+1)
return args
}
}
(async() => {
new FrontblockAdmin()
})()