em-websocket 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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