em-http-request 1.1.1 → 1.1.6

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