mirror of
https://github.com/Rolands-Laucis/Socio.git
synced 2026-05-15 14:15:57 -06:00
new features and working vite plugin
This commit is contained in:
parent
d4c5cffcde
commit
2510d05a39
10 changed files with 257 additions and 111 deletions
|
|
@ -1,22 +1,26 @@
|
|||
//https://stackoverflow.com/questions/38946112/es6-import-error-handling
|
||||
let info = null
|
||||
try { //for my logger
|
||||
await import('@rolands/log')
|
||||
var { info, log, error, done, soft_error, setPrefix, setShowTime } = await import('@rolands/log')
|
||||
setPrefix('Socio Client')
|
||||
setShowTime(false)
|
||||
} catch (e) {
|
||||
info = (...objs) => console.log('[Socio Client]', ...objs)
|
||||
console.log('[Socio Client ERROR]', e)
|
||||
var info = (...objs) => console.log('[Socio Client]', ...objs)
|
||||
var done = (...objs) => console.log('[Socio Client]', ...objs)
|
||||
var log = (...objs) => console.log('[Socio Client]', ...objs)
|
||||
var soft_error = (...objs) => console.log('[Socio Client]', ...objs)
|
||||
}
|
||||
|
||||
//"Because he not only wants to perform well, he wants to be well received — and the latter lies outside his control." /Epictetus/
|
||||
export class WSClient {
|
||||
// private:
|
||||
#queries = {} //sql:[callback]
|
||||
#queries = {} //id:[callback]
|
||||
#is_ready = false
|
||||
#verbose = false
|
||||
#ws=null
|
||||
static #key = 0 //all instances will share this number, such that they are always kept unique. Tho each of these clients would make a different session on the backend, but still
|
||||
|
||||
constructor(url, {name = '', verbose=false, keep_alive=true, reconnect_tries=3, push_callback=null} = {}) {
|
||||
constructor(url, {name = '', verbose=false, keep_alive=true, reconnect_tries=1, push_callback=null} = {}) {
|
||||
if (window || undefined && url.startsWith('ws://'))
|
||||
info('UNSECURE WEBSOCKET URL CONNECTION! Please use wss:// and https:// protocols in production to protect against man-in-the-middle attacks.')
|
||||
|
||||
|
|
@ -25,35 +29,36 @@ export class WSClient {
|
|||
this.push = push_callback
|
||||
this.#connect(url, keep_alive, verbose, reconnect_tries)
|
||||
|
||||
this.#ws.addEventListener('message', this.message.bind(this));
|
||||
this.#ws.addEventListener('message', this.#message.bind(this));
|
||||
}
|
||||
|
||||
#connect(url, keep_alive, verbose, reconnect_tries){
|
||||
this.#ws = new WebSocket(url)
|
||||
if (keep_alive && reconnect_tries)
|
||||
this.#ws.addEventListener("close", () => {
|
||||
if (this.#verbose) info(`WebSocket closed. Retrying...`, this.name);
|
||||
if (this.#verbose) soft_error(`WebSocket closed. Retrying...`, this.name);
|
||||
this.#connect(url, keep_alive, verbose, reconnect_tries - 1)
|
||||
}); // <- rise from your grave!
|
||||
}
|
||||
|
||||
message(e) {
|
||||
#message(e) {
|
||||
const [kind, data] = JSON.parse(e.data)
|
||||
if (this.#verbose) info(kind, data)
|
||||
if (this.#verbose) info('recv:',kind, data)
|
||||
|
||||
switch(kind){
|
||||
case 'CON': this.ses_id = data; this.#is_ready = true; if (this.#verbose) info(`WebSocket connected.`, this.name); break;
|
||||
case 'CON': this.ses_id = data; this.#is_ready = true; if (this.#verbose) done(`WebSocket connected.`, this.name); break;
|
||||
case 'UPD':
|
||||
if (data.sql in this.#queries)
|
||||
this.#queries[data.sql].forEach(f => f(data.result));
|
||||
else if (this.#verbose) info(`UPD message for unregistered SQL query! [${data.sql}] with data:`, data)
|
||||
if (data.id in this.#queries)
|
||||
this.#queries[data.id].f.forEach(f => f(data.result));
|
||||
else if (this.#verbose) soft_error(`${kind} message for unregistered SQL query! [${data.id}] with data:`, data)
|
||||
break;
|
||||
case 'SQL':
|
||||
if (data.sql in this.#queries)
|
||||
this.#queries[data.sql](data.result);
|
||||
if (data.id in this.#queries)
|
||||
this.#queries[data.id](data.result);
|
||||
else if (this.#verbose) soft_error(`${kind} message for unregistered SQL query! [${data.id}] with data:`, data)
|
||||
break;
|
||||
case 'PONG': if (this.#verbose) info('pong', data?.num); break;
|
||||
case 'PUSH': this.push(data); break;
|
||||
case 'PONG': if (this.#verbose) info('pong', data?.id); break;
|
||||
// case 'PUSH': this.push(data); break;
|
||||
// case '': break;
|
||||
default: info(`Unrecognized message kind! [${kind}] with data:`, data);
|
||||
}
|
||||
|
|
@ -74,29 +79,46 @@ export class WSClient {
|
|||
//private method
|
||||
#send(data=[]){
|
||||
this.#ws.send(JSON.stringify([this.ses_id, ...data]))
|
||||
if (this.#verbose) info('sent:', ...data)
|
||||
}
|
||||
|
||||
//subscribe to an sql query. Can add multiple callbacks where ever in your code, if their sql queries are identical
|
||||
subscribe({ sql = '', params = null } = {}, callback = null, t=null){
|
||||
if (sql in this.#queries)
|
||||
this.#queries[sql].push(t ? callback.bind(t) : callback)
|
||||
const found = Object.entries(this.#queries).find(q => q[1].sql === sql)
|
||||
|
||||
if (found)
|
||||
this.#queries[found[0]].f.push(t ? callback.bind(t) : callback)
|
||||
else{
|
||||
this.#queries[sql] = [t ? callback.bind(t) : callback]
|
||||
this.#send(['REG', { sql: sql, params: params }])
|
||||
const id = this.#gen_key
|
||||
this.#queries[id] = { sql: sql, f: [t ? callback.bind(t) : callback] }
|
||||
this.#send(['REG', { id: id, sql: sql, params: params }])
|
||||
}
|
||||
// info('Registered', sql)
|
||||
}
|
||||
|
||||
async query(sql='', params=null){
|
||||
//set up a promise which resolve function is in the queries data structure, such that in the message handler it can be called, therefor the promise resolved, therefor awaited and return from this function
|
||||
const id = this.#gen_key;
|
||||
const prom = new Promise((res) => {
|
||||
this.#queries[sql] = res
|
||||
this.#queries[id] = res
|
||||
})
|
||||
//send off the request, which will be resolved in the message handler
|
||||
this.#send(['SQL', { sql: sql, params: params }])
|
||||
this.#send(['SQL', { id:id, sql: sql, params: params }])
|
||||
return await prom
|
||||
}
|
||||
|
||||
//sends a ping with either the user provided number or an auto generated number, for keeping track of packets and debugging
|
||||
ping(num=0){
|
||||
this.#send(['PING', { num: num }])
|
||||
this.#send(['PING', { id: num || this.#gen_key }])
|
||||
}
|
||||
|
||||
//generates a unique key either via static counter or user provided key gen func
|
||||
get #gen_key() {
|
||||
if (this?.key_generator)
|
||||
return this.key_generator()
|
||||
else{
|
||||
WSClient.#key += 1
|
||||
return WSClient.#key //neat js trick - symbols are unique even if their strings are identical
|
||||
}
|
||||
}
|
||||
}
|
||||
131
core/core.js
131
core/core.js
|
|
@ -8,58 +8,70 @@ import { WebSocketServer } from 'ws'; //https://github.com/websockets/ws https:/
|
|||
export class SessionManager{
|
||||
// private:
|
||||
#wss=null
|
||||
#sessions = {}//ses_id:websocket
|
||||
#verbose=false
|
||||
#sessions = {}//client_id:websocket
|
||||
#secure=null
|
||||
#lifecycleHooks = { CON: [], DISCON: [], message: [], update: [] } //, '': []
|
||||
#lifecycleHooks = { con: null, discon: null, msg: null, upd: null }
|
||||
|
||||
constructor(opts = {}, DB_query_callback = null, { secure =null, verbose = false } = {}){
|
||||
constructor(opts = {}, DB_query_function = null, { secure =null, verbose = false } = {}){
|
||||
setPrefix('Socio'); setShowTime(false); //for my logger
|
||||
|
||||
this.#wss = new WebSocketServer(opts); //take a look at the WebSocketServer docs - the opts can have a server param, that can be your http server
|
||||
this.Query = DB_query_callback
|
||||
this.#verbose = verbose
|
||||
this.Query = DB_query_function
|
||||
this.verbose = verbose
|
||||
this.#secure = secure
|
||||
|
||||
this.#wss.on('connection', this.Connect.bind(this)); //https://thenewstack.io/mastering-javascript-callbacks-bind-apply-call/ have to bind 'this' to the function, otherwise it will use the .on()'s 'this', so that this.[prop] are not undefined
|
||||
this.#wss.on('connection', this.#Connect.bind(this)); //https://thenewstack.io/mastering-javascript-callbacks-bind-apply-call/ have to bind 'this' to the function, otherwise it will use the .on()'s 'this', so that this.[prop] are not undefined
|
||||
this.#wss.on('close', (...stuff) => { info('WebSocketServer close event', ...stuff) });
|
||||
this.#wss.on('error', (...stuff) => { error('WebSocketServer error event', ...stuff)});
|
||||
}
|
||||
|
||||
async Connect(conn, req){
|
||||
const ses_id = UUID()
|
||||
this.#sessions[ses_id] = new Session(ses_id, conn)
|
||||
conn.send(JSON.stringify(['CON', ses_id]));
|
||||
if (this.#verbose) info('CON', ses_id)
|
||||
#Connect(conn, req){
|
||||
//construct the new session with a unique ID
|
||||
const client_id = UUID()
|
||||
this.#sessions[client_id] = new Session(client_id, conn, this.verbose)
|
||||
|
||||
conn.on('message', this.Message.bind(this));
|
||||
//pass the object to the connection hook, if it exists
|
||||
if (this.#lifecycleHooks.con)
|
||||
this.#lifecycleHooks.con(this.#sessions[client_id])
|
||||
|
||||
//notify the client of their ID
|
||||
conn.send(JSON.stringify(['CON', client_id]));
|
||||
if (this.verbose) info('CON', client_id)
|
||||
|
||||
//set this client websockets event handlers
|
||||
conn.on('message', this.#Message.bind(this));
|
||||
conn.on('close', () => {
|
||||
if (this.#verbose) info('DISCON', ses_id)
|
||||
delete this.#sessions[ses_id]
|
||||
//trigger hook
|
||||
if (this.#lifecycleHooks.discon)
|
||||
this.#lifecycleHooks.discon(this.#sessions[client_id])
|
||||
|
||||
//delete the connection object
|
||||
delete this.#sessions[client_id]
|
||||
if (this.verbose) info('DISCON', client_id)
|
||||
});
|
||||
}
|
||||
|
||||
async Message(req, head){
|
||||
const [ses_id, kind, data] = JSON.parse(req.toString())
|
||||
if (this.#verbose) info(`received [${kind}] from [${ses_id}]`);
|
||||
async #Message(req, head){
|
||||
const [client_id, kind, data] = JSON.parse(req.toString())
|
||||
if (this.#secure && data?.sql) data.sql = this.#secure.DecryptString(data.sql) //if this is supposed to be secure and sql was received, then decrypt it before continuing
|
||||
if (this.verbose) info(`received [${kind}] from [${client_id}]`, data);
|
||||
|
||||
switch (kind) {
|
||||
case 'REG':
|
||||
if(ses_id in this.#sessions)
|
||||
this.#sessions[ses_id].Send(['UPD', { sql: data.sql, result: await this.Query(data.sql, data.params) }])
|
||||
if(client_id in this.#sessions)
|
||||
this.#sessions[client_id].Send(['UPD', { id:data.id, result: await this.Query(data.sql, data.params) }])
|
||||
|
||||
//set up hook
|
||||
if (QueryUtils.QueryIsSelect(data.sql))
|
||||
QueryUtils.ParseSQLForTables(data.sql).forEach(t => this.#sessions[ses_id].RegisterHook(t, data.sql, data.params));
|
||||
QueryUtils.ParseSQLForTables(data.sql).forEach(t => this.#sessions[client_id].RegisterHook(t, data.id, data.sql, data.params));
|
||||
|
||||
break;
|
||||
case 'SQL':
|
||||
const is_select = QueryUtils.QueryIsSelect(data.sql)
|
||||
if (ses_id in this.#sessions){
|
||||
if (client_id in this.#sessions){
|
||||
const res = this.Query(data.sql, data.params)
|
||||
if (is_select) //wait for result
|
||||
this.#sessions[ses_id].Send(['SQL', { sql: data.sql, result: await res }])
|
||||
this.#sessions[client_id].Send(['SQL', { id: data.id, result: await res }])
|
||||
}
|
||||
|
||||
//if the sql wasnt a SELECT, but altered some resource, then need to propogate that to other connection hooks
|
||||
|
|
@ -67,65 +79,94 @@ export class SessionManager{
|
|||
this.Update(QueryUtils.ParseSQLForTables(data.sql))
|
||||
|
||||
break;
|
||||
case 'PING': this.#sessions[ses_id].Send(['PONG', { num: data?.num}]); break;
|
||||
case 'PING': this.#sessions[client_id].Send(['PONG', { id: data?.id}]); break;
|
||||
// case '': break;
|
||||
default: if (this.#verbose) error(`Unrecognized message kind! [${kind}] with data:`, data);
|
||||
default: if (this.verbose) error(`Unrecognized message kind! [${kind}] with data:`, data);
|
||||
}
|
||||
}
|
||||
|
||||
//OPTIMIZATION dont await the query, but queue up all of them on another thread then await and send there
|
||||
async Update(tables=[]){
|
||||
// if (this.#lifecycleHooks.update) this.#lifecycleHooks.update.forEach(f => f(tables)) //call all the lifecycle hooks
|
||||
|
||||
Object.values(this.#sessions).forEach(async (s) => {
|
||||
tables.forEach(async (t) => {
|
||||
if(t in s.hooks)
|
||||
for await (const data of s.hooks[t]){
|
||||
s.Send(['UPD', { sql: data.sql, result: (await this.Query(data.sql, data.params)) }])
|
||||
if (s.hook_tables.includes(t)){
|
||||
for await (const hook of s.GetHookObjs(t)) {
|
||||
s.Send(['UPD', { id: hook.id, result: (await this.Query(hook.sql, hook.params)) }])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//when the server wants to send some data to a specific session client - can be any raw data
|
||||
SendTo(ses_id='', data={}){
|
||||
if (ses_id in this.#sessions)
|
||||
this.#sessions[ses_id].Send(['PUSH', data])
|
||||
else soft_error(`The provided session ID [${ses_id}] was not found in the tracked web socket connections!`)
|
||||
SendTo(client_id='', data={}){
|
||||
if (client_id in this.#sessions)
|
||||
this.#sessions[client_id].Send(['PUSH', data])
|
||||
else soft_error(`The provided session ID [${client_id}] was not found in the tracked web socket connections!`)
|
||||
}
|
||||
|
||||
Emit(data=[]){
|
||||
this.#wss.emit(JSON.stringify(['EMIT', ...data]));
|
||||
}
|
||||
|
||||
RegisterLifecycleHook(name='', callback){
|
||||
RegisterLifecycleHookHandler(name='', handler=null){
|
||||
if(name in this.#lifecycleHooks)
|
||||
this.#lifecycleHooks.push(callback)
|
||||
this.#lifecycleHooks[name] = handler
|
||||
else
|
||||
error(`Lifecycle hook [${name}] does not exist!`)
|
||||
}
|
||||
|
||||
UnRegisterLifecycleHookHandler(name = '') {
|
||||
if (name in this.#lifecycleHooks)
|
||||
delete this.#lifecycleHooks[name]
|
||||
else
|
||||
error(`Lifecycle hook [${name}] does not exist!`)
|
||||
}
|
||||
|
||||
get LifecycleHookNames(){
|
||||
return Object.keys(this.#lifecycleHooks)
|
||||
}
|
||||
|
||||
GetClientSession(client_id=''){
|
||||
return this.#sessions[client_id]
|
||||
}
|
||||
|
||||
ClientIDsOfSession(ses_id = ''){
|
||||
return this.#sessions.filter(s => s.ses_id === ses_id).map(s => s.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Homo vitae commodatus non donatus est. - Man's life is lent, not given. /Syrus/
|
||||
class Session{
|
||||
constructor(session_id='', browser_ws_conn){
|
||||
this.id = session_id
|
||||
this.ws = browser_ws_conn
|
||||
this.hooks = {} //table_name:[sql]
|
||||
#ws=null
|
||||
#hooks=[]
|
||||
|
||||
constructor(client_id='', browser_ws_conn=null, verbose=false){
|
||||
this.id = client_id
|
||||
this.ses_id = null
|
||||
this.#ws = browser_ws_conn
|
||||
this.#hooks = {} //table_name:[sql]
|
||||
this.verbose = verbose
|
||||
}
|
||||
|
||||
RegisterHook(table='', sql='', params=null){ //TODO this is actually very bad
|
||||
if (table in this.hooks && !this.hooks[table].find((t) => t.sql == sql && t.params == params))
|
||||
this.hooks[table].push({ sql: sql, params: params })
|
||||
RegisterHook(table='', id='', sql='', params=null){ //TODO this is actually very bad
|
||||
if (table in this.#hooks && !this.#hooks[table].find((t) => t.sql == sql && t.params == params))
|
||||
this.#hooks[table].push({ id: id, sql: sql, params: params })
|
||||
else
|
||||
this.hooks[table] = [{ sql:sql, params:params}]
|
||||
// log('reg hook', table, this.hooks[table])
|
||||
this.#hooks[table] = [{ id: id, sql:sql, params:params}]
|
||||
log('reg hook', table, this.#hooks[table])
|
||||
}
|
||||
|
||||
Send(data=[]){
|
||||
this.ws.send(JSON.stringify(data))
|
||||
this.#ws.send(JSON.stringify(data))
|
||||
if (this.verbose) info('sent:', ...data)
|
||||
}
|
||||
|
||||
get hook_tables(){return Object.keys(this.#hooks)}
|
||||
|
||||
GetHookObjs(table = '') { return this.#hooks[table]}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
34
core/package-lock.json
generated
34
core/package-lock.json
generated
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"name": "socio",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "socio",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@rolands/log": "^1.1.1",
|
||||
"magic-string": "^0.26.7",
|
||||
"ws": "^8.9.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -18,6 +19,22 @@
|
|||
"resolved": "https://registry.npmjs.org/@rolands/log/-/log-1.1.1.tgz",
|
||||
"integrity": "sha512-j8jZxm0tF+7ke6V+qtdoCR2KlOba3Mog39QyxU+3pbmgJ/wuIfF7QuIqTSLqrtPQY5FzQjCwlJcvIizp33UYxg=="
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.26.7",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
|
||||
"integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
|
||||
"dependencies": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz",
|
||||
|
|
@ -45,6 +62,19 @@
|
|||
"resolved": "https://registry.npmjs.org/@rolands/log/-/log-1.1.1.tgz",
|
||||
"integrity": "sha512-j8jZxm0tF+7ke6V+qtdoCR2KlOba3Mog39QyxU+3pbmgJ/wuIfF7QuIqTSLqrtPQY5FzQjCwlJcvIizp33UYxg=="
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.26.7",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
|
||||
"integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
|
||||
"requires": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "socio",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "a websocket based live and synced front and back end",
|
||||
"main": "core.js",
|
||||
"type": "module",
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
"homepage": "https://github.com/Rolands-Laucis/Socio.js/blob/master/README.md",
|
||||
"dependencies": {
|
||||
"@rolands/log": "^1.1.1",
|
||||
"magic-string": "^0.26.7",
|
||||
"ws": "^8.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,36 @@
|
|||
import { randomUUID, createCipheriv, createDecipheriv } from 'crypto'
|
||||
import MagicString from 'magic-string'; //https://github.com/Rich-Harris/magic-string
|
||||
import { randomUUID, createCipheriv, createDecipheriv, getCiphers } from 'crypto'
|
||||
|
||||
try { //for my logger
|
||||
var { info, log, error, done, setPrefix, setShowTime } = await import('@rolands/log')
|
||||
setPrefix('Socio Secure')
|
||||
setShowTime(false)
|
||||
} catch (e) {
|
||||
console.log('[Socio Secure ERROR]', e)
|
||||
var info = (...objs) => console.log('[Socio Secure]', ...objs)
|
||||
var done = (...objs) => console.log('[Socio Secure]', ...objs)
|
||||
var log = (...objs) => console.log('[Socio Secure]', ...objs)
|
||||
}
|
||||
|
||||
//https://vitejs.dev/guide/api-plugin.html
|
||||
export function SocioSecurityPlugin({ secure_private_key = '', cipther_algorithm = 'aes-256-ctr', cipher_iv = '', verbose = false } = {}){
|
||||
const ss = new SocioSecurity({secure_private_key:secure_private_key, cipther_algorithm:cipther_algorithm, cipher_iv:cipher_iv, verbose:verbose})
|
||||
return{
|
||||
name:'vite-socio-security',
|
||||
enforce: 'pre',
|
||||
transform(code, id){
|
||||
const ext = id.split('.').slice(-1)[0]
|
||||
if (['js', 'svelte', 'vue', 'jsx', 'ts'].includes(ext) && !id.match(/\/(node_modules|socio\/(core|core-client|secure))\//)) { // , 'svelte'
|
||||
const s = ss.SecureSouceCode(code) //uses MagicString lib
|
||||
// log(id, s.toString())
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: s.generateMap({source:id, includeContent:true})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//The aim of the wise is not to secure pleasure, but to avoid pain. /Aristotle/
|
||||
export class SocioSecurity{
|
||||
|
|
@ -6,28 +38,35 @@ export class SocioSecurity{
|
|||
#key=''
|
||||
#algo=''
|
||||
#iv=''
|
||||
#sql_string_regex = /(?<pre>\.subscribe\(\s*|\.query\(\s*|sql\s*:\s*)"(?<sql>[^"]+?)(?<post>--socio)"/ig
|
||||
|
||||
constructor({ secure_private_key = '', cipther_algorithm = 'AES-256-ctr', cipher_iv =''} = {}){
|
||||
constructor({ secure_private_key = '', cipther_algorithm = 'aes-256-ctr', cipher_iv ='', verbose=false} = {}){
|
||||
if (!cipher_iv) cipher_iv = UUID()
|
||||
if (!secure_private_key || !cipther_algorithm || !cipher_iv) throw `Missing constructor arguments!`
|
||||
if (secure_private_key.length < 32) throw `secure_private_key has to be at least 32 characters! Got ${secure_private_key.length}`
|
||||
if (cipher_iv.length < 16) throw `cipher_iv has to be at least 16 characters! Got ${cipher_iv.length}`
|
||||
if (!crypto.getCiphers().includes(cipther_algorithm)) throw `Unsupported algorithm [${cipther_algorithm}] by the Node.js Crypto module!`
|
||||
if (!(getCiphers().includes(cipther_algorithm))) throw `Unsupported algorithm [${cipther_algorithm}] by the Node.js Crypto module!`
|
||||
|
||||
const te = new TextEncoder()
|
||||
|
||||
this.#key = te.encode(secure_private_key).slice(0,32) //has to be this length
|
||||
this.#algo = cipther_algorithm
|
||||
this.#iv = te.encode(cipher_iv).slice(0, 16) //has to be this length
|
||||
|
||||
if (verbose) done('Initialized SocioSecurity object succesfully')
|
||||
}
|
||||
|
||||
//sql strings must be in single quotes and have an sql single line comment at the end with the name socio - "--socio"
|
||||
Secure(source_code = '') {
|
||||
const sql_string_regex = /'(?<sql>[^']+?)--socio'/i
|
||||
return source_code.split('\n').map(line => {
|
||||
const m = line.match(sql_string_regex)
|
||||
return m?.groups?.sql ? line.replace(sql_string_regex, '\'' + this.EncryptString(m.groups.sql) + '\'') : line
|
||||
}).join('\n')
|
||||
//sql strings must be in double quotes and have an sql single line comment at the end with the name socio - "--socio" ^ see the sql_string_regex pattern
|
||||
SecureSouceCode(source_code = '') {
|
||||
const s = new MagicString(source_code);
|
||||
|
||||
for (const m of source_code.matchAll(this.#sql_string_regex)){
|
||||
if (m?.groups?.sql){
|
||||
s.update(m.index, m.index + m[0].length, `${m.groups.pre}\"` + this.EncryptString(m.groups.sql) + `\"`)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
EncryptString(query = '') {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { SessionManager } from 'socio/core.js' //for using the lib as a download
|
|||
|
||||
import express from 'express'
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { log, info, setPrefix, setShowTime } from '@rolands/log'; setPrefix('EXPRESS'); setShowTime(false);
|
||||
import { log, done, setPrefix, setShowTime } from '@rolands/log'; setPrefix('EXPRESS'); setShowTime(false);
|
||||
|
||||
//constants
|
||||
const server_port = 5000, ws_port = 3000 //can be set up that the websockets run on the same port as the http server
|
||||
|
|
@ -19,7 +19,7 @@ await sequelize.query('INSERT INTO Users VALUES("John", 69);')
|
|||
//Either you in a wrapper function or your DB interface lib should do the sql validation and sanitization, as this lib does not!
|
||||
const QueryWrap = async (sql='', params={}) => (await sequelize.query(sql, { logging: false, raw: true, replacements: params }))[0]
|
||||
const manager = new SessionManager({ port: ws_port }, QueryWrap, {verbose:true} )
|
||||
info(`Created SessionManager on port`, ws_port)
|
||||
done(`Created SessionManager on port`, ws_port)
|
||||
|
||||
//init
|
||||
// const sec = Secure({})
|
||||
|
|
@ -33,5 +33,5 @@ app.use("", express.static(__dirname));
|
|||
app.use("/", express.static(__dirname + "\\..\\..\\core"));
|
||||
|
||||
app.listen(server_port, () => {
|
||||
info(`Express webserver listening on port`, server_port, `http://localhost:${server_port}/`)
|
||||
done(`Express webserver listening on port`, server_port, `http://localhost:${server_port}/`)
|
||||
})
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
// import { SessionManager } from '../../core/core.js' //i use this locally
|
||||
// import { SocioSecurity } from '../../core/secure.js' //i use this locally
|
||||
import {SessionManager} from 'socio/core.js' //for using the lib as a download from npm
|
||||
import { SocioSecurity } from 'socio/secure.js' //for using the lib as a download from npm
|
||||
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { log, info, setPrefix, setShowTime } from '@rolands/log'; setPrefix('SERVER'); setShowTime(false);
|
||||
import { log, done, setPrefix, setShowTime } from '@rolands/log'; setPrefix('SERVER'); setShowTime(false);
|
||||
|
||||
//constants
|
||||
const ws_port = 3000 //can be set up that the websockets run on the same port as the http server
|
||||
|
|
@ -17,8 +19,9 @@ await sequelize.query('INSERT INTO Users VALUES("John", 69);')
|
|||
//it needs the raw sql string, which can contain formatting parameters - insert dynamic data into the string.
|
||||
//Either you in a wrapper function or your DB interface lib should do the sql validation and sanitization, as this lib does not!
|
||||
const QueryWrap = async (sql = '', params = {}) => (await sequelize.query(sql, { logging: false, raw: true, replacements: params }))[0]
|
||||
const manager = new SessionManager({ port: ws_port }, QueryWrap, { verbose: true })
|
||||
info(`Created SessionManager on port`, ws_port)
|
||||
|
||||
//init
|
||||
// const sec = Secure({})
|
||||
//note that these key and iv are here for demonstration purposes and you should always generate your own. You may also supply any cipher algorithm supported by node's crypto module
|
||||
const ss = new SocioSecurity({ secure_private_key: 'skk#$U#Y$7643GJHKGDHJH#$K#$HLI#H$KBKDBDFKU34534', cipher_iv: 'dsjkfh45h4lu45ilULIY$%IUfdjg', verbose:true })
|
||||
const manager = new SessionManager({ port: ws_port }, QueryWrap, { verbose: true, secure:ss })
|
||||
|
||||
done(`Created SessionManager on port`, ws_port)
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
let ws = null
|
||||
let clienID = false
|
||||
const static_queries = [{text:'once:', sql:'SELECT 42+69 AS RESULT;'}, {text:'once:', sql:'SELECT COUNT(*) AS RESULT FROM users;'}]
|
||||
const static_queries = [{text:'once:', sql:"SELECT 42+69 AS RESULT;--socio"}, {text:'once:', sql:"SELECT COUNT(*) AS RESULT FROM users;--socio"}]
|
||||
let users = [], bob_count=0;
|
||||
const insert_fields = {name:'Bob', num:420}
|
||||
|
||||
|
|
@ -22,11 +22,11 @@
|
|||
await ws.ready()
|
||||
clienID = ws.ses_id
|
||||
|
||||
ws.subscribe({ sql: 'SELECT COUNT(*) AS RESULT FROM users WHERE name = :name;', params: { name: 'Bob' } }, (res) => {
|
||||
ws.subscribe({ sql: "SELECT COUNT(*) AS RESULT FROM users WHERE name = :name;--socio", params: { name: 'Bob' } }, (res) => {
|
||||
bob_count = res[0].RESULT //res is whatever object your particular DB interface lib returns from a raw query
|
||||
})
|
||||
|
||||
ws.subscribe({ sql: 'SELECT * FROM users;'}, (res) => {
|
||||
ws.subscribe({ sql: "SELECT * FROM users;--socio"}, (res) => {
|
||||
users = res //res is whatever object your particular DB interface lib returns from a raw query
|
||||
})
|
||||
})
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
<Nav></Nav>
|
||||
<main>
|
||||
<h1>Socio framework use demonstration - Svelte</h1>
|
||||
<h1>Socio framework secured use demonstration - Svelte</h1>
|
||||
{#if clienID}
|
||||
<div class="horiz">
|
||||
<h2 id="ready" class="status">Ready.</h2>
|
||||
|
|
@ -53,14 +53,14 @@
|
|||
{/each}
|
||||
|
||||
<div class="horiz">
|
||||
<Button on:click={async () => await ws.query('INSERT INTO users VALUES(:name, :num);', insert_fields)} bind:name={insert_fields.name} bind:num={insert_fields.num}></Button>
|
||||
<Button on:click={async () => await ws.query("INSERT INTO users VALUES(:name, :num);--socio", insert_fields)} bind:name={insert_fields.name} bind:num={insert_fields.num}></Button>
|
||||
<input type="text" bind:value={insert_fields.name}>
|
||||
<input type="number" bind:value={insert_fields.num}>
|
||||
</div>
|
||||
|
||||
<h2 class="row horiz">
|
||||
<h3>subscribed:</h3>
|
||||
<Code >SELECT COUNT(*) AS RESULT FROM users WHERE name = :name; && :name = 'Bob'</Code>
|
||||
<Code>SELECT COUNT(*) AS RESULT FROM users WHERE name = :name; && :name = 'Bob'</Code>
|
||||
=
|
||||
<span class="num grad_clip">{bob_count}</span>
|
||||
</h2>
|
||||
|
|
@ -69,21 +69,23 @@
|
|||
{/if}
|
||||
</main>
|
||||
|
||||
<section>
|
||||
<div class="users">
|
||||
<div class="horiz"><h3>subscribed:</h3> <Code>SELECT * AS RESULT FROM users;</Code>=</div>
|
||||
<h2 class="grad_clip">{'{'}</h2>
|
||||
{#each users as u}
|
||||
<div class="horiz user_row" transition:slide>
|
||||
<h2>name: <span class="grad_clip">{u.name}</span></h2>
|
||||
<h2>num: <span class="grad_clip">{u.num}</span></h2>
|
||||
</div>
|
||||
{/each}
|
||||
<h2 class="grad_clip">{'}'}</h2>
|
||||
</div>
|
||||
{#if users}
|
||||
<section>
|
||||
<div class="users">
|
||||
<div class="horiz"><h3>subscribed:</h3> <Code>SELECT * AS RESULT FROM users;</Code>=</div>
|
||||
<h2 class="grad_clip">{'{'}</h2>
|
||||
{#each users as u}
|
||||
<div class="horiz user_row" transition:slide>
|
||||
<h2>name: <span class="grad_clip">{u.name}</span></h2>
|
||||
<h2>num: <span class="grad_clip">{u.num}</span></h2>
|
||||
</div>
|
||||
{/each}
|
||||
<h2 class="grad_clip">{'}'}</h2>
|
||||
</div>
|
||||
|
||||
<h3>Check the dev console for verbose logs and the network panel for websocket connection messages ;)</h3>
|
||||
</section>
|
||||
<h3>Check the dev console for verbose logs and the network panel for websocket connection messages ;)</h3>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style lang="css">
|
||||
main {
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ const app = new App({
|
|||
})
|
||||
|
||||
export default app
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
import { SocioSecurityPlugin } from '../../core/secure.js'
|
||||
// import { SocioSecurityPlugin } from 'socio/secure.js'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()]
|
||||
plugins: [
|
||||
svelte(),
|
||||
//note that these key and iv are here for demonstration purposes and you should always generate your own. You may also supply any cipher algorithm supported by node's crypto module
|
||||
SocioSecurityPlugin({ secure_private_key: 'skk#$U#Y$7643GJHKGDHJH#$K#$HLI#H$KBKDBDFKU34534', cipher_iv: 'dsjkfh45h4lu45ilULIY$%IUfdjg', verbose: true })
|
||||
]
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue