ethon 0.18.0 → 0.19.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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +681 -2
  3. data/README.md +183 -40
  4. data/lib/ethon/curl.rb +22 -0
  5. data/lib/ethon/curls/classes.rb +284 -12
  6. data/lib/ethon/curls/codes.rb +28 -9
  7. data/lib/ethon/curls/constants.rb +61 -4
  8. data/lib/ethon/curls/form_options.rb +2 -1
  9. data/lib/ethon/curls/functions.rb +60 -11
  10. data/lib/ethon/curls/infos.rb +104 -5
  11. data/lib/ethon/curls/introspection.rb +139 -0
  12. data/lib/ethon/curls/introspection_constants.rb +42 -0
  13. data/lib/ethon/curls/mime.rb +187 -0
  14. data/lib/ethon/curls/optional.rb +14 -0
  15. data/lib/ethon/curls/options.rb +298 -71
  16. data/lib/ethon/curls/settings.rb +12 -3
  17. data/lib/ethon/curls/share.rb +165 -0
  18. data/lib/ethon/curls/share_constants.rb +47 -0
  19. data/lib/ethon/curls/url.rb +171 -0
  20. data/lib/ethon/curls/url_constants.rb +78 -0
  21. data/lib/ethon/easy/callbacks.rb +177 -51
  22. data/lib/ethon/easy/features.rb +4 -1
  23. data/lib/ethon/easy/files.rb +113 -0
  24. data/lib/ethon/easy/form.rb +65 -6
  25. data/lib/ethon/easy/header.rb +57 -15
  26. data/lib/ethon/easy/headers_api.rb +79 -0
  27. data/lib/ethon/easy/http/actionable.rb +25 -3
  28. data/lib/ethon/easy/http/get.rb +5 -1
  29. data/lib/ethon/easy/http/post.rb +1 -0
  30. data/lib/ethon/easy/http/postable.rb +9 -2
  31. data/lib/ethon/easy/http/putable.rb +19 -4
  32. data/lib/ethon/easy/http.rb +16 -4
  33. data/lib/ethon/easy/informations.rb +51 -2
  34. data/lib/ethon/easy/mime.rb +29 -0
  35. data/lib/ethon/easy/mirror.rb +17 -4
  36. data/lib/ethon/easy/operations.rb +259 -11
  37. data/lib/ethon/easy/options.rb +327 -8
  38. data/lib/ethon/easy/queryable.rb +59 -14
  39. data/lib/ethon/easy/response_callbacks.rb +33 -12
  40. data/lib/ethon/easy/share.rb +28 -0
  41. data/lib/ethon/easy/url.rb +32 -0
  42. data/lib/ethon/easy/util.rb +19 -0
  43. data/lib/ethon/easy.rb +185 -10
  44. data/lib/ethon/errors/fork_safety.rb +10 -0
  45. data/lib/ethon/errors/form_add.rb +12 -0
  46. data/lib/ethon/errors/global_init.rb +1 -1
  47. data/lib/ethon/errors/header.rb +12 -0
  48. data/lib/ethon/errors/invalid_option.rb +3 -1
  49. data/lib/ethon/errors/invalid_value.rb +3 -1
  50. data/lib/ethon/errors/mime.rb +17 -0
  51. data/lib/ethon/errors/multi_add.rb +1 -1
  52. data/lib/ethon/errors/multi_assign.rb +12 -0
  53. data/lib/ethon/errors/multi_fdset.rb +1 -1
  54. data/lib/ethon/errors/multi_perform.rb +12 -0
  55. data/lib/ethon/errors/multi_poll.rb +12 -0
  56. data/lib/ethon/errors/multi_remove.rb +1 -1
  57. data/lib/ethon/errors/multi_timeout.rb +2 -2
  58. data/lib/ethon/errors/multi_wait.rb +12 -0
  59. data/lib/ethon/errors/multi_wakeup.rb +12 -0
  60. data/lib/ethon/errors/select.rb +1 -1
  61. data/lib/ethon/errors/set_option.rb +27 -0
  62. data/lib/ethon/errors/share.rb +17 -0
  63. data/lib/ethon/errors/unsupported_option.rb +23 -0
  64. data/lib/ethon/errors/url.rb +17 -0
  65. data/lib/ethon/errors.rb +13 -0
  66. data/lib/ethon/libc.rb +30 -1
  67. data/lib/ethon/loggable.rb +1 -1
  68. data/lib/ethon/multi/operations.rb +269 -25
  69. data/lib/ethon/multi/options.rb +86 -12
  70. data/lib/ethon/multi/stack.rb +175 -10
  71. data/lib/ethon/multi.rb +44 -12
  72. data/lib/ethon/version.rb +1 -1
  73. data/lib/ethon.rb +8 -9
  74. metadata +35 -6
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ class Easy
4
+ # Structured access to response headers via curl_easy_header.
5
+ module HeadersApi
6
+ # libcurl's request number for the last request in a transfer. Redirects
7
+ # and multi-state authentication can produce several requests, and the
8
+ # response the caller sees comes from the last one.
9
+ LAST_REQUEST = -1
10
+
11
+ # Return a parsed response header.
12
+ #
13
+ # @param [ String ] name The header name.
14
+ # @param [ Integer ] index The instance index for repeated headers.
15
+ # @param [ Integer, Symbol, Array ] origin CURLH_* origin bits.
16
+ # @param [ Integer ] request The request number, -1 for the last request.
17
+ #
18
+ # @return [ Ethon::Curl::Header ] The header.
19
+ def header(name, index = 0, origin = Curl::HeaderOrigin[:header], request = LAST_REQUEST)
20
+ unless Curl.respond_to?(:easy_header)
21
+ raise NotImplementedError, "curl_easy_header is not supported by this libcurl"
22
+ end
23
+
24
+ output = ::FFI::MemoryPointer.new(:pointer)
25
+ code = Curl.easy_header(handle, name.to_s, index, header_origin(origin), request, output)
26
+ raise Errors::Header.new(code) unless code == :ok
27
+
28
+ Curl::Header.new(Curl::HeaderData.new(output.read_pointer))
29
+ end
30
+
31
+ # Enumerate response headers.
32
+ #
33
+ # @param [ Integer, Symbol, Array ] origin CURLH_* origin bits.
34
+ # @param [ Integer ] request The request number, -1 for the last request.
35
+ #
36
+ # @return [ Enumerator, self ] The headers.
37
+ def each_header(origin = Curl::HeaderOrigin[:header], request = LAST_REQUEST)
38
+ unless Curl.respond_to?(:easy_nextheader)
39
+ raise NotImplementedError, "curl_easy_nextheader is not supported by this libcurl"
40
+ end
41
+
42
+ origin = header_origin(origin)
43
+ return enum_for(:each_header, origin, request) unless block_given?
44
+
45
+ previous = nil
46
+ loop do
47
+ header = Curl.easy_nextheader(handle, origin, request, previous)
48
+ break if header.null?
49
+
50
+ yield Curl::Header.new(header)
51
+ previous = header
52
+ end
53
+ self
54
+ end
55
+ alias headers_list each_header
56
+
57
+ private
58
+
59
+ def header_origin(origin)
60
+ case origin
61
+ when Array
62
+ origin.inject(0) do |mask, item|
63
+ value = Curl::HeaderOrigin[item]
64
+ raise Errors::InvalidValue.new(:header_origin, item) unless value && value != 0
65
+ mask | value
66
+ end
67
+ when Symbol
68
+ value = Curl::HeaderOrigin[origin]
69
+ raise Errors::InvalidValue.new(:header_origin, origin) unless value && value != 0
70
+ value
71
+ else
72
+ origin
73
+ end
74
+ end
75
+ end
76
+
77
+ include HeadersApi
78
+ end
79
+ end
@@ -93,6 +93,18 @@ module Ethon
93
93
  # @param [ easy ] easy the easy to setup.
94
94
  def setup(easy)
95
95
  @easy = easy
96
+ easy.customrequest = nil
97
+ # A reused handle keeps the previous transfer's method and body
98
+ # options. Clear them before applying this request so a stale
99
+ # upload body cannot win over the body configured below.
100
+ # CURLOPT_POSTFIELDS selects POST even when cleared, so clear it
101
+ # before the method flags to leave the handle on GET.
102
+ easy.postfields = nil
103
+ easy.readfunction = nil
104
+ Curl.set_option(:upload, false, easy.handle)
105
+ Curl.set_option(:httpget, false, easy.handle)
106
+ Curl.set_option(:nobody, false, easy.handle)
107
+ Curl.set_option(:post, false, easy.handle)
96
108
 
97
109
  # Order is important, @easy will be used to provide access to options
98
110
  # relevant to the following operations (like whether or not to escape
@@ -100,6 +112,12 @@ module Ethon
100
112
  easy.set_attributes(options)
101
113
 
102
114
  set_form(easy) unless form.empty?
115
+ if form.empty?
116
+ easy.multipart_form = nil
117
+ # Clear any MIME body left over from an earlier request (or copied
118
+ # by Easy#dup) so this request does not inherit it.
119
+ easy.mime = nil if Ethon::Curl.respond_to?(:mime_init)
120
+ end
103
121
 
104
122
  if params.empty?
105
123
  easy.url = url
@@ -118,11 +136,15 @@ module Ethon
118
136
  params.escape = easy.escape?
119
137
  params.params_encoding = params_encoding
120
138
 
121
- base_url, base_params = url.split('?')
139
+ url_without_fragment, fragment_separator, fragment = url.partition('#')
140
+ fragment = fragment_separator.empty? ? nil : "#{fragment_separator}#{fragment}"
141
+ base_url, query_separator, base_params = url_without_fragment.partition('?')
142
+
122
143
  base_url << '?'
123
- base_url << base_params.to_s
124
- base_url << '&' if base_params
144
+ base_url << base_params
145
+ base_url << '&' if query_separator == '?' && !base_params.empty?
125
146
  base_url << params.to_s
147
+ base_url << fragment.to_s
126
148
 
127
149
  easy.url = base_url
128
150
  end
@@ -16,7 +16,11 @@ module Ethon
16
16
  # @param [ Easy ] easy The easy to setup.
17
17
  def setup(easy)
18
18
  super
19
- easy.customrequest = "GET" unless form.empty?
19
+ if form.empty?
20
+ easy.httpget = true
21
+ else
22
+ easy.customrequest = "GET"
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -16,6 +16,7 @@ module Ethon
16
16
  def setup(easy)
17
17
  super
18
18
  if form.empty?
19
+ easy.multipart_form = nil
19
20
  easy.postfieldsize = 0
20
21
  easy.copypostfields = ""
21
22
  end
@@ -18,9 +18,16 @@ module Ethon
18
18
  form.params_encoding = params_encoding
19
19
  if form.multipart?
20
20
  form.escape = false
21
- form.materialize
22
- easy.httppost = form.first.read_pointer
21
+ if Ethon::Curl.respond_to?(:mime_init)
22
+ easy.multipart_form = nil
23
+ easy.mime = form.mime
24
+ else
25
+ form.materialize
26
+ easy.multipart_form = form
27
+ end
23
28
  else
29
+ easy.multipart_form = nil
30
+ easy.mime = nil if Ethon::Curl.respond_to?(:mime_init)
24
31
  form.escape = easy.escape?
25
32
  easy.postfieldsize = form.to_s.bytesize
26
33
  easy.copypostfields = form.to_s
@@ -13,11 +13,26 @@ module Ethon
13
13
  #
14
14
  # @param [ Easy ] easy The easy to setup.
15
15
  def set_form(easy)
16
- easy.upload = true
17
- form.escape = true
16
+ easy.url ||= url
18
17
  form.params_encoding = params_encoding
19
- easy.infilesize = form.to_s.bytesize
20
- easy.set_read_callback(form.to_s)
18
+ if form.multipart?
19
+ form.escape = false
20
+ if Ethon::Curl.respond_to?(:mime_init)
21
+ easy.multipart_form = nil
22
+ easy.mime = form.mime
23
+ else
24
+ form.materialize
25
+ easy.multipart_form = form
26
+ end
27
+ easy.customrequest = "PUT"
28
+ else
29
+ easy.multipart_form = nil
30
+ easy.mime = nil if Ethon::Curl.respond_to?(:mime_init)
31
+ easy.upload = true
32
+ form.escape = easy.escape?
33
+ easy.infilesize = form.to_s.bytesize
34
+ easy.set_read_callback(form.to_s)
35
+ end
21
36
  end
22
37
  end
23
38
  end
@@ -15,6 +15,18 @@ module Ethon
15
15
  # This module contains logic about making valid HTTP requests.
16
16
  module Http
17
17
 
18
+ # Built-in request actions. Custom verbs must not resolve to helper
19
+ # modules or classes that happen to live in this namespace.
20
+ ACTION_CLASSES = {
21
+ "get" => Get,
22
+ "head" => Head,
23
+ "post" => Post,
24
+ "put" => Put,
25
+ "delete" => Delete,
26
+ "patch" => Patch,
27
+ "options" => Options
28
+ }.freeze
29
+
18
30
  # Set specified options in order to make a HTTP request.
19
31
  # Look at {Ethon::Easy::Options Options} to see what you can
20
32
  # provide in the options hash.
@@ -54,12 +66,12 @@ module Ethon
54
66
  #
55
67
  # @return [ Easy::Ethon::Actionable ] The request instance.
56
68
  def fabricate(url, action_name, options)
57
- constant_name = action_name.to_s.capitalize
69
+ action_class = ACTION_CLASSES[action_name.to_s.downcase]
58
70
 
59
- if Ethon::Easy::Http.const_defined?(constant_name)
60
- Ethon::Easy::Http.const_get(constant_name).new(url, options)
71
+ if action_class
72
+ action_class.new(url, options)
61
73
  else
62
- Ethon::Easy::Http::Custom.new(constant_name.upcase, url, options)
74
+ Ethon::Easy::Http::Custom.new(action_name.to_s.upcase, url, options)
63
75
  end
64
76
  end
65
77
 
@@ -3,13 +3,52 @@ module Ethon
3
3
  class Easy
4
4
 
5
5
  # This module contains the methods to return informations
6
- # from the easy handle. See http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html
6
+ # from the easy handle. See https://curl.se/libcurl/c/curl_easy_getinfo.html
7
7
  # for more information.
8
8
  module Informations
9
9
 
10
10
  # Holds available informations and their type, which is needed to
11
11
  # request the informations from libcurl.
12
12
  AVAILABLE_INFORMATIONS = {
13
+ # The _T variants use curl_off_t and do not lose precision for large
14
+ # values or long-running transfers.
15
+ :size_upload_t => :off_t,
16
+ :size_download_t => :off_t,
17
+ :speed_download_t => :off_t,
18
+ :speed_upload_t => :off_t,
19
+ :filetime_t => :off_t,
20
+ :content_length_download_t => :off_t,
21
+ :content_length_upload_t => :off_t,
22
+ :total_time_t => :off_t,
23
+ :namelookup_time_t => :off_t,
24
+ :connect_time_t => :off_t,
25
+ :pretransfer_time_t => :off_t,
26
+ :starttransfer_time_t => :off_t,
27
+ :redirect_time_t => :off_t,
28
+ :appconnect_time_t => :off_t,
29
+ :retry_after => :off_t,
30
+ :xfer_id => :off_t,
31
+ :conn_id => :off_t,
32
+ :queue_time_t => :off_t,
33
+ :posttransfer_time_t => :off_t,
34
+ :earlydata_sent_t => :off_t,
35
+ :size_delivered => :off_t,
36
+
37
+ # Connection/request metadata.
38
+ :http_version => :long,
39
+ :httpauth_used => :long,
40
+ :proxyauth_used => :long,
41
+ :proxy_ssl_verifyresult => :long,
42
+ :proxy_error => :long,
43
+ :used_proxy => :long,
44
+ :scheme => :string,
45
+ :effective_method => :string,
46
+ :referer => :string,
47
+ :cainfo => :string,
48
+ :capath => :string,
49
+ :activesocket => :socket,
50
+ :tls_ssl_ptr => :ptr,
51
+
13
52
  # Return the available HTTP auth methods.
14
53
  :httpauth_avail => :long,
15
54
 
@@ -95,10 +134,20 @@ module Ethon
95
134
  :speed_download => :double
96
135
  }
97
136
 
137
+ LIST_INFORMATIONS = [:ssl_engines, :cookielist].freeze
138
+
98
139
  AVAILABLE_INFORMATIONS.each do |name, type|
99
140
  eval %Q|def #{name}; Curl.send(:get_info_#{type}, :#{name}, handle); end|
100
141
  end
101
142
 
143
+ LIST_INFORMATIONS.each do |name|
144
+ define_method(name) { Curl.get_info_slist(name, handle) }
145
+ end
146
+
147
+ def certinfo
148
+ Curl.get_info_certinfo(:certinfo, handle)
149
+ end
150
+
102
151
  # Returns true if this curl version supports zlib.
103
152
  #
104
153
  # @example Return wether zlib is supported.
@@ -107,7 +156,7 @@ module Ethon
107
156
  # @return [ Boolean ] True if supported, else false.
108
157
  # @deprecated Please use the static version instead
109
158
  def supports_zlib?
110
- Kernel.warn("Ethon: Easy#supports_zlib? is deprecated and will be removed, please use Easy#.")
159
+ Kernel.warn("Ethon: Easy#supports_zlib? is deprecated and will be removed, please use Easy.supports_zlib? instead.")
111
160
  Easy.supports_zlib?
112
161
  end
113
162
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ class Easy
4
+ module Mime
5
+ attr_reader :mime
6
+
7
+ # Install a MIME structure as the request body.
8
+ #
9
+ # @param [ Ethon::Curl::Mime, nil ] mime The MIME structure.
10
+ #
11
+ # @raise [ TypeError ] If mime is not a MIME structure.
12
+ #
13
+ # @return [ Ethon::Curl::Mime, nil ] The MIME structure.
14
+ def mime=(mime)
15
+ unless mime.nil? || mime.is_a?(Curl::Mime)
16
+ raise TypeError, "expected an Ethon::Curl::Mime, got #{mime.class}"
17
+ end
18
+
19
+ mime.check_pid! if mime
20
+ Curl.set_option(:mimepost, mime, handle)
21
+ @mime = mime
22
+ mime
23
+ end
24
+ alias mimepost= mime=
25
+ end
26
+
27
+ include Mime
28
+ end
29
+ end
@@ -5,11 +5,20 @@ module Ethon
5
5
  attr_reader :options
6
6
  alias_method :to_hash, :options
7
7
 
8
- INFORMATIONS_TO_MIRROR = Informations::AVAILABLE_INFORMATIONS.keys +
9
- [:return_code, :response_headers, :response_body, :debug_info]
8
+ # Raw pointers cannot be marshaled and point into native state that can
9
+ # go away when the handle is reset. Applications serialize a mirror (for
10
+ # example as a cached Typhoeus response), so leave them on the Easy.
11
+ INFORMATIONS_TO_MIRROR = Informations::AVAILABLE_INFORMATIONS.
12
+ reject { |_, type| type == :ptr }.keys +
13
+ [:return_code, :response_headers, :response_body, :debug_info, :error_message]
10
14
 
11
15
  INFORMATIONS_TO_LOG = [:effective_url, :response_code, :return_code, :total_time]
12
16
 
17
+ # Credentials embedded in the URL are not useful in logs and may end up
18
+ # in a log aggregator. Keep the scheme and host so the line is still
19
+ # identifiable, but replace the userinfo.
20
+ URL_CREDENTIALS = %r{\A([a-z][a-z0-9+.-]*://)[^/?#@]*@}i
21
+
13
22
  def self.from_easy(easy)
14
23
  options = {}
15
24
  INFORMATIONS_TO_MIRROR.each do |info|
@@ -24,12 +33,16 @@ module Ethon
24
33
 
25
34
  def log_informations
26
35
  Hash[*INFORMATIONS_TO_LOG.map do |info|
27
- [info, options[info]]
36
+ value = options[info]
37
+ if info == :effective_url && value.is_a?(String)
38
+ value = value.sub(URL_CREDENTIALS, "\\1[REDACTED]@")
39
+ end
40
+ [info, value]
28
41
  end.flatten]
29
42
  end
30
43
 
31
44
  INFORMATIONS_TO_MIRROR.each do |info|
32
- eval %Q|def #{info}; options[#{info}]; end|
45
+ define_method(info) { options[info] }
33
46
  end
34
47
  end
35
48
  end