UNPKG

node-simple-router

Version:

Yet another minimalistic router for node.js

455 lines (410 loc) 18.8 kB
# First shot at WAMP (Web Application Messaging Protocol) implementation. events = require 'events' util = require 'util' net = require 'net' ws = require './ws' {defer} = require './promises' try {defer} = require './promises.litcoffee' catch e {defer} = require './promises' #Message Constants MESSAGE_TYPES = HELLO: 1 WELCOME: 2 ABORT: 3 CHALLENGE: 4 AUTHENTICATE: 5 GOODBYE: 6 HEARTBEAT: 7 ERROR: 8 PUBLISH: 16 PUBLISHED: 17 SUBSCRIBE: 32 SUBSCRIBED: 33 UNSUBSCRIBE: 34 UNSUBSCRIBED: 35 EVENT: 36 CALL: 48 CANCEL: 49 RESULT: 50 REGISTER: 64 REGISTERED: 65 UNREGISTER: 66 UNREGISTERED: 67 INVOCATION: 68 INTERRUPT: 69 YIELD: 70 #End of Message Constants #Transport constants TRANSPORT_TYPES = DIRECT: 1 WEBSOCKET: 2 UNIXSOCKET: 3 #End of Transport constants MAX_ID = 2 ** 53 isValidURI = (uri_string) -> !!uri_string.match(/^(([0-9a-z_]{2,}\.)|\.)*([0-9a-z_]{2,})?$/) randomNum = (len) -> parseInt ((Math.floor(Math.random() * 10)).toString() for n in [1..len]).join('') genId = -> randomNum 15 ### class WampSession extends events.EventEmitter constructor: (@id, options) -> @_options = options or {} for key, val of @_options @[key] = val @nextId = 1 ### class WampPeer extends events.EventEmitter "Peer at one end of Wamp Session. Involves acting as a session itself, as well" constructor: (@parent, @transport, @roles, options) -> @isOpen = true @transport.on 'close', (code) => @isOpen = false @cleanUp() console.log "Closing WampPeer due to transport closed." @id = null @nextId = 1 @setOptions options setOptions: (options) => @_options = options or {} for key, val of @_options @[key] = val cleanUp: => registered = @parent.realms?[@realm].registered_procedures if registered for proc, index in registered try if proc.sessionId is @id @parent.realms[@realm].registered_procedures.splice(index, 1) catch e console.log "ERROR: #{e.message}" invocations = @parent.realms?[@realm].invocations if invocations for invocation, index in invocations try if invocation.sessionId is @id @parent.realms[@realm].invocations.splice(index, 1) catch e console.log "ERROR: #{e.message}" subscriptions = @parent.realms?[@realm].subscriptions if subscriptions for key, topic of subscriptions try for subscription, index in topic if subscription.sessionId is @id topic.splice(index, 1) catch e console.log "ERROR: #{e.message}" sessions = @parent.realms?[@realm].sessions if sessions for session, index in sessions try if session.id is @id @parent.realms[@realm].sessions.splice(index, 1) break catch e console.log "ERROR: #{e.message}" sendMessage: (message) => ### if @parent.constructor.name is "WampClient" console.log @parent.constructor.name, "sends a message:", JSON.stringify(message) console.log "Open condition is:", @isOpen console.log "WebSocket state is: %s", @transport.readyState ### @transport.send(message) if @isOpen processMessage: (message) => return unless @isOpen arr = JSON.parse message [code] = arr if code not in [MESSAGE_TYPES.HELLO, MESSAGE_TYPES.WELCOME] return unless @id switch code when MESSAGE_TYPES.HELLO console.log "Router received HELLO message" [realm, details] = arr.slice(1) #@session = new WampSession @parent.nextSessionId, {peer: @, realm, details} @id = @parent.nextSessionId @realm = realm @setOptions details @parent.realms[realm] = {sessions: [], registered_procedures: [], invocations: [], subscriptions: {}} unless realm of @parent.realms @parent.realms[realm].sessions.push @ console.log "@realms[#{realm}].sessions.length: %d", @parent.realms[realm].sessions.length @sendMessage JSON.stringify [MESSAGE_TYPES.WELCOME, @parent.nextSessionId, {roles: @roles}] console.log "Router sent WELCOME message to #{@parent.nextSessionId}" @parent.nextSessionId += 1 when MESSAGE_TYPES.WELCOME console.log "Client Received Welcome Message" [sid, details] = arr.slice(1) @id = sid @routerRoles = details @realm = @parent.realm @parent.subscriptions = @subscriptions = [] @parent.publications = @publications = [] @parent.registrations = @registrations = [] @parent.calls = @calls = [] @parent.onopen?({@id, @realm, @roles, @routerRoles, @subscriptions, @registrations, @calls}) when MESSAGE_TYPES.ABORT console.log "Received Abort Message" when MESSAGE_TYPES.CHALLENGE console.log "Received Challenge Message" when MESSAGE_TYPES.AUTHENTICATE console.log "Received Authenticate Message" when MESSAGE_TYPES.GOODBYE console.log "Received GoodBye Message" @sendMessage message #@isOpen = false @cleanUp() @transport.close() when MESSAGE_TYPES.HEARTBEAT console.log "Received HeartBeat Message" when MESSAGE_TYPES.PUBLISH #console.log "Received publish message from session #{@id}" [RequestId, OptionsDict, TopicUri, ArgumentsList, ArgumentsKwDict] = arr.slice(1) PublicationId = @nextId @nextId += 1 topic = @parent.realms[@realm].subscriptions[TopicUri] if topic for suscription in topic for session in @parent.realms[@realm].sessions if session.id is suscription.sessionId msgArray = [MESSAGE_TYPES.EVENT, suscription.SubscriptionId, PublicationId, OptionsDict] msgArray.push ArgumentsList if ArgumentsList msgArray.push ArgumentsKwDict if ArgumentsKwDict session.sendMessage JSON.stringify(msgArray) break if OptionsDict.acknowledge @sendMessage(JSON.stringify [MESSAGE_TYPES.PUBLISHED, RequestId, PublicationId]) when MESSAGE_TYPES.PUBLISHED [RequestId, PublicationId] = arr.slice(1) console.log "Received Published Message for Request Id: #{RequestId} with Publication Id: #{PublicationId}" for publication in @parent.publications if publication.RequestId is RequestId publication.PublicatioId = PublicatioId console.log "Updated publication #{RequestId} with publication id: #{PublicationId}" break when MESSAGE_TYPES.SUBSCRIBE console.log "Received subscribe message from session #{@id}" [RequestId, OptionsDict, TopicUri] = arr.slice(1) SubscriptionId = @nextId @nextId += 1 @parent.realms[@realm].subscriptions[TopicUri] = [] if not @parent.realms[@realm].subscriptions[TopicUri] @parent.realms[@realm].subscriptions[TopicUri].push { sessionId: @id SubscriptionId OptionsDict } resp = [MESSAGE_TYPES.SUBSCRIBED, RequestId, SubscriptionId] @sendMessage(JSON.stringify resp) console.log "Registered subscription for #{TopicUri} in realm #{@realm}" when MESSAGE_TYPES.SUBSCRIBED [RequestId, SubscriptionId] = arr.slice(1) console.log "Received Subscribed Message for Request Id: #{RequestId} with Subscription Id: #{SubscriptionId}" for subscription in @parent.subscriptions if subscription.RequestId is RequestId subscription.SubscriptionId = SubscriptionId console.log "Updated subscription #{subscription.topic} with subscription id: #{SubscriptionId}" break when MESSAGE_TYPES.UNSUBSCRIBE [RequestId, SubscriptionId] = arr.slice(1) console.log "Received unsubscribe message from session #{@id} with requestId: #{RequestId} and subscriptionId: #{SubscriptionId}" for key, topic of @parent.realms[@realm].subscriptions for subscription, index in topic if (subscription.SubscriptionId is SubscriptionId) and (subscription.sessionId is @id) console.log "Found subscription to erase at index #{index}. Going to do it for request: #{RequestId}" topic.splice(index, 1) return @sendMessage JSON.stringify([MESSAGE_TYPES.UNSUBSCRIBED, RequestId]) console.log "Did not find a subscription to erase. Sending error message: 'wamp.error.no_such_subscription'" return @sendMessage JSON.stringify([MESSAGE_TYPES.ERROR, MESSAGE_TYPES.UNSUBSCRIBE, RequestId, {}, "wamp.error.no_such_subscription"]) when MESSAGE_TYPES.UNSUBSCRIBED console.log "Received Unsubscribed Message" when MESSAGE_TYPES.EVENT [SubscriptionId, PublicationId, Details, args, kwArgs] = arr.slice(1) console.log "Received Event Message for subscription id: #{SubscriptionId} with the following args: %j", args for subscription in @subscriptions if subscription.SubscriptionId is SubscriptionId subscription.handler args, kwArgs break when MESSAGE_TYPES.CALL #console.log "Received Call Message from session #{@id} with request Id = #{arr[1]}" [RequestId, OptionsDict, ProcedureUri, ArgumentsList, ArgumentsKwDict] = arr.slice(1) for proc in @parent.realms[@realm].registered_procedures if proc.ProcedureUri is ProcedureUri callee_sessionId = proc.sessionId registrationId = proc.RegistrationId for session in @parent.realms[@realm].sessions if session.id is callee_sessionId callee_session = session invocation_message = [ MESSAGE_TYPES.INVOCATION, RequestId, registrationId, OptionsDict, ArgumentsList or [], ArgumentsKwDict or {} ] @parent.realms[@realm].invocations.push { sessionId: @id requestId: RequestId } #console.log "Going to invoke requestId: #{RequestId} for registrationId: #{registrationId} on session: #{callee_session.id}" return callee_session.sendMessage(JSON.stringify invocation_message) when MESSAGE_TYPES.CANCEL console.log "Received Cancel Message" when MESSAGE_TYPES.RESULT [RequestId, OptionsDict, ArgumentsList, KwArguments] = arr.slice(1) console.log "Client received Result Message for request id: #{RequestId}. Results are: %j , %j", ArgumentsList, KwArguments for pendingCall, index in @calls if pendingCall.RequestId is RequestId ArgumentsList push KwArguments if Object.keys(KwArguments).length isnt 0 #console.log "Client resolving '#{pendingCall.uri}' with %j", ArgumentsList #console.log "Pending call data:" #console.log k, v for k, v of pendingCall pendingCall.deferred?.resolve ArgumentsList @calls.splice index, 1 break when MESSAGE_TYPES.REGISTER console.log "Router received register message from session #{@id}" [RequestId, OptionsDict, ProcedureUri] = arr.slice(1) for procedure in @parent.realms[@realm].registered_procedures if procedure.ProcedureUri is ProcedureUri console.log "ERROR: procedure #{ProcedureUri} already registered." return @sendMessage(JSON.stringify [MESSAGE_TYPES.ERROR, MESSAGE_TYPES.REGISTER, RequestId, {}, 'wamp.error.procedure_already_exists']) RegistrationId = @nextId @nextId += 1 @parent.realms[@realm].registered_procedures.push { sessionId: @id RegistrationId OptionsDict ProcedureUri } resp = [MESSAGE_TYPES.REGISTERED, RequestId, RegistrationId] @sendMessage(JSON.stringify resp) console.log "Router registered procedure #{ProcedureUri} in realm #{@realm}" when MESSAGE_TYPES.REGISTERED [RequestId, RegistrationId] = arr.slice(1) console.log "Client received Registered Message for RequestId: %s - RegistrationId: %s", RequestId, RegistrationId for registered_procedure in @parent.registrations if registered_procedure.RequestId is RequestId registered_procedure.RegistrationId = RegistrationId console.log "Updated registered procedure #{registered_procedure.uri} with registration id: #{RegistrationId}" break when MESSAGE_TYPES.UNREGISTER console.log "Received Unregister Message" [RequestId, RegistrationId] = arr.slice(1) for procedure, index in @parent.realms[@realm].registered_procedures if procedure.RegistrationId is RegistrationId @parent.realms[@realm].registered_procedures.splice(index, 1) return @sendMessage(JSON.stringify [MESSAGE_TYPES.UNREGISTERED, RequestId]) return @sendMessage(JSON.stringify [MESSAGE_TYPES.ERROR, MESSAGE_TYPES.UNREGISTER, RequestId, {}, 'wamp.error.no_such_registration']) when MESSAGE_TYPES.UNREGISTERED [RequestId] = arr.slice(1) console.log "Received Unregistered Message for RequestId: %s", RequestId when MESSAGE_TYPES.INVOCATION [RequestId, RegistrationId, OptionsDict, ArgumentsList, KwArguments]= arr.slice(1) console.log "Client received Invocation Message for registration id: #{RegistrationId} with arguments: %j", ArgumentsList for registration, index in @parent.registrations if registration.RegistrationId is RegistrationId #console.log "Invoking function '#{registration.uri}'" result = registration.fn.apply null, ArgumentsList, KwArguments #console.log "Result of invocation is: #{result}" @sendMessage(JSON.stringify([MESSAGE_TYPES.YIELD, RequestId, OptionsDict, [result]])) break when MESSAGE_TYPES.INTERRUPT console.log "Client received Interrupt Message" when MESSAGE_TYPES.YIELD console.log "Router received Yield Message from session #{@id}" [RequestId, OptionsDict, ArgumentsList, ArgumentsKwDict] = arr.slice(1) #console.log "Yield message data\nRequestId: #{RequestId}" #console.log "OptionsDict: %j", OptionsDict #console.log "ArgumentsList: #{ArgumentsList}" #console.log "ArgumentsKwDict: #{ArgumentsKwDict}" for invocation in @parent.realms[@realm].invocations if invocation.requestId is RequestId caller_sessionId = invocation.sessionId for session in @parent.realms[@realm].sessions if session.id is caller_sessionId caller_session = session result_message = [ MESSAGE_TYPES.RESULT, RequestId, OptionsDict, ArgumentsList or [], ArgumentsKwDict or {} ] #console.log "Sending results to session #{caller_session.id} corresponding to request Id: #{RequestId}" return caller_session.sendMessage(JSON.stringify result_message) else console.log "Unknown code received." class WampClient extends events.EventEmitter constructor: (options) -> if (not options?.url) or (not options?.realm) throw new Error "Must provide a url and a realm to connect to" @[key] = value for key, value of options @roles = @roles or {subscriber: {}, publisher: {}, callee: {}, caller: {}} connect: => @websocket = new ws.WebSocketClientConnection(@url) #return false unless @websocket.readyState is 'open' @websocket.on 'open', => @peer = new WampPeer(@, @websocket, @roles) @websocket.on 'data', (opcode, data) => @peer.processMessage(data) @peer.sendMessage(JSON.stringify [MESSAGE_TYPES.HELLO, @realm, @roles]) true register: (uri, fn, options) => reqId = @peer.nextId @peer.nextId += 1 @registrations.push RequestId: reqId, uri: uri, fn: fn @peer.sendMessage(JSON.stringify [MESSAGE_TYPES.REGISTER, reqId, options or {}, uri]) call: (procUri, args = [], kwArgs = {}, options = {}) => reqId = @peer.nextId @peer.nextId += 1 deferred = defer() callData = {RequestId: reqId, uri: procUri, args: args, kwArgs: kwArgs, options: options, deferred: deferred} @peer.calls.push callData @peer.sendMessage(JSON.stringify [MESSAGE_TYPES.CALL, reqId, options, procUri, args, kwArgs]) deferred.promise() subscribe: (topic, handler, options = {}) -> reqId = @peer.nextId @peer.nextId += 1 deferred = defer() subscriptionData = {RequestId: reqId, topic: topic, handler: handler, options: options, deferred: deferred} @peer.subscriptions.push subscriptionData @peer.sendMessage(JSON.stringify [MESSAGE_TYPES.SUBSCRIBE, reqId, options, topic]) deferred.promise() publish: (topic, args = [], kwArgs = {}, options = {}) => reqId = @peer.nextId @peer.nextId += 1 deferred = defer() publicationData = {RequestId: reqId, topic: topic, args: args, kwArgs: kwArgs, options: options, deferred: deferred} @peer.publications.push publicationData @peer.sendMessage(JSON.stringify [MESSAGE_TYPES.PUBLISH, reqId, options, topic, args, kwArgs]) deferred.promise() class WampRouter extends events.EventEmitter _webSocketHandler: (websocket) => websocket.peer = new WampPeer @, websocket, @roles websocket.on 'open', => console.log "WebSocket opened" websocket.on 'data', (opcode, data) => websocket.peer.processMessage(data) #websocket.on 'heartbeat', (roundTrip, pongTimeMillis) => # websocket.peer.processMessage() websocket.on 'close', (code, reason) => console.log "Websocket closed. Close event data:\n Code: #{code or 'no data'} - Reason: #{reason or 'no data'}" constructor: (options) -> @_options = options or {} @roles = @_options.roles or {broker: {}, dealer: {}} @nextSessionId = genId() @realms = {} @webSocketServer = ws.createWebSocketServer(@_webSocketHandler) listen: (port, host = '0.0.0.0', route = '/wamp') => @webSocketServer.listen port, host, route createWampRouter = -> new WampRouter test = -> wampRouter = createWampRouter() wampRouter.listen 8000, '0.0.0.0', '/' console.log "WAMP Router listening on port 8000" module?.exports = {MESSAGE_TYPES, TRANSPORT_TYPES, WampPeer, WampClient, WampRouter, createWampRouter} test() if not module?.parent