UNPKG

node-simple-router

Version:

Yet another minimalistic router for node.js

459 lines (380 loc) 13.5 kB
events = require("events") http = require("http") crypto = require("crypto") util = require("util") URL = require('url') uuid = require('./uuid') # opcodes for WebSocket frames # http://tools.ietf.org/html/rfc6455#section-5.2 opcodes = TEXT: 1 BINARY: 2 CLOSE: 8 PING: 9 PONG: 10 #magic constant to generate handshake KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" lowerObjKeys = (obj) -> keys = (key for key of obj) lkeys = keys.map (key) -> key.toLowerCase() resp = {} resp[lkeys[index]] = obj[keys[index]] for key, index in keys resp hashWebSocketKey = (key) -> sha1 = crypto.createHash("sha1") sha1.update key + KEY_SUFFIX, "ascii" sha1.digest "base64" genWebSocketKey = -> key = new Buffer(16) for _, index in key key.writeUInt8 Math.floor(Math.random() * 256), index key.toString('base64') genMask = -> mask = new Buffer(4) for _, index in mask mask.writeUInt8 Math.floor(Math.random() * 256), index mask unmask = (maskBytes, data) -> payload = new Buffer(data.length) i = 0 while i < data.length payload[i] = maskBytes[i % 4] ^ data[i] i++ payload encodeMessage = (opcode, payload, useMask = false) -> buf = undefined mask = undefined maskLen = if useMask then 4 else 0 # first byte: fin and opcode b1 = 0x80 | opcode # always send message as one frame (fin) # second byte: mask and length part 1 # followed by 0, 2, or 4 additional bytes of continued length b2 = if useMask then 0x80 else 0 length = payload.length if useMask mask = genMask() payload = unmask(mask, payload) if length < 126 buf = new Buffer(payload.length + 2 + 0 + maskLen) # zero extra bytes b2 |= length buf.writeUInt8 b1, 0 buf.writeUInt8 b2, 1 payload.copy(buf, 2 + maskLen) mask.copy(buf, 2) if useMask else if length < (1 << 16) buf = new Buffer(payload.length + 2 + 2 + maskLen) # two bytes extra b2 |= 126 buf.writeUInt8 b1, 0 buf.writeUInt8 b2, 1 # add two byte length buf.writeUInt16BE length, 2 payload.copy(buf, 4 + maskLen) mask.copy(buf, 4) if useMask else buf = new Buffer(payload.length + 2 + 8 + maskLen) # eight bytes extra b2 |= 127 buf.writeUInt8 b1, 0 buf.writeUInt8 b2, 1 # add eight byte length # note: this implementation cannot handle lengths greater than 2^32 # the 32 bit length is prefixed with 0x0000 buf.writeUInt32BE 0, 2 buf.writeUInt32BE length, 6 payload.copy buf, 10 + maskLen mask.copy buf, 10 if useMask #console.log "Returning this buffer:", buf buf WebSocketClientConnection = (url, options) -> parsed_url = URL.parse(url) throw new TypeError "URL scheme must be 'ws' or 'wss'" if parsed_url.protocol not in ['ws:', 'wss:'] self = @ @options = hostname: parsed_url.hostname port: parsed_url.port or (if parsed_url.protocol.match /ss/ then 443 else 80) path: parsed_url.path or "/" headers: {} @options.headers.Host = "#{@options.hostname}:#{@options.port}" @options.headers.Connection = "Upgrade" @options.headers.Upgrade = "websocket" @options.headers.Origin = "#{if parsed_url.protocol.match /ss/ then 'https' else 'http'}://#{@options.hostname}:#{@options.port}" @options.headers['Sec-WebSocket-Version'] = 13 @options.headers['Sec-WebSocket-Key'] = genWebSocketKey() @options.headers['Sec-WebSocket-Protocol'] = options['Sec-WebSocket-Protocol'] if options?['Sec-WebSocket-Protocol']? @options.headers['Sec-WebSocket-Extensions'] = options['Sec-WebSocket-Extensions'] if options?['Sec-WebSocket-Extensions']? @request = http.request @options @request.on 'upgrade', (response, socket, upgradeHead) -> self.socket = socket self.socket.on 'error', (err) -> console.log 'Client Socket error:', err.message self.socket.on "data", (buf) -> #console.log "Raw data:", buf self.buffer = Buffer.concat([ self.buffer buf ]) # process buffer while it contains complete frames continue while self._processBuffer() return self.socket.on "close", (had_error) -> unless self.closed self.emit "close", 1006 self.closed = true return self.emit 'open', if self.id then self.id else null @request.end() @buffer = new Buffer(0) @closed = false @currentRoundTrip = 0 return util.inherits WebSocketClientConnection, events.EventEmitter Object.defineProperty WebSocketClientConnection::, 'readyState', get: -> @socket?.readyState WebSocketClientConnection::_doSend = (opcode, payload) -> @socket?.write encodeMessage(opcode, payload, true) return WebSocketServerConnection = (request, socket, upgradeHead) -> self = @ key = hashWebSocketKey(lowerObjKeys(request.headers)["sec-websocket-key"]) protocol = (-> if 'sec-websocket-protocol' of request.headers protocols = lowerObjKeys(request.headers)["sec-websocket-protocol"].split /\s*,\s*/ #console.log "Protocol: #{protocols[0]}" return protocols[0] else return null )() lines = [] # handshake response # http://tools.ietf.org/html/rfc6455#section-4.2.2 lines.push "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" lines.push "Upgrade: WebSocket\r\n" lines.push "Connection: Upgrade\r\n" lines.push "sec-websocket-accept: #{key}" lines.push "\r\nsec-websocket-protocol: #{protocol}" if protocol lines.push "\r\n\r\n" socket.write lines.join('') socket.on 'connect', (evt) -> self.emit 'open', if self.id then self.id else null socket.on 'error', (err) -> console.log 'Server Socket error:', err.message socket.on "data", (buf) -> self.buffer = Buffer.concat([ self.buffer buf ]) # process buffer while it contains complete frames continue while self._processBuffer() return socket.on "close", (had_error) -> unless self.closed self.emit "close", 1006 self.closed = true return # initialize connection state @request = request @socket = socket @buffer = new Buffer(0) @closed = false @currentRoundTrip = 0 return util.inherits WebSocketServerConnection, events.EventEmitter Object.defineProperty WebSocketServerConnection::, 'readyState', get: -> @socket.readyState # Ping method WebSocketClientConnection::ping = WebSocketServerConnection::ping = -> @_doSend opcodes.PING, new Buffer(new Date().getTime().toString()) # Send a text or binary message on the WebSocket connection WebSocketClientConnection::send = WebSocketServerConnection::send = (obj) -> opcode = undefined payload = undefined if Buffer.isBuffer(obj) opcode = opcodes.BINARY payload = obj else if typeof obj is "string" opcode = opcodes.TEXT # create a new buffer containing the UTF-8 encoded string payload = new Buffer(obj, "utf8") else try obj = JSON.stringify obj opcode = opcodes.TEXT payload = new Buffer(obj, "utf8") catch e throw new Error("Cannot send object. Must be string or Buffer") @_doSend opcode, payload return # Close the WebSocket connection WebSocketClientConnection::close = WebSocketServerConnection::close = (code, reason) -> opcode = opcodes.CLOSE buffer = undefined # encode close and reason if code buffer = new Buffer(Buffer.byteLength(reason) + 2) buffer.writeUInt16BE code, 0 buffer.write reason, 2 else buffer = new Buffer(0) @_doSend opcode, buffer @closed = true try @socket.end() @socket.destroy() catch e console.log "Error while destroying underlying raw socket:", e.message return # Process incoming bytes WebSocketClientConnection::_processBuffer = WebSocketServerConnection::_processBuffer = -> buf = @buffer # insufficient data read return if buf.length < 2 idx = 2 b1 = buf.readUInt8(0) fin = b1 & 0x80 opcode = b1 & 0x0f # low four bits b2 = buf.readUInt8(1) mask = b2 & 0x80 length = b2 & 0x7f # low 7 bits if length > 125 # insufficient data read return if buf.length < 8 if length is 126 length = buf.readUInt16BE(2) idx += 2 else if length is 127 # discard high 4 bits because this server cannot handle huge lengths highBits = buf.readUInt32BE(2) @close 1009, "" unless highBits is 0 length = buf.readUInt32BE(6) idx += 8 # insufficient data read return if buf.length < (idx + (if mask isnt 0 then 4 else 0) + length) if mask isnt 0 maskBytes = buf.slice(idx, idx + 4) idx += 4 payload = buf.slice(idx, idx + length) if mask isnt 0 payload = unmask(maskBytes, payload) @_handleFrame opcode, payload @buffer = buf.slice(idx + length) true WebSocketClientConnection::_handleFrame = WebSocketServerConnection::_handleFrame = (opcode, buffer) -> payload = undefined switch opcode when opcodes.TEXT payload = buffer.toString("utf8") @emit "data", opcode, payload when opcodes.BINARY payload = buffer @emit "data", opcode, payload when opcodes.PING # respond to pings with pongs @_doSend opcodes.PONG, buffer # process pongs when opcodes.PONG #process.stdout.write "\nReceived PONG: #{buffer.toString('utf8')}\n" pong_millis = new Date().getTime() ping_millis = parseInt(buffer.toString('utf8')) @currentRoundTrip = (pong_millis - ping_millis) / 1000 @emit "heartbeat", @currentRoundTrip, pong_millis when opcodes.CLOSE # parse close and reason code = undefined reason = undefined if buffer.length >= 2 code = buffer.readUInt16BE(0) reason = buffer.toString("utf8", 2) @close code, reason @emit "close", code, reason else @close 1002, "unknown opcode" @emit "close", 1002, "unknown opcode" return # Format and send a WebSocket message WebSocketServerConnection::_doSend = (opcode, payload) -> @socket?.write encodeMessage(opcode, payload, false) return WebSocketServer = (handler) -> if handler and handler.constructor.name is "Function" @connectionHandler = handler else throw new Error("Must provide a socket handler function to instantiate a WebSocketServer") return util.inherits WebSocketServer, events.EventEmitter WebSocketServer::listen = (port, host, route = "/") -> srv = undefined self = @ switch port.constructor.name when "Server" srv = port when "String" srv = http.createServer((request, response) -> response.end "websocket server" return ) srv.listen port when "Number" srv = http.createServer((request, response) -> response.end "websocket server" return ) srv.listen port, (if host then host else "0.0.0.0") else if port._handle srv = port else throw new TypeError "WebSocketServer only listens on something that has a _handle." srv.on 'listening', => @emit 'listening' srv.on "upgrade", (request, socket, upgradeHead) -> if URL.parse(request.url).path isnt route return #console.log "websocket out of path, aborting." #ws = new WebSocketServerConnection(request, socket, upgradeHead) #ws.close() else ws = new WebSocketServerConnection(request, socket, upgradeHead) self.connectionHandler ws setTimeout (-> ws.periodicPing = setInterval (-> ws.ping() if ws.readyState is 'open'), 2000), 1000 ws.on 'close', -> #console.log "Closing server websocket connection", ws.id clearInterval ws.periodicPing if ws.periodicPing? self.emit 'upgrade' ### # Didn't work because request doesn't register upgrade event. Must be done at server level. WebSocketServer::listenOnRoute = (router, path, socket_handler_fn = null) -> self = @ socket_handler_fn = socket_handler_fn or self.connectionHandler # use ad-hoc socket handler if provided, else use "default" socket handler obj = router.get_route_handler(path, 'get') if obj http_handler_fn = obj.handler_obj.handler else http_handler_fn = (request, response) -> response.end 'websocket server listening at ' + path path = "/#{path}" unless path.charAt(0) is "/" router.get path, (request, response) -> if request.headers['upgrade'] or request.headers['Upgrade'] ( (response, socket, upgradeHead) -> console.log "Received upgrade request on path: #{request.url}" ws = new WebSocketServerConnection(request, socket, upgradeHead) socket_handler_fn ws setTimeout (-> ws.periodicPing = setInterval (-> ws.ping() if ws.readyState is 'open'), 2000), 1000 ws.on 'close', -> #console.log "Closing server websocket connection", ws.id clearInterval ws.periodicPing if ws.periodicPing? self.emit 'upgrade')(null, request.socket, '') http_handler_fn request, response ### createWebSocketServer = (socket_handler_fn) -> new WebSocketServer(socket_handler_fn) module?.exports = exports = {createWebSocketServer, WebSocketServer, WebSocketServerConnection, WebSocketClientConnection, opcodes} #Test to execute when invoked stand-alone. test = -> reverseServer = createWebSocketServer((sock) -> sock.on "data", (opcode, data) -> sock.send data.split("").reverse().join("") return return ) reverseServer.listen 8000 console.log "Reverse WebSocket Server listening on port 8000" return test() unless module?.parent