ably-em-http-request 1.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.github/workflows/ci.yml +22 -0
  4. data/.gitignore +9 -0
  5. data/.rspec +0 -0
  6. data/Changelog.md +78 -0
  7. data/Gemfile +14 -0
  8. data/LICENSE +21 -0
  9. data/README.md +66 -0
  10. data/Rakefile +10 -0
  11. data/ably-em-http-request.gemspec +33 -0
  12. data/benchmarks/clients.rb +170 -0
  13. data/benchmarks/em-excon.rb +87 -0
  14. data/benchmarks/em-profile.gif +0 -0
  15. data/benchmarks/em-profile.txt +65 -0
  16. data/benchmarks/server.rb +48 -0
  17. data/examples/.gitignore +1 -0
  18. data/examples/digest_auth/client.rb +25 -0
  19. data/examples/digest_auth/server.rb +28 -0
  20. data/examples/fetch.rb +30 -0
  21. data/examples/fibered-http.rb +51 -0
  22. data/examples/multi.rb +25 -0
  23. data/examples/oauth-tweet.rb +35 -0
  24. data/examples/socks5.rb +23 -0
  25. data/lib/em/io_streamer.rb +51 -0
  26. data/lib/em-http/client.rb +343 -0
  27. data/lib/em-http/core_ext/bytesize.rb +6 -0
  28. data/lib/em-http/decoders.rb +252 -0
  29. data/lib/em-http/http_client_options.rb +51 -0
  30. data/lib/em-http/http_connection.rb +408 -0
  31. data/lib/em-http/http_connection_options.rb +72 -0
  32. data/lib/em-http/http_encoding.rb +151 -0
  33. data/lib/em-http/http_header.rb +85 -0
  34. data/lib/em-http/http_status_codes.rb +59 -0
  35. data/lib/em-http/middleware/digest_auth.rb +114 -0
  36. data/lib/em-http/middleware/json_response.rb +17 -0
  37. data/lib/em-http/middleware/oauth.rb +42 -0
  38. data/lib/em-http/middleware/oauth2.rb +30 -0
  39. data/lib/em-http/multi.rb +59 -0
  40. data/lib/em-http/request.rb +25 -0
  41. data/lib/em-http/version.rb +7 -0
  42. data/lib/em-http-request.rb +1 -0
  43. data/lib/em-http.rb +20 -0
  44. data/spec/client_fiber_spec.rb +23 -0
  45. data/spec/client_spec.rb +1000 -0
  46. data/spec/digest_auth_spec.rb +48 -0
  47. data/spec/dns_spec.rb +41 -0
  48. data/spec/encoding_spec.rb +49 -0
  49. data/spec/external_spec.rb +146 -0
  50. data/spec/fixtures/google.ca +16 -0
  51. data/spec/fixtures/gzip-sample.gz +0 -0
  52. data/spec/gzip_spec.rb +91 -0
  53. data/spec/helper.rb +27 -0
  54. data/spec/http_proxy_spec.rb +268 -0
  55. data/spec/middleware/oauth2_spec.rb +15 -0
  56. data/spec/middleware_spec.rb +143 -0
  57. data/spec/multi_spec.rb +104 -0
  58. data/spec/pipelining_spec.rb +62 -0
  59. data/spec/redirect_spec.rb +430 -0
  60. data/spec/socksify_proxy_spec.rb +56 -0
  61. data/spec/spec_helper.rb +25 -0
  62. data/spec/ssl_spec.rb +67 -0
  63. data/spec/stallion.rb +334 -0
  64. data/spec/stub_server.rb +45 -0
  65. metadata +269 -0
@@ -0,0 +1,252 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+
4
+ ##
5
+ # Provides a unified callback interface to decompression libraries.
6
+ module EventMachine::AblyHttpRequest::HttpDecoders
7
+
8
+ class DecoderError < StandardError
9
+ end
10
+
11
+ class << self
12
+ def accepted_encodings
13
+ DECODERS.inject([]) { |r, d| r + d.encoding_names }
14
+ end
15
+
16
+ def decoder_for_encoding(encoding)
17
+ DECODERS.each { |d|
18
+ return d if d.encoding_names.include? encoding
19
+ }
20
+ nil
21
+ end
22
+ end
23
+
24
+ class Base
25
+ def self.encoding_names
26
+ name = to_s.split('::').last.downcase
27
+ [name]
28
+ end
29
+
30
+ ##
31
+ # chunk_callback:: [Block] To handle a decompressed chunk
32
+ def initialize(&chunk_callback)
33
+ @chunk_callback = chunk_callback
34
+ end
35
+
36
+ def <<(compressed)
37
+ return unless compressed && compressed.size > 0
38
+
39
+ decompressed = decompress(compressed)
40
+ receive_decompressed decompressed
41
+ end
42
+
43
+ def finalize!
44
+ decompressed = finalize
45
+ receive_decompressed decompressed
46
+ end
47
+
48
+ private
49
+
50
+ def receive_decompressed(decompressed)
51
+ if decompressed && decompressed.size > 0
52
+ @chunk_callback.call(decompressed)
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ ##
59
+ # Must return a part of decompressed
60
+ def decompress(compressed)
61
+ nil
62
+ end
63
+
64
+ ##
65
+ # May return last part
66
+ def finalize
67
+ nil
68
+ end
69
+ end
70
+
71
+ class Deflate < Base
72
+ def decompress(compressed)
73
+ begin
74
+ @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS)
75
+ @zstream.inflate(compressed)
76
+ rescue Zlib::Error
77
+ raise DecoderError
78
+ end
79
+ end
80
+
81
+ def finalize
82
+ return nil unless @zstream
83
+
84
+ begin
85
+ r = @zstream.inflate(nil)
86
+ @zstream.close
87
+ r
88
+ rescue Zlib::Error
89
+ raise DecoderError
90
+ end
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Partial implementation of RFC 1952 to extract the deflate stream from a gzip file
96
+ class GZipHeader
97
+ def initialize
98
+ @state = :begin
99
+ @data = ""
100
+ @pos = 0
101
+ end
102
+
103
+ def finished?
104
+ @state == :finish
105
+ end
106
+
107
+ def read(n, buffer)
108
+ if (@pos + n) <= @data.size
109
+ buffer << @data[@pos..(@pos + n - 1)]
110
+ @pos += n
111
+ return true
112
+ else
113
+ return false
114
+ end
115
+ end
116
+
117
+ def readbyte
118
+ if (@pos + 1) <= @data.size
119
+ @pos += 1
120
+ @data.getbyte(@pos - 1)
121
+ end
122
+ end
123
+
124
+ def eof?
125
+ @pos >= @data.size
126
+ end
127
+
128
+ def extract_stream(compressed)
129
+ @data << compressed
130
+
131
+ while !eof? && !finished?
132
+ buffer = ""
133
+
134
+ case @state
135
+ when :begin
136
+ break if !read(10, buffer)
137
+
138
+ if buffer.getbyte(0) != 0x1f || buffer.getbyte(1) != 0x8b
139
+ raise DecoderError.new("magic header not found")
140
+ end
141
+
142
+ if buffer.getbyte(2) != 0x08
143
+ raise DecoderError.new("unknown compression method")
144
+ end
145
+
146
+ @flags = buffer.getbyte(3)
147
+ if (@flags & 0xe0).nonzero?
148
+ raise DecoderError.new("unknown header flags set")
149
+ end
150
+
151
+ # We don't care about these values, I'm leaving the code for reference
152
+ # @time = buffer[4..7].unpack("V")[0] # little-endian uint32
153
+ # @extra_flags = buffer.getbyte(8)
154
+ # @os = buffer.getbyte(9)
155
+
156
+ @state = :extra_length
157
+
158
+ when :extra_length
159
+ if (@flags & 0x04).nonzero?
160
+ break if !read(2, buffer)
161
+ @extra_length = buffer.unpack("v")[0] # little-endian uint16
162
+ @state = :extra
163
+ else
164
+ @state = :extra
165
+ end
166
+
167
+ when :extra
168
+ if (@flags & 0x04).nonzero?
169
+ break if read(@extra_length, buffer)
170
+ @state = :name
171
+ else
172
+ @state = :name
173
+ end
174
+
175
+ when :name
176
+ if (@flags & 0x08).nonzero?
177
+ while !(buffer = readbyte).nil?
178
+ if buffer == 0
179
+ @state = :comment
180
+ break
181
+ end
182
+ end
183
+ else
184
+ @state = :comment
185
+ end
186
+
187
+ when :comment
188
+ if (@flags & 0x10).nonzero?
189
+ while !(buffer = readbyte).nil?
190
+ if buffer == 0
191
+ @state = :hcrc
192
+ break
193
+ end
194
+ end
195
+ else
196
+ @state = :hcrc
197
+ end
198
+
199
+ when :hcrc
200
+ if (@flags & 0x02).nonzero?
201
+ break if !read(2, buffer)
202
+ @state = :finish
203
+ else
204
+ @state = :finish
205
+ end
206
+ end
207
+ end
208
+
209
+ if finished?
210
+ compressed[(@pos - (@data.length - compressed.length))..-1]
211
+ else
212
+ ""
213
+ end
214
+ end
215
+ end
216
+
217
+ class GZip < Base
218
+ def self.encoding_names
219
+ %w(gzip compressed)
220
+ end
221
+
222
+ def decompress(compressed)
223
+ @header ||= GZipHeader.new
224
+ if !@header.finished?
225
+ compressed = @header.extract_stream(compressed)
226
+ end
227
+
228
+ @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS)
229
+ @zstream.inflate(compressed)
230
+ rescue Zlib::Error
231
+ raise DecoderError
232
+ end
233
+
234
+ def finalize
235
+ if @zstream
236
+ if !@zstream.finished?
237
+ r = @zstream.finish
238
+ end
239
+ @zstream.close
240
+ r
241
+ else
242
+ nil
243
+ end
244
+ rescue Zlib::Error
245
+ raise DecoderError
246
+ end
247
+
248
+ end
249
+
250
+ DECODERS = [Deflate, GZip]
251
+
252
+ end
@@ -0,0 +1,51 @@
1
+ module AblyHttpRequest
2
+ class HttpClientOptions
3
+ attr_reader :uri, :method, :host, :port
4
+ attr_reader :headers, :file, :body, :query, :path
5
+ attr_reader :keepalive, :pass_cookies, :decoding, :compressed
6
+
7
+ attr_accessor :followed, :redirects
8
+
9
+ def initialize(uri, options, method)
10
+ @keepalive = options[:keepalive] || false # default to single request per connection
11
+ @redirects = options[:redirects] ||= 0 # default number of redirects to follow
12
+ @followed = options[:followed] ||= 0 # keep track of number of followed requests
13
+
14
+ @method = method.to_s.upcase
15
+ @headers = options[:head] || {}
16
+
17
+ @file = options[:file]
18
+ @body = options[:body]
19
+
20
+ @pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects
21
+ @decoding = options.fetch(:decoding, true) # auto-decode compressed response
22
+ @compressed = options.fetch(:compressed, true) # auto-negotiated compressed response
23
+
24
+ set_uri(uri, options[:path], options[:query])
25
+ end
26
+
27
+ def follow_redirect?; @followed < @redirects; end
28
+ def ssl?; @uri.scheme == "https" || @uri.port == 443; end
29
+ def no_body?; @method == "HEAD"; end
30
+
31
+ def set_uri(uri, path = nil, query = nil)
32
+ uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
33
+ uri.path = path if path
34
+ uri.path = '/' if uri.path.empty?
35
+
36
+ @uri = uri
37
+ @path = uri.path
38
+ @host = uri.hostname
39
+ @port = uri.port
40
+ @query = query
41
+
42
+ # Make sure the ports are set as Addressable::URI doesn't
43
+ # set the port if it isn't there
44
+ if @port.nil?
45
+ @port = @uri.scheme == "https" ? 443 : 80
46
+ end
47
+
48
+ uri
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,408 @@
1
+ require 'em/io_streamer'
2
+
3
+ module EventMachine
4
+ module AblyHttpRequest
5
+
6
+ module HTTPMethods
7
+ def get options = {}, &blk; setup_request(:get, options, &blk); end
8
+ def head options = {}, &blk; setup_request(:head, options, &blk); end
9
+ def delete options = {}, &blk; setup_request(:delete, options, &blk); end
10
+ def put options = {}, &blk; setup_request(:put, options, &blk); end
11
+ def post options = {}, &blk; setup_request(:post, options, &blk); end
12
+ def patch options = {}, &blk; setup_request(:patch, options, &blk); end
13
+ def options options = {}, &blk; setup_request(:options, options, &blk); end
14
+ end
15
+
16
+ class HttpStubConnection < Connection
17
+ include Deferrable
18
+ attr_reader :parent
19
+
20
+ def parent=(p)
21
+ @parent = p
22
+ @parent.conn = self
23
+ end
24
+
25
+ def receive_data(data)
26
+ begin
27
+ @parent.receive_data data
28
+ rescue EventMachine::Connectify::CONNECTError => e
29
+ @parent.close(e.message)
30
+ end
31
+ end
32
+
33
+ def connection_completed
34
+ @parent.connection_completed
35
+ end
36
+
37
+ def unbind(reason=nil)
38
+ @parent.unbind(reason)
39
+ end
40
+
41
+ # TLS verification support, original implementation by Mislav Marohnić
42
+ # https://github.com/lostisland/faraday/blob/63cf47c95b573539f047c729bd9ad67560bc83ff/lib/faraday/adapter/em_http_ssl_patch.rb
43
+ #
44
+ # Updated by Ably, here’s why:
45
+ #
46
+ # We noticed that the existing verification mechanism is failing in the
47
+ # case where the certificate chain presented by the server contains a
48
+ # certificate that’s signed by an expired trust anchor. At the time of
49
+ # writing, this is the case with some Let’s Encrypt certificate chains,
50
+ # which contain a cross-sign by the expired DST Root X3 CA.
51
+ #
52
+ # This isn’t meant to be an issue; the certificate chain presented by the
53
+ # server still contains a certificate that’s a trust anchor in most
54
+ # modern systems. So in the case where this trust anchor exists, OpenSSL
55
+ # would instead construct a certification path that goes straight to that
56
+ # anchor, bypassing the expired certificate.
57
+ #
58
+ # Unfortunately, as described in
59
+ # https://github.com/eventmachine/eventmachine/issues/954#issue-1014842247,
60
+ # EventMachine misuses OpenSSL in a variety of ways. One of them is that
61
+ # it does not configure a list of trust anchors, meaning that OpenSSL is
62
+ # unable to construct the correct certification path in the manner
63
+ # described above.
64
+ #
65
+ # This means that we end up in a degenerate situation where
66
+ # ssl_verify_peer just receives the certificates in the chain provided by
67
+ # the peer. In the scenario described above, one of these certificates is
68
+ # expired and hence the existing verification mechanism, which "verifies"
69
+ # each certificate provided to ssl_verify_peer, fails.
70
+ #
71
+ # So, instead we remove the existing ad-hoc mechanism for verification
72
+ # (which did things I’m not sure it should have done, like putting
73
+ # non-trust-anchors into an OpenSSL::X509::Store) and instead employ
74
+ # OpenSSL (configured to use the system trust store, and hence able to
75
+ # construct the correct certification path) to do all the hard work of
76
+ # constructing the certification path and then verifying the peer
77
+ # certificate. (This is what, in my opinion, EventMachine ideally would
78
+ # be allowing OpenSSL to do in the first place. Instead, as far as I can
79
+ # tell, it pushes all of this responsibility onto its users, and then
80
+ # provides them with an insufficient API for meeting this
81
+ # responsibility.)
82
+ def ssl_verify_peer(cert_string)
83
+ # We use ssl_verify_peer simply as a mechanism for gathering the
84
+ # certificate chain presented by the peer. In ssl_handshake_completed,
85
+ # we’ll make use of this information in order to verify the peer.
86
+ @peer_certificate_chain ||= []
87
+ begin
88
+ cert = OpenSSL::X509::Certificate.new(cert_string)
89
+ @peer_certificate_chain << cert
90
+ true
91
+ rescue OpenSSL::X509::CertificateError
92
+ return false
93
+ end
94
+ end
95
+
96
+ def ssl_handshake_completed
97
+ # Warning message updated by Ably — the previous message suggested that
98
+ # when verify_peer is false, the server certificate would be verified
99
+ # but not checked against the hostname. This is not true — when
100
+ # verify_peer is false, the server certificate is not verified at all.
101
+ unless verify_peer?
102
+ warn "[WARNING; ably-em-http-request] TLS server certificate validation is disabled (use 'tls: {verify_peer: true}'), see" +
103
+ " CVE-2020-13482 and https://github.com/igrigorik/em-http-request/issues/339 for details" unless parent.connopts.tls.has_key?(:verify_peer)
104
+ return true
105
+ end
106
+
107
+ # It’s not great to have to perform the server certificate verification
108
+ # after the handshake has completed, because it means:
109
+ #
110
+ # - We have to be sure that we don’t send any data over the TLS
111
+ # connection until we’ve verified the certificate. Created
112
+ # https://github.com/ably/ably-ruby/issues/400 to understand whether
113
+ # there’s anything we need to change to be sure of this.
114
+ #
115
+ # - If verification does fail, we have no way of failing the handshake
116
+ # with a bad_certificate error.
117
+ #
118
+ # Unfortunately there doesn’t seem to be a better alternative within
119
+ # the TLS-related APIs provided to us by EventMachine. (Admittedly I am
120
+ # not familiar with EventMachine.)
121
+ #
122
+ # (Performing the verification post-handshake is not new to the Ably
123
+ # implementation of certificate verification; the previous
124
+ # implementation performed hostname verification after the handshake
125
+ # was complete.)
126
+
127
+ # I was quite worried by the description in the aforementioned issue
128
+ # eventmachine/eventmachine#954 of how EventMachine "ignores all errors
129
+ # from the chain construction" and hence I don’t know if there is some
130
+ # weird scenario where, somehow, the calls to ssl_verify_peer terminate
131
+ # with some intermediate certificate instead of with the certificate of
132
+ # the server we’re communicating with. (It's quite possible that this
133
+ # can’t occur and I’m just being paranoid, but I think a bit of
134
+ # paranoia when it comes to security isn't a bad thing.)
135
+ #
136
+ # That's why, instead of the previous code which passed
137
+ # certificate_store.verify the final certificate received by
138
+ # ssl_verify_peer, I explicitly use the result of get_peer_cert, to be
139
+ # sure that the certificate that we’re verifying is the one that the
140
+ # server has demonstrated that they hold the private key for.
141
+ server_certificate = OpenSSL::X509::Certificate.new(get_peer_cert)
142
+
143
+ # A sense check to confirm my understanding of what’s in @peer_certificate_chain.
144
+ #
145
+ # (As mentioned above, unless something has gone very wrong, these two
146
+ # certificates should be identical.)
147
+ unless server_certificate.to_der == @peer_certificate_chain.last.to_der
148
+ raise OpenSSL::SSL::SSLError.new(%(Peer certificate sense check failed for "#{host}"));
149
+ end
150
+
151
+ # Verify the server’s certificate against the default trust anchors,
152
+ # aided by the intermediate certificates provided by the server.
153
+ unless create_certificate_store.verify(server_certificate, @peer_certificate_chain[0...-1])
154
+ raise OpenSSL::SSL::SSLError.new(%(unable to verify the server certificate for "#{host}"))
155
+ end
156
+
157
+ # Verify that the peer’s certificate matches the hostname.
158
+ unless OpenSSL::SSL.verify_certificate_identity(server_certificate, host)
159
+ raise OpenSSL::SSL::SSLError.new(%(host "#{host}" does not match the server certificate))
160
+ else
161
+ true
162
+ end
163
+ end
164
+
165
+ def verify_peer?
166
+ parent.connopts.tls[:verify_peer]
167
+ end
168
+
169
+ def host
170
+ parent.connopts.host
171
+ end
172
+
173
+ def create_certificate_store
174
+ store = OpenSSL::X509::Store.new
175
+ store.set_default_paths
176
+ ca_file = parent.connopts.tls[:cert_chain_file]
177
+ store.add_file(ca_file) if ca_file
178
+ store
179
+ end
180
+ end
181
+
182
+ class HttpConnection
183
+ include HTTPMethods
184
+ include Socksify
185
+ include Connectify
186
+
187
+ attr_reader :deferred, :conn
188
+ attr_accessor :error, :connopts, :uri
189
+
190
+ def initialize
191
+ @deferred = true
192
+ @middleware = []
193
+ end
194
+
195
+ def conn=(c)
196
+ @conn = c
197
+ @deferred = false
198
+ end
199
+
200
+ def activate_connection(client)
201
+ begin
202
+ EventMachine.bind_connect(@connopts.bind, @connopts.bind_port,
203
+ @connopts.host, @connopts.port,
204
+ HttpStubConnection) do |conn|
205
+ post_init
206
+
207
+ @deferred = false
208
+ @conn = conn
209
+
210
+ conn.parent = self
211
+ conn.pending_connect_timeout = @connopts.connect_timeout
212
+ conn.comm_inactivity_timeout = @connopts.inactivity_timeout
213
+ end
214
+
215
+ finalize_request(client)
216
+ rescue EventMachine::ConnectionError => e
217
+ #
218
+ # Currently, this can only fire on initial connection setup
219
+ # since #connect is a synchronous method. Hence, rescue the exception,
220
+ # and return a failed deferred which fail any client request at next
221
+ # tick. We fail at next tick to keep a consistent API when the newly
222
+ # created HttpClient is failed. This approach has the advantage to
223
+ # remove a state check of @deferred_status after creating a new
224
+ # HttpRequest. The drawback is that users may setup a callback which we
225
+ # know won't be used.
226
+ #
227
+ # Once there is async-DNS, then we'll iterate over the outstanding
228
+ # client requests and fail them in order.
229
+ #
230
+ # Net outcome: failed connection will invoke the same ConnectionError
231
+ # message on the connection deferred, and on the client deferred.
232
+ #
233
+ EM.next_tick{client.close(e.message)}
234
+ end
235
+ end
236
+
237
+ def setup_request(method, options = {}, c = nil)
238
+ c ||= HttpClient.new(self, ::AblyHttpRequest::HttpClientOptions.new(@uri, options, method))
239
+ @deferred ? activate_connection(c) : finalize_request(c)
240
+ c
241
+ end
242
+
243
+ def finalize_request(c)
244
+ @conn.callback { c.connection_completed }
245
+
246
+ middleware.each do |m|
247
+ c.callback(&m.method(:response)) if m.respond_to?(:response)
248
+ end
249
+
250
+ @clients.push c
251
+ end
252
+
253
+ def middleware
254
+ [HttpRequest.middleware, @middleware].flatten
255
+ end
256
+
257
+ def post_init
258
+ @clients = []
259
+ @pending = []
260
+
261
+ @p = Http::Parser.new
262
+ @p.header_value_type = :mixed
263
+ @p.on_headers_complete = proc do |h|
264
+ if client
265
+ if @p.status_code == 100
266
+ client.send_request_body
267
+ @p.reset!
268
+ else
269
+ client.parse_response_header(h, @p.http_version, @p.status_code)
270
+ :reset if client.req.no_body?
271
+ end
272
+ else
273
+ # if we receive unexpected data without a pending client request
274
+ # reset the parser to avoid firing any further callbacks and close
275
+ # the connection because we're processing invalid HTTP
276
+ @p.reset!
277
+ unbind
278
+ :stop
279
+ end
280
+ end
281
+
282
+ @p.on_body = proc do |b|
283
+ client.on_body_data(b)
284
+ end
285
+
286
+ @p.on_message_complete = proc do
287
+ if !client.continue?
288
+ c = @clients.shift
289
+ c.state = :finished
290
+ c.on_request_complete
291
+ end
292
+ end
293
+ end
294
+
295
+ def use(klass, *args, &block)
296
+ @middleware << klass.new(*args, &block)
297
+ end
298
+
299
+ def peer
300
+ Socket.unpack_sockaddr_in(@peer)[1] rescue nil
301
+ end
302
+
303
+ def receive_data(data)
304
+ begin
305
+ @p << data
306
+ rescue HTTP::Parser::Error => e
307
+ c = @clients.shift
308
+ c.nil? ? unbind(e.message) : c.on_error(e.message)
309
+ end
310
+ end
311
+
312
+ def connection_completed
313
+ @peer = @conn.get_peername
314
+
315
+ if @connopts.socks_proxy?
316
+ socksify(client.req.uri.hostname, client.req.uri.inferred_port, *@connopts.proxy[:authorization]) { start }
317
+ elsif @connopts.connect_proxy?
318
+ connectify(client.req.uri.hostname, client.req.uri.inferred_port, *@connopts.proxy[:authorization]) { start }
319
+ else
320
+ start
321
+ end
322
+ end
323
+
324
+ def start
325
+ @conn.start_tls(@connopts.tls) if client && client.req.ssl?
326
+ @conn.succeed
327
+ end
328
+
329
+ def redirect(client, new_location)
330
+ old_location = client.req.uri
331
+ new_location = client.req.set_uri(new_location)
332
+
333
+ if client.req.keepalive
334
+ # Application requested a keep-alive connection but one of the requests
335
+ # hits a cross-origin redirect. We need to open a new connection and
336
+ # let both connections proceed simultaneously.
337
+ if old_location.origin != new_location.origin
338
+ conn = HttpConnection.new
339
+ client.conn = conn
340
+ conn.connopts = @connopts
341
+ conn.connopts.https = new_location.scheme == "https"
342
+ conn.uri = client.req.uri
343
+ conn.activate_connection(client)
344
+
345
+ # If the redirect is a same-origin redirect on a keep-alive request
346
+ # then immidiately dispatch the request over existing connection.
347
+ else
348
+ @clients.push client
349
+ client.connection_completed
350
+ end
351
+ else
352
+ # If connection is not keep-alive the unbind will fire and we'll
353
+ # reconnect using the same connection object.
354
+ @pending.push client
355
+ end
356
+ end
357
+
358
+ def unbind(reason = nil)
359
+ @clients.map { |c| c.unbind(reason) }
360
+
361
+ if r = @pending.shift
362
+ @clients.push r
363
+
364
+ r.reset!
365
+ @p.reset!
366
+
367
+ begin
368
+ @conn.set_deferred_status :unknown
369
+
370
+ if @connopts.proxy
371
+ @conn.reconnect(@connopts.host, @connopts.port)
372
+ else
373
+ @conn.reconnect(r.req.host, r.req.port)
374
+ end
375
+
376
+ @conn.pending_connect_timeout = @connopts.connect_timeout
377
+ @conn.comm_inactivity_timeout = @connopts.inactivity_timeout
378
+ @conn.callback { r.connection_completed }
379
+ rescue EventMachine::ConnectionError => e
380
+ @clients.pop.close(e.message)
381
+ end
382
+ else
383
+ @deferred = true
384
+ @conn.close_connection
385
+ end
386
+ end
387
+ alias :close :unbind
388
+
389
+ def send_data(data)
390
+ @conn.send_data data
391
+ end
392
+
393
+ def stream_file_data(filename, args = {})
394
+ @conn.stream_file_data filename, args
395
+ end
396
+
397
+ def stream_data(io, opts = {})
398
+ EventMachine::AblyHttpRequest::IOStreamer.new(self, io, opts)
399
+ end
400
+
401
+ private
402
+
403
+ def client
404
+ @clients.first
405
+ end
406
+ end
407
+ end
408
+ end