em-websocket 0.3.8 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,137 @@
1
+ require "http/parser"
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+
6
+ # Resposible for creating the server handshake response
7
+ class Handshake
8
+ include EM::Deferrable
9
+
10
+ attr_reader :parser, :protocol_version
11
+
12
+ # Unfortunately drafts 75 & 76 require knowledge of whether the
13
+ # connection is being terminated as ws/wss in order to generate the
14
+ # correct handshake response
15
+ def initialize(secure)
16
+ @parser = Http::Parser.new
17
+ @secure = secure
18
+
19
+ @parser.on_headers_complete = proc { |headers|
20
+ @headers = Hash[headers.map { |k,v| [k.downcase, v] }]
21
+ }
22
+ end
23
+
24
+ def receive_data(data)
25
+ @parser << data
26
+
27
+ if @headers
28
+ process(@headers, @parser.upgrade_data)
29
+ end
30
+ rescue HTTP::Parser::Error => e
31
+ fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
32
+ end
33
+
34
+ # Returns the WebSocket upgrade headers as a hash.
35
+ #
36
+ # Keys are strings, unmodified from the request.
37
+ #
38
+ def headers
39
+ @parser.headers
40
+ end
41
+
42
+ # The same as headers, except that the hash keys are downcased
43
+ #
44
+ def headers_downcased
45
+ @headers
46
+ end
47
+
48
+ # Returns the request path (excluding any query params)
49
+ #
50
+ def path
51
+ @parser.request_path
52
+ end
53
+
54
+ # Returns the query params as a string foo=bar&baz=...
55
+ def query_string
56
+ @parser.query_string
57
+ end
58
+
59
+ def query
60
+ Hash[query_string.split('&').map { |c| c.split('=', 2) }]
61
+ end
62
+
63
+ # Returns the WebSocket origin header if provided
64
+ #
65
+ def origin
66
+ @headers["origin"] || @headers["sec-websocket-origin"] || nil
67
+ end
68
+
69
+ private
70
+
71
+ def process(headers, remains)
72
+ unless @parser.http_method == "GET"
73
+ raise HandshakeError, "Must be GET request"
74
+ end
75
+
76
+ # Validate Upgrade
77
+ unless @parser.upgrade?
78
+ raise HandshakeError, "Not an upgrade request"
79
+ end
80
+ upgrade = @headers['upgrade']
81
+ unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
82
+ raise HandshakeError, "Invalid upgrade header: #{upgrade}"
83
+ end
84
+
85
+ # Determine version heuristically
86
+ version = if @headers['sec-websocket-version']
87
+ # Used from drafts 04 onwards
88
+ @headers['sec-websocket-version'].to_i
89
+ elsif @headers['sec-websocket-draft']
90
+ # Used in drafts 01 - 03
91
+ @headers['sec-websocket-draft'].to_i
92
+ elsif @headers['sec-websocket-key1']
93
+ 76
94
+ else
95
+ 75
96
+ end
97
+
98
+ # Additional handling of bytes after the header if required
99
+ case version
100
+ when 75
101
+ if !remains.empty?
102
+ raise HandshakeError, "Extra bytes after header"
103
+ end
104
+ when 76, 1..3
105
+ if remains.length < 8
106
+ # The whole third-key has not been received yet.
107
+ return nil
108
+ elsif remains.length > 8
109
+ raise HandshakeError, "Extra bytes after third key"
110
+ end
111
+ @headers['third-key'] = remains
112
+ end
113
+
114
+ handshake_klass = case version
115
+ when 75
116
+ Handshake75
117
+ when 76, 1..3
118
+ Handshake76
119
+ when 5, 6, 7, 8, 13
120
+ Handshake04
121
+ else
122
+ # According to spec should abort the connection
123
+ raise HandshakeError, "Protocol version #{version} not supported"
124
+ end
125
+
126
+ upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
127
+
128
+ handler_klass = Handler.klass_factory(version)
129
+
130
+ @protocol_version = version
131
+ succeed(upgrade_response, handler_klass)
132
+ rescue HandshakeError => e
133
+ fail(e)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -4,30 +4,28 @@ require 'base64'
4
4
  module EventMachine
5
5
  module WebSocket
6
6
  module Handshake04
7
- def handshake
7
+ def self.handshake(headers, _, __)
8
8
  # Required
9
- unless key = request['sec-websocket-key']
10
- raise HandshakeError, "Sec-WebSocket-Key header is required"
9
+ unless key = headers['sec-websocket-key']
10
+ raise HandshakeError, "sec-websocket-key header is required"
11
11
  end
12
-
12
+
13
13
  # Optional
14
- origin = request['sec-websocket-origin']
15
- protocols = request['sec-websocket-protocol']
16
- extensions = request['sec-websocket-extensions']
17
-
14
+ origin = headers['sec-websocket-origin']
15
+ protocols = headers['sec-websocket-protocol']
16
+ extensions = headers['sec-websocket-extensions']
17
+
18
18
  string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
19
19
  signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
20
-
20
+
21
21
  upgrade = ["HTTP/1.1 101 Switching Protocols"]
22
22
  upgrade << "Upgrade: websocket"
23
23
  upgrade << "Connection: Upgrade"
24
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
-
25
+
26
+ # TODO: Support sec-websocket-protocol
27
+ # TODO: sec-websocket-extensions
28
+
31
29
  return upgrade.join("\r\n") + "\r\n\r\n"
32
30
  end
33
31
  end
@@ -1,19 +1,16 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
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']
4
+ def self.handshake(headers, path, secure)
5
+ scheme = (secure ? "wss" : "ws")
6
+ location = "#{scheme}://#{headers['host']}#{path}"
8
7
 
9
8
  upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
9
  upgrade << "Upgrade: WebSocket\r\n"
11
10
  upgrade << "Connection: Upgrade\r\n"
12
- upgrade << "WebSocket-Origin: #{request['origin']}\r\n"
11
+ upgrade << "WebSocket-Origin: #{headers['origin']}\r\n"
13
12
  upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
13
 
15
- debug [:upgrade_headers, upgrade]
16
-
17
14
  return upgrade
18
15
  end
19
16
  end
@@ -1,33 +1,30 @@
1
1
  require 'digest/md5'
2
2
 
3
- module EventMachine
4
- module WebSocket
5
- module Handshake76
6
- def handshake
3
+ module EventMachine::WebSocket
4
+ module Handshake76
5
+ class << self
6
+ def handshake(headers, path, secure)
7
7
  challenge_response = solve_challenge(
8
- request['sec-websocket-key1'],
9
- request['sec-websocket-key2'],
10
- request['third-key']
8
+ headers['sec-websocket-key1'],
9
+ headers['sec-websocket-key2'],
10
+ headers['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
+ scheme = (secure ? "wss" : "ws")
14
+ location = "#{scheme}://#{headers['host']}#{path}"
16
15
 
17
16
  upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
18
17
  upgrade << "Upgrade: WebSocket\r\n"
19
18
  upgrade << "Connection: Upgrade\r\n"
20
19
  upgrade << "Sec-WebSocket-Location: #{location}\r\n"
21
- upgrade << "Sec-WebSocket-Origin: #{request['origin']}\r\n"
22
- if protocol = request['sec-websocket-protocol']
20
+ upgrade << "Sec-WebSocket-Origin: #{headers['origin']}\r\n"
21
+ if protocol = headers['sec-websocket-protocol']
23
22
  validate_protocol!(protocol)
24
23
  upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
25
24
  end
26
25
  upgrade << "\r\n"
27
26
  upgrade << challenge_response
28
27
 
29
- debug [:upgrade_headers, upgrade]
30
-
31
28
  return upgrade
32
29
  end
33
30
 
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Websocket
3
- VERSION = "0.3.8"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
@@ -1,16 +1,18 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
+ class << self
4
+ attr_accessor :max_frame_size
5
+ end
6
+ @max_frame_size = 10 * 1024 * 1024 # 10MB
7
+
3
8
  # All errors raised by em-websocket should descend from this class
4
- #
5
9
  class WebSocketError < RuntimeError; end
6
10
 
7
11
  # Used for errors that occur during WebSocket handshake
8
- #
9
12
  class HandshakeError < WebSocketError; end
10
13
 
11
14
  # Used for errors which should cause the connection to close.
12
15
  # See RFC6455 §7.4.1 for a full description of meanings
13
- #
14
16
  class WSProtocolError < WebSocketError
15
17
  def code; 1002; end
16
18
  end
@@ -20,28 +22,28 @@ module EventMachine
20
22
  def code; 1009; end
21
23
  end
22
24
 
25
+ # Start WebSocket server, including starting eventmachine run loop
23
26
  def self.start(options, &blk)
24
27
  EM.epoll
25
- EM.run do
26
-
28
+ EM.run {
27
29
  trap("TERM") { stop }
28
30
  trap("INT") { stop }
29
31
 
30
- EventMachine::start_server(options[:host], options[:port],
31
- EventMachine::WebSocket::Connection, options) do |c|
32
- blk.call(c)
33
- end
32
+ run(options, &blk)
33
+ }
34
+ end
35
+
36
+ # Start WebSocket server inside eventmachine run loop
37
+ def self.run(options)
38
+ host, port = options.values_at(:host, :port)
39
+ EM.start_server(host, port, Connection, options) do |c|
40
+ yield c
34
41
  end
35
42
  end
36
43
 
37
44
  def self.stop
38
45
  puts "Terminating WebSocket Server"
39
- EventMachine.stop
40
- end
41
-
42
- class << self
43
- attr_accessor :max_frame_size
46
+ EM.stop
44
47
  end
45
- @max_frame_size = 10 * 1024 * 1024 # 10MB
46
48
  end
47
49
  end
@@ -103,7 +103,10 @@ class Draft75WebSocketClient
103
103
  def onmessage(&blk); @onmessage = blk; end
104
104
 
105
105
  def initialize
106
- @ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:timeout => 0)
106
+ @ws = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get({
107
+ :timeout => 0,
108
+ :origin => 'http://example.com',
109
+ })
107
110
  @ws.errback { @onerror.call if @onerror }
108
111
  @ws.callback { @onopen.call if @onopen }
109
112
  @ws.stream { |msg| @onmessage.call(msg) if @onmessage }
@@ -133,8 +136,23 @@ def format_response(r)
133
136
  data
134
137
  end
135
138
 
136
- RSpec::Matchers.define :send_handshake do |response|
139
+ RSpec::Matchers.define :succeed_with_upgrade do |response|
137
140
  match do |actual|
138
- actual.handshake.lines.sort == format_response(response).lines.sort
141
+ success = nil
142
+ actual.callback { |upgrade_response, handler_klass|
143
+ success = (upgrade_response.lines.sort == format_response(response).lines.sort)
144
+ }
145
+ success
146
+ end
147
+ end
148
+
149
+ RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
150
+ match do |actual|
151
+ success = nil
152
+ actual.errback { |e|
153
+ success = (e.class == error_klass)
154
+ success &= (e.message == error_message) if error_message
155
+ }
156
+ success
139
157
  end
140
158
  end
@@ -1,27 +1,27 @@
1
1
  require 'helper'
2
2
 
3
3
  # These tests are not specific to any particular draft of the specification
4
- #
4
+ #
5
5
  describe "WebSocket server" do
6
6
  include EM::SpecHelper
7
7
  default_timeout 1
8
8
 
9
9
  it "should fail on non WebSocket requests" do
10
10
  em {
11
- EventMachine.add_timer(0.1) do
12
- http = EventMachine::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
11
+ EM.add_timer(0.1) do
12
+ http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
13
13
  http.errback { done }
14
14
  http.callback { fail }
15
15
  end
16
16
 
17
- EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) {}
17
+ EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) {}
18
18
  }
19
19
  end
20
-
21
- it "should populate ws.request with appropriate headers" do
20
+
21
+ it "should expose the WebSocket request headers, path and query params" do
22
22
  em {
23
- EventMachine.add_timer(0.1) do
24
- http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
23
+ EM.add_timer(0.1) do
24
+ http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
25
25
  http.errback { fail }
26
26
  http.callback {
27
27
  http.response_header.status.should == 101
@@ -30,27 +30,32 @@ describe "WebSocket server" do
30
30
  http.stream { |msg| }
31
31
  end
32
32
 
33
- EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
34
- ws.onopen {
35
- ws.request["user-agent"].should == "EventMachine HttpClient"
36
- ws.request["connection"].should == "Upgrade"
37
- ws.request["upgrade"].should == "WebSocket"
38
- ws.request["path"].should == "/"
39
- ws.request["origin"].should == "127.0.0.1"
40
- ws.request["host"].to_s.should == "ws://127.0.0.1:12345"
33
+ EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
34
+ ws.onopen { |handshake|
35
+ headers = handshake.headers
36
+ headers["User-Agent"].should == "EventMachine HttpClient"
37
+ headers["Connection"].should == "Upgrade"
38
+ headers["Upgrade"].should == "WebSocket"
39
+ headers["Host"].to_s.should == "127.0.0.1:12345"
40
+ handshake.path.should == "/"
41
+ handshake.query.should == {}
42
+ handshake.origin.should == "127.0.0.1"
41
43
  }
42
44
  ws.onclose {
43
45
  ws.state.should == :closed
44
- EventMachine.stop
46
+ done
45
47
  }
46
48
  end
47
49
  }
48
50
  end
49
-
50
- it "should allow sending and retrieving query string args passed in on the connection request." do
51
+
52
+ it "should expose the WebSocket path and query params when nonempty" do
51
53
  em {
52
- EventMachine.add_timer(0.1) do
53
- http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:query => {'foo' => 'bar', 'baz' => 'qux'}, :timeout => 0)
54
+ EM.add_timer(0.1) do
55
+ http = EM::HttpRequest.new('ws://127.0.0.1:12345/hello').get({
56
+ :query => {'foo' => 'bar', 'baz' => 'qux'},
57
+ :timeout => 0
58
+ })
54
59
  http.errback { fail }
55
60
  http.callback {
56
61
  http.response_header.status.should == 101
@@ -59,26 +64,41 @@ describe "WebSocket server" do
59
64
  http.stream { |msg| }
60
65
  end
61
66
 
62
- EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
63
- ws.onopen {
64
- path, query = ws.request["path"].split('?')
65
- path.should == '/'
66
- Hash[*query.split(/&|=/)].should == {"foo"=>"bar", "baz"=>"qux"}
67
- ws.request["query"]["foo"].should == "bar"
68
- ws.request["query"]["baz"].should == "qux"
67
+ EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
68
+ ws.onopen { |handshake|
69
+ handshake.path.should == '/hello'
70
+ handshake.query_string.split('&').sort.
71
+ should == ["baz=qux", "foo=bar"]
72
+ handshake.query.should == {"foo"=>"bar", "baz"=>"qux"}
69
73
  }
70
74
  ws.onclose {
71
75
  ws.state.should == :closed
72
- EventMachine.stop
76
+ done
73
77
  }
74
78
  end
75
79
  }
76
80
  end
77
-
78
- it "should ws.response['Query'] to empty hash when no query string params passed in connection URI" do
81
+
82
+ it "should raise an exception if frame sent before handshake complete" do
79
83
  em {
80
- EventMachine.add_timer(0.1) do
81
- http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:timeout => 0)
84
+ # 1. Start WebSocket server
85
+ EM::WebSocket.start(:host => "0.0.0.0", :port => 12345) { |ws|
86
+ # 3. Try to send a message to the socket
87
+ lambda {
88
+ ws.send('early message')
89
+ }.should raise_error('Cannot send data before onopen callback')
90
+ done
91
+ }
92
+
93
+ # 2. Connect a dumb TCP connection (will not send handshake)
94
+ EM.connect('0.0.0.0', 12345, EM::Connection)
95
+ }
96
+ end
97
+
98
+ it "should allow the server to be started inside an existing EM" do
99
+ em {
100
+ EM.add_timer(0.1) do
101
+ http = EM::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
82
102
  http.errback { fail }
83
103
  http.callback {
84
104
  http.response_header.status.should == 101
@@ -87,32 +107,15 @@ describe "WebSocket server" do
87
107
  http.stream { |msg| }
88
108
  end
89
109
 
90
- EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
91
- ws.onopen {
92
- ws.request["path"].should == "/"
93
- ws.request["query"].should == {}
110
+ EM::WebSocket.run(:host => "0.0.0.0", :port => 12345) do |ws|
111
+ ws.onopen { |handshake|
112
+ handshake.headers["User-Agent"].should == "EventMachine HttpClient"
94
113
  }
95
114
  ws.onclose {
96
115
  ws.state.should == :closed
97
- EventMachine.stop
98
- }
99
- end
100
- }
101
- end
102
-
103
- it "should raise an exception if frame sent before handshake complete" do
104
- em {
105
- EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) { |c|
106
- # We're not using a real client so the handshake will not be sent
107
- EM.add_timer(0.1) {
108
- lambda {
109
- c.send('early message')
110
- }.should raise_error('Cannot send data before onopen callback')
111
116
  done
112
117
  }
113
- }
114
-
115
- client = EM.connect('0.0.0.0', 12345, EM::Connection)
118
+ end
116
119
  }
117
120
  end
118
121
  end