em-http-request-samesite 1.1.7

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +0 -0
  5. data/.travis.yml +7 -0
  6. data/Changelog.md +68 -0
  7. data/Gemfile +14 -0
  8. data/README.md +63 -0
  9. data/Rakefile +10 -0
  10. data/benchmarks/clients.rb +170 -0
  11. data/benchmarks/em-excon.rb +87 -0
  12. data/benchmarks/em-profile.gif +0 -0
  13. data/benchmarks/em-profile.txt +65 -0
  14. data/benchmarks/server.rb +48 -0
  15. data/em-http-request.gemspec +32 -0
  16. data/examples/.gitignore +1 -0
  17. data/examples/digest_auth/client.rb +25 -0
  18. data/examples/digest_auth/server.rb +28 -0
  19. data/examples/fetch.rb +30 -0
  20. data/examples/fibered-http.rb +51 -0
  21. data/examples/multi.rb +25 -0
  22. data/examples/oauth-tweet.rb +35 -0
  23. data/examples/socks5.rb +23 -0
  24. data/lib/em-http-request.rb +1 -0
  25. data/lib/em-http.rb +20 -0
  26. data/lib/em-http/client.rb +341 -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 +49 -0
  30. data/lib/em-http/http_connection.rb +321 -0
  31. data/lib/em-http/http_connection_options.rb +70 -0
  32. data/lib/em-http/http_encoding.rb +149 -0
  33. data/lib/em-http/http_header.rb +83 -0
  34. data/lib/em-http/http_status_codes.rb +57 -0
  35. data/lib/em-http/middleware/digest_auth.rb +112 -0
  36. data/lib/em-http/middleware/json_response.rb +15 -0
  37. data/lib/em-http/middleware/oauth.rb +40 -0
  38. data/lib/em-http/middleware/oauth2.rb +28 -0
  39. data/lib/em-http/multi.rb +57 -0
  40. data/lib/em-http/request.rb +23 -0
  41. data/lib/em-http/version.rb +5 -0
  42. data/lib/em/io_streamer.rb +49 -0
  43. data/spec/client_fiber_spec.rb +23 -0
  44. data/spec/client_spec.rb +1000 -0
  45. data/spec/digest_auth_spec.rb +48 -0
  46. data/spec/dns_spec.rb +41 -0
  47. data/spec/encoding_spec.rb +49 -0
  48. data/spec/external_spec.rb +150 -0
  49. data/spec/fixtures/google.ca +16 -0
  50. data/spec/fixtures/gzip-sample.gz +0 -0
  51. data/spec/gzip_spec.rb +91 -0
  52. data/spec/helper.rb +31 -0
  53. data/spec/http_proxy_spec.rb +268 -0
  54. data/spec/middleware/oauth2_spec.rb +15 -0
  55. data/spec/middleware_spec.rb +143 -0
  56. data/spec/multi_spec.rb +104 -0
  57. data/spec/pipelining_spec.rb +66 -0
  58. data/spec/redirect_spec.rb +430 -0
  59. data/spec/socksify_proxy_spec.rb +60 -0
  60. data/spec/spec_helper.rb +25 -0
  61. data/spec/ssl_spec.rb +71 -0
  62. data/spec/stallion.rb +334 -0
  63. data/spec/stub_server.rb +45 -0
  64. metadata +265 -0
@@ -0,0 +1,6 @@
1
+ # bytesize was introduced in 1.8.7+
2
+ if RUBY_VERSION <= "1.8.6"
3
+ class String
4
+ def bytesize; self.size; end
5
+ end
6
+ end
@@ -0,0 +1,252 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+
4
+ ##
5
+ # Provides a unified callback interface to decompression libraries.
6
+ module EventMachine::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,49 @@
1
+ class HttpClientOptions
2
+ attr_reader :uri, :method, :host, :port
3
+ attr_reader :headers, :file, :body, :query, :path
4
+ attr_reader :keepalive, :pass_cookies, :decoding, :compressed
5
+
6
+ attr_accessor :followed, :redirects
7
+
8
+ def initialize(uri, options, method)
9
+ @keepalive = options[:keepalive] || false # default to single request per connection
10
+ @redirects = options[:redirects] ||= 0 # default number of redirects to follow
11
+ @followed = options[:followed] ||= 0 # keep track of number of followed requests
12
+
13
+ @method = method.to_s.upcase
14
+ @headers = options[:head] || {}
15
+
16
+ @file = options[:file]
17
+ @body = options[:body]
18
+
19
+ @pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects
20
+ @decoding = options.fetch(:decoding, true) # auto-decode compressed response
21
+ @compressed = options.fetch(:compressed, true) # auto-negotiated compressed response
22
+
23
+ set_uri(uri, options[:path], options[:query])
24
+ end
25
+
26
+ def follow_redirect?; @followed < @redirects; end
27
+ def ssl?; @uri.scheme == "https" || @uri.port == 443; end
28
+ def no_body?; @method == "HEAD"; end
29
+
30
+ def set_uri(uri, path = nil, query = nil)
31
+ uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
32
+ uri.path = path if path
33
+ uri.path = '/' if uri.path.empty?
34
+
35
+ @uri = uri
36
+ @path = uri.path
37
+ @host = uri.hostname
38
+ @port = uri.port
39
+ @query = query
40
+
41
+ # Make sure the ports are set as Addressable::URI doesn't
42
+ # set the port if it isn't there
43
+ if @port.nil?
44
+ @port = @uri.scheme == "https" ? 443 : 80
45
+ end
46
+
47
+ uri
48
+ end
49
+ end
@@ -0,0 +1,321 @@
1
+ require 'em/io_streamer'
2
+
3
+ module EventMachine
4
+
5
+ module HTTPMethods
6
+ def get options = {}, &blk; setup_request(:get, options, &blk); end
7
+ def head options = {}, &blk; setup_request(:head, options, &blk); end
8
+ def delete options = {}, &blk; setup_request(:delete, options, &blk); end
9
+ def put options = {}, &blk; setup_request(:put, options, &blk); end
10
+ def post options = {}, &blk; setup_request(:post, options, &blk); end
11
+ def patch options = {}, &blk; setup_request(:patch, options, &blk); end
12
+ def options options = {}, &blk; setup_request(:options, options, &blk); end
13
+ end
14
+
15
+ class HttpStubConnection < Connection
16
+ include Deferrable
17
+ attr_reader :parent
18
+
19
+ def parent=(p)
20
+ @parent = p
21
+ @parent.conn = self
22
+ end
23
+
24
+ def receive_data(data)
25
+ begin
26
+ @parent.receive_data data
27
+ rescue EventMachine::Connectify::CONNECTError => e
28
+ @parent.close(e.message)
29
+ end
30
+ end
31
+
32
+ def connection_completed
33
+ @parent.connection_completed
34
+ end
35
+
36
+ def unbind(reason=nil)
37
+ @parent.unbind(reason)
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" unless parent.connopts.tls.has_key?(:verify_peer)
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
95
+ end
96
+
97
+ class HttpConnection
98
+ include HTTPMethods
99
+ include Socksify
100
+ include Connectify
101
+
102
+ attr_reader :deferred, :conn
103
+ attr_accessor :error, :connopts, :uri
104
+
105
+ def initialize
106
+ @deferred = true
107
+ @middleware = []
108
+ end
109
+
110
+ def conn=(c)
111
+ @conn = c
112
+ @deferred = false
113
+ end
114
+
115
+ def activate_connection(client)
116
+ begin
117
+ EventMachine.bind_connect(@connopts.bind, @connopts.bind_port,
118
+ @connopts.host, @connopts.port,
119
+ HttpStubConnection) do |conn|
120
+ post_init
121
+
122
+ @deferred = false
123
+ @conn = conn
124
+
125
+ conn.parent = self
126
+ conn.pending_connect_timeout = @connopts.connect_timeout
127
+ conn.comm_inactivity_timeout = @connopts.inactivity_timeout
128
+ end
129
+
130
+ finalize_request(client)
131
+ rescue EventMachine::ConnectionError => e
132
+ #
133
+ # Currently, this can only fire on initial connection setup
134
+ # since #connect is a synchronous method. Hence, rescue the exception,
135
+ # and return a failed deferred which fail any client request at next
136
+ # tick. We fail at next tick to keep a consistent API when the newly
137
+ # created HttpClient is failed. This approach has the advantage to
138
+ # remove a state check of @deferred_status after creating a new
139
+ # HttpRequest. The drawback is that users may setup a callback which we
140
+ # know won't be used.
141
+ #
142
+ # Once there is async-DNS, then we'll iterate over the outstanding
143
+ # client requests and fail them in order.
144
+ #
145
+ # Net outcome: failed connection will invoke the same ConnectionError
146
+ # message on the connection deferred, and on the client deferred.
147
+ #
148
+ EM.next_tick{client.close(e.message)}
149
+ end
150
+ end
151
+
152
+ def setup_request(method, options = {}, c = nil)
153
+ c ||= HttpClient.new(self, HttpClientOptions.new(@uri, options, method))
154
+ @deferred ? activate_connection(c) : finalize_request(c)
155
+ c
156
+ end
157
+
158
+ def finalize_request(c)
159
+ @conn.callback { c.connection_completed }
160
+
161
+ middleware.each do |m|
162
+ c.callback(&m.method(:response)) if m.respond_to?(:response)
163
+ end
164
+
165
+ @clients.push c
166
+ end
167
+
168
+ def middleware
169
+ [HttpRequest.middleware, @middleware].flatten
170
+ end
171
+
172
+ def post_init
173
+ @clients = []
174
+ @pending = []
175
+
176
+ @p = Http::Parser.new
177
+ @p.header_value_type = :mixed
178
+ @p.on_headers_complete = proc do |h|
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
194
+ end
195
+
196
+ @p.on_body = proc do |b|
197
+ client.on_body_data(b)
198
+ end
199
+
200
+ @p.on_message_complete = proc do
201
+ if !client.continue?
202
+ c = @clients.shift
203
+ c.state = :finished
204
+ c.on_request_complete
205
+ end
206
+ end
207
+ end
208
+
209
+ def use(klass, *args, &block)
210
+ @middleware << klass.new(*args, &block)
211
+ end
212
+
213
+ def peer
214
+ Socket.unpack_sockaddr_in(@peer)[1] rescue nil
215
+ end
216
+
217
+ def receive_data(data)
218
+ begin
219
+ @p << data
220
+ rescue HTTP::Parser::Error => e
221
+ c = @clients.shift
222
+ c.nil? ? unbind(e.message) : c.on_error(e.message)
223
+ end
224
+ end
225
+
226
+ def connection_completed
227
+ @peer = @conn.get_peername
228
+
229
+ if @connopts.socks_proxy?
230
+ socksify(client.req.uri.hostname, client.req.uri.inferred_port, *@connopts.proxy[:authorization]) { start }
231
+ elsif @connopts.connect_proxy?
232
+ connectify(client.req.uri.hostname, client.req.uri.inferred_port, *@connopts.proxy[:authorization]) { start }
233
+ else
234
+ start
235
+ end
236
+ end
237
+
238
+ def start
239
+ @conn.start_tls(@connopts.tls) if client && client.req.ssl?
240
+ @conn.succeed
241
+ end
242
+
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
270
+ end
271
+
272
+ def unbind(reason = nil)
273
+ @clients.map { |c| c.unbind(reason) }
274
+
275
+ if r = @pending.shift
276
+ @clients.push r
277
+
278
+ r.reset!
279
+ @p.reset!
280
+
281
+ begin
282
+ @conn.set_deferred_status :unknown
283
+
284
+ if @connopts.proxy
285
+ @conn.reconnect(@connopts.host, @connopts.port)
286
+ else
287
+ @conn.reconnect(r.req.host, r.req.port)
288
+ end
289
+
290
+ @conn.pending_connect_timeout = @connopts.connect_timeout
291
+ @conn.comm_inactivity_timeout = @connopts.inactivity_timeout
292
+ @conn.callback { r.connection_completed }
293
+ rescue EventMachine::ConnectionError => e
294
+ @clients.pop.close(e.message)
295
+ end
296
+ else
297
+ @deferred = true
298
+ @conn.close_connection
299
+ end
300
+ end
301
+ alias :close :unbind
302
+
303
+ def send_data(data)
304
+ @conn.send_data data
305
+ end
306
+
307
+ def stream_file_data(filename, args = {})
308
+ @conn.stream_file_data filename, args
309
+ end
310
+
311
+ def stream_data(io, opts = {})
312
+ EventMachine::IOStreamer.new(self, io, opts)
313
+ end
314
+
315
+ private
316
+
317
+ def client
318
+ @clients.first
319
+ end
320
+ end
321
+ end