UNPKG

conextra

Version:

Conextra for Web Development. Helps create a SPWA (Single-page Progressive Web Application).

348 lines (253 loc) 10.5 kB
'use script'; var { workerData } = require('node:worker_threads'), { isTLS, host, port, url, protocol } = workerData, http = require("node:http"), https = require("node:https"), fs = require('node:fs'), path = require('node:path'), configSets = require('config-sets'), options_server = configSets('tiny-https-server'), conextraConfig = configSets('conextra', { isDebug: false, pathToResourseMap: 'data/resourse_map.json', pathToDataDir: "data" }), CreateWsUser = require('ws-user'), DataContext = require('data-context'), wsList = [], exitTimeout = null, datacontext = null, resourseMap = DataContext.watchJsonFile({ filePath: conextraConfig.pathToResourseMap, onDataChange: function (event) { if (!wsList.length || wsList[0].protocol !== 'ws-user') { return; } wsList.forEach((ws) => { resourceHandler.call(ws); }); }, data: { accessRoles: { user: { description: "User", view: true }, admin: { description: "Administrator", create: true, view: true, edit: true, delete: true, admin_create: true, admin_view: true, admin_edit: true, admin_delete: true } }, "/public.json": { description: "Public data", organization: null, permissions: ["public"] }, "/logged_users.json": { description: "Get the list of logged users", organization: null, permissions: ["admin_view"] } } }), server = (isTLS ? https : http) .createServer(function (req, res) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('404 Not Found\n'); }) .on('upgrade', function (request, socket, head) { var wsUser = CreateWsUser(request); if (wsUser) { clearTimeout(exitTimeout); wsUser.onerror = pError; wsUser.onclose = onClose; if (wsUser.protocol === 'ws-user') { wsUser.onopen = onOpenUser; return; } if (wsUser.protocol === 'ws-link' && url.endsWith('.json')) { wsUser.onopen = onOpenLink; wsUser.onmessage = onMessageLink; return; } } pDebug('Unsupported websocket', request.headers); socket.destroy(); }) .on('clientError', (err, socket) => { socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); }) .on('error', function (err) { pError(err); }) .listen({ port, host, exclusive: true }, function () { pDebug('WebSocket-Worker server started', protocol + url, port); }); if (isTLS && options_server.pathToCert) { server.setSecureContext({ key: fs.readFileSync(options_server.pathToPrivkey), cert: fs.readFileSync(options_server.pathToCert) }); var certUpdateWaitTimeout; fs.watch(path.dirname(options_server.pathToCert), function (eventType, filename) { if (!fs.existsSync(options_server.pathToPrivkey) || !fs.existsSync(options_server.pathToCert)) { return; } clearTimeout(certUpdateWaitTimeout); certUpdateWaitTimeout = setTimeout(function () { server.setSecureContext({ key: fs.readFileSync(options_server.pathToPrivkey), cert: fs.readFileSync(options_server.pathToCert) }); }, 1000); }); } if (url.endsWith('.json')) { datacontext = DataContext.watchJsonFile({ filePath: path.join(conextraConfig.pathToDataDir, url), onFileChange: function (event) { if (event.strChanges === undefined) { return; } pDebug('onFileChange', event.strChanges, url); wsList.forEach((ws) => { if (!hasPermission(['public', 'view', 'admin_view'], url, ws)) { return; } ws.send(event.strChanges); }); } }); } function onOpenUser() { pDebug('WebSocket-Worker open', this.protocol, url); this.extensionCommands.resources = resourceHandler; this.onloggedin = resourcesUpdate.bind(this); setTimeout(function (ws) { resourcesUpdate.call(ws); wsList.push(ws); }, 0, this); return; function resourcesUpdate() { resourceHandler.call(this); } } function resourceHandler(event) { if (event?.message) { if (hasPermission(['admin_edit'], '/resource_map.json', this)) { var obj = JSON.parse(event.message); Object.keys(obj).forEach((key) => { resourseMap[key] = obj[key]; }); } event.done(); return; } clearTimeout(this.resourceHandlerTimeout); this.resourceHandlerTimeout = setTimeout(function (ws) { var resources = Object.keys(resourseMap) .filter((key) => { if (key === '/public.json') { return true; } return hasPermission(resourseMap[key]?.permissions, key, ws); }) .map((name) => { return { name, description: resourseMap[name].description }; }); var msg = '$resources:' + DataContext(resources).toString(true); ws.send(msg); }, 1000, this); event?.done(); } function onOpenLink() { pDebug('open', this.protocol, this.connID); if (!url.endsWith('.json')) { this.close(1008, 'Unsupported file type'); } // TODO: [ ] hasPermission to continue open if (!hasPermission( ['public', 'create', 'view', 'edit', 'delete', 'admin_create', 'admin_view', 'admin_edit', 'admin_delete'], url, this)) { this.close(1008, 'No permission'); return; } // TODO: [ ] hasPermission to continue send data if (!hasPermission(['public', 'view', 'admin_view'], url, this)) { return; } this.send(datacontext.toString(true)); setTimeout(function (ws) { wsList.push(ws); }, 0, this); } function onMessageLink(event) { if (hasPermission(['public', 'edit', 'admin_edit'], url, this)) { pDebug('message', this.protocol, event.data); datacontext?.overwritingData(event.data); broadcastChanges.call(this, event.data); } setTimeout(this.messageHandled); } function onClose(event) { pDebug('WebSocket-Worker close', this.protocol, url); // removing an WebSocket from list wsList = wsList.filter(ws => ws !== this); if (!wsList.length) { exitTimeout = setTimeout(function () { pDebug('WebSocket-Worker exit', url); process.exit(); }, 30000); } } function broadcastChanges(strChanges) { if (!strChanges) { return; } if (!this.protocol) { return; } wsList.forEach((ws, i) => { if (ws === this) { return; } if (ws.readyState !== CreateWsUser.OPEN && ws.readyState !== CreateWsUser.PAUSE) { return; } if (!hasPermission(['public', 'view', 'admin_view'], url, ws)) { return; } pDebug('broadcastChanges', i, this.protocol, strChanges); ws.send(strChanges); }); } /** * * @param {[string]} permissionActions * @param {string} path * @param {[string]} userRoles * @param {[string]} organizations * @returns {boolean} */ function hasPermission(permissionActions, path, user) { //permissionActions -> ["view", "admin_view"] //user.roles -> ["user", "organization.admin"] //organizations -> ["organization"] var permissions = resourseMap[path]?.permissions, //["admin_view", "view"] organizations = resourseMap[path]?.organizations, accessRoles = resourseMap.accessRoles; //{ user: { view: true }, admin: {...} } if (!permissions) { return false; } if (!Array.isArray(permissionActions)) { permissionActions = [permissionActions + '']; } if (!Array.isArray(organizations)) { organizations = [organizations + '']; } if (permissions?.includes('public') && permissionActions.includes('public')) { return true; } if (!permissions?.length) { return false; } if (!user.isLoggedIn) { return false; } if (!user.roles?.length || !permissions?.length) { return false; } if (!permissionActions.length) { return; } //permissionActions -> ["view", "admin_view"] //user.roles -> ["user", "organization.admin"] //organizations -> ["organization"] return user.roles.some(function (userRole) { // userRole -> "user" for (var permissionAction of permissionActions) { if (accessRoles?.[userRole]?.[permissionAction] && permissions.includes(permissionAction)) { return true; } } if (organizations.length) { //userRole -> "organization.admin" userRole = userRole.split('.'); var role = userRole.pop(), accessOrganization = userRole.join('.') if (accessOrganization) { for (var organization of organizations) { for (var permissionAction of permissionActions) { if (accessOrganization === organization && accessRoles?.[role]?.[permissionAction] && permissions.includes(permissionAction)) { return true; } } } } } }); } // Debugging function pDebug(...args) { if (conextraConfig.isDebug) { console.log(`[ DEBUG ] 'ws-user-link' `, ...args); } } function pError(...args) { if (conextraConfig.isDebug) { console.error(`[ ERROR ] 'ws-user-link' `, ...args); } }