ethon 0.14.0 → 0.16.0

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
2
  SHA256:
3
- metadata.gz: 87e1825a32b82c558aded416768e7dffc8a1be6918bca888187b343e17159432
4
- data.tar.gz: 4773483d3d4e5bf46298d55c4d4967622666bf28f6665445d621ab2b4de30213
3
+ metadata.gz: 30f75feeae963db1b4648a0b88e028361c78fda7aec43524a35f93b287ab2305
4
+ data.tar.gz: eb0ccd002c324dedaaceaa001172a61c520cd58e34d1a92440182faada17df74
5
5
  SHA512:
6
- metadata.gz: bc51f162c86e89ddc1176368a508870e0be217a11027ae17d93149cbc04ae822d2d03ce6e2fa0e15269bbafefbda9e2dbfd1e64aba0b4e3fddbf820b958a3b82
7
- data.tar.gz: 8c755c396544992d6062fe28f3a7a741c3269941665e89995a5f5060d08b4ed8d4f5768bc0601efa7780b1407c49d1839e4557f3ac591abc0529438ad8506163
6
+ metadata.gz: b6ec09378cd37ec552caee9a9153fb9e0582a770b18da882ec27f59028799885e1a9d9d7c496f1cfb4803ba3eb14bdd5e508f930cdc9523d16747d2a87ba1dfb
7
+ data.tar.gz: 77a50827108e8c6bc44f293bae5c2bfe6e871c94f40f93904ff611245143ef7900cf4cc2aa8a1511d0797dd80c03578e06b4e256f929deab544c73f9ef0d9e33
@@ -20,7 +20,7 @@ jobs:
20
20
  fail-fast: false
21
21
  matrix:
22
22
  os: [ubuntu, macos]
23
- ruby-version: [2.5, 2.6, 2.7, 3.0, head, debug, truffleruby, truffleruby-head]
23
+ ruby-version: [2.5, 2.6, 2.7, 3.0, head, debug, truffleruby]
24
24
  continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
25
25
  steps:
26
26
  - uses: actions/checkout@v2
@@ -30,7 +30,7 @@ jobs:
30
30
  then
31
31
  brew install curl
32
32
  else
33
- sudo apt install -y libcurl4-openssl-dev
33
+ sudo apt update && sudo apt install -y --no-install-recommends libcurl4-openssl-dev
34
34
  fi
35
35
  - name: Set up Ruby
36
36
  uses: ruby/setup-ruby@v1
data/CHANGELOG.md CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  ## Master
4
4
 
5
- [Full Changelog](https://github.com/typhoeus/ethon/compare/v0.12.0...master)
5
+ [Full Changelog](https://github.com/typhoeus/ethon/compare/v0.15.0...master)
6
+
7
+ * Added `redirect_url` value to available informations and `Easy::Mirror`.
8
+ ([Adrien Rey-Jarthon](https://github.com/jarthod)
9
+
10
+ ## 0.15.0
11
+
12
+ [Full Changelog](https://github.com/typhoeus/ethon/compare/v0.14.0...v0.15.0)
6
13
 
7
14
  ## 0.12.0
8
15
 
data/README.md CHANGED
@@ -70,6 +70,29 @@ easy.perform
70
70
  This is really handy when making requests since you don't have to care about setting
71
71
  everything up correctly.
72
72
 
73
+ ## Http2
74
+ Standard http2 servers require the client to connect once and create a session (multi) and then add simple requests to the multi handler.
75
+ The `perform` method then takes all the requests in the multi handler and sends them to the server.
76
+
77
+ See the following example
78
+ ```ruby
79
+ multi = Ethon::Multi.new
80
+ easy = Ethon::Easy.new
81
+
82
+ easy.http_request("www.example.com/get", :get, { http_version: :httpv2_0 })
83
+
84
+ # Sending a request with http version 2 will send an Upgrade header to the server, which many older servers will not support
85
+ # See below for more info: https://everything.curl.dev/http/http2
86
+ # If this is a problem, send the below:
87
+ easy.http_request("www.example.com/get", :get, { http_version: :httpv2_prior_knowledge })
88
+
89
+ # To set the server to use http2 with https and http1 with http, send the following:
90
+ easy.http_request("www.example.com/get", :get, { http_version: :httpv2_tls }
91
+
92
+ multi.add(easy)
93
+ multi.perform
94
+ ```
95
+
73
96
  ## LICENSE
74
97
 
75
98
  (The MIT License)
@@ -32,8 +32,18 @@ module Ethon
32
32
 
33
33
  def clear; self[:fd_count] = 0; end
34
34
  else
35
- # FD Set size.
36
- FD_SETSIZE = ::Ethon::Libc.getdtablesize
35
+ # https://github.com/typhoeus/ethon/issues/182
36
+ FD_SETSIZE = begin
37
+ # Allow to override the (new) default cap
38
+ if ENV['ETHON_FD_SIZE']
39
+ ENV['ETHON_FD_SIZE']
40
+
41
+ # auto-detect ulimit, but cap at 2^16
42
+ else
43
+ [::Ethon::Libc.getdtablesize, 65_536].min
44
+ end
45
+ end
46
+
37
47
  layout :fds_bits, [:long, FD_SETSIZE / ::FFI::Type::LONG.size]
38
48
 
39
49
  # :nodoc:
@@ -59,5 +59,22 @@ module Ethon
59
59
  VERSION_NTLM_WB = (1<<15) # NTLM delegating to winbind helper
60
60
  VERSION_HTTP2 = (1<<16) # HTTP2 support built
61
61
  VERSION_GSSAPI = (1<<17) # GSS-API is supported
62
+
63
+ SOCKET_BAD = -1
64
+ SOCKET_TIMEOUT = SOCKET_BAD
65
+
66
+ PollAction = enum(:poll_action, [
67
+ :none,
68
+ :in,
69
+ :out,
70
+ :inout,
71
+ :remove
72
+ ])
73
+
74
+ SocketReadiness = bitmask(:socket_readiness, [
75
+ :in, # CURL_CSELECT_IN - 0x01 (bit 0)
76
+ :out, # CURL_CSELECT_OUT - 0x02 (bit 1)
77
+ :err, # CURL_CSELECT_ERR - 0x04 (bit 2)
78
+ ])
62
79
  end
63
80
  end
@@ -36,6 +36,7 @@ module Ethon
36
36
  base.attach_function :multi_fdset, :curl_multi_fdset, [:pointer, Curl::FDSet.ptr, Curl::FDSet.ptr, Curl::FDSet.ptr, :pointer], :multi_code
37
37
  base.attach_function :multi_strerror, :curl_multi_strerror, [:int], :string
38
38
  base.attach_function :multi_setopt, :curl_multi_setopt, [:pointer, :multi_option, :varargs], :multi_code
39
+ base.attach_function :multi_socket_action, :curl_multi_socket_action, [:pointer, :int, :socket_readiness, :pointer], :multi_code
39
40
 
40
41
  base.attach_function :version, :curl_version, [], :string
41
42
  base.attach_function :version_info, :curl_version_info, [], Curl::VersionInfoData.ptr
@@ -82,6 +82,12 @@ module Ethon
82
82
  when :callback
83
83
  va_type=:callback
84
84
  raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
85
+ when :socket_callback
86
+ va_type=:socket_callback
87
+ raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
88
+ when :timer_callback
89
+ va_type=:timer_callback
90
+ raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
85
91
  when :debug_callback
86
92
  va_type=:debug_callback
87
93
  raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
@@ -137,6 +143,8 @@ module Ethon
137
143
  :dontuse_object => :objectpoint, # An object we don't support (e.g. FILE*)
138
144
  :cbdata => :objectpoint,
139
145
  :callback => :functionpoint,
146
+ :socket_callback => :functionpoint,
147
+ :timer_callback => :functionpoint,
140
148
  :debug_callback => :functionpoint,
141
149
  :progress_callback => :functionpoint,
142
150
  :off_t => :off_t,
@@ -208,10 +216,10 @@ module Ethon
208
216
  # Documentation @ http://curl.haxx.se/libcurl/c/curl_multi_setopt.html
209
217
  option_type :multi
210
218
 
211
- option :multi, :socketfunction, :callback, 1
219
+ option :multi, :socketfunction, :socket_callback, 1
212
220
  option :multi, :socketdata, :cbdata, 2
213
221
  option :multi, :pipelining, :int, 3
214
- option :multi, :timerfunction, :callback, 4
222
+ option :multi, :timerfunction, :timer_callback, 4
215
223
  option :multi, :timerdata, :cbdata, 5
216
224
  option :multi, :maxconnects, :int, 6
217
225
  option :multi, :max_host_connections, :int, 7
@@ -299,6 +307,7 @@ module Ethon
299
307
  option :easy, :port, :int, 3
300
308
  option :easy, :tcp_nodelay, :bool, 121
301
309
  option :easy, :address_scope, :int, 171
310
+ option :easy, :tcp_fastopen, :bool, 212
302
311
  option :easy, :tcp_keepalive, :bool, 213
303
312
  option :easy, :tcp_keepidle, :int, 214
304
313
  option :easy, :tcp_keepintvl, :int, 215
@@ -344,7 +353,7 @@ module Ethon
344
353
  option :easy, :cookiesession, :bool, 96
345
354
  option :easy, :cookielist, :string, 135
346
355
  option :easy, :httpget, :bool, 80
347
- option :easy, :http_version, :enum, 84, [:none, :httpv1_0, :httpv1_1, :httpv2_0]
356
+ option :easy, :http_version, :enum, 84, [:none, :httpv1_0, :httpv1_1, :httpv2_0, :httpv2_tls, :httpv2_prior_knowledge]
348
357
  option :easy, :ignore_content_length, :bool, 136
349
358
  option :easy, :http_content_decoding, :bool, 158
350
359
  option :easy, :http_transfer_decoding, :bool, 157
@@ -432,7 +441,7 @@ module Ethon
432
441
  option_alias :easy, :keypasswd, :sslkeypasswd
433
442
  option :easy, :sslengine, :string, 89
434
443
  option :easy, :sslengine_default, :none, 90
435
- option :easy, :sslversion, :enum, 32, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2]
444
+ option :easy, :sslversion, :enum, 32, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2, :tlsv1_3]
436
445
  option :easy, :ssl_verifypeer, :bool, 64
437
446
  option :easy, :cainfo, :string, 65
438
447
  option :easy, :issuercert, :string, 170
@@ -450,6 +459,29 @@ module Ethon
450
459
  option :easy, :gssapi_delegation, :bitmask, 210, [:none, :policy_flag, :flag]
451
460
  option :easy, :pinnedpublickey, :string, 230
452
461
  option_alias :easy, :pinnedpublickey, :pinned_public_key
462
+ ## PROXY SSL OPTIONS
463
+ option :easy, :proxy_cainfo, :string, 246
464
+ option :easy, :proxy_capath, :string, 247
465
+ option :easy, :proxy_ssl_verifypeer, :bool, 248
466
+ option :easy, :proxy_ssl_verifyhost, :int, 249
467
+ option :easy, :proxy_sslversion, :enum, 250, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2, :tlsv1_3]
468
+ option :easy, :proxy_tlsauth_username, :string, 251
469
+ option :easy, :proxy_tlsauth_password, :string, 252
470
+ option :easy, :proxy_tlsauth_type, :enum, 253, [:none, :srp]
471
+ option :easy, :proxy_sslcert, :string, 254
472
+ option :easy, :proxy_sslcerttype, :string, 255
473
+ option :easy, :proxy_sslkey, :string, 256
474
+ option :easy, :proxy_sslkeytype, :string, 257
475
+ option :easy, :proxy_keypasswd, :string, 258
476
+ option_alias :easy, :proxy_keypasswd, :proxy_sslcertpasswd
477
+ option_alias :easy, :proxy_keypasswd, :proxy_sslkeypasswd
478
+ option :easy, :proxy_ssl_cipher_list, :string, 259
479
+ option :easy, :proxy_crlfile, :string, 260
480
+ option :easy, :proxy_ssl_options, :bitmask, 261, [nil, :allow_beast]
481
+ option :easy, :pre_proxy, :string, 262
482
+ option :easy, :proxy_pinnedpublickey, :string, 263
483
+ option_alias :easy, :proxy_pinnedpublickey, :proxy_pinned_public_key
484
+ option :easy, :proxy_issuercert, :string, 296
453
485
  ## SSH OPTIONS
454
486
  option :easy, :ssh_auth_types, :bitmask, 151, [:none, :publickey, :password, :host, :keyboard, :agent, {:any => [:all], :default => [:any]}]
455
487
  option :easy, :ssh_host_public_key_md5, :string, 162
@@ -2,6 +2,8 @@
2
2
  module Ethon
3
3
  module Curl
4
4
  callback :callback, [:pointer, :size_t, :size_t, :pointer], :size_t
5
+ callback :socket_callback, [:pointer, :int, :poll_action, :pointer, :pointer], :multi_code
6
+ callback :timer_callback, [:pointer, :long, :pointer], :multi_code
5
7
  callback :debug_callback, [:pointer, :debug_info_type, :pointer, :size_t, :pointer], :int
6
8
  callback :progress_callback, [:pointer, :long_long, :long_long, :long_long, :long_long], :int
7
9
  ffi_lib_flags :now, :global
@@ -52,8 +52,9 @@ module Ethon
52
52
  # @return [ Proc ] The callback.
53
53
  def header_write_callback
54
54
  @header_write_callback ||= proc {|stream, size, num, object|
55
+ result = headers
55
56
  @response_headers << stream.read_string(size * num)
56
- size * num
57
+ result != :abort ? size * num : -1
57
58
  }
58
59
  end
59
60
 
@@ -71,7 +71,28 @@ module Ethon
71
71
 
72
72
  # Return the total number of redirections that were
73
73
  # actually followed.
74
- :redirect_count => :long
74
+ :redirect_count => :long,
75
+
76
+ # URL a redirect would take you to, had you enabled redirects (Added in 7.18.2)
77
+ :redirect_url => :string,
78
+
79
+ # Return the bytes, the total amount of bytes that were uploaded
80
+ :size_upload => :double,
81
+
82
+ # Return the bytes, the total amount of bytes that were downloaded.
83
+ # The amount is only for the latest transfer and will be reset again
84
+ # for each new transfer. This counts actual payload data, what's
85
+ # also commonly called body. All meta and header data are excluded
86
+ # and will not be counted in this number.
87
+ :size_download => :double,
88
+
89
+ # Return the bytes/second, the average upload speed that curl
90
+ # measured for the complete upload
91
+ :speed_upload => :double,
92
+
93
+ # Return the bytes/second, the average download speed that curl
94
+ # measured for the complete download
95
+ :speed_download => :double
75
96
  }
76
97
 
77
98
  AVAILABLE_INFORMATIONS.each do |name, type|
@@ -4,20 +4,6 @@ module Ethon
4
4
  # This module contains the logic to prepare and perform
5
5
  # an easy.
6
6
  module Operations
7
-
8
- class PointerHelper
9
- class<<self
10
- def synchronize( &block )
11
- (@mutex ||= Mutex.new).synchronize( &block )
12
- end
13
-
14
- def release( pointer )
15
- synchronize { Curl.easy_cleanup pointer }
16
- end
17
- end
18
- synchronize{}
19
- end
20
-
21
7
  # Returns a pointer to the curl easy handle.
22
8
  #
23
9
  # @example Return the handle.
@@ -25,7 +11,7 @@ module Ethon
25
11
  #
26
12
  # @return [ FFI::Pointer ] A pointer to the curl easy handle.
27
13
  def handle
28
- @handle ||= FFI::AutoPointer.new(Curl.easy_init, PointerHelper.method(:release) )
14
+ @handle ||= FFI::AutoPointer.new(Curl.easy_init, Curl.method(:easy_cleanup))
29
15
  end
30
16
 
31
17
  # Sets a pointer to the curl easy handle.
@@ -43,7 +43,12 @@ module Ethon
43
43
  return if @headers_called
44
44
  @headers_called = true
45
45
  if defined?(@on_headers) and not @on_headers.nil?
46
- @on_headers.each{ |callback| callback.call(self) }
46
+ result = nil
47
+ @on_headers.each do |callback|
48
+ result = callback.call(self)
49
+ break if result == :abort
50
+ end
51
+ result
47
52
  end
48
53
  end
49
54
 
@@ -24,12 +24,16 @@ module Ethon
24
24
  #
25
25
  # @return [ void ]
26
26
  def init_vars
27
- @timeout = ::FFI::MemoryPointer.new(:long)
28
- @timeval = Curl::Timeval.new
29
- @fd_read = Curl::FDSet.new
30
- @fd_write = Curl::FDSet.new
31
- @fd_excep = Curl::FDSet.new
32
- @max_fd = ::FFI::MemoryPointer.new(:int)
27
+ if @execution_mode == :perform
28
+ @timeout = ::FFI::MemoryPointer.new(:long)
29
+ @timeval = Curl::Timeval.new
30
+ @fd_read = Curl::FDSet.new
31
+ @fd_write = Curl::FDSet.new
32
+ @fd_excep = Curl::FDSet.new
33
+ @max_fd = ::FFI::MemoryPointer.new(:int)
34
+ elsif @execution_mode == :socket_action
35
+ @running_count_pointer = FFI::MemoryPointer.new(:int)
36
+ end
33
37
  end
34
38
 
35
39
  # Perform multi.
@@ -39,6 +43,8 @@ module Ethon
39
43
  # @example Perform multi.
40
44
  # multi.perform
41
45
  def perform
46
+ ensure_execution_mode(:perform)
47
+
42
48
  Ethon.logger.debug(STARTED_MULTI)
43
49
  while ongoing?
44
50
  run
@@ -67,9 +73,38 @@ module Ethon
67
73
  )
68
74
  end
69
75
 
70
- private
76
+ # Continue execution with an external IO loop.
77
+ #
78
+ # @example When no sockets are ready yet, or to begin.
79
+ # multi.socket_action
80
+ #
81
+ # @example When a socket is readable
82
+ # multi.socket_action(io_object, [:in])
83
+ #
84
+ # @example When a socket is readable and writable
85
+ # multi.socket_action(io_object, [:in, :out])
86
+ #
87
+ # @return [ Symbol ] The Curl.multi_socket_action return code.
88
+ def socket_action(io = nil, readiness = 0)
89
+ ensure_execution_mode(:socket_action)
90
+
91
+ fd = if io.nil?
92
+ ::Ethon::Curl::SOCKET_TIMEOUT
93
+ elsif io.is_a?(Integer)
94
+ io
95
+ else
96
+ io.fileno
97
+ end
98
+
99
+ code = Curl.multi_socket_action(handle, fd, readiness, @running_count_pointer)
100
+ @running_count = @running_count_pointer.read_int
71
101
 
72
- # Return wether the multi still requests or not.
102
+ check
103
+
104
+ code
105
+ end
106
+
107
+ # Return whether the multi still contains requests or not.
73
108
  #
74
109
  # @example Return if ongoing.
75
110
  # multi.ongoing?
@@ -79,6 +114,8 @@ module Ethon
79
114
  easy_handles.size > 0 || (!defined?(@running_count) || running_count > 0)
80
115
  end
81
116
 
117
+ private
118
+
82
119
  # Get timeout.
83
120
  #
84
121
  # @example Get timeout.
data/lib/ethon/multi.rb CHANGED
@@ -74,10 +74,21 @@ module Ethon
74
74
  # is 1 will not be treated as unlimited. Instead it will open only 1
75
75
  # connection and try to pipeline on it.
76
76
  # (Added in 7.30.0)
77
+ # @option options :execution_mode [Boolean]
78
+ # Either :perform (default) or :socket_action, specifies the usage
79
+ # method that will be used on this multi object. The default :perform
80
+ # mode provides a #perform function that uses curl_multi_perform
81
+ # behind the scenes to automatically continue execution until all
82
+ # requests have completed. The :socket_action mode provides an API
83
+ # that allows the {Multi} object to be integrated into an external
84
+ # IO loop, by calling #socket_action and responding to the
85
+ # socketfunction and timerfunction callbacks, using the underlying
86
+ # curl_multi_socket_action semantics.
77
87
  #
78
88
  # @return [ Multi ] The new multi.
79
89
  def initialize(options = {})
80
90
  Curl.init
91
+ @execution_mode = options.delete(:execution_mode) || :perform
81
92
  set_attributes(options)
82
93
  init_vars
83
94
  end
@@ -100,5 +111,16 @@ module Ethon
100
111
  method("#{key}=").call(value)
101
112
  end
102
113
  end
114
+
115
+ private
116
+
117
+ # Internal function to gate functions to a specific execution mode
118
+ #
119
+ # @raise ArgumentError
120
+ #
121
+ # @api private
122
+ def ensure_execution_mode(expected_mode)
123
+ raise ArgumentError, "Expected the Multi to be in #{expected_mode} but it was in #{@execution_mode}" if expected_mode != @execution_mode
124
+ end
103
125
  end
104
126
  end
data/lib/ethon/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  module Ethon
3
3
 
4
4
  # Ethon version.
5
- VERSION = '0.14.0'
5
+ VERSION = '0.16.0'
6
6
  end
@@ -56,4 +56,26 @@ describe Ethon::Easy::Callbacks do
56
56
  end
57
57
  end
58
58
  end
59
+
60
+ describe "#header_write_callback" do
61
+ let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
62
+ let(:stream) { double(:read_string => "") }
63
+ context "when header returns not :abort" do
64
+ it "returns number bigger than 0" do
65
+ expect(header_write_callback.call(stream, 1, 1, nil) > 0).to be(true)
66
+ end
67
+ end
68
+
69
+ context "when header returns :abort" do
70
+ before do
71
+ easy.on_headers.clear
72
+ easy.on_headers { :abort }
73
+ end
74
+ let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
75
+
76
+ it "returns -1 to indicate abort to libcurl" do
77
+ expect(header_write_callback.call(stream, 1, 1, nil)).to eq(-1)
78
+ end
79
+ end
80
+ end
59
81
  end
@@ -81,6 +81,12 @@ describe Ethon::Easy::Informations do
81
81
  end
82
82
  end
83
83
 
84
+ describe "#redirect_url" do
85
+ it "returns nil as there is no redirect" do
86
+ expect(easy.redirect_url).to be(nil)
87
+ end
88
+ end
89
+
84
90
  describe "#request_size" do
85
91
  it "returns 53" do
86
92
  expect(easy.request_size).to eq(53)
@@ -94,5 +100,27 @@ describe Ethon::Easy::Informations do
94
100
  end
95
101
  end
96
102
 
103
+ describe "#size_upload" do
104
+ it "returns float" do
105
+ expect(easy.size_upload).to be_a(Float)
106
+ end
107
+ end
97
108
 
109
+ describe "#size_download" do
110
+ it "returns float" do
111
+ expect(easy.size_download).to be_a(Float)
112
+ end
113
+ end
114
+
115
+ describe "#speed_upload" do
116
+ it "returns float" do
117
+ expect(easy.speed_upload).to be_a(Float)
118
+ end
119
+ end
120
+
121
+ describe "#speed_download" do
122
+ it "returns float" do
123
+ expect(easy.speed_download).to be_a(Float)
124
+ end
125
+ end
98
126
  end
@@ -10,7 +10,8 @@ describe Ethon::Easy::Mirror do
10
10
  :return_code, :response_code, :response_body, :response_headers,
11
11
  :total_time, :starttransfer_time, :appconnect_time,
12
12
  :pretransfer_time, :connect_time, :namelookup_time, :redirect_time,
13
- :effective_url, :primary_ip, :redirect_count, :debug_info
13
+ :size_upload, :size_download, :speed_upload, :speed_upload,
14
+ :effective_url, :primary_ip, :redirect_count, :redirect_url, :debug_info
14
15
  ].each do |name|
15
16
  it "contains #{name}" do
16
17
  expect(described_class::INFORMATIONS_TO_MIRROR).to include(name)
@@ -106,6 +106,7 @@ describe Ethon::Easy::Operations do
106
106
 
107
107
  it "doesn't follow" do
108
108
  expect(easy.response_code).to eq(302)
109
+ expect(easy.redirect_url).to eq("http://localhost:3001/")
109
110
  end
110
111
  end
111
112
 
@@ -115,6 +116,7 @@ describe Ethon::Easy::Operations do
115
116
 
116
117
  it "follows" do
117
118
  expect(easy.response_code).to eq(200)
119
+ expect(easy.redirect_url).to eq(nil)
118
120
  end
119
121
 
120
122
  context "when infinite redirect loop" do
@@ -124,6 +126,7 @@ describe Ethon::Easy::Operations do
124
126
  context "when max redirect set" do
125
127
  it "follows only x times" do
126
128
  expect(easy.response_code).to eq(302)
129
+ expect(easy.redirect_url).to eq("http://localhost:3001/bad_redirect")
127
130
  end
128
131
  end
129
132
  end
@@ -20,6 +20,119 @@ describe Ethon::Multi::Options do
20
20
  end
21
21
  end
22
22
 
23
+ context "socket_action mode" do
24
+ let(:multi) { Ethon::Multi.new(execution_mode: :socket_action) }
25
+
26
+ describe "#socketfunction callbacks" do
27
+ it "allows multi_code return values" do
28
+ calls = []
29
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
30
+ calls << what
31
+ :ok
32
+ end
33
+
34
+ easy = Ethon::Easy.new
35
+ easy.url = "http://localhost:3001/?delay=1"
36
+ multi.add(easy)
37
+ expect(calls).to eq([])
38
+ 5.times do
39
+ multi.socket_action
40
+ break unless calls.empty?
41
+ sleep 0.1
42
+ end
43
+ expect(calls.last).to eq(:in).or(eq(:out))
44
+ multi.delete(easy)
45
+ expect(calls.last).to eq(:remove)
46
+ end
47
+
48
+ it "allows integer return values (compatibility)" do
49
+ called = false
50
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
51
+ called = true
52
+ 0
53
+ end
54
+
55
+ easy = Ethon::Easy.new
56
+ easy.url = "http://localhost:3001/?delay=1"
57
+ multi.add(easy)
58
+ 5.times do
59
+ multi.socket_action
60
+ break if called
61
+ sleep 0.1
62
+ end
63
+ multi.delete(easy)
64
+
65
+ expect(called).to be_truthy
66
+ end
67
+
68
+ it "errors on invalid return codes" do
69
+ called = false
70
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
71
+ called = true
72
+ "hi"
73
+ end
74
+
75
+ easy = Ethon::Easy.new
76
+ easy.url = "http://localhost:3001/?delay=1"
77
+ multi.add(easy)
78
+ expect {
79
+ 5.times do
80
+ multi.socket_action
81
+ break if called
82
+ sleep 0.1
83
+ end
84
+ }.to raise_error(ArgumentError)
85
+ expect { multi.delete(easy) }.to raise_error(ArgumentError)
86
+ end
87
+ end
88
+
89
+ describe "#timerfunction callbacks" do
90
+ it "allows multi_code return values" do
91
+ calls = []
92
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
93
+ calls << timeout_ms
94
+ :ok
95
+ end
96
+
97
+ easy = Ethon::Easy.new
98
+ easy.url = "http://localhost:3001/?delay=1"
99
+ multi.add(easy)
100
+ expect(calls.last).to be >= 0 # adds an immediate timeout
101
+
102
+ multi.delete(easy)
103
+ expect(calls.last).to eq(-1) # cancels the timer
104
+ end
105
+
106
+ it "allows integer return values (compatibility)" do
107
+ called = false
108
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
109
+ called = true
110
+ 0
111
+ end
112
+
113
+ easy = Ethon::Easy.new
114
+ easy.url = "http://localhost:3001/?delay=1"
115
+ multi.add(easy)
116
+ multi.socket_action
117
+ multi.delete(easy)
118
+
119
+ expect(called).to be_truthy
120
+ end
121
+
122
+ it "errors on invalid return codes" do
123
+ called = false
124
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
125
+ called = true
126
+ "hi"
127
+ end
128
+
129
+ easy = Ethon::Easy.new
130
+ easy.url = "http://localhost:3001/?delay=1"
131
+ expect { multi.add(easy) }.to raise_error(ArgumentError)
132
+ end
133
+ end
134
+ end
135
+
23
136
  describe "#value_for" do
24
137
  context "when option in bool" do
25
138
  context "when value true" do
@@ -8,6 +8,16 @@ describe Ethon::Multi do
8
8
  Ethon::Multi.new
9
9
  end
10
10
 
11
+ context "with default options" do
12
+ it "allows running #perform with the default execution_mode" do
13
+ Ethon::Multi.new.perform
14
+ end
15
+
16
+ it "refuses to run #socket_action" do
17
+ expect { Ethon::Multi.new.socket_action }.to raise_error(ArgumentError)
18
+ end
19
+ end
20
+
11
21
  context "when options not empty" do
12
22
  context "when pipelining is set" do
13
23
  let(:options) { { :pipelining => true } }
@@ -17,6 +27,126 @@ describe Ethon::Multi do
17
27
  Ethon::Multi.new(options)
18
28
  end
19
29
  end
30
+
31
+ context "when execution_mode option is :socket_action" do
32
+ let(:options) { { :execution_mode => :socket_action } }
33
+ let(:multi) { Ethon::Multi.new(options) }
34
+
35
+ it "refuses to run #perform" do
36
+ expect { multi.perform }.to raise_error(ArgumentError)
37
+ end
38
+
39
+ it "allows running #socket_action" do
40
+ multi.socket_action
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#socket_action" do
47
+ let(:options) { { :execution_mode => :socket_action } }
48
+ let(:select_state) { { :readers => [], :writers => [], :timeout => 0 } }
49
+ let(:multi) {
50
+ multi = Ethon::Multi.new(options)
51
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
52
+ timeout_ms = nil if timeout_ms == -1
53
+ select_state[:timeout] = timeout_ms
54
+ :ok
55
+ end
56
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
57
+ case what
58
+ when :remove
59
+ select_state[:readers].delete(sock)
60
+ select_state[:writers].delete(sock)
61
+ when :in
62
+ select_state[:readers].push(sock) unless select_state[:readers].include? sock
63
+ select_state[:writers].delete(sock)
64
+ when :out
65
+ select_state[:readers].delete(sock)
66
+ select_state[:writers].push(sock) unless select_state[:writers].include? sock
67
+ when :inout
68
+ select_state[:readers].push(sock) unless select_state[:readers].include? sock
69
+ select_state[:writers].push(sock) unless select_state[:writers].include? sock
70
+ else
71
+ raise ArgumentError, "invalid value for 'what' in socketfunction callback"
72
+ end
73
+ :ok
74
+ end
75
+ multi
76
+ }
77
+
78
+ def fds_to_ios(fds)
79
+ fds.map do |fd|
80
+ IO.for_fd(fd).tap { |io| io.autoclose = false }
81
+ end
82
+ end
83
+
84
+ def perform_socket_action_until_complete
85
+ multi.socket_action # start things off
86
+
87
+ while multi.ongoing?
88
+ readers, writers, _ = IO.select(
89
+ fds_to_ios(select_state[:readers]),
90
+ fds_to_ios(select_state[:writers]),
91
+ [],
92
+ select_state[:timeout]
93
+ )
94
+
95
+ to_notify = Hash.new { |hash, key| hash[key] = [] }
96
+ unless readers.nil?
97
+ readers.each do |reader|
98
+ to_notify[reader] << :in
99
+ end
100
+ end
101
+ unless writers.nil?
102
+ writers.each do |writer|
103
+ to_notify[writer] << :out
104
+ end
105
+ end
106
+
107
+ to_notify.each do |io, readiness|
108
+ multi.socket_action(io, readiness)
109
+ end
110
+
111
+ # if we didn't have anything to notify, then we timed out
112
+ multi.socket_action if to_notify.empty?
113
+ end
114
+ ensure
115
+ multi.easy_handles.dup.each do |h|
116
+ multi.delete(h)
117
+ end
118
+ end
119
+
120
+ it "supports an end-to-end request" do
121
+ easy = Ethon::Easy.new
122
+ easy.url = "http://localhost:3001/"
123
+ multi.add(easy)
124
+
125
+ perform_socket_action_until_complete
126
+
127
+ expect(multi.ongoing?).to eq(false)
128
+ end
129
+
130
+ it "supports multiple concurrent requests" do
131
+ handles = []
132
+ 10.times do
133
+ easy = Ethon::Easy.new
134
+ easy.url = "http://localhost:3001/?delay=1"
135
+ multi.add(easy)
136
+ handles << easy
137
+ end
138
+
139
+ start = Time.now
140
+ perform_socket_action_until_complete
141
+ duration = Time.now - start
142
+
143
+ # these should have happened concurrently
144
+ expect(duration).to be < 2
145
+ expect(multi.ongoing?).to eq(false)
146
+
147
+ handles.each do |handle|
148
+ expect(handle.response_code).to eq(200)
149
+ end
20
150
  end
21
151
  end
22
152
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ethon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hans Hasselberg
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-26 00:00:00.000000000 Z
11
+ date: 2022-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -137,7 +137,7 @@ homepage: https://github.com/typhoeus/ethon
137
137
  licenses:
138
138
  - MIT
139
139
  metadata: {}
140
- post_install_message:
140
+ post_install_message:
141
141
  rdoc_options: []
142
142
  require_paths:
143
143
  - lib
@@ -152,8 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  - !ruby/object:Gem::Version
153
153
  version: 1.3.6
154
154
  requirements: []
155
- rubygems_version: 3.0.3
156
- signing_key:
155
+ rubygems_version: 3.3.7
156
+ signing_key:
157
157
  specification_version: 4
158
158
  summary: Libcurl wrapper.
159
159
  test_files: