UNPKG

message-relay-services

Version:

A simple server to route message to other services, such as email, admin, etc.

458 lines (412 loc) 18.8 kB
'use strict'; // load the Node.js TCP library const net = require('net'); const tls = require('tls'); const fs = require('fs'); const PORT = 1234; const HOST = 'localhost'; // const NOISY = true const Path_handler_factory = require('../path-handler/path-handler') const JSONMessageQueue = require('../json-message-queue'); // For pub/sub, this is a relay server. So, there is forwarding, whereas there is no forwarding from an endpoint. // All pub/sub message go along a path. With the path semantics, pub/sub is broken down into subnets in some sense. // An MQTT may correspond more to a built out endpoint server. If this were used for MQTT, it would pass messages through. // When a sub message comes in, it means that a client somewhere upstream sent a request to receive publication from an // endpoint server at the end of a path. As such the path handler is given the job of handling the subscription relay. // When a publish message comes in, it mean that a client somewhere upstream sent a request for thes messages to go out // to all the subscribers to the topic at an endpoint. The path handler forwards the publication to the endpoint. // Regular messages are sent along paths. So, the relay server acts as a multiplexer/demux for messages along paths. Many clients can // be connected to a path, and the path handler will send the messages to endpoint on the path. If that path handler is customsized // for an application, it might send a message out to more than one endpoint, or it might mutate that path, etc. It is left up // to the application do determine the nature of the path hanlders. The default path handler connects to a single endpoint. // There is just one JsonMessageHandler per socket. So, if a subscription comes into the socket, the topic_handlers_by_path field may map // a subscription handler for a path for the socket. When the unusb message is received, the handler in topic_handlers_by_path will be // recalled to remove the event lister from the PathHandler. // // ~140 lines class JsonMessageHandler extends JSONMessageQueue { // constructor(initObj) { super(false,false) this.writer = initObj.writer this.server = initObj.server this.client_name = initObj.client_name this.topic_handlers_by_path = {} this.message_paths = initObj.message_paths ? initObj.message_paths : false for ( let p_ky in this.message_paths ) { this.topic_handlers_by_path[p_ky] = {} } } async data_handler(data) { this.add_data(data) this.message_complete() if ( this.message_queue.length ) { await this.dequeue_and_forward() } } async object_handler(object) { this.add_object(object) if ( this.message_queue.length ) { await this.dequeue_and_forward() } } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- // async dequeue_and_forward() { // while ( this.message_queue.length !== 0 ) { // this.dequeue() // if ( this.message_paths === false ) continue; // let path = (this.current_message ? this.current_message._m_path : undefined) if ( path ) { let shared_path_handler = this.message_paths[path] // don't forget this is the path handler instance if ( shared_path_handler === undefined ) { this.writer.write("ERROR: paths improperly loaded in service") return } if ( shared_path_handler && (typeof shared_path_handler.send === 'function') ) { // defer to the path handler how to take care of operations... let op = this.current_message._tx_op switch ( op ) { case "G" : { let old_response_id = this.current_message._response_id let result = await shared_path_handler.get(this.current_message) result = result !== false ? result : "ERROR" let response = { "_response_id" : old_response_id, "msg" : result } this.writer.write(JSON.stringify(response)) break; } case "D" : { let old_response_id = this.current_message._response_id let result = await shared_path_handler.del(this.current_message) result = result !== false ? result : "ERROR" let response = { "_response_id" : old_response_id, "msg" : result } this.writer.write(JSON.stringify(response)) break; } case "S" : default : { // sending forward op message or any other message. May be a subscription.. let result = false let old_response_id = this.current_message._response_id if ( (this.current_message._ps_op !== undefined ) && (this.current_message._ps_op !== 'pub') ) { // take care of handlers if ( this.current_message._ps_op === 'sub' ) { // sub from pub/sub on this path. // topic on path let topic = this.current_message.topic if ( topic ) { let listener = ((wrtr,tt) => { // forward publication to the client (this socket) return (msg) => { msg.topic = tt let forwarded = this.encode_message(msg) wrtr.write(forwarded) } } )(this.writer,topic) // listener is the intenral thunk let topics = this.topic_handlers_by_path[path] topics[topic] = listener // for a generic cleanup await shared_path_handler.subscribe(topic,this.current_message,listener,this) result = "OK" } } else if ( this.current_message._ps_op === 'unsub' ) { let topic = this.current_message.topic if ( topic ) { let topics = this.topic_handlers_by_path[path] let listener = topics[topic] await shared_path_handler.unsubscribe(topic,listener,this) // remove this socket's handler delete topics[topic] result = "OK" } } } else { // just send the message through on this path. if ( (this.current_message._ps_op !== undefined ) && (this.current_message._ps_op === 'pub') ) { result = await shared_path_handler.send_pub(this.current_message,this) } else { result = await shared_path_handler.send(this.current_message) } } // return results to sender -- message went or was copied out for publication. // Tell the sender/publisher how well the process went result = result !== false ? result : "ERROR" let response = { "_response_id" : old_response_id, "msg" : result } this.writer.write(JSON.stringify(response)) break; } } } else { this.writer.write("ERROR") } } } } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- cleanup() { let path = this.current_message._m_path let shared_path_handler = this.message_paths[path] if ( shared_path_handler ) { // does nothing to the path handler except remove the listener let topics = this.topic_handlers_by_path[path] for ( let topic in topics ) { let listener = topics[topic] shared_path_handler.unsubscribe(topic,listener,this) delete topics[topic] } shared_path_handler.request_cleanup() } } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- } // ~60 lines class Communicator { // constructor(conf,fanoutRelayer) { // PATHWAY HANDLER CLASS DEFINITION let path_types = conf.path_types let ph_factory = Path_handler_factory if ( conf.path_handler_factory && (typeof conf.path_handler_factory === 'string') ) { ph_factory = require.main.require(conf.path_handler_factory) // for application requiring more than the included factory. } else if ( conf.path_handler_factory && (typeof conf.path_handler_factory === 'function') ) { ph_factory = conf.path_handler_factory } if ( conf.MessageHandlerClass ) { let mqClass = require(conf.MessageHandlerClass) this.jsonMHandler = new mqClass(false) } else { this.jsonMHandler = JsonMessageHandler } this.replierClass // // PATHWAY HANDLER CONSTRUCTION this.messenger_connections = {} this.message_paths = {} for ( let a_path in path_types ) { let mpath = ph_factory(a_path,path_types[a_path],fanoutRelayer) this.message_paths[a_path] = mpath // mpath is the class instance (default is PathHandler) } // note: no pub/sub mechanism in the server proper... // start serving this._init(conf); } _init(conf) { throw new Error("Application must write an init method of a descendant of class Communicator - Message Relay") } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- // add_connection // -- set up the message queue for this socket connection /** * add_connection * * @param {*} client_name * @param {*} writer * @returns */ add_connection(client_name,writer) { // if ( NOISY ) console.log(`message relay server :: new client connected: ${client_name}`); // // CREATE A MESSAGE HANDLER OBJECT this.messenger_connections[client_name] = new this.jsonMHandler({ 'writer' : writer, 'server' : this, 'client_name' : client_name, 'message_paths' : this.message_paths }) // return client_name } get_connection_writer(client_name) { let handler = this.messenger_connections[client_name] if ( handler !== undefined ) { return handler.writer } return false } /** * add_data_and_react * * @param {*} client_name * @param {*} data */ add_data_and_react(client_name,data) { //... when ready, use the data handler object to determine the fate of the message. let mescon = this.messenger_connections[client_name] if ( mescon ) mescon.data_handler(data) // RESPOND TO DATA } /** * add_object_and_react * * @param {*} client_name * @param {*} data */ add_object_and_react(client_name,obj) { //... when ready, use the data handler object to determine the fate of the message. let mescon = this.messenger_connections[client_name] if ( mescon ) mescon.object_handler(obj) // RESPOND TO DATA } /** * close * * @param {*} client_name */ close(client_name) { let mescon = this.messenger_connections[client_name] if ( mescon ) { mescon.cleanup() delete this.messenger_connections[client_name] } } } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- // ~60 lines class Server extends Communicator { // constructor(conf,fanoutRelayer) { super(conf,fanoutRelayer) } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- _init_members(conf) { this.port = conf ? conf.port || PORT : PORT this.address = conf ? conf.address || HOST : HOST let host_to_port = [ { 'host' : this.address, 'port' : this.port } ] let uds_path = conf.UDS_Path || conf.uds_path if ( uds_path !== undefined ) { this.UDS_path = uds_path this.uds_path_count = 0 if ( (conf.uds_path_count !== undefined) && !(isNaN(conf.uds_path_count)) && (conf.uds_path_count > 0) ) { this.uds_path_count = parseInt(conf.uds_path_count) for ( let i = 0; i < his.uds_path_count; i++ ) { host_to_port.push({ 'path' : `${uds_path}-${i}`}) } } else { host_to_port.push({ 'path' : `${uds_path}`}) } } // // TLS this.use_tls = ((conf.tls !== undefined) && (conf.tls !== false)) || ((conf.default_tls !== undefined) && (conf.default_tls !== false)) this.tls_conf = conf.tls this.default_tls = conf.default_tls this.extended_tls_options = ((conf.extended_tls_options !== undefined) && (conf.extended_tls_options !== false)) ? conf.extended_tls_options : false // if ( this.use_tls ) { this.preloaded_tls_keys = this._load_tls_keys() } } _load_tls_keys() { // let base = process.cwd() // // allow exceptions to be thrown if ( this.tls_conf ) { // let server_key = false if ( this.tls_conf.preloaded && this.tls_conf.preloaded.server_key ) { server_key = this.tls_conf.preloaded.server_key } else { server_key = fs.readFileSync(`${base}/${this.tls_conf.server_key}`) } // let server_cert = false if ( this.tls_conf.preloaded && this.tls_conf.preloaded.server_cert ) { server_cert = this.tls_conf.preloaded.server_cert } else { server_cert = fs.readFileSync(`${base}/${this.tls_conf.server_cert}`) } // let client_cert = false if ( this.tls_conf.preloaded && this.tls_conf.preloaded.client_cert ) { client_cert = this.tls_conf.preloaded.client_cert } else { client_cert = fs.readFileSync(`${base}/${this.tls_conf.client_cert}`) } let tls_options = { key: server_key, cert: client_cert, requestCert: true, ca: [ client_cert ] }; return tls_options } // return false } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- // onClientConnected_func // -- handle a connection onClientConnected_func(sock,connector) { // // add_connection let client_name = (connector.path === undefined) ? `${sock.remoteAddress}:${sock.remotePort}` : connector.path; this.add_connection(client_name,sock) // // 1. data sock.on('data',((com) => { return (data) => { com.add_data_and_react(client_name,data) }})(this)) // // 2. close sock.on('close', ((com) => { return () => { com.close(client_name) }})(this)); // // 3. error sock.on('error', (err) => { console.error(`Connection ${client_name} error: ${err.message}`); }); } // _create_connection() { for ( let connector of host_to_port ) { if ( !(this.use_tls) ) { this.net_con = net.createServer((sock) => { this.onClientConnected_func(sock,connector) }); } else { if ( this.default_tls ) { this.net_con = tls.createServer((sock) => { this.onClientConnected_func(sock,connector) }); } else { const options = this.preloaded_tls_keys // if ( this.extended_tls_options !== false ) { options = Object.assign({},options,this.extended_tls_options) } this.net_con = tls.createServer(options,((sock) => { this.onClientConnected_func(sock,connector) })); } } // if ( this.net_con ) { this.net_con.listen(connector, () => { if ( connector.address !== undefined ) { console.log(`Server started at: ${connector.address}:${connector.port}`); } else { console.log(`Server started at: ${connector.path}`); } }); } } } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- _init(conf) { // if ( conf === undefined ) { console.log("message relay client: cannot initialize -- no configuration") return; } // this._init_members(conf) this._create_connection() } // ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- } module.exports = Server; module.exports.Communicator = Communicator module.exports.JsonMessageHandlerRelay = JsonMessageHandler