ably-em-http-request 1.1.8

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.
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