em-websocket 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Framing76
4
+
5
+ # Set the max frame lenth to very high value (10MB) until there is a
6
+ # limit specified in the spec to protect against malicious attacks
7
+ MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
8
+
9
+ def initialize_framing
10
+ @data = ''
11
+ end
12
+
13
+ def process_data
14
+ debug [:message, @data]
15
+
16
+ # This algorithm comes straight from the spec
17
+ # http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-4.2
18
+
19
+ error = false
20
+
21
+ while !error
22
+ pointer = 0
23
+ frame_type = @data[pointer].to_i
24
+ pointer += 1
25
+
26
+ if (frame_type & 0x80) == 0x80
27
+ # If the high-order bit of the /frame type/ byte is set
28
+ length = 0
29
+
30
+ loop do
31
+ return false if !@data[pointer]
32
+ b = @data[pointer].to_i
33
+ pointer += 1
34
+ b_v = b & 0x7F
35
+ length = length * 128 + b_v
36
+ break unless (b & 0x80) == 0x80
37
+ end
38
+
39
+ # Addition to the spec to protect against malicious requests
40
+ if length > MAXIMUM_FRAME_LENGTH
41
+ @connection.close_with_error(DataError.new("Frame length too long (#{length} bytes)"))
42
+ return false
43
+ end
44
+
45
+ if @data[pointer+length-1] == nil
46
+ debug [:buffer_incomplete, @data.inspect]
47
+ # Incomplete data - leave @data to accumulate
48
+ error = true
49
+ else
50
+ # Straight from spec - I'm sure this isn't crazy...
51
+ # 6. Read /length/ bytes.
52
+ # 7. Discard the read bytes.
53
+ @data = @data[(pointer+length)..-1]
54
+
55
+ # If the /frame type/ is 0xFF and the /length/ was 0, then close
56
+ if length == 0
57
+ @connection.send_data("\xff\x00")
58
+ @state = :closing
59
+ @connection.close_connection_after_writing
60
+ else
61
+ error = true
62
+ end
63
+ end
64
+ else
65
+ # If the high-order bit of the /frame type/ byte is _not_ set
66
+ msg = @data.slice!(/\A\x00([^\xff]*)\xff/)
67
+ if msg
68
+ msg.gsub!(/\A\x00|\xff\z/, '')
69
+ if @state == :closing
70
+ debug [:ignored_message, msg]
71
+ else
72
+ msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding)
73
+ @connection.trigger_on_message(msg)
74
+ end
75
+ else
76
+ error = true
77
+ end
78
+ end
79
+ end
80
+
81
+ false
82
+ end
83
+
84
+ # frames need to start with 0x00-0x7f byte and end with
85
+ # an 0xFF byte. Per spec, we can also set the first
86
+ # byte to a value betweent 0x80 and 0xFF, followed by
87
+ # a leading length indicator
88
+ def send_text_frame(data)
89
+ ary = ["\x00", data, "\xff"]
90
+ ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
91
+ @connection.send_data(ary.join)
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -3,17 +3,41 @@ module EventMachine
3
3
  class Handler
4
4
  include Debugger
5
5
 
6
- attr_reader :request
6
+ attr_reader :request, :state
7
7
 
8
- def initialize(request, response, debug = false)
9
- @request = request
10
- @response = response
8
+ def initialize(connection, request, debug = false)
9
+ @connection, @request = connection, request
11
10
  @debug = debug
11
+ @state = :handshake
12
+ initialize_framing
12
13
  end
13
14
 
15
+ def run
16
+ @connection.send_data handshake
17
+ @state = :connected
18
+ @connection.trigger_on_open
19
+ end
20
+
21
+ # Handshake response
14
22
  def handshake
15
23
  # Implemented in subclass
16
24
  end
25
+
26
+ def receive_data(data)
27
+ @data << data
28
+ process_data
29
+ end
30
+
31
+ def close_websocket
32
+ # Unless redefined in a subclass, just close the connection
33
+ @state = :closed
34
+ @connection.close_connection_after_writing
35
+ end
36
+
37
+ def unbind
38
+ @state = :closed
39
+ @connection.trigger_on_close
40
+ end
17
41
  end
18
42
  end
19
43
  end
@@ -0,0 +1,14 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler03 < Handler
4
+ include Handshake76
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
12
+ end
13
+ end
14
+ end
@@ -1,21 +1,8 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler75 < Handler
4
- def handshake
5
- location = "#{@request['Host'].scheme}://#{@request['Host'].host}"
6
- location << ":#{@request['Host'].port}" if @request['Host'].port
7
- location << @request['Path']
8
-
9
- upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
- upgrade << "Upgrade: WebSocket\r\n"
11
- upgrade << "Connection: Upgrade\r\n"
12
- upgrade << "WebSocket-Origin: #{@request['Origin']}\r\n"
13
- upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
-
15
- debug [:upgrade_headers, upgrade]
16
-
17
- return upgrade
18
- end
4
+ include Handshake75
5
+ include Framing76
19
6
  end
20
7
  end
21
8
  end
@@ -1,64 +1,11 @@
1
- require 'digest/md5'
2
-
3
1
  module EventMachine
4
2
  module WebSocket
5
3
  class Handler76 < Handler
4
+ include Handshake76
5
+ include Framing76
6
+
6
7
  # "\377\000" is octet version and "\xff\x00" is hex version
7
8
  TERMINATE_STRING = "\xff\x00"
8
-
9
- def handshake
10
- challenge_response = solve_challenge(
11
- @request['Sec-WebSocket-Key1'],
12
- @request['Sec-WebSocket-Key2'],
13
- @request['Third-Key']
14
- )
15
-
16
- location = "#{@request['Host'].scheme}://#{@request['Host'].host}"
17
- location << ":#{@request['Host'].port}" if @request['Host'].port
18
- location << @request['Path']
19
-
20
- upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
21
- upgrade << "Upgrade: WebSocket\r\n"
22
- upgrade << "Connection: Upgrade\r\n"
23
- upgrade << "Sec-WebSocket-Location: #{location}\r\n"
24
- upgrade << "Sec-WebSocket-Origin: #{@request['Origin']}\r\n"
25
- if protocol = @request['Sec-WebSocket-Protocol']
26
- validate_protocol!(protocol)
27
- upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
28
- end
29
- upgrade << "\r\n"
30
- upgrade << challenge_response
31
-
32
- debug [:upgrade_headers, upgrade]
33
-
34
- return upgrade
35
- end
36
-
37
- private
38
-
39
- def solve_challenge(first, second, third)
40
- # Refer to 5.2 4-9 of the draft 76
41
- sum = [(extract_nums(first) / count_spaces(first))].pack("N*") +
42
- [(extract_nums(second) / count_spaces(second))].pack("N*") +
43
- third
44
- Digest::MD5.digest(sum)
45
- end
46
-
47
- def extract_nums(string)
48
- string.scan(/[0-9]/).join.to_i
49
- end
50
-
51
- def count_spaces(string)
52
- spaces = string.scan(/ /).size
53
- # As per 5.2.5, abort the connection if spaces are zero.
54
- raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0
55
- return spaces
56
- end
57
-
58
- def validate_protocol!(protocol)
59
- raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
60
- # TODO: Validate characters
61
- end
62
9
  end
63
10
  end
64
11
  end
@@ -4,11 +4,16 @@ module EventMachine
4
4
  PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.1$/
5
5
  HEADER = /^([^:]+):\s*(.+)$/
6
6
 
7
- def self.build(data, secure = false, debug = false)
7
+ def self.build(connection, data, secure = false, debug = false)
8
+ (header, remains) = data.split("\r\n\r\n", 2)
9
+ unless remains
10
+ # The whole header has not been received yet.
11
+ return nil
12
+ end
13
+
8
14
  request = {}
9
- response = nil
10
15
 
11
- lines = data.split("\r\n")
16
+ lines = header.split("\r\n")
12
17
 
13
18
  # extract request path
14
19
  first_line = lines.shift.match(PATH)
@@ -27,7 +32,24 @@ module EventMachine
27
32
  h = HEADER.match(line)
28
33
  request[h[1].strip] = h[2].strip if h
29
34
  end
30
- request['Third-Key'] = lines.last
35
+
36
+ version = request['Sec-WebSocket-Key1'] ? 76 : 75
37
+ case version
38
+ when 75
39
+ if !remains.empty?
40
+ raise HandshakeError, "Extra bytes after header"
41
+ end
42
+ when 76
43
+ if remains.length < 8
44
+ # The whole third-key has not been received yet.
45
+ return nil
46
+ elsif remains.length > 8
47
+ raise HandshakeError, "Extra bytes after third key"
48
+ end
49
+ request['Third-Key'] = remains
50
+ else
51
+ raise WebSocketError, "Must not happen"
52
+ end
31
53
 
32
54
  unless request['Connection'] == 'Upgrade' and request['Upgrade'] == 'WebSocket'
33
55
  raise HandshakeError, "Connection and Upgrade headers required"
@@ -37,15 +59,18 @@ module EventMachine
37
59
  protocol = (secure ? "wss" : "ws")
38
60
  request['Host'] = Addressable::URI.parse("#{protocol}://"+request['Host'])
39
61
 
40
- version = request['Sec-WebSocket-Key1'] ? 76 : 75
41
-
42
- case version
43
- when 75
44
- Handler75.new(request, response, debug)
45
- when 76
46
- Handler76.new(request, response, debug)
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']
71
+ Handler76.new(connection, request, debug)
47
72
  else
48
- raise WebSocketError, "Must not happen"
73
+ Handler75.new(connection, request, debug)
49
74
  end
50
75
  end
51
76
  end
@@ -0,0 +1,21 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Handshake75
4
+ def handshake
5
+ location = "#{request['Host'].scheme}://#{request['Host'].host}"
6
+ location << ":#{request['Host'].port}" if request['Host'].port
7
+ location << request['Path']
8
+
9
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
+ upgrade << "Upgrade: WebSocket\r\n"
11
+ upgrade << "Connection: Upgrade\r\n"
12
+ upgrade << "WebSocket-Origin: #{request['Origin']}\r\n"
13
+ upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
+
15
+ debug [:upgrade_headers, upgrade]
16
+
17
+ return upgrade
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,61 @@
1
+ require 'digest/md5'
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ module Handshake76
6
+ def handshake
7
+ challenge_response = solve_challenge(
8
+ request['Sec-WebSocket-Key1'],
9
+ request['Sec-WebSocket-Key2'],
10
+ request['Third-Key']
11
+ )
12
+
13
+ location = "#{request['Host'].scheme}://#{request['Host'].host}"
14
+ location << ":#{request['Host'].port}" if request['Host'].port
15
+ location << request['Path']
16
+
17
+ upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
18
+ upgrade << "Upgrade: WebSocket\r\n"
19
+ upgrade << "Connection: Upgrade\r\n"
20
+ upgrade << "Sec-WebSocket-Location: #{location}\r\n"
21
+ upgrade << "Sec-WebSocket-Origin: #{request['Origin']}\r\n"
22
+ if protocol = request['Sec-WebSocket-Protocol']
23
+ validate_protocol!(protocol)
24
+ upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
25
+ end
26
+ upgrade << "\r\n"
27
+ upgrade << challenge_response
28
+
29
+ debug [:upgrade_headers, upgrade]
30
+
31
+ return upgrade
32
+ end
33
+
34
+ private
35
+
36
+ def solve_challenge(first, second, third)
37
+ # Refer to 5.2 4-9 of the draft 76
38
+ sum = [(extract_nums(first) / count_spaces(first))].pack("N*") +
39
+ [(extract_nums(second) / count_spaces(second))].pack("N*") +
40
+ third
41
+ Digest::MD5.digest(sum)
42
+ end
43
+
44
+ def extract_nums(string)
45
+ string.scan(/[0-9]/).join.to_i
46
+ end
47
+
48
+ def count_spaces(string)
49
+ spaces = string.scan(/ /).size
50
+ # As per 5.2.5, abort the connection if spaces are zero.
51
+ raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0
52
+ return spaces
53
+ end
54
+
55
+ def validate_protocol!(protocol)
56
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
57
+ # TODO: Validate characters
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module Websocket
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -1,9 +1,13 @@
1
1
  require 'rubygems'
2
- require 'spec'
2
+ require 'rspec'
3
3
  require 'pp'
4
4
  require 'em-http'
5
5
 
6
- require 'lib/em-websocket'
6
+ require 'em-websocket'
7
+
8
+ Rspec.configure do |c|
9
+ c.mock_with :rspec
10
+ end
7
11
 
8
12
  class FakeWebSocketClient < EM::Connection
9
13
  attr_writer :onopen, :onclose, :onmessage
@@ -21,7 +25,7 @@ class FakeWebSocketClient < EM::Connection
21
25
  @onopen.call if @onopen
22
26
  @state = :open
23
27
  else
24
- @onmessage.call if @onmessage
28
+ @onmessage.call(data) if @onmessage
25
29
  @packets << data
26
30
  end
27
31
  end
@@ -55,11 +59,12 @@ def format_response(r)
55
59
  end
56
60
 
57
61
  def handler(request, secure = false)
58
- EM::WebSocket::HandlerFactory.build(format_request(request), secure)
62
+ connection = Object.new
63
+ EM::WebSocket::HandlerFactory.build(connection, format_request(request), secure)
59
64
  end
60
65
 
61
- def send_handshake(response)
62
- simple_matcher do |given|
63
- given.handshake.lines.sort == format_response(response).lines.sort
66
+ RSpec::Matchers.define :send_handshake do |response|
67
+ match do |actual|
68
+ actual.handshake.lines.sort == format_response(response).lines.sort
64
69
  end
65
70
  end