em-http-request 1.1.1 → 1.1.6

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
- SHA1:
3
- metadata.gz: a80a1bc2ca4e1dda7527f249949cb056b0beb886
4
- data.tar.gz: 4819cb821fa74543d537416994fbd65b0e0f676c
2
+ SHA256:
3
+ metadata.gz: 2f4f8a6c2da0b0c94da02566ece7c1d21b8eb0215a8c9cda13fe9c83ac8ee9b3
4
+ data.tar.gz: 674236e422a3e242e3e3ac32383b048fe12932392ceb21134a990a4f7c8302ff
5
5
  SHA512:
6
- metadata.gz: b7c257493787ea81e1c1b26287aa88e4e98eba7278fa4734bdf4efe136ef1184a9f7d464dc821f35775535c649204b2331a2518bdc02e92167e062e8d5f74919
7
- data.tar.gz: 1e3f32f26b6bee0e85a11318e1cce1df5fbb9a1c5122e04e9d0610b18e2ce82ec92e04d33482d8981964b33fa36fb61090e8915be3027a86608166997b9549a3
6
+ metadata.gz: da7d9c33b2275b7974b8fbe0aa8dabda4a675b72e589764afc4af0b19831d566066884368b61c0a18f72509b8b8f98bec87681568deaaa5791a692d323f3c5ac
7
+ data.tar.gz: c2ebbf1c4ddfc08b9d7fc708b604cf1a95c342f09433959ab4ea5018e0704c0f68bc81004244b24f233993161e6453607d655e88634cd42eea0ce37cc9e9f554
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.4
4
+ script: bundle exec rake spec
5
+ notifications:
6
+ email:
7
+ - ilya@igvita.com
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # EM-HTTP-Request
1
+ # EM-HTTP-Request
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/em-http-request.svg)](http://rubygems.org/gems/em-http-request) [![Build Status](https://travis-ci.org/igrigorik/em-http-request.svg)](https://travis-ci.org/igrigorik/em-http-request)
2
4
 
3
5
  Async (EventMachine) HTTP client, with support for:
4
6
 
@@ -10,7 +12,7 @@ Async (EventMachine) HTTP client, with support for:
10
12
  - Streaming file uploads
11
13
  - HTTP proxy and SOCKS5 support
12
14
  - Basic Auth & OAuth
13
- - Connection-level & Global middleware support
15
+ - Connection-level & global middleware support
14
16
  - HTTP parser via [http_parser.rb](https://github.com/tmm1/http_parser.rb)
15
17
  - Works wherever EventMachine runs: Rubinius, JRuby, MRI
16
18
 
@@ -18,7 +20,7 @@ Async (EventMachine) HTTP client, with support for:
18
20
 
19
21
  gem install em-http-request
20
22
 
21
- - Introductory [screencast](http://everburning.com/news/eventmachine-screencast-em-http-request/)
23
+ - Introductory [screencast](http://everburning.com/news/eventmachine-screencast-em-http-request)
22
24
  - [Issuing GET/POST/etc requests](https://github.com/igrigorik/em-http-request/wiki/Issuing-Requests)
23
25
  - [Issuing parallel requests with Multi interface](https://github.com/igrigorik/em-http-request/wiki/Parallel-Requests)
24
26
  - [Handling Redirects & Timeouts](https://github.com/igrigorik/em-http-request/wiki/Redirects-and-Timeouts)
@@ -54,7 +56,6 @@ Several higher-order Ruby projects have incorporated em-http and other Ruby HTTP
54
56
  - [Firering](https://github.com/EmmanuelOga/firering) - Eventmachine powered Campfire API
55
57
  - [RDaneel](https://github.com/hasmanydevelopers/RDaneel) - Ruby crawler which respects robots.txt
56
58
  - [em-eventsource](https://github.com/AF83/em-eventsource) - EventSource client for EventMachine
57
- - [sinatra-synchrony](https://github.com/kyledrake/sinatra-synchrony) - Sinatra plugin for synchronous use of EM
58
59
  - and many others.. drop me a link if you want yours included!
59
60
 
60
61
  ### License
@@ -12,17 +12,18 @@ Gem::Specification.new do |s|
12
12
  s.homepage = 'http://github.com/igrigorik/em-http-request'
13
13
  s.summary = 'EventMachine based, async HTTP Request client'
14
14
  s.description = s.summary
15
+ s.license = 'MIT'
15
16
  s.rubyforge_project = 'em-http-request'
16
17
 
17
18
  s.add_dependency 'addressable', '>= 2.3.4'
18
- s.add_dependency 'cookiejar'
19
+ s.add_dependency 'cookiejar', '!= 0.3.1'
19
20
  s.add_dependency 'em-socksify', '>= 0.3'
20
21
  s.add_dependency 'eventmachine', '>= 1.0.3'
21
- s.add_dependency 'http_parser.rb', '>= 0.6.0.beta.2'
22
+ s.add_dependency 'http_parser.rb', '>= 0.6.0'
22
23
 
23
24
  s.add_development_dependency 'mongrel', '~> 1.2.0.pre2'
24
25
  s.add_development_dependency 'multi_json'
25
- s.add_development_dependency 'rack'
26
+ s.add_development_dependency 'rack', '< 2.0'
26
27
  s.add_development_dependency 'rake'
27
28
  s.add_development_dependency 'rspec'
28
29
 
@@ -5,6 +5,7 @@ require 'http/parser'
5
5
 
6
6
  require 'base64'
7
7
  require 'socket'
8
+ require 'openssl'
8
9
 
9
10
  require 'em-http/core_ext/bytesize'
10
11
  require 'em-http/http_connection'
@@ -21,7 +21,7 @@ module EventMachine
21
21
 
22
22
  CRLF="\r\n"
23
23
 
24
- attr_accessor :state, :response
24
+ attr_accessor :state, :response, :conn
25
25
  attr_reader :response_header, :error, :content_charset, :req, :cookies
26
26
 
27
27
  def initialize(conn, options)
@@ -100,14 +100,13 @@ module EventMachine
100
100
 
101
101
  @cookies.clear
102
102
  @cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies
103
- @req.set_uri(@response_header.location)
104
103
 
105
- @conn.redirect(self)
104
+ @conn.redirect(self, @response_header.location)
106
105
  else
107
106
  succeed(self)
108
107
  end
109
108
 
110
- rescue Exception => e
109
+ rescue => e
111
110
  on_error(e.message)
112
111
  end
113
112
  else
@@ -156,11 +155,16 @@ module EventMachine
156
155
 
157
156
  # Set the User-Agent if it hasn't been specified
158
157
  if !head.key?('user-agent')
159
- head['user-agent'] = "EventMachine HttpClient"
158
+ head['user-agent'] = 'EventMachine HttpClient'
160
159
  elsif head['user-agent'].nil?
161
160
  head.delete('user-agent')
162
161
  end
163
162
 
163
+ # Set the Accept-Encoding header if none is provided
164
+ if !head.key?('accept-encoding') && req.compressed
165
+ head['accept-encoding'] = 'gzip, compressed'
166
+ end
167
+
164
168
  # Set the auth from the URI if given
165
169
  head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo
166
170
 
@@ -178,7 +182,7 @@ module EventMachine
178
182
  # Set the Content-Length if body is given,
179
183
  # or we're doing an empty post or put
180
184
  if body
181
- head['content-length'] = body.bytesize
185
+ head['content-length'] ||= body.respond_to?(:bytesize) ? body.bytesize : body.size
182
186
  elsif @req.method == 'POST' or @req.method == 'PUT'
183
187
  # wont happen if body is set and we already set content-length above
184
188
  head['content-length'] ||= 0
@@ -189,16 +193,13 @@ module EventMachine
189
193
  head['content-type'] = 'application/x-www-form-urlencoded'
190
194
  end
191
195
 
192
- request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts.proxy)
196
+ request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts)
193
197
  request_header << encode_headers(head)
194
198
  request_header << CRLF
195
199
  @conn.send_data request_header
196
200
 
197
- if body
198
- @conn.send_data body
199
- elsif @req.file
200
- @conn.stream_file_data @req.file, :http_chunks => false
201
- end
201
+ @req_body = body || (@req.file && Pathname.new(@req.file))
202
+ send_request_body unless @req.headers['expect'] == '100-continue'
202
203
  end
203
204
 
204
205
  def on_body_data(data)
@@ -222,6 +223,28 @@ module EventMachine
222
223
  end
223
224
  end
224
225
 
226
+ def request_body_pending?
227
+ !!@req_body
228
+ end
229
+
230
+ def send_request_body
231
+ return if @req_body.nil?
232
+
233
+ if @req_body.is_a?(String)
234
+ @conn.send_data @req_body
235
+
236
+ elsif @req_body.is_a?(Pathname)
237
+ @conn.stream_file_data @req_body.to_path, http_chunks: false
238
+
239
+ elsif @req_body.respond_to?(:read) && @req_body.respond_to?(:eof?) # IO or IO-like object
240
+ @conn.stream_data @req_body
241
+
242
+ else
243
+ raise "Don't know how to send request body: #{@req_body.inspect}"
244
+ end
245
+ @req_body = nil
246
+ end
247
+
225
248
  def parse_response_header(header, version, status)
226
249
  @response_header.raw = header
227
250
  header.each do |key, val|
@@ -127,7 +127,6 @@ module EventMachine::HttpDecoders
127
127
 
128
128
  def extract_stream(compressed)
129
129
  @data << compressed
130
- pos = @pos
131
130
 
132
131
  while !eof? && !finished?
133
132
  buffer = ""
@@ -208,7 +207,7 @@ module EventMachine::HttpDecoders
208
207
  end
209
208
 
210
209
  if finished?
211
- compressed[(@pos - pos)..-1]
210
+ compressed[(@pos - (@data.length - compressed.length))..-1]
212
211
  else
213
212
  ""
214
213
  end
@@ -1,7 +1,7 @@
1
1
  class HttpClientOptions
2
2
  attr_reader :uri, :method, :host, :port
3
3
  attr_reader :headers, :file, :body, :query, :path
4
- attr_reader :keepalive, :pass_cookies, :decoding
4
+ attr_reader :keepalive, :pass_cookies, :decoding, :compressed
5
5
 
6
6
  attr_accessor :followed, :redirects
7
7
 
@@ -12,40 +12,38 @@ class HttpClientOptions
12
12
 
13
13
  @method = method.to_s.upcase
14
14
  @headers = options[:head] || {}
15
- @query = options[:query]
16
-
17
15
 
18
16
  @file = options[:file]
19
17
  @body = options[:body]
20
18
 
21
19
  @pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects
22
20
  @decoding = options.fetch(:decoding, true) # auto-decode compressed response
21
+ @compressed = options.fetch(:compressed, true) # auto-negotiated compressed response
23
22
 
24
- set_uri(uri, options[:path])
23
+ set_uri(uri, options[:path], options[:query])
25
24
  end
26
25
 
27
26
  def follow_redirect?; @followed < @redirects; end
28
27
  def ssl?; @uri.scheme == "https" || @uri.port == 443; end
29
28
  def no_body?; @method == "HEAD"; end
30
29
 
31
- def set_uri(uri, path = nil)
30
+ def set_uri(uri, path = nil, query = nil)
32
31
  uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
33
32
  uri.path = path if path
34
33
  uri.path = '/' if uri.path.empty?
35
34
 
36
35
  @uri = uri
37
36
  @path = uri.path
37
+ @host = uri.hostname
38
+ @port = uri.port
39
+ @query = query
38
40
 
39
41
  # Make sure the ports are set as Addressable::URI doesn't
40
42
  # set the port if it isn't there
41
- if @uri.scheme == "https"
42
- @uri.port ||= 443
43
- else
44
- @uri.port ||= 80
43
+ if @port.nil?
44
+ @port = @uri.scheme == "https" ? 443 : 80
45
45
  end
46
46
 
47
- @host = @uri.host
48
- @port = @uri.port
49
-
47
+ uri
50
48
  end
51
49
  end
@@ -1,3 +1,5 @@
1
+ require 'em/io_streamer'
2
+
1
3
  module EventMachine
2
4
 
3
5
  module HTTPMethods
@@ -20,7 +22,11 @@ module EventMachine
20
22
  end
21
23
 
22
24
  def receive_data(data)
23
- @parent.receive_data data
25
+ begin
26
+ @parent.receive_data data
27
+ rescue EventMachine::Connectify::CONNECTError => e
28
+ @parent.close(e.message)
29
+ end
24
30
  end
25
31
 
26
32
  def connection_completed
@@ -30,6 +36,62 @@ module EventMachine
30
36
  def unbind(reason=nil)
31
37
  @parent.unbind(reason)
32
38
  end
39
+
40
+ # TLS verification support, original implementation by Mislav Marohnić
41
+ # https://github.com/lostisland/faraday/blob/63cf47c95b573539f047c729bd9ad67560bc83ff/lib/faraday/adapter/em_http_ssl_patch.rb
42
+ def ssl_verify_peer(cert_string)
43
+ cert = nil
44
+ begin
45
+ cert = OpenSSL::X509::Certificate.new(cert_string)
46
+ rescue OpenSSL::X509::CertificateError
47
+ return false
48
+ end
49
+
50
+ @last_seen_cert = cert
51
+
52
+ if certificate_store.verify(@last_seen_cert)
53
+ begin
54
+ certificate_store.add_cert(@last_seen_cert)
55
+ rescue OpenSSL::X509::StoreError => e
56
+ raise e unless e.message == 'cert already in hash table'
57
+ end
58
+ true
59
+ else
60
+ raise OpenSSL::SSL::SSLError.new(%(unable to verify the server certificate for "#{host}"))
61
+ end
62
+ end
63
+
64
+ def ssl_handshake_completed
65
+ unless verify_peer?
66
+ warn "[WARNING; em-http-request] TLS hostname validation is disabled (use 'tls: {verify_peer: true}'), see" +
67
+ " CVE-2020-13482 and https://github.com/igrigorik/em-http-request/issues/339 for details"
68
+ return true
69
+ end
70
+
71
+ unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, host)
72
+ raise OpenSSL::SSL::SSLError.new(%(host "#{host}" does not match the server certificate))
73
+ else
74
+ true
75
+ end
76
+ end
77
+
78
+ def verify_peer?
79
+ parent.connopts.tls[:verify_peer]
80
+ end
81
+
82
+ def host
83
+ parent.connopts.host
84
+ end
85
+
86
+ def certificate_store
87
+ @certificate_store ||= begin
88
+ store = OpenSSL::X509::Store.new
89
+ store.set_default_paths
90
+ ca_file = parent.connopts.tls[:cert_chain_file]
91
+ store.add_file(ca_file) if ca_file
92
+ store
93
+ end
94
+ end
33
95
  end
34
96
 
35
97
  class HttpConnection
@@ -37,8 +99,8 @@ module EventMachine
37
99
  include Socksify
38
100
  include Connectify
39
101
 
40
- attr_reader :deferred
41
- attr_accessor :error, :connopts, :uri, :conn
102
+ attr_reader :deferred, :conn
103
+ attr_accessor :error, :connopts, :uri
42
104
 
43
105
  def initialize
44
106
  @deferred = true
@@ -97,7 +159,7 @@ module EventMachine
97
159
  @conn.callback { c.connection_completed }
98
160
 
99
161
  middleware.each do |m|
100
- c.callback &m.method(:response) if m.respond_to?(:response)
162
+ c.callback(&m.method(:response)) if m.respond_to?(:response)
101
163
  end
102
164
 
103
165
  @clients.push c
@@ -114,8 +176,21 @@ module EventMachine
114
176
  @p = Http::Parser.new
115
177
  @p.header_value_type = :mixed
116
178
  @p.on_headers_complete = proc do |h|
117
- client.parse_response_header(h, @p.http_version, @p.status_code)
118
- :reset if client.req.no_body?
179
+ if client
180
+ if @p.status_code == 100
181
+ client.send_request_body
182
+ @p.reset!
183
+ else
184
+ client.parse_response_header(h, @p.http_version, @p.status_code)
185
+ :reset if client.req.no_body?
186
+ end
187
+ else
188
+ # if we receive unexpected data without a pending client request
189
+ # reset the parser to avoid firing any further callbacks and close
190
+ # the connection because we're processing invalid HTTP
191
+ @p.reset!
192
+ unbind
193
+ end
119
194
  end
120
195
 
121
196
  @p.on_body = proc do |b|
@@ -152,9 +227,9 @@ module EventMachine
152
227
  @peer = @conn.get_peername
153
228
 
154
229
  if @connopts.socks_proxy?
155
- socksify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start }
230
+ socksify(client.req.uri.hostname, client.req.uri.inferred_port, *@connopts.proxy[:authorization]) { start }
156
231
  elsif @connopts.connect_proxy?
157
- connectify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start }
232
+ connectify(client.req.uri.hostname, client.req.uri.inferred_port, *@connopts.proxy[:authorization]) { start }
158
233
  else
159
234
  start
160
235
  end
@@ -165,8 +240,33 @@ module EventMachine
165
240
  @conn.succeed
166
241
  end
167
242
 
168
- def redirect(client)
169
- @pending.push client
243
+ def redirect(client, new_location)
244
+ old_location = client.req.uri
245
+ new_location = client.req.set_uri(new_location)
246
+
247
+ if client.req.keepalive
248
+ # Application requested a keep-alive connection but one of the requests
249
+ # hits a cross-origin redirect. We need to open a new connection and
250
+ # let both connections proceed simultaneously.
251
+ if old_location.origin != new_location.origin
252
+ conn = HttpConnection.new
253
+ client.conn = conn
254
+ conn.connopts = @connopts
255
+ conn.connopts.https = new_location.scheme == "https"
256
+ conn.uri = client.req.uri
257
+ conn.activate_connection(client)
258
+
259
+ # If the redirect is a same-origin redirect on a keep-alive request
260
+ # then immidiately dispatch the request over existing connection.
261
+ else
262
+ @clients.push client
263
+ client.connection_completed
264
+ end
265
+ else
266
+ # If connection is not keep-alive the unbind will fire and we'll
267
+ # reconnect using the same connection object.
268
+ @pending.push client
269
+ end
170
270
  end
171
271
 
172
272
  def unbind(reason = nil)
@@ -208,6 +308,10 @@ module EventMachine
208
308
  @conn.stream_file_data filename, args
209
309
  end
210
310
 
311
+ def stream_data(io, opts = {})
312
+ EventMachine::IOStreamer.new(self, io, opts)
313
+ end
314
+
211
315
  private
212
316
 
213
317
  def client
@@ -1,13 +1,13 @@
1
1
  class HttpConnectionOptions
2
2
  attr_reader :host, :port, :tls, :proxy, :bind, :bind_port
3
3
  attr_reader :connect_timeout, :inactivity_timeout
4
+ attr_writer :https
4
5
 
5
6
  def initialize(uri, options)
6
7
  @connect_timeout = options[:connect_timeout] || 5 # default connection setup timeout
7
8
  @inactivity_timeout = options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout
8
9
 
9
10
  @tls = options[:tls] || options[:ssl] || {}
10
- @proxy = options[:proxy]
11
11
 
12
12
  if bind = options[:bind]
13
13
  @bind = bind[:host] || '0.0.0.0'
@@ -20,12 +20,15 @@ class HttpConnectionOptions
20
20
  uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
21
21
  @https = uri.scheme == "https"
22
22
  uri.port ||= (@https ? 443 : 80)
23
+ @tls[:sni_hostname] = uri.hostname
23
24
 
24
- if proxy = options[:proxy]
25
+ @proxy = options[:proxy] || proxy_from_env
26
+
27
+ if proxy
25
28
  @host = proxy[:host]
26
29
  @port = proxy[:port]
27
30
  else
28
- @host = uri.host
31
+ @host = uri.hostname
29
32
  @port = uri.port
30
33
  end
31
34
  end
@@ -41,4 +44,27 @@ class HttpConnectionOptions
41
44
  def socks_proxy?
42
45
  @proxy && (@proxy[:type] == :socks5)
43
46
  end
47
+
48
+ def proxy_from_env
49
+ # TODO: Add support for $http_no_proxy or $no_proxy ?
50
+ proxy_str = if @https
51
+ ENV['HTTPS_PROXY'] || ENV['https_proxy']
52
+ else
53
+ ENV['HTTP_PROXY'] || ENV['http_proxy']
54
+
55
+ # Fall-back to $ALL_PROXY if none of the above env-vars have values
56
+ end || ENV['ALL_PROXY']
57
+
58
+ # Addressable::URI::parse will return `nil` if given `nil` and an empty URL for an empty string
59
+ # so, let's short-circuit that:
60
+ return if !proxy_str || proxy_str.empty?
61
+
62
+ proxy_env_uri = Addressable::URI::parse(proxy_str)
63
+ { :host => proxy_env_uri.host, :port => proxy_env_uri.port, :type => :http }
64
+
65
+ rescue Addressable::URI::InvalidURIError
66
+ # An invalid env-var shouldn't crash the config step, IMHO.
67
+ # We should somehow log / warn about this, perhaps...
68
+ return
69
+ end
44
70
  end