websocket-driver 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1a13b9adf6ecd4d2e375153c60898f884fb60de1
4
- data.tar.gz: adf67dbc8b4eb9f4c582531f026b03c029a596cd
3
+ metadata.gz: 8bb8b233ccc5e031bc6da97c6c716ed50aab3d07
4
+ data.tar.gz: 47be97004eb7ff7714b91d1e970b14fa53a8adb9
5
5
  SHA512:
6
- metadata.gz: 40a8dffd61f2aeb1ebb28ca1d368a6bbbc66ab6422d4a7ba68afec740e456d2bf20a783353ce3c0bda0f43dbbbb9a42d68cd61c5c43744647ce544b0049daadd
7
- data.tar.gz: 1b93c39aaa658aaf9c736307a4a7e3866768e7c1703aa981b9603a50e76798297aecfc7c20a09699cf96e9d2eb0e24257db90a245bc11ecf9898549ea6211a1a
6
+ metadata.gz: 7b48d1dd1e68a35529b77f1f775146e6ea40961939ec124e2c31f42356473a9071533e61465574a0ddc2e8f482908316f36775c5a02f9447c8839ca435ff9b17
7
+ data.tar.gz: 2d07d08ae2caa027f6b22d07325bcb2ba509c2d9349362731ef34a3f47396fc29ef3ebccb60afba594740b8e024b7d09162115695eaf094f2063d606d74f28cb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ### 0.4.0 / 2014-11-08
2
+
3
+ * Support connection via HTTP proxies using `CONNECT`
4
+
1
5
  ### 0.3.5 / 2014-10-04
2
6
 
3
7
  * Fix bug where the `Server` driver doesn't pass `ping` callbacks to its delegate
data/README.md CHANGED
@@ -193,6 +193,51 @@ sent back by the server:
193
193
  * `driver.headers` - a hash-like object containing the response headers
194
194
 
195
195
 
196
+ ### HTTP Proxies
197
+
198
+ The client driver supports connections via HTTP proxies using the `CONNECT`
199
+ method. Instead of sending the WebSocket handshake immediately, it will send a
200
+ `CONNECT` request, wait for a `200` response, and then proceed as normal.
201
+
202
+ To use this feature, call `proxy = driver.proxy(url)` where `url` is the origin
203
+ of the proxy, including a username and password if required. This produces an
204
+ object that manages the process of connecting via the proxy. You should call
205
+ `proxy.start` to begin the connection process, and pass data you receive via the
206
+ socket to `proxy.parse(data)`. When the proxy emits `:connect`, you should then
207
+ start sending incoming data to `driver.parse(data)` as normal, and call
208
+ `driver.start`.
209
+
210
+ ```rb
211
+ proxy = driver.proxy('http://username:password@proxy.example.com')
212
+
213
+ proxy.on :connect do
214
+ driver.start
215
+ end
216
+ ```
217
+
218
+ The proxy's `:connect` event is also where you should perform a TLS handshake on
219
+ your TCP stream, if you are connecting to a `wss:` endpoint.
220
+
221
+ In the event that proxy connection fails, `proxy` will emit an `:error`. You can
222
+ inspect the proxy's response via `proxy.status` and `proxy.headers`.
223
+
224
+ ```rb
225
+ proxy.on :error do |error|
226
+ puts error.message
227
+ puts proxy.status
228
+ puts proxy.headers.inspect
229
+ end
230
+ ```
231
+
232
+ Before calling `proxy.start` you can set custom headers using
233
+ `proxy.set_header`:
234
+
235
+ ```rb
236
+ proxy.set_header('User-Agent', 'ruby')
237
+ proxy.start
238
+ ```
239
+
240
+
196
241
  ### Driver API
197
242
 
198
243
  Drivers are created using one of the following methods:
@@ -13,19 +13,20 @@ void Init_websocket_mask() {
13
13
  }
14
14
 
15
15
  VALUE method_websocket_mask(VALUE self, VALUE payload, VALUE mask) {
16
+ int n, i, p, m;
17
+ int mask_array[4];
18
+ VALUE unmasked;
19
+
16
20
  if (mask == Qnil || RARRAY_LEN(mask) == 0) {
17
21
  return payload;
18
22
  }
19
23
 
20
- int n = RARRAY_LEN(payload), i, p, m;
21
- VALUE unmasked = rb_ary_new2(n);
24
+ n = RARRAY_LEN(payload);
25
+ unmasked = rb_ary_new2(n);
22
26
 
23
- int mask_array[] = {
24
- NUM2INT(rb_ary_entry(mask, 0)),
25
- NUM2INT(rb_ary_entry(mask, 1)),
26
- NUM2INT(rb_ary_entry(mask, 2)),
27
- NUM2INT(rb_ary_entry(mask, 3))
28
- };
27
+ for (i = 0; i < 4; i++) {
28
+ mask_array[i] = NUM2INT(rb_ary_entry(mask, i));
29
+ }
29
30
 
30
31
  for (i = 0; i < n; i++) {
31
32
  p = NUM2INT(rb_ary_entry(payload, i));
@@ -51,6 +51,7 @@ module WebSocket
51
51
  autoload :EventEmitter, root + '/event_emitter'
52
52
  autoload :Headers, root + '/headers'
53
53
  autoload :Hybi, root + '/hybi'
54
+ autoload :Proxy, root + '/proxy'
54
55
  autoload :Server, root + '/server'
55
56
 
56
57
  include EventEmitter
@@ -15,12 +15,36 @@ module WebSocket
15
15
  @key = Client.generate_key
16
16
  @accept = Hybi.generate_accept(@key)
17
17
  @http = HTTP::Response.new
18
+
19
+ uri = URI.parse(@socket.url)
20
+ host = uri.host + (uri.port ? ":#{uri.port}" : '')
21
+ path = (uri.path == '') ? '/' : uri.path
22
+ @pathname = path + (uri.query ? '?' + uri.query : '')
23
+
24
+ @headers['Host'] = host
25
+ @headers['Upgrade'] = 'websocket'
26
+ @headers['Connection'] = 'Upgrade'
27
+ @headers['Sec-WebSocket-Key'] = @key
28
+ @headers['Sec-WebSocket-Version'] = '13'
29
+
30
+ if @protocols.size > 0
31
+ @headers['Sec-WebSocket-Protocol'] = @protocols * ', '
32
+ end
33
+
34
+ if uri.user
35
+ auth = Base64.encode64([uri.user, uri.password] * ':').gsub(/\n/, '')
36
+ @headers['Authorization'] = 'Basic ' + auth
37
+ end
18
38
  end
19
39
 
20
40
  def version
21
41
  'hybi-13'
22
42
  end
23
43
 
44
+ def proxy(origin, options = {})
45
+ Proxy.new(self, origin, options)
46
+ end
47
+
24
48
  def start
25
49
  return false unless @ready_state == -1
26
50
  @socket.write(Driver.encode(handshake_request, :binary))
@@ -30,8 +54,10 @@ module WebSocket
30
54
 
31
55
  def parse(buffer)
32
56
  return super if @ready_state > 0
57
+
33
58
  @http.parse(buffer)
34
59
  return fail_handshake('Invalid HTTP response') if @http.error?
60
+
35
61
  validate_handshake if @http.complete?
36
62
  parse(@http.body) if @ready_state == 1
37
63
  end
@@ -39,29 +65,9 @@ module WebSocket
39
65
  private
40
66
 
41
67
  def handshake_request
42
- uri = URI.parse(@socket.url)
43
- host = uri.host + (uri.port ? ":#{uri.port}" : '')
44
- path = (uri.path == '') ? '/' : uri.path
45
- query = uri.query ? "?#{uri.query}" : ''
46
-
47
- headers = [ "GET #{path}#{query} HTTP/1.1",
48
- "Host: #{host}",
49
- "Upgrade: websocket",
50
- "Connection: Upgrade",
51
- "Sec-WebSocket-Key: #{@key}",
52
- "Sec-WebSocket-Version: 13"
53
- ]
54
-
55
- if @protocols.size > 0
56
- headers << "Sec-WebSocket-Protocol: #{@protocols * ', '}"
57
- end
58
-
59
- if uri.user
60
- auth = Base64.encode64([uri.user, uri.password] * ':').gsub(/\n/, '')
61
- headers << "Authorization: Basic #{auth}"
62
- end
63
-
64
- (headers + [@headers.to_s, '']).join("\r\n")
68
+ start = "GET #{@pathname} HTTP/1.1"
69
+ headers = [start, @headers.to_s, '']
70
+ headers.join("\r\n")
65
71
  end
66
72
 
67
73
  def fail_handshake(message)
@@ -5,6 +5,11 @@ module WebSocket
5
5
  def initialize(socket, options = {})
6
6
  super
7
7
  @stage = 0
8
+
9
+ @headers['Upgrade'] = 'WebSocket'
10
+ @headers['Connection'] = 'Upgrade'
11
+ @headers['WebSocket-Origin'] = @socket.env['HTTP_ORIGIN']
12
+ @headers['WebSocket-Location'] = @socket.url
8
13
  end
9
14
 
10
15
  def version
@@ -73,14 +78,9 @@ module WebSocket
73
78
  private
74
79
 
75
80
  def handshake_response
76
- upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
77
- upgrade << "Upgrade: WebSocket\r\n"
78
- upgrade << "Connection: Upgrade\r\n"
79
- upgrade << "WebSocket-Origin: #{@socket.env['HTTP_ORIGIN']}\r\n"
80
- upgrade << "WebSocket-Location: #{@socket.url}\r\n"
81
- upgrade << @headers.to_s
82
- upgrade << "\r\n"
83
- upgrade
81
+ start = 'HTTP/1.1 101 Web Socket Protocol Handshake'
82
+ headers = [start, @headers.to_s, '']
83
+ headers.join("\r\n")
84
84
  end
85
85
 
86
86
  def parse_leading_byte(data)
@@ -9,6 +9,12 @@ module WebSocket
9
9
  input = @socket.env['rack.input']
10
10
  @stage = -1
11
11
  @body = input ? input.read.bytes.to_a : []
12
+
13
+ @headers.clear
14
+ @headers['Upgrade'] = 'WebSocket'
15
+ @headers['Connection'] = 'Upgrade'
16
+ @headers['Sec-WebSocket-Origin'] = @socket.env['HTTP_ORIGIN']
17
+ @headers['Sec-WebSocket-Location'] = @socket.url
12
18
  end
13
19
 
14
20
  def version
@@ -32,14 +38,9 @@ module WebSocket
32
38
  private
33
39
 
34
40
  def handshake_response
35
- upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
36
- upgrade << "Upgrade: WebSocket\r\n"
37
- upgrade << "Connection: Upgrade\r\n"
38
- upgrade << "Sec-WebSocket-Origin: #{@socket.env['HTTP_ORIGIN']}\r\n"
39
- upgrade << "Sec-WebSocket-Location: #{@socket.url}\r\n"
40
- upgrade << @headers.to_s
41
- upgrade << "\r\n"
42
- upgrade
41
+ start = 'HTTP/1.1 101 WebSocket Protocol Handshake'
42
+ headers = [start, @headers.to_s, '']
43
+ headers.join("\r\n")
43
44
  end
44
45
 
45
46
  def handshake_signature
@@ -6,13 +6,17 @@ module WebSocket
6
6
 
7
7
  def initialize(received = {})
8
8
  @raw = received
9
- @sent = Set.new
10
- @lines = []
9
+ clear
11
10
 
12
11
  @received = {}
13
12
  @raw.each { |k,v| @received[HTTP.normalize_header(k)] = v }
14
13
  end
15
14
 
15
+ def clear
16
+ @sent = Set.new
17
+ @lines = []
18
+ end
19
+
16
20
  def [](name)
17
21
  @received[HTTP.normalize_header(name)]
18
22
  end
@@ -62,9 +62,17 @@ module WebSocket
62
62
 
63
63
  return unless @socket.respond_to?(:env)
64
64
 
65
+ sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
66
+ protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
67
+
68
+ @headers['Upgrade'] = 'websocket'
69
+ @headers['Connection'] = 'Upgrade'
70
+ @headers['Sec-WebSocket-Accept'] = Hybi.generate_accept(sec_key)
71
+
65
72
  if protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
66
73
  protos = protos.split(/\s*,\s*/) if String === protos
67
74
  @protocol = protos.find { |p| @protocols.include?(p) }
75
+ @headers['Sec-WebSocket-Protocol'] = @protocol if @protocol
68
76
  end
69
77
  end
70
78
 
@@ -197,21 +205,9 @@ module WebSocket
197
205
  private
198
206
 
199
207
  def handshake_response
200
- sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
201
- return '' unless String === sec_key
202
-
203
- headers = [
204
- "HTTP/1.1 101 Switching Protocols",
205
- "Upgrade: websocket",
206
- "Connection: Upgrade",
207
- "Sec-WebSocket-Accept: #{Hybi.generate_accept(sec_key)}"
208
- ]
209
-
210
- if @protocol
211
- headers << "Sec-WebSocket-Protocol: #{@protocol}"
212
- end
213
-
214
- (headers + [@headers.to_s, '']).join("\r\n")
208
+ start = 'HTTP/1.1 101 Switching Protocols'
209
+ headers = [start, @headers.to_s, '']
210
+ headers.join("\r\n")
215
211
  end
216
212
 
217
213
  def shutdown(code, reason)
@@ -0,0 +1,68 @@
1
+ module WebSocket
2
+ class Driver
3
+
4
+ class Proxy
5
+ include EventEmitter
6
+
7
+ PORTS = {'ws' => 80, 'wss' => 443}
8
+
9
+ attr_reader :status, :headers
10
+
11
+ def initialize(client, origin, options)
12
+ super()
13
+
14
+ @client = client
15
+ @http = HTTP::Response.new
16
+ @socket = client.instance_variable_get(:@socket)
17
+ @origin = URI.parse(@socket.url)
18
+ @url = URI.parse(origin)
19
+ @options = options
20
+ @state = 0
21
+
22
+ @headers = Headers.new
23
+ @headers['Host'] = @origin.host + (@origin.port ? ":#{@origin.port}" : '')
24
+ @headers['Connection'] = 'keep-alive'
25
+ @headers['Proxy-Connection'] = 'keep-alive'
26
+
27
+ if @url.user
28
+ auth = Base64.encode64([@url.user, @url.password] * ':').gsub(/\n/, '')
29
+ @headers['Proxy-Authorization'] = 'Basic ' + auth
30
+ end
31
+ end
32
+
33
+ def set_header(name, value)
34
+ return false unless @state == 0
35
+ @headers[name] = value
36
+ true
37
+ end
38
+
39
+ def start
40
+ return false unless @state == 0
41
+ @state = 1
42
+
43
+ port = @origin.port || PORTS[@origin.scheme]
44
+ start = "CONNECT #{@origin.host}:#{port} HTTP/1.1"
45
+ headers = [start, @headers.to_s, '']
46
+
47
+ @socket.write(headers.join("\r\n"))
48
+ true
49
+ end
50
+
51
+ def parse(buffer)
52
+ @http.parse(buffer)
53
+ return unless @http.complete?
54
+
55
+ @status = @http.code
56
+ @headers = Headers.new(@http.headers)
57
+
58
+ if @status == 200
59
+ emit(:connect)
60
+ else
61
+ message = "Can't establish a connection to the server at #{@socket.url}"
62
+ emit(:error, ProtocolError.new(message))
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: websocket-driver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Coglan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-04 00:00:00.000000000 Z
11
+ date: 2014-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: eventmachine
@@ -74,6 +74,7 @@ files:
74
74
  - lib/websocket/driver/headers.rb
75
75
  - lib/websocket/driver/hybi.rb
76
76
  - lib/websocket/driver/hybi/stream_reader.rb
77
+ - lib/websocket/driver/proxy.rb
77
78
  - lib/websocket/driver/server.rb
78
79
  - lib/websocket/driver/utf8_match.rb
79
80
  - lib/websocket/http.rb