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
@@ -42,12 +42,11 @@ module EventMachine
42
42
 
43
43
  # Addition to the spec to protect against malicious requests
44
44
  if length > MAXIMUM_FRAME_LENGTH
45
- @connection.close_with_error(DataError.new("Frame length too long (#{length} bytes)"))
46
- return false
45
+ raise DataError, "Frame length too long (#{length} bytes)"
47
46
  end
48
47
 
49
48
  if @data.getbyte(pointer+length-1) == nil
50
- debug [:buffer_incomplete, @data.inspect]
49
+ debug [:buffer_incomplete, @data]
51
50
  # Incomplete data - leave @data to accumulate
52
51
  error = true
53
52
  else
@@ -70,13 +69,12 @@ module EventMachine
70
69
 
71
70
  if @data.getbyte(0) != 0x00
72
71
  # Close the connection since this buffer can never match
73
- @connection.close_with_error(DataError.new("Invalid frame received"))
72
+ raise DataError, "Invalid frame received"
74
73
  end
75
74
 
76
75
  # Addition to the spec to protect against malicious requests
77
76
  if @data.size > MAXIMUM_FRAME_LENGTH
78
- @connection.close_with_error(DataError.new("Frame length too long (#{@data.size} bytes)"))
79
- return false
77
+ raise DataError, "Frame length too long (#{@data.size} bytes)"
80
78
  end
81
79
 
82
80
  # Optimization to avoid calling slice! unnecessarily
@@ -105,6 +103,7 @@ module EventMachine
105
103
  # byte to a value betweent 0x80 and 0xFF, followed by
106
104
  # a leading length indicator
107
105
  def send_text_frame(data)
106
+ debug [:sending_text_frame, data]
108
107
  ary = ["\x00", data, "\xff"]
109
108
  ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
110
109
  @connection.send_data(ary.join)
@@ -28,10 +28,8 @@ module EventMachine
28
28
  process_data(data)
29
29
  end
30
30
 
31
- def close_websocket
32
- # Unless redefined in a subclass, just close the connection
33
- @state = :closed
34
- @connection.close_connection_after_writing
31
+ def close_websocket(code, body)
32
+ # Implemented in subclass
35
33
  end
36
34
 
37
35
  def unbind
@@ -3,12 +3,8 @@ module EventMachine
3
3
  class Handler03 < Handler
4
4
  include Handshake76
5
5
  include Framing03
6
-
7
- def close_websocket
8
- # TODO: Should we send data and check the response matches?
9
- send_frame(:close, '')
10
- @state = :closing
11
- end
6
+ include MessageProcessor03
7
+ include Close03
12
8
  end
13
9
  end
14
10
  end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler05 < Handler
4
+ include Handshake04
5
+ include Framing05
6
+ include MessageProcessor03
7
+ include Close05
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler06 < Handler
4
+ include Handshake04
5
+ include Framing05
6
+ include MessageProcessor06
7
+ include Close06
8
+ end
9
+ end
10
+ end
@@ -3,6 +3,7 @@ module EventMachine
3
3
  class Handler75 < Handler
4
4
  include Handshake75
5
5
  include Framing76
6
+ include Close75
6
7
  end
7
8
  end
8
9
  end
@@ -3,6 +3,7 @@ module EventMachine
3
3
  class Handler76 < Handler
4
4
  include Handshake76
5
5
  include Framing76
6
+ include Close75
6
7
 
7
8
  # "\377\000" is octet version and "\xff\x00" is hex version
8
9
  TERMINATE_STRING = "\xff\x00"
@@ -18,59 +18,78 @@ module EventMachine
18
18
  # extract request path
19
19
  first_line = lines.shift.match(PATH)
20
20
  raise HandshakeError, "Invalid HTTP header" unless first_line
21
- request['Method'] = first_line[1].strip
22
- request['Path'] = first_line[2].strip
21
+ request['method'] = first_line[1].strip
22
+ request['path'] = first_line[2].strip
23
23
 
24
- unless request["Method"] == "GET"
24
+ unless request["method"] == "GET"
25
25
  raise HandshakeError, "Must be GET request"
26
26
  end
27
27
 
28
28
  # extract query string values
29
- request['Query'] = Addressable::URI.parse(request['Path']).query_values ||= {}
29
+ request['query'] = Addressable::URI.parse(request['path']).query_values ||= {}
30
30
  # extract remaining headers
31
31
  lines.each do |line|
32
32
  h = HEADER.match(line)
33
- request[h[1].strip] = h[2].strip if h
33
+ request[h[1].strip.downcase] = h[2].strip if h
34
34
  end
35
35
 
36
- version = request['Sec-WebSocket-Key1'] ? 76 : 75
36
+ build_with_request(connection, request, remains, secure, debug)
37
+ end
38
+
39
+ def self.build_with_request(connection, request, remains, secure = false, debug = false)
40
+ # Determine version heuristically
41
+ version = if request['sec-websocket-version']
42
+ # Used from drafts 04 onwards
43
+ request['sec-websocket-version'].to_i
44
+ elsif request['sec-websocket-draft']
45
+ # Used in drafts 01 - 03
46
+ request['sec-websocket-draft'].to_i
47
+ elsif request['sec-websocket-key1']
48
+ 76
49
+ else
50
+ 75
51
+ end
52
+
53
+ # Additional handling of bytes after the header if required
37
54
  case version
38
55
  when 75
39
56
  if !remains.empty?
40
57
  raise HandshakeError, "Extra bytes after header"
41
58
  end
42
- when 76
59
+ when 76, 1..3
43
60
  if remains.length < 8
44
61
  # The whole third-key has not been received yet.
45
62
  return nil
46
63
  elsif remains.length > 8
47
64
  raise HandshakeError, "Extra bytes after third key"
48
65
  end
49
- request['Third-Key'] = remains
50
- else
51
- raise WebSocketError, "Must not happen"
66
+ request['third-key'] = remains
52
67
  end
53
68
 
54
- unless request['Connection'] == 'Upgrade' and request['Upgrade'] == 'WebSocket'
69
+ # Validate that Connection and Upgrade headers
70
+ unless request['connection'] && request['connection'] =~ /Upgrade/ && request['upgrade'] && request['upgrade'].downcase == 'websocket'
55
71
  raise HandshakeError, "Connection and Upgrade headers required"
56
72
  end
57
73
 
58
74
  # transform headers
59
75
  protocol = (secure ? "wss" : "ws")
60
- request['Host'] = Addressable::URI.parse("#{protocol}://"+request['Host'])
76
+ request['host'] = Addressable::URI.parse("#{protocol}://"+request['host'])
61
77
 
62
- if version = request['Sec-WebSocket-Draft']
63
- if version == '1' || version == '2' || version == '3'
64
- # We'll use handler03 - I believe they're all compatible
65
- Handler03.new(connection, request, debug)
66
- else
67
- # According to spec should abort the connection
68
- raise WebSocketError, "Unknown draft version: #{version}"
69
- end
70
- elsif request['Sec-WebSocket-Key1']
78
+ case version
79
+ when 75
80
+ Handler75.new(connection, request, debug)
81
+ when 76
71
82
  Handler76.new(connection, request, debug)
83
+ when 1..3
84
+ # We'll use handler03 - I believe they're all compatible
85
+ Handler03.new(connection, request, debug)
86
+ when 5
87
+ Handler05.new(connection, request, debug)
88
+ when 6
89
+ Handler06.new(connection, request, debug)
72
90
  else
73
- Handler75.new(connection, request, debug)
91
+ # According to spec should abort the connection
92
+ raise WebSocketError, "Protocol version #{version} not supported"
74
93
  end
75
94
  end
76
95
  end
@@ -0,0 +1,35 @@
1
+ require 'digest/sha1'
2
+ require 'base64'
3
+
4
+ module EventMachine
5
+ module WebSocket
6
+ module Handshake04
7
+ def handshake
8
+ # Required
9
+ unless key = request['sec-websocket-key']
10
+ raise HandshakeError, "Sec-WebSocket-Key header is required"
11
+ end
12
+
13
+ # Optional
14
+ origin = request['sec-websocket-origin']
15
+ protocols = request['sec-websocket-protocol']
16
+ extensions = request['sec-websocket-extensions']
17
+
18
+ string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
19
+ signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
20
+
21
+ upgrade = ["HTTP/1.1 101 Switching Protocols"]
22
+ upgrade << "Upgrade: websocket"
23
+ upgrade << "Connection: Upgrade"
24
+ upgrade << "Sec-WebSocket-Accept: #{signature}"
25
+
26
+ # TODO: Support Sec-WebSocket-Protocol
27
+ # TODO: Sec-WebSocket-Extensions
28
+
29
+ debug [:upgrade_headers, upgrade]
30
+
31
+ return upgrade.join("\r\n") + "\r\n\r\n"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,14 +2,14 @@ module EventMachine
2
2
  module WebSocket
3
3
  module Handshake75
4
4
  def handshake
5
- location = "#{request['Host'].scheme}://#{request['Host'].host}"
6
- location << ":#{request['Host'].port}" if request['Host'].port
7
- location << request['Path']
5
+ location = "#{request['host'].scheme}://#{request['host'].host}"
6
+ location << ":#{request['host'].port}" if request['host'].port
7
+ location << request['path']
8
8
 
9
9
  upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
10
  upgrade << "Upgrade: WebSocket\r\n"
11
11
  upgrade << "Connection: Upgrade\r\n"
12
- upgrade << "WebSocket-Origin: #{request['Origin']}\r\n"
12
+ upgrade << "WebSocket-Origin: #{request['origin']}\r\n"
13
13
  upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
14
 
15
15
  debug [:upgrade_headers, upgrade]
@@ -5,21 +5,21 @@ module EventMachine
5
5
  module Handshake76
6
6
  def handshake
7
7
  challenge_response = solve_challenge(
8
- request['Sec-WebSocket-Key1'],
9
- request['Sec-WebSocket-Key2'],
10
- request['Third-Key']
8
+ request['sec-websocket-key1'],
9
+ request['sec-websocket-key2'],
10
+ request['third-key']
11
11
  )
12
12
 
13
- location = "#{request['Host'].scheme}://#{request['Host'].host}"
14
- location << ":#{request['Host'].port}" if request['Host'].port
15
- location << request['Path']
13
+ location = "#{request['host'].scheme}://#{request['host'].host}"
14
+ location << ":#{request['host'].port}" if request['host'].port
15
+ location << request['path']
16
16
 
17
17
  upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
18
18
  upgrade << "Upgrade: WebSocket\r\n"
19
19
  upgrade << "Connection: Upgrade\r\n"
20
20
  upgrade << "Sec-WebSocket-Location: #{location}\r\n"
21
- upgrade << "Sec-WebSocket-Origin: #{request['Origin']}\r\n"
22
- if protocol = request['Sec-WebSocket-Protocol']
21
+ upgrade << "Sec-WebSocket-Origin: #{request['origin']}\r\n"
22
+ if protocol = request['sec-websocket-protocol']
23
23
  validate_protocol!(protocol)
24
24
  upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
25
25
  end
@@ -0,0 +1,27 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class MaskedString < String
4
+ def read_mask
5
+ raise "Too short" if bytesize < 4 # TODO - change
6
+ @masking_key = String.new(self[0..3])
7
+ end
8
+
9
+ def slice_mask
10
+ slice!(0, 4)
11
+ end
12
+
13
+ def getbyte(index)
14
+ masked_char = super(index + 4)
15
+ masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
16
+ end
17
+
18
+ def getbytes(start_index, count)
19
+ data = ''
20
+ count.times do |i|
21
+ data << getbyte(start_index + i)
22
+ end
23
+ data
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: BINARY
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ module MessageProcessor03
6
+ def message(message_type, extension_data, application_data)
7
+ case message_type
8
+ when :close
9
+ if @state == :closing
10
+ # TODO: Check that message body matches sent data
11
+ # We can close connection immediately since there is no more data
12
+ # is allowed to be sent or received on this connection
13
+ @connection.close_connection
14
+ @state = :closed
15
+ else
16
+ # Acknowlege close
17
+ # The connection is considered closed
18
+ send_frame(:close, application_data)
19
+ @state = :closed
20
+ @connection.close_connection_after_writing
21
+ end
22
+ when :ping
23
+ # Pong back the same data
24
+ send_frame(:pong, application_data)
25
+ when :pong
26
+ # TODO: Do something. Complete a deferrable established by a ping?
27
+ when :text, :binary
28
+ @connection.trigger_on_message(application_data)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module MessageProcessor06
4
+ def message(message_type, extension_data, application_data)
5
+ debug [:message_received, message_type, application_data]
6
+
7
+ case message_type
8
+ when :close
9
+ status_code = case application_data.length
10
+ when 0
11
+ # close messages MAY contain a body
12
+ nil
13
+ when 1
14
+ # Illegal close frame
15
+ raise DataError, "Close frames with a body must contain a 2 byte status code"
16
+ else
17
+ application_data.slice!(0, 2).unpack('n').first
18
+ end
19
+
20
+ debug [:close_frame_received, status_code, application_data]
21
+
22
+ if @state == :closing
23
+ # We can close connection immediately since there is no more data
24
+ # is allowed to be sent or received on this connection
25
+ @connection.close_connection
26
+ @state = :closed
27
+ else
28
+ # Acknowlege close
29
+ # The connection is considered closed
30
+ send_frame(:close, '')
31
+ @state = :closed
32
+ @connection.close_connection_after_writing
33
+ # TODO: Send close status code and body to app code
34
+ end
35
+ when :ping
36
+ # Pong back the same data
37
+ send_frame(:pong, application_data)
38
+ when :pong
39
+ # TODO: Do something. Complete a deferrable established by a ping?
40
+ when :text, :binary
41
+ @connection.trigger_on_message(application_data)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Websocket
3
- VERSION = "0.2.1"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
@@ -10,9 +10,13 @@ Rspec.configure do |c|
10
10
  end
11
11
 
12
12
  class FakeWebSocketClient < EM::Connection
13
- attr_writer :onopen, :onclose, :onmessage
14
13
  attr_reader :handshake_response, :packets
15
14
 
15
+ def onopen(&blk); @onopen = blk; end
16
+ def onclose(&blk); @onclose = blk; end
17
+ def onerror(&blk); @onerror = blk; end
18
+ def onmessage(&blk); @onmessage = blk; end
19
+
16
20
  def initialize
17
21
  @state = :new
18
22
  @packets = []
@@ -39,6 +43,54 @@ class FakeWebSocketClient < EM::Connection
39
43
  end
40
44
  end
41
45
 
46
+ class Draft03FakeWebSocketClient < FakeWebSocketClient
47
+ def send(application_data)
48
+ frame = ''
49
+ opcode = 4 # fake only supports text frames
50
+ byte1 = opcode # since more, rsv1-3 are 0
51
+ frame << byte1
52
+
53
+ length = application_data.size
54
+ if length <= 125
55
+ byte2 = length # since rsv4 is 0
56
+ frame << byte2
57
+ elsif length < 65536 # write 2 byte length
58
+ frame << 126
59
+ frame << [length].pack('n')
60
+ else # write 8 byte length
61
+ frame << 127
62
+ frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
63
+ end
64
+
65
+ frame << application_data
66
+
67
+ send_data(frame)
68
+ end
69
+ end
70
+
71
+ # Wrap EM:HttpRequest in a websocket like interface so that it can be used in the specs with the same interface as FakeWebSocketClient
72
+ class Draft75WebSocketClient
73
+ def onopen(&blk); @onopen = blk; end
74
+ def onclose(&blk); @onclose = blk; end
75
+ def onerror(&blk); @onerror = blk; end
76
+ def onmessage(&blk); @onmessage = blk; end
77
+
78
+ def initialize
79
+ @ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:timeout => 0)
80
+ @ws.errback { @onerror.call if @onerror }
81
+ @ws.callback { @onopen.call if @onopen }
82
+ @ws.stream { |msg| @onmessage.call(msg) if @onmessage }
83
+ end
84
+
85
+ def send(message)
86
+ @ws.send(message)
87
+ end
88
+
89
+ def close_connection
90
+ @ws.close_connection
91
+ end
92
+ end
93
+
42
94
  def failed
43
95
  EventMachine.stop
44
96
  fail
@@ -52,7 +104,7 @@ def format_request(r)
52
104
  end
53
105
 
54
106
  def format_response(r)
55
- data = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
107
+ data = r[:protocol] || "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
56
108
  header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
57
109
  data << [header_lines, '', r[:body]].join("\r\n")
58
110
  data