em-http-request-samesite 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
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