em-websocket 0.3.8 → 0.4.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,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