Browse Source

initial commit

master
peter 2 years ago
commit
369e9171ad

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+node_modules
2
+lib

+ 3
- 0
conf/FrontblockService View File

@@ -0,0 +1,3 @@
1
+{
2
+  "httpPort": 8080
3
+}

+ 7
- 0
jestconfig.json View File

@@ -0,0 +1,7 @@
1
+{
2
+    "transform": {
3
+      "^.+\\.(t|j)sx?$": "ts-jest"
4
+    },
5
+    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
6
+    "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
7
+  }

+ 7851
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 43
- 0
package.json View File

@@ -0,0 +1,43 @@
1
+{
2
+  "name": "assignment1",
3
+  "version": "1.0.0",
4
+  "scripts": {
5
+    "start": "npm run build; node --experimental-modules lib/FrontblockService.js",
6
+    "build": "npm run clean; tsc; node_modules/.bin/webpack",
7
+    "clean": "rm -rf lib",
8
+    "update-frontblock": "rm -rf node_modules/frontblock*; npm install"
9
+  },
10
+  "repository": {
11
+    "type": "git",
12
+    "url": "http://todo"
13
+  },
14
+  "author": "frontblock.me",
15
+  "license": "ISC",
16
+  "dependencies": {
17
+    "@types/express": "^4.16.1",
18
+    "@types/node": "^11.13.13",
19
+    "bsock": "^0.1.9",
20
+    "express": "^4.16.4",
21
+    "frontblock": "latest",
22
+    "http": "0.0.0",
23
+    "key-file-storage": "^2.1.5",
24
+    "log4js": "^4.3.1",
25
+    "node-fetch": "^2.6.0",
26
+    "socket.io": "^2.2.0"
27
+  },
28
+  "devDependencies": {
29
+    "@types/jest": "^24.0.11",
30
+    "jest": "^24.7.1",
31
+    "prettier": "^1.16.4",
32
+    "terser-webpack-plugin": "^1.3.0",
33
+    "ts-jest": "^24.0.2",
34
+    "tslint": "^5.15.0",
35
+    "tslint-config-prettier": "^1.18.0",
36
+    "typescript": "^3.4.2",
37
+    "webpack": "^4.30.0",
38
+    "webpack-cli": "^3.3.1"
39
+  },
40
+  "files": [
41
+    "lib/**/*"
42
+  ]
43
+}

+ 75
- 0
src/FrontblockLib.ts View File

@@ -0,0 +1,75 @@
1
+import { parseSubResponse, parseResponse } from "frontblock-generic/Service";
2
+var bsock = require('bsock')
3
+
4
+/**
5
+ * Dynamic library to communicate with FrontblockService remotely
6
+ *
7
+ * This will be automatically injected into the webpages served by FrontblockService
8
+ * Will ask it's service for available RPCs and parse them into methods of this object
9
+ * for convenient access.
10
+ */
11
+export class FrontblockConfigLib{
12
+    private socket
13
+    
14
+    constructor(){
15
+        this.socket = bsock.connect(20000, 'localhost', false/*tls*/)
16
+        this.init()
17
+    }
18
+
19
+    // need this-context for eval-magic below 
20
+    // DO NOT REMOVE. They're not really unused
21
+    private parseSubResponse = parseSubResponse
22
+    private parseResponse = parseResponse
23
+
24
+    private async init(){
25
+        const info = await this.info()
26
+        for (const i of info) {
27
+            let f: any
28
+            switch (i.info.type) {
29
+                case 'call':
30
+                    f = this.callGenerator(i.name, i.args)    
31
+                    break                
32
+                case 'hook':
33
+                    f = this.hookGenerator(i.name, i.args)                    
34
+                    break
35
+                case 'unhook':
36
+                    f = this.unhookGenerator(i.name, i.args)                    
37
+                    break
38
+            }
39
+
40
+            this[i.name] = f
41
+            this[i.name].bind(this)
42
+        }
43
+    }
44
+
45
+    async info(){
46
+        return await this.socket.call('info')
47
+    }
48
+
49
+    private callGenerator(fnName, fnArgs): Function{
50
+        return eval( '( () => async ('+fnArgs+') => { return await this.socket.call("'+fnName+'", '+fnArgs+')} )()' )
51
+    }
52
+
53
+    private hookGenerator(fnName, fnArgs): Function{
54
+        return eval( `( () => async (`+fnArgs+(fnArgs.length!==0?",":"")+` callback) => {
55
+                            const r = await this.socket.call("`+fnName+`", `+fnArgs+`)
56
+                            const res = await this.parseSubResponse(r);
57
+                            if(res.uid != null){
58
+                                this.socket.hook(res.uid, callback)
59
+                            }
60
+                            return res
61
+                        } )()` )
62
+    }
63
+    private unhookGenerator(fnName, fnArgs): Function{
64
+        return eval( `( () => async (`+fnArgs+`) => {
65
+                            const r = await this.socket.call("`+fnName+`", `+fnArgs+`)
66
+                            const res = await this.parseResponse(r)
67
+                            if(res.uid != null)
68
+                                this.socket.unhook(res.uid)
69
+                            return res
70
+                        } )()` )
71
+    }
72
+
73
+}
74
+
75
+window['fb'] = new FrontblockConfigLib()

+ 222
- 0
src/FrontblockService.ts View File

@@ -0,0 +1,222 @@
1
+'use strict'
2
+
3
+import * as Logger from 'log4js'
4
+import { Plugin, socketioRPC } from 'frontblock-generic/Plugin';
5
+import { ErrorResponse, SuccessResponse } from 'frontblock-generic/Service';
6
+
7
+const pluginList = [
8
+    '../../paymentmanager/static/Plugin',
9
+    'frontblock/FrontblockLib'
10
+]
11
+
12
+
13
+const express = require('express')
14
+const http = require('http')
15
+const bsock = require('bsock')
16
+const kfs = require("key-file-storage")('conf') //'conf' is a directory that will be generated if it doesn't exist
17
+
18
+const logger = Logger.getLogger() // logs to STDOUT  
19
+logger.level = 'debug'
20
+
21
+
22
+type hookRpc = { type: 'hook', generator: (socket) => Function, unhook:(uid:string)=>Promise<ErrorResponse|SuccessResponse>}
23
+type unhookRPC = { type: 'unhook', fn: Function}
24
+type callRPC = { type: 'call', fn: Function}
25
+
26
+export type rpcInfo = {
27
+    name: string,
28
+    args: string,
29
+    info: hookRpc | unhookRPC | callRPC
30
+}
31
+
32
+/**
33
+ * FrontblockAdmin
34
+ * 
35
+ * The customer-facing dynamic component of the customer backend
36
+ * Supports (un)loading plugins which will be communicated to its library component (See FrontblockLib.ts and the info() RPC)
37
+ * 
38
+ * The list of available plugins is published via the frontblock API and downloaded via gitea-releases
39
+ */
40
+export class FrontblockAdmin{
41
+    private plugins: Plugin[]
42
+
43
+    private express
44
+    private httpServer
45
+    private io = bsock.createServer()
46
+    private wsServer = http.createServer()
47
+
48
+    constructor(){
49
+        if(!('FrontblockService' in kfs)){
50
+            logger.warn('No config file found! Generating one')
51
+            kfs.FrontblockService = { httpPort: 8080 }
52
+        }
53
+
54
+        this.initialize()
55
+    }
56
+
57
+    private async initialize(){
58
+        await this.loadPlugins()
59
+        this.plugins.forEach(plugin => plugin.start())
60
+        this.startWebsocket()
61
+        this.startWebserver()
62
+    }
63
+
64
+    private async loadPlugins(){
65
+        const promises = pluginList.map(path => {
66
+            return import(path)
67
+        })
68
+        const pluginsClasses = await Promise.all(promises)
69
+        this.plugins = pluginsClasses.map(clazz => new clazz.default())
70
+    }
71
+
72
+    private initApis(socket){
73
+        const rpcInfos:rpcInfo[] = [
74
+            {
75
+                name: 'restartWebserver',
76
+                args: 'port',
77
+                info:{
78
+                    type:'call', 
79
+                    fn: (port:number) => { this.restartWebserver(port) } 
80
+                }
81
+            },{
82
+                name: 'info', 
83
+                args: '',
84
+                info:{
85
+                    type:'call', 
86
+                    fn: () => { return rpcInfos } 
87
+                }
88
+            }
89
+        ]
90
+
91
+        this.plugins.forEach(plugin => {
92
+            plugin.exportRPCs().forEach(rpc => rpcInfos.push(this.rpcToRpcInfo(rpc)))
93
+        })
94
+
95
+        for(const api of rpcInfos){
96
+            switch(api.info.type){
97
+                case 'call':
98
+                    socket.hook(api.name, api.info.fn)
99
+                break
100
+                case 'hook':
101
+                    const hook = api.info.generator(socket)
102
+                    hook.bind(this)
103
+                    socket.hook(api.name, hook)
104
+                break
105
+                case 'unhook':
106
+                    socket.hook(api.name, api.info.fn)
107
+                break
108
+            }
109
+        }
110
+
111
+        socket.on('close', () => {
112
+            logger.info("Client disconnected")
113
+            rpcInfos.forEach(rpc => {
114
+                socket.unhook(rpc.name)
115
+            })
116
+        })
117
+  
118
+    }
119
+
120
+    private startWebserver(){
121
+        if(this.httpServer != null || this.express != null){
122
+            logger.warn("Webserver is already running")
123
+            return
124
+        }
125
+        
126
+        let port:number = kfs.FrontblockService.httpPort
127
+        this.express = express()
128
+        this.express.use(express.static('static'))
129
+
130
+        this.httpServer = http.Server(this.express)
131
+        this.httpServer.listen(port, () => {
132
+            logger.info('listening on *'+port)
133
+        })
134
+    }
135
+
136
+    private stopWebserver(){
137
+        if(this.httpServer == null || this.express == null){
138
+            logger.warn("Webserver is not running")
139
+            return
140
+        }
141
+        this.httpServer.close()
142
+        this.httpServer = null
143
+        this.express = null
144
+        logger.info("Webserver stopped")
145
+    }
146
+
147
+    private restartWebserver(port:number){
148
+        this.stopWebserver()
149
+        kfs.FrontblockService = { httpPort: port }
150
+        this.startWebserver()
151
+    }
152
+
153
+    private startWebsocket(){
154
+        try{
155
+            this.io.attach(this.wsServer)
156
+            this.io.on('socket', (socket) => {
157
+                logger.info("New Websocket connection on port", socket.port)
158
+
159
+                const handleError = (e: any) => {
160
+                    logger.info("Websocket closing", String(e))
161
+                    socket.close()
162
+                }
163
+                
164
+                socket.on('error', handleError)
165
+                this.initApis(socket)
166
+            })
167
+            logger.info('websocket listening on *20000')
168
+            this.wsServer.listen(20000)
169
+        }catch(e){
170
+            logger.error(String(e))
171
+        }
172
+    }
173
+
174
+    private rpcToRpcInfo(rpc:socketioRPC):rpcInfo{
175
+        switch(rpc.type){
176
+            case 'hook':
177
+                let f = this.hookGenerator(rpc)
178
+                return {name: rpc.name, args: this.extractArgs(f(null)), info: { type: 'hook', generator: f, unhook: rpc.unhook } }
179
+            case 'unhook':
180
+                return {name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'unhook', fn: rpc.rpc } }
181
+            case 'call':
182
+                return {name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'call', fn: rpc.rpc } }
183
+        }
184
+    }
185
+
186
+    /**
187
+     * Generates RPC hooks which support a callback. 
188
+     * 
189
+     * Note callbacks *need* to accept a singular argument and they have to be the last parameter!
190
+     * 
191
+     * TODO: Maybe kill open callbacks when socket closes? See rpc.info.unhook for the appropriate function to call.
192
+     */
193
+    hookGenerator = (rpc) => { 
194
+        const argsArr = this.extractArgs(rpc.rpc).split(',')
195
+        argsArr.pop()
196
+        const args = argsArr.join(',')
197
+
198
+        return eval(`(socket) => async (`+args+`) => { 
199
+            const res = await rpc.rpc(`+args+(args.length!==0?',':'')+` (x) => {
200
+                if(res.uid != null){
201
+                    console.log("calling "+res.uid)
202
+
203
+                    socket.call(res.uid, x).catch(e => {
204
+                        rpc.unhook(res.uid)
205
+                    })
206
+                }
207
+            })
208
+            return res
209
+        }`)
210
+    }
211
+
212
+    private extractArgs(f:Function):string{
213
+        let fn = String(f)
214
+        let args = fn.substr(0, fn.indexOf(")"))
215
+        args = args.substr(fn.indexOf("(")+1)
216
+        return args
217
+    }
218
+}
219
+
220
+(async() => {
221
+    new FrontblockAdmin()
222
+})()

+ 4
- 0
src/__tests__/Greeter.test.ts View File

@@ -0,0 +1,4 @@
1
+import { Greeter } from '../index';
2
+test('My Greeter', () => {
3
+  expect(Greeter('Carl')).toBe('Hello Carl');
4
+});

+ 19
- 0
static/FrontblockLib.js
File diff suppressed because it is too large
View File


+ 12
- 0
static/index.html View File

@@ -0,0 +1,12 @@
1
+<html>
2
+    <head>
3
+        <script src="FrontblockLib.js"></script>
4
+    </head>
5
+
6
+    <body>
7
+
8
+        yo whatup <a href="/wallet">wallets are here</a><br />
9
+        yo whatup <a href="/paymentmanager">paymentmanager is here</a><br />
10
+    </body>
11
+
12
+</html>

+ 167
- 0
static/paymentmanager/Plugin.js
File diff suppressed because it is too large
View File


+ 3
- 0
static/paymentmanager/css/app.css
File diff suppressed because it is too large
View File


+ 1
- 0
static/paymentmanager/index.html View File

@@ -0,0 +1 @@
1
+<!doctype html> <html lang=en> <head> <meta charset=UTF-8> <title>Document</title> <link href="css/app.css?v=a5f963f6" rel="stylesheet"></head> <body> <root></root> <script type="text/javascript" src="js/vendor.461d1c56.js"></script><script type="text/javascript" src="js/app.5d10c2e6.js"></script></body> </html> 

+ 24
- 0
static/paymentmanager/js/app.5d10c2e6.js
File diff suppressed because it is too large
View File


+ 7
- 0
static/paymentmanager/js/vendor.461d1c56.js
File diff suppressed because it is too large
View File


+ 4
- 0
static/wallet/css/app.css
File diff suppressed because it is too large
View File


+ 1
- 0
static/wallet/index.html View File

@@ -0,0 +1 @@
1
+<!doctype html> <html lang=en> <head> <meta charset=UTF-8> <title>Document</title> <link href="css/app.css?v=a54d3946" rel="stylesheet"></head> <body> <root></root> <script type="text/javascript" src="js/vendor.195e4982.js"></script><script type="text/javascript" src="js/app.10653f4e.js"></script></body> </html> 

+ 24
- 0
static/wallet/js/app.10653f4e.js
File diff suppressed because it is too large
View File


+ 7
- 0
static/wallet/js/vendor.195e4982.js
File diff suppressed because it is too large
View File


+ 13
- 0
tsconfig.json View File

@@ -0,0 +1,13 @@
1
+{
2
+    "compilerOptions": {
3
+      "strictPropertyInitialization": false,
4
+      "noImplicitAny": false,
5
+      "target": "ESnext",
6
+      "module": "commonjs",
7
+      "declaration": true,
8
+      "outDir": "./lib",
9
+      "strict": true
10
+    },
11
+    "include": ["src"],
12
+    "exclude": ["node_modules", "**/__tests__/*"],
13
+}

+ 3
- 0
tslint.json View File

@@ -0,0 +1,3 @@
1
+{
2
+    "extends": ["tslint:recommended", "tslint-config-prettier"]
3
+}

+ 46
- 0
webpack.config.js View File

@@ -0,0 +1,46 @@
1
+var path = require('path');
2
+const TerserPlugin = require('terser-webpack-plugin');
3
+
4
+
5
+module.exports = {
6
+    mode: 'production',
7
+    entry: path.resolve(__dirname, 'lib/FrontblockLib.js'),
8
+    output: {
9
+        path: path.resolve(__dirname, 'static'),
10
+        filename: 'FrontblockLib.js',
11
+    },
12
+    module: {
13
+        rules: [
14
+          { test: /\FrontblockLib.ts/, use: 'ts-loader' }
15
+        ]
16
+    },
17
+    optimization: {
18
+        minimizer: [new TerserPlugin({
19
+          cache: true,
20
+          parallel: true,
21
+          terserOptions:{
22
+            mangle: false,
23
+            keep_classnames: true        
24
+          }
25
+        })],
26
+        splitChunks: {
27
+          cacheGroups: {
28
+            common: {
29
+              chunks: 'all',
30
+              minChunks: 2,
31
+              maxInitialRequests: 5,
32
+              minSize: 0,
33
+              name: 'common'
34
+            },
35
+            vendor: {
36
+              test: /node_modules/,
37
+              chunks: 'all',
38
+              name: 'vendor',
39
+              priority: 10,
40
+              enforce: true,
41
+              minChunks: 2
42
+            }
43
+          }
44
+        }
45
+      },
46
+};

Loading…
Cancel
Save