PageRenderTime 56ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/src/server/websocket.coffee

http://github.com/stephank/orona
CoffeeScript | 166 lines | 94 code | 29 blank | 43 comment | 15 complexity | c2d9503819f86a836e02db1b88536583 MD5 | raw file
Possible License(s): GPL-2.0
  1. # This is an extract from Socket.IO-node.
  2. #
  3. # © 2010 Guillermo Rauch <guillermo@learnboost.com>
  4. # Adapted by Stéphan Kochen for Orona.
  5. #
  6. # (The MIT License)
  7. #
  8. # Copyright (c) 2010 LearnBoost <dev@learnboost.com>
  9. #
  10. # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  11. # associated documentation files (the 'Software'), to deal in the Software without restriction,
  12. # including without limitation the rights to use, copy, modify, merge, publish, distribute,
  13. # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  14. # furnished to do so, subject to the following conditions:
  15. # The above copyright notice and this permission notice shall be included in all copies or
  16. # substantial portions of the Software.
  17. # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  18. # BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  20. # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. {EventEmitter} = require 'events'
  23. {createHash} = require 'crypto'
  24. class WebSocket extends EventEmitter
  25. constructor: (@request, @connection, initialData) ->
  26. super
  27. @connection.setTimeout 0
  28. @connection.setEncoding 'utf8'
  29. @connection.setNoDelay yes
  30. # FIXME: should be buffer
  31. @data = initialData.toString('binary')
  32. # A temporary queue of messages while the handshake is in progress.
  33. # The `@request` attribute is also temporary. Both will be deleted eventually.
  34. @queued = []
  35. # Start processing the handshake. We will emit events, so postpone it
  36. # until the next event loop tick, allowing the user to install handlers.
  37. process.nextTick => @_handshake()
  38. @connection.on 'data', (data) => @_onData(data)
  39. # Delegate socket methods and events.
  40. @connection.on 'end', => @_onEnd()
  41. @connection.on 'timeout', => @_onTimeout()
  42. @connection.on 'drain', => @_onDrain()
  43. @connection.on 'error', => @_onError()
  44. @connection.on 'close', => @_onClose()
  45. _handshake: ->
  46. return if @data.length < 8
  47. # Get the keys.
  48. k1 = @request.headers['sec-websocket-key1']
  49. k2 = @request.headers['sec-websocket-key2']
  50. @emit 'error', new Error("Keys missing in client handshake") unless k1 and k2
  51. k3 = @data.slice(0, 8)
  52. @data = @data.slice(8)
  53. # Calculate the challenge from the keys given by the client.
  54. md5 = createHash 'md5'
  55. for k in [k1, k2]
  56. n = parseInt(k.replace(/[^\d]/g, ''))
  57. spaces = k.replace(/[^ ]/g, '').length
  58. if spaces == 0 or n % spaces != 0
  59. @emit 'error', new Error("Invalid Keys in client handshake")
  60. n /= spaces
  61. md5.update(new Buffer([
  62. (n & 0xFF000000) >> 24,
  63. (n & 0x00FF0000) >> 16,
  64. (n & 0x0000FF00) >> 8,
  65. (n & 0x000000FF) >> 0
  66. ]))
  67. md5.update k3
  68. # There's no direct way to get a buffer, yet.
  69. md5 = new Buffer(md5.digest('base64'), 'base64')
  70. # Build the response headers.
  71. origin = @request.headers.origin
  72. headers = [
  73. 'HTTP/1.1 101 WebSocket Protocol Handshake',
  74. 'Upgrade: WebSocket',
  75. 'Connection: Upgrade',
  76. 'Sec-WebSocket-Origin: ' + (origin || 'null'),
  77. 'Sec-WebSocket-Location: ws://' + @request.headers.host + @request.url
  78. ]
  79. if 'sec-websocket-protocol' in @request.headers
  80. headers.push('Sec-WebSocket-Protocol: ' + @request.headers['sec-websocket-protocol'])
  81. headers = headers.concat('', '').join('\r\n')
  82. # Send handshake.
  83. @connection.write headers, 'utf-8'
  84. @connection.write md5
  85. # Flush queued messages, and clean up stuff we no longer need.
  86. delete @request
  87. for message in @queued
  88. @sendMessage(message)
  89. delete @queued
  90. # Signal the user.
  91. @emit 'connect'
  92. # Flush any remaining data.
  93. @_onData() if @data.length > 0
  94. _onData: (data) ->
  95. # FIXME: cannot concatenate and slice buffers easily.
  96. @data += data if data?
  97. if @request?
  98. # The handshake is still waiting for the challenge.
  99. @_handshake()
  100. else
  101. # Split at the ending sentinel.
  102. chunks = @data.split '\ufffd'
  103. # The list element is either an empty string or incomplete message.
  104. @data = chunks.pop()
  105. # Check messages, then signal to the user.
  106. for chunk in chunks
  107. return @connection.end() unless chunk[0] == '\u0000'
  108. @emit 'message', chunk.slice(1)
  109. return
  110. # Send a single message.
  111. sendMessage: (message) ->
  112. if @request?
  113. # Queue the message if needed. This happens during the handshake.
  114. @queued.push message
  115. return
  116. messageLength = Buffer.byteLength message, 'utf-8'
  117. buffer = new Buffer(messageLength + 2)
  118. buffer[0] = 0x00
  119. buffer.write message, 1, 'utf-8'
  120. buffer[messageLength + 1] = 0xFF
  121. try
  122. @connection.write buffer
  123. catch e
  124. @emit 'error', e
  125. # Delegate socket methods and events.
  126. end: (message) ->
  127. @sendMessage(message) if message?
  128. @connection.end()
  129. setTimeout: (ms) -> @connection.setTimeout(ms)
  130. destroy: -> @connection.destroy()
  131. _onEnd: -> @emit 'end'
  132. _onTimeout: -> @emit 'timeout'
  133. _onDrain: -> @emit 'drain'
  134. _onError: (exception) -> @emit 'error', exception
  135. _onClose: (had_error) -> @emit 'close', had_error
  136. ## Exports
  137. module.exports = WebSocket