websocket-driver 0.1.0 → 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.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ### 0.2.0 / 2013-05-12
2
+
3
+ * Add API for setting and reading headers
4
+ * Add Driver.server() method for getting a driver for TCP servers
5
+
6
+ ### 0.1.0 / 2013-05-04
7
+
8
+ * First stable release
9
+
1
10
  ### 0.0.0 / 2013-04-22
2
11
 
3
12
  * First release
data/README.md CHANGED
@@ -61,7 +61,7 @@ Server-side sockets require one additional method:
61
61
  * `REQUEST_METHOD`, the request's HTTP verb
62
62
 
63
63
 
64
- ### Server-side
64
+ ### Server-side with Rack
65
65
 
66
66
  To handle a server-side WebSocket connection, you need to check whether the
67
67
  request is a WebSocket handshake, and if so create a protocol driver for it.
@@ -127,6 +127,53 @@ end
127
127
  The driver API is described in full below.
128
128
 
129
129
 
130
+ ### Server-side with TCP
131
+
132
+ You can also handle WebSocket connections in a bare TCP server, if you're not
133
+ using Rack and don't want to implement HTTP parsing yourself. For this, your
134
+ socket object only needs a `write` method.
135
+
136
+ The driver will emit a `:connect` event when a request is received, and at this
137
+ point you can detect whether it's a WebSocket and handle it as such. Here's an
138
+ example using an EventMachine TCP server.
139
+
140
+ ```ruby
141
+ module Connection
142
+ def initialize
143
+ @driver = WebSocket::Driver.server(self)
144
+
145
+ @driver.on(:connect) do
146
+ if WebSocket::Driver.websocket?(@driver.env)
147
+ @driver.start
148
+ else
149
+ # handle other HTTP requests
150
+ end
151
+ end
152
+
153
+ @driver.on(:message) { |e| @driver.text(e.data) }
154
+ @driver.on(:close) { |e| close_connection_after_writing }
155
+ end
156
+
157
+ def receive_data(data)
158
+ @driver.parse(data)
159
+ end
160
+
161
+ def write(data)
162
+ send_data(data)
163
+ end
164
+ end
165
+
166
+ EM.run {
167
+ EM.start_server('127.0.0.1', 4180, Connection)
168
+ }
169
+ ```
170
+
171
+ In the `:connect` event, `@driver.env` is a Rack env representing the request.
172
+ If the request has a body, it will be in the `@driver.env['rack.input']`
173
+ stream, but only as much of the body as you have so far routed to it using the
174
+ `parse` method.
175
+
176
+
130
177
  ### Client-side
131
178
 
132
179
  Similarly, to implement a WebSocket client you need an object with `url` and
@@ -139,6 +186,12 @@ driver = WebSocket::Driver.client(socket)
139
186
  After this you use the driver API as described below to process incoming data
140
187
  and send outgoing data.
141
188
 
189
+ Client drivers have two additional methods for reading the HTTP data that was
190
+ sent back by the server:
191
+
192
+ * `driver.status` - the integer value of the HTTP status code
193
+ * `driver.headers` - a hash-like object containing the response headers
194
+
142
195
 
143
196
  ### Driver API
144
197
 
@@ -146,12 +199,15 @@ Drivers are created using one of the following methods:
146
199
 
147
200
  ```ruby
148
201
  driver = WebSocket::Driver.rack(socket, options)
202
+ driver = WebSocket::Driver.server(socket, options)
149
203
  driver = WebSocket::Driver.client(socket, options)
150
204
  ```
151
205
 
152
206
  The `rack` method returns a driver chosen using the socket's `env`. The
153
- `client` method always returns a driver for the RFC version of the protocol
154
- with masking enabled on outgoing frames.
207
+ `server` method returns a driver that will parse an HTTP request and then
208
+ decide which driver to use for it using the `rack` method. The `client` method
209
+ always returns a driver for the RFC version of the protocol with masking
210
+ enabled on outgoing frames.
155
211
 
156
212
  The `options` argument is optional, and is a hash. It may contain the following
157
213
  keys:
@@ -188,6 +244,12 @@ describing the error.
188
244
  Sets the callback block to execute when the socket becomes closed. The `event`
189
245
  object has `code` and `reason` attributes.
190
246
 
247
+ #### `driver.set_header(name, value)`
248
+
249
+ Sets a custom header to be sent as part of the handshake response, either from
250
+ the server or from the client. Must be called before `start`, since this is
251
+ when the headers are serialized and sent.
252
+
191
253
  #### `driver.start`
192
254
 
193
255
  Initiates the protocol by sending the handshake - either the response for a
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'eventmachine'
4
+ require 'websocket/driver'
5
+
6
+ module Connection
7
+ def initialize
8
+ @driver = WebSocket::Driver.server(self)
9
+
10
+ @driver.on(:connect) { |e| @driver.start if WebSocket::Driver.websocket? @driver.env }
11
+ @driver.on(:message) { |e| @driver.frame(e.data) }
12
+ @driver.on(:close) { |e| close_connection_after_writing }
13
+ end
14
+
15
+ def receive_data(data)
16
+ @driver.parse(data)
17
+ end
18
+
19
+ def write(data)
20
+ send_data(data)
21
+ end
22
+ end
23
+
24
+ EM.run {
25
+ EM.start_server('127.0.0.1', ARGV[0], Connection)
26
+ }
27
+
@@ -7,11 +7,13 @@
7
7
  require 'base64'
8
8
  require 'digest/md5'
9
9
  require 'digest/sha1'
10
- require 'net/http'
10
+ require 'set'
11
11
  require 'stringio'
12
12
  require 'uri'
13
13
 
14
14
  module WebSocket
15
+ autoload :HTTP, File.expand_path('../http', __FILE__)
16
+
15
17
  class Driver
16
18
 
17
19
  root = File.expand_path('../driver', __FILE__)
@@ -35,17 +37,20 @@ module WebSocket
35
37
 
36
38
  STATES = [:connecting, :open, :closing, :closed]
37
39
 
38
- class OpenEvent < Struct.new(nil) ; end
40
+ class ConnectEvent < Struct.new(nil) ; end
41
+ class OpenEvent < Struct.new(nil) ; end
39
42
  class MessageEvent < Struct.new(:data) ; end
40
- class CloseEvent < Struct.new(:code, :reason) ; end
43
+ class CloseEvent < Struct.new(:code, :reason) ; end
41
44
 
42
45
  class ProtocolError < StandardError ; end
43
46
 
44
- autoload :EventEmitter, root + '/event_emitter'
47
+ autoload :Client, root + '/client'
45
48
  autoload :Draft75, root + '/draft75'
46
49
  autoload :Draft76, root + '/draft76'
50
+ autoload :EventEmitter, root + '/event_emitter'
51
+ autoload :Headers, root + '/headers'
47
52
  autoload :Hybi, root + '/hybi'
48
- autoload :Client, root + '/client'
53
+ autoload :Server, root + '/server'
49
54
 
50
55
  include EventEmitter
51
56
  attr_reader :protocol, :ready_state
@@ -55,6 +60,7 @@ module WebSocket
55
60
 
56
61
  @socket = socket
57
62
  @options = options
63
+ @headers = Headers.new
58
64
  @queue = []
59
65
  @ready_state = 0
60
66
  end
@@ -64,6 +70,12 @@ module WebSocket
64
70
  STATES[@ready_state]
65
71
  end
66
72
 
73
+ def set_header(name, value)
74
+ return false unless @ready_state <= 0
75
+ @headers[name] = value
76
+ true
77
+ end
78
+
67
79
  def start
68
80
  return false unless @ready_state == 0
69
81
  @socket.write(handshake_response)
@@ -116,6 +128,10 @@ module WebSocket
116
128
  Client.new(socket, options.merge(:masking => true))
117
129
  end
118
130
 
131
+ def self.server(socket, options = {})
132
+ Server.new(socket, options.merge(:require_masking => true))
133
+ end
134
+
119
135
  def self.rack(socket, options = {})
120
136
  env = socket.env
121
137
  if env['HTTP_SEC_WEBSOCKET_VERSION']
@@ -6,12 +6,15 @@ module WebSocket
6
6
  Base64.encode64((1..16).map { rand(255).chr } * '').strip
7
7
  end
8
8
 
9
+ attr_reader :status, :headers
10
+
9
11
  def initialize(socket, options = {})
10
12
  super
11
13
 
12
14
  @ready_state = -1
13
15
  @key = Client.generate_key
14
16
  @accept = Hybi.generate_accept(@key)
17
+ @http = HTTP::Response.new
15
18
  end
16
19
 
17
20
  def version
@@ -27,17 +30,10 @@ module WebSocket
27
30
 
28
31
  def parse(buffer)
29
32
  return super if @ready_state > 0
30
- message = []
31
- buffer.each_byte do |data|
32
- case @ready_state
33
- when 0 then
34
- @buffer << data
35
- validate_handshake if @buffer[-4..-1] == [0x0D, 0x0A, 0x0D, 0x0A]
36
- when 1 then
37
- message << data
38
- end
39
- end
40
- parse(message) if @ready_state == 1
33
+ @http.parse(buffer)
34
+ return fail_handshake('Invalid HTTP response') if @http.error?
35
+ validate_handshake if @http.complete?
36
+ parse(@http.body) if @ready_state == 1
41
37
  end
42
38
 
43
39
  private
@@ -60,7 +56,7 @@ module WebSocket
60
56
  headers << "Sec-WebSocket-Protocol: #{@protocols * ', '}"
61
57
  end
62
58
 
63
- (headers + ['', '']).join("\r\n")
59
+ (headers + [@headers.to_s, '']).join("\r\n")
64
60
  end
65
61
 
66
62
  def fail_handshake(message)
@@ -71,18 +67,17 @@ module WebSocket
71
67
  end
72
68
 
73
69
  def validate_handshake
74
- data = Driver.encode(@buffer)
75
- @buffer = []
76
- response = Net::HTTPResponse.read_new(Net::BufferedIO.new(StringIO.new(data)))
70
+ @status = @http.code
71
+ @headers = Headers.new(@http.headers)
77
72
 
78
- unless response.code.to_i == 101
79
- return fail_handshake("Unexpected response code: #{response.code}")
73
+ unless @http.code == 101
74
+ return fail_handshake("Unexpected response code: #{@http.code}")
80
75
  end
81
76
 
82
- upgrade = response['Upgrade'] || ''
83
- connection = response['Connection'] || ''
84
- accept = response['Sec-WebSocket-Accept'] || ''
85
- protocol = response['Sec-WebSocket-Protocol'] || ''
77
+ upgrade = @http['Upgrade'] || ''
78
+ connection = @http['Connection'] || ''
79
+ accept = @http['Sec-WebSocket-Accept'] || ''
80
+ protocol = @http['Sec-WebSocket-Protocol'] || ''
86
81
 
87
82
  if upgrade == ''
88
83
  return fail_handshake("'Upgrade' header is missing")
@@ -71,6 +71,7 @@ module WebSocket
71
71
  upgrade << "Connection: Upgrade\r\n"
72
72
  upgrade << "WebSocket-Origin: #{@socket.env['HTTP_ORIGIN']}\r\n"
73
73
  upgrade << "WebSocket-Location: #{@socket.url}\r\n"
74
+ upgrade << @headers.to_s
74
75
  upgrade << "\r\n"
75
76
  upgrade
76
77
  end
@@ -37,6 +37,7 @@ module WebSocket
37
37
  upgrade << "Connection: Upgrade\r\n"
38
38
  upgrade << "Sec-WebSocket-Origin: #{@socket.env['HTTP_ORIGIN']}\r\n"
39
39
  upgrade << "Sec-WebSocket-Location: #{@socket.url}\r\n"
40
+ upgrade << @headers.to_s
40
41
  upgrade << "\r\n"
41
42
  upgrade
42
43
  end
@@ -0,0 +1,42 @@
1
+ module WebSocket
2
+ class Driver
3
+
4
+ class Headers
5
+ ALLOWED_DUPLICATES = %w[set-cookie set-cookie2 warning www-authenticate]
6
+
7
+ def initialize(received = {})
8
+ @raw = received
9
+ @sent = Set.new
10
+ @lines = []
11
+
12
+ @received = {}
13
+ @raw.each { |k,v| @received[HTTP.normalize_header(k)] = v }
14
+ end
15
+
16
+ def [](name)
17
+ @received[HTTP.normalize_header(name)]
18
+ end
19
+
20
+ def []=(name, value)
21
+ return if value.nil?
22
+ key = HTTP.normalize_header(name)
23
+ return unless @sent.add?(key) or ALLOWED_DUPLICATES.include?(key)
24
+ @lines << "#{name.strip}: #{value.to_s.strip}\r\n"
25
+ end
26
+
27
+ def inspect
28
+ @raw.inspect
29
+ end
30
+
31
+ def to_h
32
+ @raw.dup
33
+ end
34
+
35
+ def to_s
36
+ @lines.join('')
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+
@@ -213,7 +213,7 @@ module WebSocket
213
213
  end
214
214
  end
215
215
 
216
- (headers + ['','']).join("\r\n")
216
+ (headers + [@headers.to_s, '']).join("\r\n")
217
217
  end
218
218
 
219
219
  def shutdown(code, reason)
@@ -0,0 +1,73 @@
1
+ module WebSocket
2
+ class Driver
3
+
4
+ class Server < Driver
5
+ EVENTS = %w[open message error close]
6
+
7
+ def initialize(socket, options = {})
8
+ super
9
+ @http = HTTP::Request.new
10
+ end
11
+
12
+ def env
13
+ @http.complete? ? @http.env : nil
14
+ end
15
+
16
+ def url
17
+ return nil unless e = env
18
+
19
+ url = "ws://#{e['HTTP_HOST']}"
20
+ url << e['PATH_INFO']
21
+ url << "?#{e['QUERY_STRING']}" unless e['QUERY_STRING'] == ''
22
+ url
23
+ end
24
+
25
+ %w[set_header start state frame text binary ping close].each do |method|
26
+ define_method(method) do |*args|
27
+ if @delegate
28
+ @delegate.__send__(method, *args)
29
+ else
30
+ @queue << [method, args]
31
+ true
32
+ end
33
+ end
34
+ end
35
+
36
+ def parse(buffer)
37
+ return @delegate.parse(buffer) if @delegate
38
+
39
+ @http.parse(buffer)
40
+ return fail_request('Invalid HTTP request') if @http.error?
41
+ return unless @http.complete?
42
+
43
+ @delegate = Driver.rack(self, @options)
44
+ @delegate.on(:open) { open }
45
+ EVENTS.each do |event|
46
+ @delegate.on(event) { |e| emit(event, e) }
47
+ end
48
+
49
+ emit(:connect, ConnectEvent.new)
50
+ end
51
+
52
+ def write(data)
53
+ @socket.write(data)
54
+ end
55
+
56
+ private
57
+
58
+ def fail_request
59
+ emit(:error, ProtocolError.new(message))
60
+ emit(:close, CloseEvent.new(Hybi::ERRORS[:protocol_error], message))
61
+ end
62
+
63
+ def open
64
+ @queue.each do |message|
65
+ @delegate.__send__(message[0], *message[1])
66
+ end
67
+ @queue = []
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+
@@ -0,0 +1,16 @@
1
+ module WebSocket
2
+ module HTTP
3
+
4
+ root = File.expand_path('../http', __FILE__)
5
+
6
+ autoload :Headers, root + '/headers'
7
+ autoload :Request, root + '/request'
8
+ autoload :Response, root + '/response'
9
+
10
+ def self.normalize_header(name)
11
+ name.to_s.strip.downcase.gsub(/^http_/, '').gsub(/_/, '-')
12
+ end
13
+
14
+ end
15
+ end
16
+
@@ -0,0 +1,77 @@
1
+ module WebSocket
2
+ module HTTP
3
+
4
+ module Headers
5
+ MAX_LINE_LENGTH = 4096
6
+ CR = 0x0D
7
+ LF = 0x0A
8
+
9
+ HEADER_LINE = /^([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+):\s*((?:\t|[\x20-\x7e])*?)\s*$/i
10
+
11
+ attr_reader :headers
12
+
13
+ def initialize
14
+ @buffer = []
15
+ @headers = {}
16
+ @stage = 0
17
+ end
18
+
19
+ def complete?
20
+ @stage == 2
21
+ end
22
+
23
+ def error?
24
+ @stage == -1
25
+ end
26
+
27
+ def parse(data)
28
+ data.each_byte do |byte|
29
+ if byte == LF and @stage < 2
30
+ @buffer.pop if @buffer.last == CR
31
+ if @buffer.empty?
32
+ complete if @stage == 1
33
+ else
34
+ result = case @stage
35
+ when 0 then start_line(string_buffer)
36
+ when 1 then header_line(string_buffer)
37
+ end
38
+
39
+ if result
40
+ @stage = 1
41
+ else
42
+ error
43
+ end
44
+ end
45
+ @buffer = []
46
+ else
47
+ @buffer << byte if @stage >= 0
48
+ error if @stage < 2 and @buffer.size > MAX_LINE_LENGTH
49
+ end
50
+ end
51
+ @env['rack.input'] = StringIO.new(string_buffer) if @env
52
+ end
53
+
54
+ private
55
+
56
+ def complete
57
+ @stage = 2
58
+ end
59
+
60
+ def error
61
+ @stage = -1
62
+ end
63
+
64
+ def header_line(line)
65
+ return false unless parsed = line.scan(HEADER_LINE).first
66
+ @headers[HTTP.normalize_header(parsed[0])] = parsed[1].strip
67
+ true
68
+ end
69
+
70
+ def string_buffer
71
+ @buffer.pack('C*')
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+
@@ -0,0 +1,46 @@
1
+ module WebSocket
2
+ module HTTP
3
+
4
+ class Request
5
+ include Headers
6
+
7
+ REQUEST_LINE = /^([A-Z]+) +([\x21-\x7e]+) +(HTTP\/[0-9]\.[0-9])$/
8
+ REQUEST_TARGET = /^(.*?)(\?(.*))?$/
9
+ RESERVED_HEADERS = %w[content-length content-type]
10
+
11
+ attr_reader :env
12
+
13
+ private
14
+
15
+ def start_line(line)
16
+ return false unless parsed = line.scan(REQUEST_LINE).first
17
+
18
+ target = parsed[1].scan(REQUEST_TARGET).first
19
+
20
+ @env = {
21
+ 'REQUEST_METHOD' => parsed[0],
22
+ 'SCRIPT_NAME' => '',
23
+ 'PATH_INFO' => target[0],
24
+ 'QUERY_STRING' => target[2] || ''
25
+ }
26
+ true
27
+ end
28
+
29
+ def complete
30
+ super
31
+ @headers.each do |name, value|
32
+ rack_name = name.upcase.gsub(/-/, '_')
33
+ rack_name = "HTTP_#{rack_name}" unless RESERVED_HEADERS.include?(name)
34
+ @env[rack_name] = value
35
+ end
36
+ if host = @env['HTTP_HOST']
37
+ uri = URI.parse("http://#{host}")
38
+ @env['SERVER_NAME'] = uri.host
39
+ @env['SERVER_PORT'] = uri.port.to_s
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+
@@ -0,0 +1,30 @@
1
+ module WebSocket
2
+ module HTTP
3
+
4
+ class Response
5
+ include Headers
6
+
7
+ STATUS_LINE = /^(HTTP\/[0-9]\.[0-9]) +([0-9]{3}) +(.*)$/
8
+
9
+ attr_reader :code
10
+
11
+ def [](name)
12
+ @headers[HTTP.normalize_header(name)]
13
+ end
14
+
15
+ def body
16
+ @buffer.pack('C*')
17
+ end
18
+
19
+ private
20
+
21
+ def start_line(line)
22
+ return false unless parsed = line.scan(STATUS_LINE).first
23
+ @code = parsed[1].to_i
24
+ true
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: websocket-driver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-04 00:00:00.000000000 Z
12
+ date: 2013-05-12 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: rake-compiler
16
32
  requirement: !ruby/object:Gem::Requirement
@@ -56,14 +72,21 @@ files:
56
72
  - ext/websocket_mask/websocket_mask.c
57
73
  - ext/websocket_mask/WebsocketMaskService.java
58
74
  - ext/websocket_mask/extconf.rb
75
+ - examples/tcp_server.rb
76
+ - lib/websocket/http/request.rb
77
+ - lib/websocket/http/headers.rb
78
+ - lib/websocket/http/response.rb
79
+ - lib/websocket/driver.rb
80
+ - lib/websocket/http.rb
81
+ - lib/websocket/driver/hybi/stream_reader.rb
82
+ - lib/websocket/driver/utf8_match.rb
83
+ - lib/websocket/driver/headers.rb
84
+ - lib/websocket/driver/hybi.rb
85
+ - lib/websocket/driver/server.rb
59
86
  - lib/websocket/driver/client.rb
60
87
  - lib/websocket/driver/draft75.rb
61
- - lib/websocket/driver/draft76.rb
62
88
  - lib/websocket/driver/event_emitter.rb
63
- - lib/websocket/driver/hybi/stream_reader.rb
64
- - lib/websocket/driver/hybi.rb
65
- - lib/websocket/driver/utf8_match.rb
66
- - lib/websocket/driver.rb
89
+ - lib/websocket/driver/draft76.rb
67
90
  homepage: http://github.com/faye/websocket-driver-ruby
68
91
  licenses: []
69
92
  post_install_message: