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,165 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Curl
4
+
5
+ # Thin wrapper around libcurl's share API.
6
+ class Share < OwnedPointer
7
+ CALLBACKS_MUTEX = Mutex.new
8
+
9
+ attr_reader :lockfunction, :unlockfunction
10
+
11
+ def self.native_release(pointer)
12
+ Curl.share_cleanup(pointer) if Curl.respond_to?(:share_cleanup)
13
+ end
14
+
15
+ # Callback functions must outlive the Share because libcurl invokes them
16
+ # again from curl_share_cleanup.
17
+ def self.callbacks
18
+ @callbacks ||= {}
19
+ end
20
+
21
+ def self.release(pointer)
22
+ super
23
+ ensure
24
+ key = [Process.pid, pointer.address]
25
+ begin
26
+ CALLBACKS_MUTEX.synchronize { callbacks.delete(key) }
27
+ rescue ThreadError, SignalException
28
+ # Finalizers can run in a trap context where locking is forbidden.
29
+ callbacks.delete(key)
30
+ end
31
+ end
32
+
33
+ # Build the dispatcher on the class so the closure does not retain the
34
+ # Share instance; otherwise the callback registry itself would keep every
35
+ # share handle alive.
36
+ #
37
+ # curl_lock_function and curl_unlock_function do not take the same
38
+ # arguments. An unlock callback receives the user pointer where a lock
39
+ # callback receives the access mode.
40
+ def self.lock_function(callback)
41
+ FFI::Function.new(:void, [:pointer, :int, :int, :pointer], proc do |handle, data, access, userp|
42
+ callback.call(handle, Curl::LockData[data], Curl::LockAccess[access], userp)
43
+ end)
44
+ end
45
+
46
+ def self.unlock_function(callback)
47
+ FFI::Function.new(:void, [:pointer, :int, :pointer], proc do |handle, data, userp|
48
+ callback.call(handle, Curl::LockData[data], userp)
49
+ end)
50
+ end
51
+
52
+ def initialize
53
+ Curl.init
54
+ unless Curl.respond_to?(:share_init)
55
+ raise NotImplementedError, "curl_share_init is not supported by this libcurl"
56
+ end
57
+
58
+ pointer = Curl.share_init
59
+ raise NoMemoryError, "curl_share_init failed" if pointer.null?
60
+
61
+ super(pointer)
62
+ end
63
+
64
+ # Share a data type between easy handles.
65
+ #
66
+ # @param [ Symbol, String ] data One of :cookie, :dns, :ssl_session,
67
+ # :connect, :psl, or :hsts.
68
+ #
69
+ # @return [ Share ] self
70
+ def share(data)
71
+ setopt(:share, data)
72
+ self
73
+ end
74
+
75
+ # Stop sharing a data type.
76
+ #
77
+ # @param [ Symbol, String ] data The data type.
78
+ #
79
+ # @return [ Share ] self
80
+ def unshare(data)
81
+ setopt(:unshare, data)
82
+ self
83
+ end
84
+
85
+ # Set the callback libcurl calls before accessing shared data.
86
+ #
87
+ # The block receives the easy handle, the shared data type, the access
88
+ # mode, and the user pointer. libcurl requires lock and unlock callbacks
89
+ # when a share handle is used from more than one thread.
90
+ #
91
+ # @example Lock shared cookies.
92
+ # share.lockfunction = proc { |handle, data, access, userp| mutex.synchronize { ... } }
93
+ #
94
+ # @param [ Proc, nil ] callback The lock callback.
95
+ #
96
+ # @return [ Proc, nil ] The callback.
97
+ def lockfunction=(callback)
98
+ set_callback(:lockfunc, callback)
99
+ end
100
+
101
+ # Set the callback libcurl calls before releasing a share lock.
102
+ #
103
+ # The block receives the easy handle, the shared data type, and the user
104
+ # pointer. libcurl's unlock callback has no access mode argument.
105
+ #
106
+ # @param [ Proc, nil ] callback The unlock callback.
107
+ #
108
+ # @return [ Proc, nil ] The callback.
109
+ def unlockfunction=(callback)
110
+ set_callback(:unlockfunc, callback)
111
+ end
112
+
113
+ private
114
+
115
+ def setopt(option, data)
116
+ check_pid!
117
+ unless data.is_a?(Symbol) || data.is_a?(String)
118
+ raise Errors::InvalidValue.new(option, data)
119
+ end
120
+
121
+ value = Curl::LockData[data.to_sym]
122
+ raise Errors::InvalidValue.new(option, data) unless value
123
+
124
+ code = Curl.share_setopt(self, option, :long, value)
125
+ raise Errors::Share.new(code) unless code == :ok
126
+
127
+ code
128
+ end
129
+
130
+ def set_callback(option, callback)
131
+ check_pid!
132
+ unless callback.nil? || callback.is_a?(Proc)
133
+ raise Errors::InvalidValue.new(option, callback)
134
+ end
135
+
136
+ # libcurl calls the lock and unlock callbacks again during
137
+ # curl_share_cleanup, so the FFI::Function itself must stay alive.
138
+ function = if callback
139
+ option == :lockfunc ? self.class.lock_function(callback) : self.class.unlock_function(callback)
140
+ end
141
+
142
+ code = Curl.share_setopt(self, option, :pointer, function)
143
+ raise Errors::Share.new(code) unless code == :ok
144
+
145
+ key = [Process.pid, address]
146
+ CALLBACKS_MUTEX.synchronize do
147
+ registry = self.class.callbacks[key] ||= {}
148
+ if function
149
+ registry[option] = function
150
+ else
151
+ registry.delete(option)
152
+ self.class.callbacks.delete(key) if registry.empty?
153
+ end
154
+ end
155
+
156
+ if option == :lockfunc
157
+ @lockfunction = callback
158
+ else
159
+ @unlockfunction = callback
160
+ end
161
+ callback
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Curl
4
+ # CURLSHcode values.
5
+ ShareCode = enum(:share_code, [
6
+ :ok,
7
+ :bad_option,
8
+ :in_use,
9
+ :invalid,
10
+ :nomem,
11
+ :not_built_in,
12
+ :last
13
+ ])
14
+
15
+ # CURLSHoption values.
16
+ ShareOption = enum(:share_option, [
17
+ :none,
18
+ :share,
19
+ :unshare,
20
+ :lockfunc,
21
+ :unlockfunc,
22
+ :userdata,
23
+ :last
24
+ ])
25
+
26
+ # curl_lock_data values.
27
+ LockData = enum(:lock_data, [
28
+ :none,
29
+ :share,
30
+ :cookie,
31
+ :dns,
32
+ :ssl_session,
33
+ :connect,
34
+ :psl,
35
+ :hsts,
36
+ :last
37
+ ])
38
+
39
+ # curl_lock_access values.
40
+ LockAccess = enum(:lock_access, [
41
+ :none,
42
+ :shared,
43
+ :single,
44
+ :last
45
+ ])
46
+ end
47
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Curl
4
+
5
+ # CURLOPT_CURLU takes a CURLU handle.
6
+ Ethon::Curls::Options.option(:easy, :curlu, :ffipointer, 282)
7
+
8
+ # Thin wrapper around libcurl's URL API (curl_url).
9
+ class Url < OwnedPointer
10
+ def self.native_release(pointer)
11
+ Curl.url_cleanup(pointer) if Curl.respond_to?(:url_cleanup)
12
+ end
13
+
14
+ # Create a URL handle, optionally parsing a URL string.
15
+ #
16
+ # @param [ String, Url, FFI::Pointer, nil ] url The URL, a URL handle
17
+ # to copy, or a native handle.
18
+ # @param [ Integer, Symbol, Array ] flags CURLU_* flags. These are only
19
+ # applied when parsing a String; a copied handle already has its parts.
20
+ def initialize(url = nil, flags = 0)
21
+ Curl.init
22
+ unless Curl.respond_to?(:url)
23
+ raise NotImplementedError, "curl_url is not supported by this libcurl"
24
+ end
25
+
26
+ copying = url.is_a?(Ethon::Curl::Url)
27
+ if copying
28
+ url.check_pid!
29
+ if normalize_flags(flags) != 0
30
+ raise ArgumentError, "flags cannot be applied when copying an Ethon::Curl::Url"
31
+ end
32
+ # Parsing the copied handle's string form would fail for partial
33
+ # handles and could normalize a complete one. Duplicate the parts
34
+ # instead.
35
+ pointer = Curl.url_dup(url)
36
+ raise NoMemoryError, "curl_url_dup failed" if pointer.null?
37
+ elsif url.is_a?(::FFI::Pointer)
38
+ pointer = url
39
+ else
40
+ pointer = Curl.url
41
+ raise NoMemoryError, "curl_url failed" if pointer.null?
42
+ end
43
+
44
+ super(pointer)
45
+ begin
46
+ set(:url, url, flags) if url && !url.is_a?(::FFI::Pointer) && !copying
47
+ rescue Exception
48
+ # A rejected URL must not leave the native handle to the finalizer.
49
+ free
50
+ raise
51
+ end
52
+ end
53
+
54
+ # Set a URL part.
55
+ #
56
+ # @param [ Symbol, String, Integer ] part The CURLUPart name or value.
57
+ # @param [ String, nil ] value The new value, or nil to clear the part.
58
+ # @param [ Integer, Symbol, Array ] flags CURLU_* flags.
59
+ #
60
+ # @raise [ Errors::InvalidValue ] If the part or a flag is unknown, or
61
+ # the value contains a NUL byte, CR, or LF.
62
+ #
63
+ # @return [ Url ] self
64
+ def set(part, value, flags = 0)
65
+ check_pid!
66
+ code = Curl.url_set(self, normalize_part(part), normalize_value(value),
67
+ normalize_flags(flags))
68
+ raise Errors::Url.new(code) unless code == :ok
69
+
70
+ self
71
+ end
72
+
73
+ # Get a URL part.
74
+ #
75
+ # @param [ Symbol, String, Integer ] part The CURLUPart name or value.
76
+ # @param [ Integer, Symbol, Array ] flags CURLU_* flags.
77
+ #
78
+ # @raise [ Errors::InvalidValue ] If the part or a flag is unknown.
79
+ #
80
+ # @return [ String ] The part value.
81
+ def get(part, flags = 0)
82
+ check_pid!
83
+ output = ::FFI::MemoryPointer.new(:pointer)
84
+ code = Curl.url_get(self, normalize_part(part), output, normalize_flags(flags))
85
+ raise Errors::Url.new(code) unless code == :ok
86
+
87
+ pointer = output.read_pointer
88
+ begin
89
+ pointer.read_string
90
+ ensure
91
+ Curl.free(pointer)
92
+ end
93
+ end
94
+
95
+ # Duplicate the URL handle.
96
+ #
97
+ # @return [ Url ] The copied URL.
98
+ def dup
99
+ check_pid!
100
+ pointer = Curl.url_dup(self)
101
+ raise NoMemoryError, "curl_url_dup failed" if pointer.null?
102
+
103
+ self.class.new(pointer)
104
+ end
105
+
106
+ # Cloning shares the native handle in Ruby, so route it through #dup.
107
+ def clone(freeze: true)
108
+ copy = dup
109
+ copy.freeze if freeze && frozen?
110
+ copy
111
+ end
112
+
113
+ def to_s
114
+ get(:url)
115
+ end
116
+
117
+ private
118
+
119
+ # Only names that exist in CURLUPart reach libcurl. FFI would raise its
120
+ # own ArgumentError for anything else. Direct enum values stay supported,
121
+ # like the named option setters.
122
+ def normalize_part(part)
123
+ name = part.is_a?(String) ? part.to_sym : part
124
+ values = Curl::UrlPart.to_h
125
+ unless values.key?(name) || (name.is_a?(Integer) && values.value?(name))
126
+ raise Errors::InvalidValue.new(:url_part, part)
127
+ end
128
+
129
+ name
130
+ end
131
+
132
+ # libcurl takes a C string, so an embedded NUL would silently truncate
133
+ # the value. nil clears the part.
134
+ #
135
+ # The URL API also accepts CR and LF, which libcurl writes verbatim into
136
+ # the request line or query. A path or query containing CRLF can add
137
+ # attackers' headers to the request, so reject them here just like the
138
+ # string URL parser does. Callers that need such bytes in a URL must
139
+ # percent-encode them.
140
+ def normalize_value(value)
141
+ return if value.nil?
142
+
143
+ string = value.to_s
144
+ if string.include?("\0") || string.include?("\r") || string.include?("\n")
145
+ raise Errors::InvalidValue.new(:url_value, value)
146
+ end
147
+
148
+ string
149
+ end
150
+
151
+ # Only CURLU_* flag names reach libcurl. FFI would raise its own
152
+ # ArgumentError for anything else. Direct bit values stay supported, like
153
+ # the named option setters.
154
+ def normalize_flags(flags)
155
+ return flags if flags.is_a?(Integer)
156
+
157
+ Array(flags).inject(0) { |mask, flag| mask | normalize_flag(flag) }
158
+ end
159
+
160
+ def normalize_flag(flag)
161
+ return flag if flag.is_a?(Integer)
162
+
163
+ name = flag.is_a?(String) ? flag.to_sym : flag
164
+ value = name.is_a?(Symbol) && Curl::UrlFlags.to_h[name]
165
+ raise Errors::InvalidValue.new(:url_flags, flag) unless value
166
+
167
+ value
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Curl
4
+ # CURLUPart values from curl/urlapi.h.
5
+ UrlPart = enum(:url_part, [
6
+ :url,
7
+ :scheme,
8
+ :user,
9
+ :password,
10
+ :options,
11
+ :host,
12
+ :port,
13
+ :path,
14
+ :query,
15
+ :fragment,
16
+ :zoneid
17
+ ])
18
+
19
+ # CURLUcode values from curl/urlapi.h.
20
+ UrlCode = enum(:url_code, [
21
+ :ok,
22
+ :bad_handle,
23
+ :bad_partpointer,
24
+ :malformed_input,
25
+ :bad_port_number,
26
+ :unsupported_scheme,
27
+ :urldecode,
28
+ :out_of_memory,
29
+ :user_not_allowed,
30
+ :unknown_part,
31
+ :no_scheme,
32
+ :no_user,
33
+ :no_password,
34
+ :no_options,
35
+ :no_host,
36
+ :no_port,
37
+ :no_query,
38
+ :no_fragment,
39
+ :no_zoneid,
40
+ :bad_file_url,
41
+ :bad_fragment,
42
+ :bad_hostname,
43
+ :bad_ipv6,
44
+ :bad_login,
45
+ :bad_password,
46
+ :bad_path,
47
+ :bad_query,
48
+ :bad_scheme,
49
+ :bad_slashes,
50
+ :bad_user,
51
+ :lacks_idn,
52
+ :too_large,
53
+ :backslash,
54
+ :last
55
+ ])
56
+
57
+ # CURLU_* flag bits from curl/urlapi.h.
58
+ UrlFlags = bitmask(:url_flags, [
59
+ nil,
60
+ :default_port,
61
+ :no_default_port,
62
+ :default_scheme,
63
+ :non_support_scheme,
64
+ :path_as_is,
65
+ :disallow_user,
66
+ :urldecode,
67
+ :urlencode,
68
+ :appendquery,
69
+ :guess_scheme,
70
+ :no_authority,
71
+ :allow_space,
72
+ :punycode,
73
+ :puny2idn,
74
+ :get_empty,
75
+ :no_guess_scheme
76
+ ])
77
+ end
78
+ end