em-websocket 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG.rdoc +10 -0
  2. data/README.md +16 -0
  3. data/examples/test.html +11 -8
  4. data/lib/em-websocket.rb +6 -3
  5. data/lib/em-websocket/close03.rb +11 -0
  6. data/lib/em-websocket/close05.rb +11 -0
  7. data/lib/em-websocket/close06.rb +16 -0
  8. data/lib/em-websocket/close75.rb +10 -0
  9. data/lib/em-websocket/connection.rb +58 -32
  10. data/lib/em-websocket/framing03.rb +9 -30
  11. data/lib/em-websocket/framing04.rb +15 -0
  12. data/lib/em-websocket/framing05.rb +157 -0
  13. data/lib/em-websocket/framing76.rb +5 -6
  14. data/lib/em-websocket/handler.rb +2 -4
  15. data/lib/em-websocket/handler03.rb +2 -6
  16. data/lib/em-websocket/handler05.rb +10 -0
  17. data/lib/em-websocket/handler06.rb +10 -0
  18. data/lib/em-websocket/handler75.rb +1 -0
  19. data/lib/em-websocket/handler76.rb +1 -0
  20. data/lib/em-websocket/handler_factory.rb +41 -22
  21. data/lib/em-websocket/handshake04.rb +35 -0
  22. data/lib/em-websocket/handshake75.rb +4 -4
  23. data/lib/em-websocket/handshake76.rb +8 -8
  24. data/lib/em-websocket/masking04.rb +27 -0
  25. data/lib/em-websocket/message_processor_03.rb +33 -0
  26. data/lib/em-websocket/message_processor_06.rb +46 -0
  27. data/lib/em-websocket/version.rb +1 -1
  28. data/spec/helper.rb +54 -2
  29. data/spec/integration/common_spec.rb +115 -0
  30. data/spec/integration/draft03_spec.rb +26 -11
  31. data/spec/integration/draft05_spec.rb +45 -0
  32. data/spec/integration/draft06_spec.rb +79 -0
  33. data/spec/integration/draft75_spec.rb +115 -0
  34. data/spec/integration/draft76_spec.rb +25 -10
  35. data/spec/integration/shared_examples.rb +62 -0
  36. data/spec/unit/framing_spec.rb +55 -0
  37. data/spec/unit/masking_spec.rb +18 -0
  38. metadata +29 -33
  39. data/spec/websocket_spec.rb +0 -210
@@ -1,5 +1,15 @@
1
1
  = Changelog
2
2
 
3
+ == 0.3.0 / 2011-05-06
4
+
5
+ - new features:
6
+ - Support WebSocket drafts 05 & 06
7
+ - changes:
8
+ - Accept request headers in a case insensitive manner
9
+ - Change handling of errors. Previously some application errors were caught
10
+ internally and were invisible unless an onerror callback was supplied. See
11
+ readme for details
12
+
3
13
  == 0.2.1 / 2011-03-01
4
14
 
5
15
  - bugfixes:
data/README.md CHANGED
@@ -42,6 +42,22 @@ For example,
42
42
  ...
43
43
  end
44
44
 
45
+ ## Handling errors
46
+
47
+ There are two kinds of errors that need to be handled - errors caused by incompatible WebSocket clients sending invalid data and errors in application code. They are handled as follows:
48
+
49
+ Errors caused by invalid WebSocket data (for example invalid errors in the WebSocket handshake or invalid message frames) raise errors which descend from `EventMachine::WebSocket::WebSocketError`. Such errors are rescued internally and the WebSocket connection will be closed immediately or an error code sent to the browser in accordance to the WebSocket specification. However it is possible to be notified in application code on such errors by including an `onerror` callback.
50
+
51
+ ws.onerror { |error|
52
+ if e.kind_of?(EM::WebSocket::WebSocketError)
53
+ ...
54
+ end
55
+ }
56
+
57
+ Application errors are treated differently. If no `onerror` callback has been defined these errors will propagate to the EventMachine reactor, typically causing your program to terminate. If you wish to handle exceptions, simply supply an `onerror callback` and check for exceptions which are not decendant from `EventMachine::WebSocket::WebSocketError`.
58
+
59
+ It is also possible to log all errors when developing by including the `:debug => true` option when initialising the WebSocket connection.
60
+
45
61
  ## Examples & Projects using em-websocket
46
62
 
47
63
  * [Pusher](http://pusherapp.com) - Realtime client push
@@ -1,26 +1,29 @@
1
1
  <html>
2
2
  <head>
3
- <script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'></script>
4
3
  <script src='js/swfobject.js'></script>
5
4
  <script src='js/FABridge.js'></script>
6
5
  <script src='js/web_socket.js'></script>
7
6
  <script>
8
- $(document).ready(function(){
9
- function debug(str){ $("#debug").append("<p>" + str); };
7
+ function init() {
8
+ function debug(string) {
9
+ var element = document.getElementById("debug");
10
+ var p = document.createElement("p");
11
+ p.appendChild(document.createTextNode(string));
12
+ element.appendChild(p);
13
+ }
10
14
 
11
15
  ws = new WebSocket("ws://localhost:8080/");
12
- ws.onmessage = function(evt) { $("#msg").append("<p>"+evt.data+"</p>"); };
16
+ ws.onmessage = function(evt) { debug("Message: " + evt.data); };
13
17
  ws.onclose = function() { debug("socket closed"); };
14
18
  ws.onopen = function() {
15
19
  debug("connected...");
16
20
  ws.send("hello server");
17
21
  ws.send("hello again");
18
22
  };
19
- });
23
+ };
20
24
  </script>
21
25
  </head>
22
- <body>
26
+ <body onload="init();">
23
27
  <div id="debug"></div>
24
- <div id="msg"></div>
25
28
  </body>
26
- </html>
29
+ </html>
@@ -4,9 +4,12 @@ require "eventmachine"
4
4
 
5
5
  %w[
6
6
  debugger websocket connection
7
- handshake75 handshake76
8
- framing76 framing03
9
- handler_factory handler handler75 handler76 handler03
7
+ handshake75 handshake76 handshake04
8
+ framing76 framing03 framing04 framing05
9
+ close75 close03 close05 close06
10
+ masking04
11
+ message_processor_03 message_processor_06
12
+ handler_factory handler handler75 handler76 handler03 handler05 handler06
10
13
  ].each do |file|
11
14
  require "em-websocket/#{file}"
12
15
  end
@@ -0,0 +1,11 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Close03
4
+ def close_websocket(code, body)
5
+ # TODO: Ideally send body data and check that it matches in ack
6
+ send_frame(:close, '')
7
+ @state = :closing
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Close05
4
+ def close_websocket(code, body)
5
+ # TODO: Ideally send body data and check that it matches in ack
6
+ send_frame(:close, "\x53")
7
+ @state = :closing
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Close06
4
+ def close_websocket(code, body)
5
+ if code
6
+ close_data = [code].pack('n')
7
+ close_data << body if body
8
+ send_frame(:close, close_data)
9
+ else
10
+ send_frame(:close, '')
11
+ end
12
+ @state = :closing
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Close75
4
+ def close_websocket(code, body)
5
+ @state = :closed
6
+ @connection.close_connection_after_writing
7
+ end
8
+ end
9
+ end
10
+ end
@@ -20,6 +20,11 @@ module EventMachine
20
20
  def trigger_on_close
21
21
  @onclose.call if @onclose
22
22
  end
23
+ def trigger_on_error(reason)
24
+ return false unless @onerror
25
+ @onerror.call(reason)
26
+ true
27
+ end
23
28
 
24
29
  def initialize(options)
25
30
  @options = options
@@ -34,13 +39,15 @@ module EventMachine
34
39
  # Use this method to close the websocket connection cleanly
35
40
  # This sends a close frame and waits for acknowlegement before closing
36
41
  # the connection
37
- def close_websocket
38
- if @handler
39
- @handler.close_websocket
40
- else
41
- # The handshake hasn't completed - should be safe to terminate
42
- close_connection
42
+ def close_websocket(code = nil, body = nil)
43
+ if code && !(4000..4999).include?(code)
44
+ raise "Application code may only use codes in the range 4000-4999"
43
45
  end
46
+
47
+ # If code not defined then set to 1000 (normal closure)
48
+ code ||= 1000
49
+
50
+ close_websocket_private(code, body)
44
51
  end
45
52
 
46
53
  def post_init
@@ -55,12 +62,32 @@ module EventMachine
55
62
  else
56
63
  dispatch(data)
57
64
  end
65
+ rescue HandshakeError => e
66
+ debug [:error, e]
67
+ trigger_on_error(e)
68
+ # Errors during the handshake require the connection to be aborted
69
+ abort
70
+ rescue WebSocketError => e
71
+ debug [:error, e]
72
+ trigger_on_error(e)
73
+ close_websocket_private(1002) # 1002 indicates a protocol error
74
+ rescue => e
75
+ debug [:error, e]
76
+ # These are application errors - raise unless onerror defined
77
+ trigger_on_error(e) || raise(e)
78
+ # There is no code defined for application errors, so use 3000
79
+ # (which is reserved for frameworks)
80
+ close_websocket_private(3000)
58
81
  end
59
82
 
60
83
  def unbind
61
84
  debug [:unbind, :connection]
62
85
 
63
86
  @handler.unbind if @handler
87
+ rescue => e
88
+ debug [:error, e]
89
+ # These are application errors - raise unless onerror defined
90
+ trigger_on_error(e) || raise(e)
64
91
  end
65
92
 
66
93
  def dispatch(data)
@@ -69,30 +96,18 @@ module EventMachine
69
96
  return false
70
97
  else
71
98
  debug [:inbound_headers, data]
72
- begin
73
- @data << data
74
- @handler = HandlerFactory.build(self, @data, @secure, @debug)
75
- unless @handler
76
- # The whole header has not been received yet.
77
- return false
78
- end
79
- @data = nil
80
- @handler.run
81
- return true
82
- rescue => e
83
- debug [:error, e]
84
- process_bad_request(e)
99
+ @data << data
100
+ @handler = HandlerFactory.build(self, @data, @secure, @debug)
101
+ unless @handler
102
+ # The whole header has not been received yet.
85
103
  return false
86
104
  end
105
+ @data = nil
106
+ @handler.run
107
+ return true
87
108
  end
88
109
  end
89
110
 
90
- def process_bad_request(reason)
91
- @onerror.call(reason) if @onerror
92
- send_data "HTTP/1.1 400 Bad request\r\n\r\n"
93
- close_connection_after_writing
94
- end
95
-
96
111
  def send_flash_cross_domain_file
97
112
  file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
98
113
  debug [:cross_domain, file]
@@ -105,8 +120,6 @@ module EventMachine
105
120
  end
106
121
 
107
122
  def send(data)
108
- debug [:send, data]
109
-
110
123
  if @handler
111
124
  @handler.send_text_frame(data)
112
125
  else
@@ -114,11 +127,6 @@ module EventMachine
114
127
  end
115
128
  end
116
129
 
117
- def close_with_error(message)
118
- @onerror.call(message) if @onerror
119
- close_connection_after_writing
120
- end
121
-
122
130
  def request
123
131
  @handler ? @handler.request : {}
124
132
  end
@@ -126,6 +134,24 @@ module EventMachine
126
134
  def state
127
135
  @handler ? @handler.state : :handshake
128
136
  end
137
+
138
+ private
139
+
140
+ # As definited in draft 06 7.2.2, some failures require that the server
141
+ # abort the websocket connection rather than close cleanly
142
+ def abort
143
+ close_connection
144
+ end
145
+
146
+ def close_websocket_private(code, body = nil)
147
+ if @handler
148
+ debug [:closing, code]
149
+ @handler.close_websocket(code, body)
150
+ else
151
+ # The handshake hasn't completed - should be safe to terminate
152
+ abort
153
+ end
154
+ end
129
155
  end
130
156
  end
131
157
  end
@@ -3,7 +3,7 @@
3
3
  module EventMachine
4
4
  module WebSocket
5
5
  module Framing03
6
-
6
+
7
7
  def initialize_framing
8
8
  @data = ''
9
9
  @application_data_buffer = '' # Used for MORE frames
@@ -15,7 +15,7 @@ module EventMachine
15
15
  while !error && @data.size > 1
16
16
  pointer = 0
17
17
 
18
- more = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
18
+ more = ((@data.getbyte(pointer) & 0b10000000) == 0b10000000) ^ fin
19
19
  # Ignoring rsv1-3 for now
20
20
  opcode = @data.getbyte(0) & 0b00001111
21
21
  pointer += 1
@@ -28,7 +28,7 @@ module EventMachine
28
28
  when 127 # Length defined by 8 bytes
29
29
  # Check buffer size
30
30
  if @data.getbyte(pointer+8-1) == nil
31
- debug [:buffer_incomplete, @data.inspect]
31
+ debug [:buffer_incomplete, @data]
32
32
  error = true
33
33
  next
34
34
  end
@@ -41,7 +41,7 @@ module EventMachine
41
41
  when 126 # Length defined by 2 bytes
42
42
  # Check buffer size
43
43
  if @data.getbyte(pointer+2-1) == nil
44
- debug [:buffer_incomplete, @data.inspect]
44
+ debug [:buffer_incomplete, @data]
45
45
  error = true
46
46
  next
47
47
  end
@@ -55,7 +55,7 @@ module EventMachine
55
55
 
56
56
  # Check buffer size
57
57
  if @data.getbyte(pointer+payload_length-1) == nil
58
- debug [:buffer_incomplete, @data.inspect]
58
+ debug [:buffer_incomplete, @data]
59
59
  error = true
60
60
  next
61
61
  end
@@ -91,6 +91,8 @@ module EventMachine
91
91
  end
92
92
 
93
93
  def send_frame(frame_type, application_data)
94
+ debug [:sending_frame, frame_type, application_data]
95
+
94
96
  if @state == :closing && data_frame?(frame_type)
95
97
  raise WebSocketError, "Cannot send data frame since connection is closing"
96
98
  end
@@ -124,31 +126,8 @@ module EventMachine
124
126
 
125
127
  private
126
128
 
127
- def message(message_type, extension_data, application_data)
128
- case message_type
129
- when :close
130
- if @state == :closing
131
- # TODO: Check that message body matches sent data
132
- # We can close connection immediately since there is no more data
133
- # is allowed to be sent or received on this connection
134
- @connection.close_connection
135
- @state = :closed
136
- else
137
- # Acknowlege close
138
- # The connection is considered closed
139
- send_frame(:close, application_data)
140
- @state = :closed
141
- @connection.close_connection_after_writing
142
- end
143
- when :ping
144
- # Pong back the same data
145
- send_frame(:pong, application_data)
146
- when :pong
147
- # TODO: Do something. Complete a deferrable established by a ping?
148
- when :text, :binary
149
- @connection.trigger_on_message(application_data)
150
- end
151
- end
129
+ # This allows flipping the more bit to fin for draft 04
130
+ def fin; false; end
152
131
 
153
132
  FRAME_TYPES = {
154
133
  :continuation => 0,
@@ -0,0 +1,15 @@
1
+ # encoding: BINARY
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ # The only difference between draft 03 framing and draft 04 framing is
6
+ # that the MORE bit has been changed to a FIN bit
7
+ module Framing04
8
+ include Framing03
9
+
10
+ private
11
+
12
+ def fin; true; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,157 @@
1
+ # encoding: BINARY
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ module Framing05
6
+
7
+ def initialize_framing
8
+ @data = MaskedString.new
9
+ @application_data_buffer = '' # Used for MORE frames
10
+ end
11
+
12
+ def process_data(newdata)
13
+ error = false
14
+
15
+ while !error && @data.size > 5 # mask plus first byte present
16
+ pointer = 0
17
+
18
+ @data.read_mask
19
+
20
+ fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
21
+ # Ignoring rsv1-3 for now
22
+ opcode = @data.getbyte(pointer) & 0b00001111
23
+ pointer += 1
24
+
25
+ # Ignoring rsv4
26
+ length = @data.getbyte(pointer) & 0b01111111
27
+ pointer += 1
28
+
29
+ payload_length = case length
30
+ when 127 # Length defined by 8 bytes
31
+ # Check buffer size
32
+ if @data.getbyte(pointer+8-1) == nil
33
+ debug [:buffer_incomplete, @data]
34
+ error = true
35
+ next
36
+ end
37
+
38
+ # Only using the last 4 bytes for now, till I work out how to
39
+ # unpack 8 bytes. I'm sure 4GB frames will do for now :)
40
+ l = @data.getbytes(pointer+4, 4).unpack('N').first
41
+ pointer += 8
42
+ l
43
+ when 126 # Length defined by 2 bytes
44
+ # Check buffer size
45
+ if @data.getbyte(pointer+2-1) == nil
46
+ debug [:buffer_incomplete, @data]
47
+ error = true
48
+ next
49
+ end
50
+
51
+ l = @data.getbytes(pointer, 2).unpack('n').first
52
+ pointer += 2
53
+ l
54
+ else
55
+ length
56
+ end
57
+
58
+ # Check buffer size
59
+ if @data.getbyte(pointer+payload_length-1) == nil
60
+ debug [:buffer_incomplete, @data]
61
+ error = true
62
+ next
63
+ end
64
+
65
+ # Read application data
66
+ application_data = @data.getbytes(pointer, payload_length)
67
+ pointer += payload_length
68
+
69
+ # Throw away data up to pointer
70
+ @data.slice!(0...(pointer + 4))
71
+
72
+ frame_type = opcode_to_type(opcode)
73
+
74
+ if frame_type == :continuation && !@frame_type
75
+ raise WebSocketError, 'Continuation frame not expected'
76
+ end
77
+
78
+ if !fin
79
+ debug [:moreframe, frame_type, application_data]
80
+ @application_data_buffer << application_data
81
+ @frame_type = frame_type
82
+ else
83
+ # Message is complete
84
+ if frame_type == :continuation
85
+ @application_data_buffer << application_data
86
+ message(@frame_type, '', @application_data_buffer)
87
+ @application_data_buffer = ''
88
+ @frame_type = nil
89
+ else
90
+ message(frame_type, '', application_data)
91
+ end
92
+ end
93
+ end # end while
94
+ end
95
+
96
+ def send_frame(frame_type, application_data)
97
+ debug [:sending_frame, frame_type, application_data]
98
+
99
+ if @state == :closing && data_frame?(frame_type)
100
+ raise WebSocketError, "Cannot send data frame since connection is closing"
101
+ end
102
+
103
+ frame = ''
104
+
105
+ opcode = type_to_opcode(frame_type)
106
+ byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
107
+ frame << byte1
108
+
109
+ length = application_data.size
110
+ if length <= 125
111
+ byte2 = length # since rsv4 is 0
112
+ frame << byte2
113
+ elsif length < 65536 # write 2 byte length
114
+ frame << 126
115
+ frame << [length].pack('n')
116
+ else # write 8 byte length
117
+ frame << 127
118
+ frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
119
+ end
120
+
121
+ frame << application_data
122
+
123
+ @connection.send_data(frame)
124
+ end
125
+
126
+ def send_text_frame(data)
127
+ send_frame(:text, data)
128
+ end
129
+
130
+ private
131
+
132
+ FRAME_TYPES = {
133
+ :continuation => 0,
134
+ :close => 1,
135
+ :ping => 2,
136
+ :pong => 3,
137
+ :text => 4,
138
+ :binary => 5
139
+ }
140
+ FRAME_TYPES_INVERSE = FRAME_TYPES.invert
141
+ # Frames are either data frames or control frames
142
+ DATA_FRAMES = [:text, :binary, :continuation]
143
+
144
+ def type_to_opcode(frame_type)
145
+ FRAME_TYPES[frame_type] || raise("Unknown frame type")
146
+ end
147
+
148
+ def opcode_to_type(opcode)
149
+ FRAME_TYPES_INVERSE[opcode] || raise("Unknown opcode")
150
+ end
151
+
152
+ def data_frame?(type)
153
+ DATA_FRAMES.include?(type)
154
+ end
155
+ end
156
+ end
157
+ end