websocket-driver 0.1.0 → 0.2.0

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