UNPKG

copious-transitions

Version:
652 lines (582 loc) 30.3 kB
/** * @namespace CopiousTransitions */ const fs = require('fs') const crypto = require('crypto') const clone = require('clone') const ShutdownManager = require('./lib/shutdown-manager') //const shutdown_server_helper_factory = require('http-shutdown') const UserHandling = require('./contractual/user_processing') const MimeHandling = require('./contractual/mime_processing') const TranstionHandling = require('./contractual/transition_processing'); const { EventEmitter } = require('events'); // https://github.com/fastify/fastify let g_password_store = [] const PASSWORD_DEPLETION_MIN = 3 const PASSWORD_BLOCKSIZE = 100 // const g_expected_modules = [ "custom_transitions", "db", "middleware", "authorizer", "validator", "static_assets", "dynamic_assets", "expression", "business", "transition_engine", "web_sockets", "endpoint_server", "link_manager" ] // const g_hex_re = /^[0-9a-fA-F]+$/; // // global_appwide_token global.global_appwide_token = () => { // was uuid -- may change return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * * The class provided in this module provides the main method for the entry point to an application running * a CopiousTransitions web server. * * Here is a typical application. * * ``` const CopiousTransitions = require('copious-transitions') // include this module let [config,debug] = [false,false] // define the variables config = "./user-service-myapp.conf" // set the configuration file name if ( process.argv[2] !== undefined ) { config = process.argv[2]; } if ( process.argv[3] !== undefined ) { // maybe use debugging if ( process.argv[3] === 'debug' ) { debug = true } } let transition_app = new CopiousTransitions(config,debug,__dirname) // Initialize everything according to the configuration. // in this application the local directory of the calling module is the place to look for mod path modules to be loaded. transition_app.on('ready',async () => { // when the configuration is done the application is ready to run await transition_app.run() // run the application }) ``` * * The constructor loads all the modules that are configured for execution. * Take note that the module are not specified at the top of file where this class is defined. * The modules that this process will use are required to be specified in the configuration file. * * The constuctor first calls `load_parameters` which can reset the module offsets as needed. * In some cases, a module might not be listed in the configuration, for instance. * * The `load_parameters` call also defines a set of global variables and methods that can be used throughout the process * as needed. A restriction of the basic module architecture is that these globals be defined here and nowhere else. * (While there is no way to regulate the management of globals in an application, it is recommended to keep any global definitions * in one place.) * * After the `load_parameters` call is performed, the required modules are loaded. Each module is responsible for returning a * constucted object based on the classes defined with it. * * In the module stack, most of the salient variables that must be configured are set in the intialization methods. * The same is true here, and the constructor defers the all the initializations to the initialization method. * The main initialization method is asynchronous, and so this constructor parsels its call out to a thunk. * * The only modules that are loaded by `requires` at the top of this file are the `contractual` modules. * The `contractual` modules are supposed to be universal enough so as to not require overrides. In some cases, * an application might choose to replace the. Such applications can override the class provided here. The method `initlialize_contractuals` * is set aside for the purpose of overriding these methods. However, it is recommended that the existing methods be kept in tact, * as they define the basic structure for handling requests in applications derived from CopiousTransitions. * * @memberof CopiousTransitions */ class CopiousTransitions extends EventEmitter { // constructor(config,debug,caller_dir) { super() this.debug = debug const conf_obj = load_parameters(config,caller_dir) // configuration parameters to select modules, etc. this.conf_obj = conf_obj this.port = conf_obj.port // this.custom_transitions = require(conf_obj.mod_path.custom_transitions) // SPECIAL NAMED TRANSITIONS (PATHS) this.custom_transitions.initialize(conf_obj) // CONFIGURE this.db = require(conf_obj.mod_path.db) // The database interface. Sets up session store, static store, and other DB pathways this.middleware = require(conf_obj.mod_path.middleware) // This is middleware for Express applications this.authorizer = require(conf_obj.mod_path.authorizer) // Put authorization mechanics here. this.statics = require(conf_obj.mod_path.static_assets) // Special handling for fetching static assets. this.dynamics = require(conf_obj.mod_path.dynamic_assets) // Program spends some time creating the asset. this.validator = require(conf_obj.mod_path.validator) // Custom access to field specification from configuration and the application through DB or inline code this.business = require(conf_obj.mod_path.business) // backend tasks that don't return values, but may send them out to other services.. (some procesing involved) this.transition_engine = require(conf_obj.mod_path.transition_engine) this.web_sockets = require(conf_obj.mod_path.web_sockets) // web sockets - serveral types of application supported -- use app chosen ws interface this.endpoint_server = require(conf_obj.mod_path.endpoint_server) // certain applications will handle transition from the backend // let LinkManagerClass = require(conf_obj.mod_path.link_manager) // this class only, will initialize a server on construction this.link_manager = new LinkManagerClass(conf_obj.link_manager) // queries modules for the type of connections they want and manages linkage // this.app = require(conf_obj.mod_path.expression)(conf_obj,this.db); // exports a function this.session_manager = null // this.max_cache_time = 3600000*2 // once every two hours // this.user_handler = false this.mime_handler = false this.transition_processing = false // this.caller_dir = caller_dir // let b = (async () => { await this.initialize_all(conf_obj) this.setup_paths(conf_obj) this.emit('ready') }) b() } // INITIALIZE /** * * @param {object} conf_obj */ async initialize_all(conf_obj) { // await this.db.initialize(conf_obj) await this.business.initialize(conf_obj,this.db) await this.transition_engine.initialize(conf_obj,this.db) // this.session_manager = this.authorizer.sessions(this.app,this.db,this.business,this.transition_engine) // setup session management, session, cookies, tokens, etc. Use database and Express api. // sessions inializes the custom session manager determined in the application authorizer. this.middleware.initialize(conf_obj) this.middleware.setup(this.app,this.db,this.session_manager) // use a module to cusomize the use of Express middleware. this.validator.initialize(conf_obj,this.db,this.session_manager) // The validator may refer to stored items and look at other context dependent information await this.statics.initialize(this.db,conf_obj) // Static assets may be taken out of DB storage or from disk, etc. await this.dynamics.initialize(this.db,conf_obj) // Dynamichk assets may be taken out of DB storage or from disk, etc. this.transition_engine.install(this.statics,this.dynamics,this.session_manager) // --- contractual logic --- this.initlialize_contractuals() // websocket access to contractual logic await this.web_sockets.initialize(conf_obj,this.app) this.web_sockets.set_contractual_filters(this.transition_processing,this.user_handler,this.mime_handler) // this.endpoint_server.set_link_manager(this.link_manager) //await this.endpoint_server.initialize(conf_obj,this.db) this.endpoint_server.set_contractual_filters(this.transition_processing,this.user_handler,this.mime_handler) this.endpoint_server.set_ws(this.web_sockets) // // transition engine access to web sockets and contractual logic this.transition_engine.set_ws(this.web_sockets) this.transition_engine.set_contractual_filters(this.transition_processing,this.user_handler,this.mime_handler) // await this.link_manager.initialize(conf_obj,this.db,this.transition_engine,this.web_sockets, this.session_manager, this.statics, this.dynamics, this.business, this.validator) // await this.endpoint_server.initialize(conf_obj,this.db) } /** * Construct the contractual handlers. Simply new object instances. * Pass in the previously allocated handlers. */ initlialize_contractuals() { let use_foreign = false // conf_obj.foreign_auth.allowed (deprecated) this.user_handler = new UserHandling(this.session_manager,this.validator,this.statics,this.dynamics,this.transition_engine,use_foreign,this.max_cache_time) this.mime_handler = new MimeHandling(this.session_manager,this.validator,this.statics,this.dynamics,this.max_cache_time) this.transition_processing = new TranstionHandling(this.session_manager,this.validator,this.dynamics,this.max_cache_time) } /** * Set up the web api paths which will be used by the router to find the handler methods. * The paths specified in this method are a short list of paths describing the general types of transactions * that are needed to carry out actions for fetching something, creating something to be returned as a mime type asset, * or for performing a state transition. Some paths are of the type that are *guarded*. Guarded paths require user identification * via session identification and token matching, especially if there is a secondary action. * * Here is a list of the paths that this module defines: * * * /static/:asset            -- previously static_mime. This just returns a static (not computed) piece of information. The app should be able to set the mime type. * * /guarded/static/:asset    -- previously keyed_mime.   Same as above, but checks the session for access. * * /guarded/dynamic/:asset   -- does the same as static but calls on functions that generate assets -- could be a function service. * * /guarded/secondary        -- a handshake that may be required of some assets before delivery of the asset. * * /transition/:transition   -- a kind of transition of the session -- this may return a dynamic asset or it might just process a form -- a change of state expected * * /transition/secondary     -- a handshake that may be required of some assets before delivery of the asset and always finalization of transition * * '/users/login','/users/logout','/users/register','/users/forgot' -- the start, stop sessions, manage passwords. * * /users/secondary/:action -- the handler for user session management that may require a handshake or finalization. * * Each of these paths hands off processing to a type of contractual class. Each contractual class defines the checks on permission * and availability that is needed to retrieve an asset or perform an action. (Note: contractual class in this case do not have to * do with blockchain contract processing [yet such versions of these classes can be imagined]) What is meant by contractual here * is that the client (browser... usually) and the server will provide authorization, session identity, and token handshakes * in a particular, well-defined way. * * Note that that there are three types of contractual classes: * * 1. user processing - this type of processing has to do with the user session being established or terminated and user existence. * 2. mime processing - This processing has to do with items being returned to the client, e.g. images, text, HTML, JSON, etc. * 3. transition processing - This proessing has to do with driving a state machine in some way. * * In the case of state machine processing, most of the application are just taking in data, sending it somewhere, and updating a database * about the state of the data and/or the user. In some applications, an actual state machine (for the user) might be implemented and the finalization * of the state will result in reseting the state of the machine for a particular user. * * How the methods map to the contractual methods: * * * /static/:asset            -- `mime_handler.static_asset_handler` * * /guarded/static/:asset    -- `mime_handler.guarded_static_asset_handler` * * /guarded/dynamic/:asset   -- `mime_handler.guarded_dynamic_asset_handler` * * /guarded/secondary        -- `mime_handler.guarded_secondary_asset_handler` * * /transition/:transition   -- `transition_processing.transition_handler` * * /transition/secondary     -- `transition_processing.secondary_transition_handler` * * '/users/login','/users/logout','/users/register','/users/forgot' -- user_handler.user_sessions_processing * * /users/secondary/:action -- `transition_processing.secondary_transition_handler` * * @param {object} conf_obj */ setup_paths(conf_obj) { /* /static/:asset            -- previously static_mime. This just returns a static (not computed) piece of information. The app should be able to set the mime type. /guarded/static/:asset    -- previously keyed_mime.   Same as above, but checks the session for access. /guarded/dynamic/:asset   -- does the same as static but calls on functions that generate assets -- could be a function service. /guarded/secondary        -- a handshake that may be required of some assets before delivery of the asset. /transition/:transition   -- a kind of transition of the session -- this may return a dynamic asset or it might just process a form -- a change of state expected /transition/secondary     -- a handshake that may be required of some assets before delivery of the asset and always finalization of transition '/users/login','/users/logout','/users/register','/users/forgot' -- the start, stop sessions, manage passwords. /users/secondary/:action -- the handler for user session management that may require a handshake or finalization. */ // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- // ROOT ... unauthorized entry point -- this is likely to be done by Nginx and will not be needed here. this.app.get('/', async (req, res) => { try { let html = await this.statics.fetch('index.html'); if ( typeof html !== 'string' ) { if ( (typeof html === 'object') && (html.ftype === 'html') ) { html = html.data } if ( typeof html !== 'string' ) { throw new Error(`the static module returned data of the wrong type for key asset 'index.html'`) } } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } catch (e) { console.log(e) res.end('system check') } }); // STATIC FETCH this.app.get('/static/:asset', async (req, res) => { let asset = req.params['asset'] let body = {} let [code,header,data] = await this.mime_handler.static_asset_handler(asset,body,req.headers) if ( data !== false ) { res.writeHead(code,header); res.end(data); } else { res.status(code).send(JSON.stringify(header)) } }); // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- // STATIC KEYED MIME_TYPES -- no form validation, but GUARD asset within a session this.app.post('/guarded/static/:asset', async (req, res) => { let asset = req.params['asset'] let body = req.body let [code,header,data] = await this.mime_handler.guarded_static_asset_handler(asset,body,req.headers) if ( typeof data === 'boolean' ) { res.status(code).send(JSON.stringify(header)) } else { res.writeHead(code,header); res.end(data); } }); this.app.post('/guarded/dynamic/:asset', async (req, res) => { let asset = req.params['asset'] let body = req.body let [code,header,data] = await this.mime_handler.guarded_dynamic_asset_handler(asset,body,req.headers) if ( typeof data === 'boolean' ) { res.status(code).send(JSON.stringify(header)) } else { res.writeHead(code,header); res.end(data); } }); // KEYED MIME_TYPES - TRANSITION ACCEPTED -- the body should send back the token it got with the asset. this.app.post('/secondary/guarded', async (req,res) => { let body = req.body let [code,header,data] = await this.mime_handler.guarded_secondary_asset_handler(body) if ( typeof data === 'boolean' ) { res.status(code).send(JSON.stringify(header)) } else { res.writeHead(code,header); res.end(data); } }) // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- // TRANSITIONS - pure state transition dynamics for sessions this.app.post('/transition/:transition', async (req, res) => { // the transition is a name or key let body = req.body let transition = req.params.transition let [code,data] = await this.transition_processing.transition_handler(transition,body,req.headers) this.app.responder(res).status(code).send(data) }); // TRANSITIONS - TRANSITION ACCEPTED -- the body should send back the token it got with the asset. this.app.post('/secondary/transition',async (req, res) => { let body = req.body // let [code,data] = await this.transition_processing.secondary_transition_handler(body) this.app.responder(res).status(code).send(data) }) // let setup_foreign_auth = false /// conf_obj.foreign_auth.allowed // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- if ( conf_obj.login_app && Array.isArray(conf_obj.login_app) ) { // LOGIN APPS OPTION (START) // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- /* if ( setup_foreign_auth ) { setup_foreign_auth = (ws) => { this.user_handler.foreign_auth_initializer(ws) } } */ // // USER MANAGEMENT - handle authorization and user presence. for ( let path of conf_obj.login_app ) { // this.app.post(path, async (req,res) => { // let body = req.body let user_op = body['action'] // let [code,result] = await this.user_handler.user_sessions_processing(user_op,body) if ( result.OK === 'true' ) { let transitionObj = result.data this.session_manager.handle_cookies(result,res,transitionObj) } this.app.responder(res).status(code).send(result) // }) } // USER MANAGEMENT - finalize the user action. this.app.post('/secondary/users/:action', async (req, res) => { let body = req.body let action = req.params['action'] let [code,result] = await this.user_handler.secondary_processing(action,body) if ( result.OK === 'true' ) { let transitionObj = result.data this.session_manager.handle_cookies(result,res,transitionObj) } this.app.responder(res).status(code).send(result) }) // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- } // LOGIN APPS OPTION (END) // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- } // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- // // RUN AND STOP // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- /** * The web server will start running. * Before listening, finalize the DB initialization. * Once listening, complete the initialization of the web socket service if it is being used. */ async run() { // APPLICATION STARTUP await this.db.last_step_initalization() this.app.listen(this.port,() => { console.log(`listening on ${this.port}`) if ( this.web_sockets ) { this.web_sockets.final() } }); } /** * Establish the signal handler that will allow an external process to bring this process down. * The handler will shutdown the DB and wait for the DB shutdown to complete. * In many cases, the DB will write final data to the disk before disconnecting. * Then, use the shutdown manager to clear out any connections and timers. * Finally, exit the process. */ setup_stopping() { process.on('SIGINT', async () => { try { await this.db.disconnect() global_shutdown_manager.shutdown_all() process.exit(0) /* this.shutdown_server_helper.shutdown(async (err) => { process.exit(0) }) */ } catch (e) { console.log(e) process.exit(0) } }); } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- } // ------------- ------------- ------------- ------------- ------------- ------------- ------------- ------------- /** * */ function generate_password_block() { // var passwords = passwordGenerator.generateMultiple(PASSWORD_BLOCKSIZE, { length: 10, uppercase: false }); // g_password_store = g_password_store.concat(passwords) } /** * * @param {caller_dir|undefined} caller_dir * @returns {string} - the directory from which the mod path modules will be loaded. */ function module_top(caller_dir) { if ( typeof caller_dir === "string" ) { return caller_dir } return process.cwd() } /** * Load the configuration. Called by `load_parameters`. * It is absolutely required that there be a configuration file for any application using this module. * * Pleases refer to documentation referened in the readme.md file for a description of the configuration file. * * @param {string} cpath * @param {boolean} if_module_top - must be a string to be used (top directory) otherwise current working directory * @returns {object} if successful, the configuration object made from reading and parsing the file */ function load_configuration(cpath,if_module_top) { cpath = cpath.trim() try { let conf = fs.readFileSync(cpath,'ascii').toString() return conf } catch (e) { console.log(`Error loading configuration ${cpath}... trying another candidate path`) console.log(e) let cpath2 = '' + cpath while ( cpath2[0] === '.' ) { cpath2 = cpath2.substring(1) } cpath2 = process.cwd() + '/' + cpath2 try { let conf = fs.readFileSync(cpath2,'ascii').toString() return conf } catch (e2) { if ( if_module_top ) { console.log(`Error loading configuration ${cpath2}... trying another candidate path`) let cpath3 = module_top(if_module_top) + '/' + cpath try { let conf = fs.readFileSync(cpath3,'ascii').toString() return conf } catch (e3) { console.log(`Error loading configuration ${cpath3}... failing`) console.log(e) } } } } throw new Error(`could not find the file ${cpath}`) } /** * load_parameters * * @param {object} config * @param {boolean} if_module_top -- if given, the directory of the caller * * @returns {object} - the updated configuration object */ function load_parameters(config,if_module_top) { // // some gratuitous globals // 1) global.g_debug = false // // // 2) // clonify global.clonify = (obj) => { return(clone(obj)) /* if ( typeof obj == "object" ) { return(JSON.parse(JSON.stringify(obj))) } return(obj) */ } // 3) // global_appwide_token global.global_appwide_token = () => { // was uuid -- may change return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // 4) // isHex global.isHex = (str) => { let check = g_hex_re.test(str) g_hex_re.lastIndex = 0; // be sure to reset the index after using .text() return(check) } // 5) // do_hash if ( config.session_token_hasher ) { global.do_hash = require(config.session_token_hasher) config.session_token_hasher = global.do_hash // pass on the override } else { global.do_hash = (text) => { const hash = crypto.createHash('sha256'); hash.update(text); let ehash = hash.digest('base64url'); return(ehash) } } // 6) // global_hasher if ( config.global_hasher ) { global.global_hasher = require(config.global_hasher) config.global_hasher = global.global_hasher // pass on the override } else { global.global_hasher = (str,specials) => { // global_hasher(user_str,ipfs) // can ask to store the record on chain... if ( str ) { return do_hash(str) } return('') } } global.global_shutdown_manager = new ShutdownManager() try { let data = load_configuration(config,if_module_top) // //console.log(data[p] + " --- " + data.substr(p,40)) // let confJSON = JSON.parse(data) let module_path = confJSON.module_path global.global_module_path = module_path confJSON.mod_path = {} g_expected_modules.forEach(mname => { let modName = confJSON.modules[mname] if ( (typeof modName === 'string') || ( modName === undefined ) ) { if ( modName ) { // e.g. node_modules/app_module/lib/auth_module confJSON.mod_path[mname] = module_top(if_module_top) + `/${module_path}/${modName}` } else { // module not included in the module map... use the module supplied by copious-transitions confJSON.mod_path[mname] = __dirname + `/defaults/lib/default_${mname}` } } else if ( typeof modName === 'object' ) { // allow for modules from other locations modName = modName.module // the name of the module file let alternate_mod_path = modName.mod_path // perhaps filter this in the future to attain some standard in locations.. confJSON.mod_path[mname] = module_top(if_module_top) + `/${alternate_mod_path}/${modName}` } else { console.log(mname,modName) throw new Error("ill formed module name in config file") } }) return(confJSON) } catch (e) { console.log(e) process.exit(1) } } module.exports = CopiousTransitions