ethon 0.18.0 → 0.19.1
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 +4 -4
- data/CHANGELOG.md +705 -2
- data/README.md +183 -40
- data/lib/ethon/curl.rb +22 -0
- data/lib/ethon/curls/classes.rb +284 -12
- data/lib/ethon/curls/codes.rb +28 -9
- data/lib/ethon/curls/constants.rb +61 -4
- data/lib/ethon/curls/form_options.rb +2 -1
- data/lib/ethon/curls/functions.rb +60 -11
- data/lib/ethon/curls/infos.rb +104 -5
- data/lib/ethon/curls/introspection.rb +139 -0
- data/lib/ethon/curls/introspection_constants.rb +42 -0
- data/lib/ethon/curls/mime.rb +187 -0
- data/lib/ethon/curls/optional.rb +14 -0
- data/lib/ethon/curls/options.rb +298 -71
- data/lib/ethon/curls/settings.rb +12 -3
- data/lib/ethon/curls/share.rb +165 -0
- data/lib/ethon/curls/share_constants.rb +47 -0
- data/lib/ethon/curls/url.rb +171 -0
- data/lib/ethon/curls/url_constants.rb +78 -0
- data/lib/ethon/easy/callbacks.rb +177 -51
- data/lib/ethon/easy/features.rb +4 -1
- data/lib/ethon/easy/files.rb +113 -0
- data/lib/ethon/easy/form.rb +65 -6
- data/lib/ethon/easy/header.rb +57 -15
- data/lib/ethon/easy/headers_api.rb +79 -0
- data/lib/ethon/easy/http/actionable.rb +25 -3
- data/lib/ethon/easy/http/get.rb +5 -1
- data/lib/ethon/easy/http/post.rb +1 -0
- data/lib/ethon/easy/http/postable.rb +9 -2
- data/lib/ethon/easy/http/putable.rb +19 -4
- data/lib/ethon/easy/http.rb +16 -4
- data/lib/ethon/easy/informations.rb +51 -2
- data/lib/ethon/easy/mime.rb +29 -0
- data/lib/ethon/easy/mirror.rb +17 -4
- data/lib/ethon/easy/operations.rb +264 -11
- data/lib/ethon/easy/options.rb +327 -8
- data/lib/ethon/easy/queryable.rb +59 -14
- data/lib/ethon/easy/response_callbacks.rb +33 -12
- data/lib/ethon/easy/share.rb +28 -0
- data/lib/ethon/easy/url.rb +32 -0
- data/lib/ethon/easy/util.rb +19 -0
- data/lib/ethon/easy.rb +185 -10
- data/lib/ethon/errors/fork_safety.rb +10 -0
- data/lib/ethon/errors/form_add.rb +12 -0
- data/lib/ethon/errors/global_init.rb +1 -1
- data/lib/ethon/errors/header.rb +12 -0
- data/lib/ethon/errors/invalid_option.rb +3 -1
- data/lib/ethon/errors/invalid_value.rb +3 -1
- data/lib/ethon/errors/mime.rb +17 -0
- data/lib/ethon/errors/multi_add.rb +1 -1
- data/lib/ethon/errors/multi_assign.rb +12 -0
- data/lib/ethon/errors/multi_fdset.rb +1 -1
- data/lib/ethon/errors/multi_perform.rb +12 -0
- data/lib/ethon/errors/multi_poll.rb +12 -0
- data/lib/ethon/errors/multi_remove.rb +1 -1
- data/lib/ethon/errors/multi_timeout.rb +2 -2
- data/lib/ethon/errors/multi_wait.rb +12 -0
- data/lib/ethon/errors/multi_wakeup.rb +12 -0
- data/lib/ethon/errors/select.rb +1 -1
- data/lib/ethon/errors/set_option.rb +27 -0
- data/lib/ethon/errors/share.rb +17 -0
- data/lib/ethon/errors/unsupported_option.rb +23 -0
- data/lib/ethon/errors/url.rb +17 -0
- data/lib/ethon/errors.rb +13 -0
- data/lib/ethon/libc.rb +30 -1
- data/lib/ethon/loggable.rb +1 -1
- data/lib/ethon/multi/operations.rb +269 -25
- data/lib/ethon/multi/options.rb +114 -12
- data/lib/ethon/multi/stack.rb +181 -10
- data/lib/ethon/multi.rb +51 -15
- data/lib/ethon/version.rb +1 -1
- data/lib/ethon.rb +8 -9
- metadata +35 -6
|
@@ -9,15 +9,34 @@ module Ethon
|
|
|
9
9
|
# @example Return the handle.
|
|
10
10
|
# easy.handle
|
|
11
11
|
#
|
|
12
|
-
# @return [
|
|
12
|
+
# @return [ Ethon::Curl::EasyHandle ] The owned wrapper around the curl
|
|
13
|
+
# easy handle. It is an FFI::Pointer subclass.
|
|
13
14
|
def handle
|
|
14
|
-
|
|
15
|
+
raise Errors::EthonError, "Easy has been cleaned up" if @cleaned_up
|
|
16
|
+
|
|
17
|
+
@handle ||= create_handle
|
|
18
|
+
handle = @handle
|
|
19
|
+
handle.check_pid!
|
|
20
|
+
handle.owner ||= self
|
|
21
|
+
handle
|
|
15
22
|
end
|
|
16
23
|
|
|
17
24
|
# Sets a pointer to the curl easy handle.
|
|
18
|
-
#
|
|
25
|
+
#
|
|
26
|
+
# Raw pointers are wrapped in an owned {Curl::EasyHandle}, so the
|
|
27
|
+
# handle is registered for fork safety and freed by this Easy. Assigning
|
|
28
|
+
# nil installs a fresh handle. Assigning a different handle installs
|
|
29
|
+
# Ethon's response callbacks and replays callbacks previously assigned to
|
|
30
|
+
# this Easy, then frees the handle it replaces when this Easy owned it.
|
|
31
|
+
#
|
|
32
|
+
# Request options that are not replayed are cleared, because the
|
|
33
|
+
# replacement handle does not have them. Read them before replacing the
|
|
34
|
+
# handle if they are needed again.
|
|
35
|
+
#
|
|
36
|
+
# @param [ ::FFI::Pointer, Curl::EasyHandle, nil ] h Easy handle that will
|
|
37
|
+
# be assigned.
|
|
19
38
|
def handle=(h)
|
|
20
|
-
|
|
39
|
+
assign_handle(h, true)
|
|
21
40
|
end
|
|
22
41
|
|
|
23
42
|
# Perform the easy request.
|
|
@@ -25,24 +44,93 @@ module Ethon
|
|
|
25
44
|
# @example Perform the request.
|
|
26
45
|
# easy.perform
|
|
27
46
|
#
|
|
28
|
-
# @return [
|
|
47
|
+
# @return [ Symbol ] The return code.
|
|
29
48
|
def perform
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
ensure_not_performing!
|
|
50
|
+
# curl_easy_perform must not drive a handle that is owned by a multi.
|
|
51
|
+
ensure_not_attached_to_multi!
|
|
52
|
+
# Touch the handle before changing transfer state so a cleaned up or
|
|
53
|
+
# forked handle cannot truncate a response file or reset the previous
|
|
54
|
+
# result while raising.
|
|
55
|
+
handle
|
|
56
|
+
|
|
57
|
+
prepare_response_body_io
|
|
58
|
+
reset_transfer_state
|
|
59
|
+
clear_callback_error
|
|
60
|
+
# Only the destination that belongs to this transfer may be finished.
|
|
61
|
+
# A completion callback can install the next transfer's destination.
|
|
62
|
+
response_body_io = @response_body_io
|
|
63
|
+
@performing = true
|
|
64
|
+
|
|
65
|
+
error = nil
|
|
66
|
+
begin
|
|
67
|
+
code = Curl.easy_perform(handle)
|
|
68
|
+
@return_code = code
|
|
69
|
+
if Ethon.logger.debug?
|
|
70
|
+
Ethon.logger.debug { "ETHON: performed #{log_inspect}" }
|
|
71
|
+
end
|
|
72
|
+
# Make the complete response visible before on_complete runs. An
|
|
73
|
+
# owned file is closed by the ensure below after the callbacks.
|
|
74
|
+
begin
|
|
75
|
+
flush_response_body_io
|
|
76
|
+
rescue Exception
|
|
77
|
+
# A callback error already failed this transfer; a flush error is
|
|
78
|
+
# secondary and must not hide it.
|
|
79
|
+
raise_callback_error
|
|
80
|
+
raise
|
|
81
|
+
end
|
|
82
|
+
complete
|
|
83
|
+
raise_callback_error
|
|
84
|
+
# on_complete may reset the Easy, which clears @return_code. Return
|
|
85
|
+
# the result of the transfer that just ran.
|
|
86
|
+
code
|
|
87
|
+
rescue Exception => exception
|
|
88
|
+
error = exception
|
|
89
|
+
raise
|
|
90
|
+
ensure
|
|
91
|
+
@performing = false
|
|
92
|
+
begin
|
|
93
|
+
finish_response_body_io(response_body_io)
|
|
94
|
+
rescue Exception
|
|
95
|
+
# Keep the exception that interrupted the transfer or completion.
|
|
96
|
+
raise unless error
|
|
97
|
+
end
|
|
33
98
|
end
|
|
34
|
-
complete
|
|
35
|
-
@return_code
|
|
36
99
|
end
|
|
37
100
|
|
|
38
101
|
# Clean up the easy.
|
|
39
102
|
#
|
|
103
|
+
# Cleanup is terminal: the native handle is freed and the Easy cannot be
|
|
104
|
+
# used again. Reusing it raises Ethon::Errors::EthonError.
|
|
105
|
+
#
|
|
106
|
+
# Cleanup is allowed from an on_complete callback: the transfer has
|
|
107
|
+
# finished and a Multi detaches the handle before running its callbacks.
|
|
108
|
+
# Cleanup from body, header, and progress callbacks is rejected because
|
|
109
|
+
# those run inside libcurl.
|
|
110
|
+
#
|
|
40
111
|
# @example Perform clean up.
|
|
41
112
|
# easy.cleanup
|
|
42
113
|
#
|
|
43
114
|
# @return the result of the free which is nil
|
|
44
115
|
def cleanup
|
|
45
|
-
|
|
116
|
+
ensure_not_in_transfer!
|
|
117
|
+
ensure_not_attached_to_multi!
|
|
118
|
+
|
|
119
|
+
# Resolve the handle first: cleanup must not clear the response body if
|
|
120
|
+
# the handle is unusable (cleaned up or inherited across a fork).
|
|
121
|
+
handle_to_free = handle
|
|
122
|
+
begin
|
|
123
|
+
clear_response_body_io
|
|
124
|
+
ensure
|
|
125
|
+
# Free the native handle even when closing the response body fails.
|
|
126
|
+
begin
|
|
127
|
+
handle_to_free.free
|
|
128
|
+
ensure
|
|
129
|
+
clear_native_state
|
|
130
|
+
@handle = nil
|
|
131
|
+
@cleaned_up = true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
46
134
|
end
|
|
47
135
|
|
|
48
136
|
# Prepare the easy. Options, headers and callbacks
|
|
@@ -59,6 +147,171 @@ module Ethon
|
|
|
59
147
|
"in future versions."
|
|
60
148
|
)
|
|
61
149
|
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
# Allocate the native handle. Kept separate so #handle= can replace a
|
|
154
|
+
# handle and install the callbacks before the next option setter runs.
|
|
155
|
+
def create_handle
|
|
156
|
+
Curl.check_pid!
|
|
157
|
+
pointer = Curl.easy_init
|
|
158
|
+
raise NoMemoryError, "curl_easy_init failed" if pointer.null?
|
|
159
|
+
Curl::EasyHandle.new(pointer)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Reapply options Ethon installed on the old handle. These are not user
|
|
163
|
+
# request options: without them #error_message and progress callbacks
|
|
164
|
+
# silently stop working on the replacement.
|
|
165
|
+
def restore_handle_options
|
|
166
|
+
if @error_buffer_enabled && @error_buffer
|
|
167
|
+
Curl.set_option(:errorbuffer, @error_buffer, @handle)
|
|
168
|
+
@handle.error_buffer = @error_buffer
|
|
169
|
+
# Do not report the previous transfer's error from the new handle.
|
|
170
|
+
@error_buffer.put_char(0, 0)
|
|
171
|
+
end
|
|
172
|
+
self.noprogress = @noprogress_value unless @noprogress_value.nil?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Install a handle and, unless the native duplicate already carries the
|
|
176
|
+
# request options, clear the Ruby-side state that belonged to the handle
|
|
177
|
+
# it replaces.
|
|
178
|
+
def assign_handle(h, clear_state)
|
|
179
|
+
raise Errors::EthonError, "Easy has been cleaned up" if @cleaned_up
|
|
180
|
+
|
|
181
|
+
ensure_not_performing!
|
|
182
|
+
ensure_not_attached_to_multi!
|
|
183
|
+
|
|
184
|
+
previous = @handle
|
|
185
|
+
@handle =
|
|
186
|
+
case h
|
|
187
|
+
when nil
|
|
188
|
+
# Create the replacement now so it gets Ethon's callbacks even
|
|
189
|
+
# before an option setter or #perform touches it.
|
|
190
|
+
create_handle
|
|
191
|
+
when Curl::EasyHandle
|
|
192
|
+
h.check_pid!
|
|
193
|
+
# Two Easies must not share one owned wrapper: whichever cleans up
|
|
194
|
+
# first invalidates the handle for the other.
|
|
195
|
+
if h.owner && !h.owner.equal?(self)
|
|
196
|
+
raise ArgumentError, "easy handle is already owned by another Ethon handle"
|
|
197
|
+
end
|
|
198
|
+
h
|
|
199
|
+
when Curl::OwnedPointer
|
|
200
|
+
raise TypeError, "expected an easy handle, got #{h.class}"
|
|
201
|
+
when ::FFI::Pointer
|
|
202
|
+
raise ArgumentError, "cannot assign a null easy handle" if h.null?
|
|
203
|
+
|
|
204
|
+
Curl::EasyHandle.new(h)
|
|
205
|
+
else
|
|
206
|
+
raise TypeError, "expected an easy handle pointer, got #{h.class}"
|
|
207
|
+
end
|
|
208
|
+
@handle.owner = self if @handle
|
|
209
|
+
|
|
210
|
+
replaced = @handle && !@handle.equal?(previous)
|
|
211
|
+
if replaced
|
|
212
|
+
# A replacement handle has no Ethon callbacks or Ethon-managed
|
|
213
|
+
# options, so its response would otherwise not be collected. Retained
|
|
214
|
+
# user callbacks are replayed on top of the internal ones.
|
|
215
|
+
install_callbacks(@handle)
|
|
216
|
+
restore_handle_options
|
|
217
|
+
restore_callbacks_on(self)
|
|
218
|
+
end
|
|
219
|
+
release_replaced_handle(previous)
|
|
220
|
+
# Free the old handle before dropping the Ruby references it owned.
|
|
221
|
+
clear_replaced_handle_state if replaced && clear_state
|
|
222
|
+
@handle
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The replacement handle does not have the old handle's request options.
|
|
226
|
+
# Clear the Ruby-side state so the accessors do not describe options the
|
|
227
|
+
# new handle never received. Callbacks, the error buffer, and the
|
|
228
|
+
# progress setting are replayed by #assign_handle and stay in place.
|
|
229
|
+
def clear_replaced_handle_state
|
|
230
|
+
@url = nil
|
|
231
|
+
@headers = nil
|
|
232
|
+
@header_list = nil
|
|
233
|
+
@postfields = nil
|
|
234
|
+
@postfields_pointer = nil
|
|
235
|
+
@postfield_body_size = nil
|
|
236
|
+
@postfieldsize_value = nil
|
|
237
|
+
@curlu = nil
|
|
238
|
+
@mime = nil
|
|
239
|
+
@multipart_form = nil
|
|
240
|
+
@slists = nil
|
|
241
|
+
@blobs = nil
|
|
242
|
+
@share = nil
|
|
243
|
+
@escape = nil
|
|
244
|
+
@multipart = nil
|
|
245
|
+
reset_transfer_state
|
|
246
|
+
clear_callback_error
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Free the handle this Easy owned before the replacement. A handle owned
|
|
250
|
+
# by another Easy (the source of #dup) must stay alive for its owner.
|
|
251
|
+
def release_replaced_handle(previous)
|
|
252
|
+
return unless previous
|
|
253
|
+
return if previous.equal?(@handle)
|
|
254
|
+
return unless previous.owner.equal?(self)
|
|
255
|
+
|
|
256
|
+
previous.free
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Drop references to native data and callbacks once the handle is gone.
|
|
260
|
+
def clear_native_state
|
|
261
|
+
@body_write_callback = nil
|
|
262
|
+
@header_write_callback = nil
|
|
263
|
+
@debug_callback = nil
|
|
264
|
+
@progress_callback = nil
|
|
265
|
+
@read_callback = nil
|
|
266
|
+
@request_body = nil
|
|
267
|
+
@request_body_start = nil
|
|
268
|
+
@postfields = nil
|
|
269
|
+
@postfields_pointer = nil
|
|
270
|
+
@postfield_body_size = nil
|
|
271
|
+
@postfieldsize_value = nil
|
|
272
|
+
@header_list = nil
|
|
273
|
+
@multipart_form = nil
|
|
274
|
+
@mime = nil
|
|
275
|
+
@curlu = nil
|
|
276
|
+
@blobs = nil
|
|
277
|
+
@slists = nil
|
|
278
|
+
@procs = nil
|
|
279
|
+
@on_complete = nil
|
|
280
|
+
@on_headers = nil
|
|
281
|
+
@on_body = nil
|
|
282
|
+
@on_progress = nil
|
|
283
|
+
@progress_source = nil
|
|
284
|
+
@internal_progress_option = nil
|
|
285
|
+
@user_debug_callback = nil
|
|
286
|
+
@share = nil
|
|
287
|
+
@error_buffer = nil
|
|
288
|
+
@error_buffer_enabled = nil
|
|
289
|
+
@noprogress_value = nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def ensure_not_performing!
|
|
293
|
+
if @completing
|
|
294
|
+
raise Errors::EthonError, "Easy is currently completing; wait for the callbacks to finish"
|
|
295
|
+
end
|
|
296
|
+
if @performing
|
|
297
|
+
raise Errors::EthonError, "Easy is currently performing; wait for the transfer to finish"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Resetting is safe from a completion callback: the transfer has already
|
|
302
|
+
# finished and the handle no longer belongs to a Multi. A reset from a
|
|
303
|
+
# body, header, or progress callback still runs inside libcurl.
|
|
304
|
+
def ensure_not_in_transfer!
|
|
305
|
+
return if @completing
|
|
306
|
+
|
|
307
|
+
ensure_not_performing!
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def ensure_not_attached_to_multi!
|
|
311
|
+
return unless defined?(@multi) && @multi
|
|
312
|
+
|
|
313
|
+
raise Errors::EthonError, "Easy is still attached to a Multi; remove it first"
|
|
314
|
+
end
|
|
62
315
|
end
|
|
63
316
|
end
|
|
64
317
|
end
|
data/lib/ethon/easy/options.rb
CHANGED
|
@@ -5,11 +5,48 @@ module Ethon
|
|
|
5
5
|
# This module contains the logic and knowledge about the
|
|
6
6
|
# available options on easy.
|
|
7
7
|
module Options
|
|
8
|
+
CALLBACK_TYPES = [:callback, :debug_callback, :progress_callback, :xferinfo_callback].freeze
|
|
9
|
+
|
|
10
|
+
# Ethon installs these callbacks on every handle. Clearing the option must
|
|
11
|
+
# restore Ethon's callback instead of leaving libcurl's default writer
|
|
12
|
+
# active, which would send response bytes to the process's stdout or
|
|
13
|
+
# stderr.
|
|
14
|
+
CALLBACK_DEFAULTS = {
|
|
15
|
+
:writefunction => :body_write_callback,
|
|
16
|
+
:headerfunction => :header_write_callback,
|
|
17
|
+
:debugfunction => :debug_callback
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
8
20
|
attr_reader :url
|
|
9
21
|
|
|
10
22
|
def url=(value)
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
# libcurl copies the URL, but the accessor must describe the exact
|
|
24
|
+
# string libcurl received. Snapshot it so later caller mutations cannot
|
|
25
|
+
# make #url disagree with the handle, and assign it only once libcurl
|
|
26
|
+
# accepts the value. Do this before touching the URL handle so a
|
|
27
|
+
# rejected value cannot leave it detached.
|
|
28
|
+
snapshot = value.nil? ? nil : value.to_s
|
|
29
|
+
snapshot = snapshot.dup.freeze if snapshot && !snapshot.frozen?
|
|
30
|
+
|
|
31
|
+
previous_curlu = @curlu
|
|
32
|
+
if previous_curlu
|
|
33
|
+
Curl.set_option(:curlu, nil, handle)
|
|
34
|
+
@curlu = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
Curl.set_option(:url, snapshot, handle)
|
|
39
|
+
rescue Exception
|
|
40
|
+
# Detaching the URL handle was only necessary to install the new
|
|
41
|
+
# value. Put it back when that value was rejected.
|
|
42
|
+
if previous_curlu
|
|
43
|
+
Curl.set_option(:curlu, previous_curlu, handle)
|
|
44
|
+
@curlu = previous_curlu
|
|
45
|
+
end
|
|
46
|
+
raise
|
|
47
|
+
end
|
|
48
|
+
@url = snapshot
|
|
49
|
+
value
|
|
13
50
|
end
|
|
14
51
|
|
|
15
52
|
def escape=( b )
|
|
@@ -29,22 +66,304 @@ module Ethon
|
|
|
29
66
|
!!@multipart
|
|
30
67
|
end
|
|
31
68
|
|
|
69
|
+
def multipart_form=(form)
|
|
70
|
+
self.httppost = form ? form.first.read_pointer : nil
|
|
71
|
+
@multipart_form = form
|
|
72
|
+
rescue Errors::UnsupportedOption
|
|
73
|
+
# CURLOPT_HTTPPOST is compiled out of builds configured with
|
|
74
|
+
# --disable-form-api. Clearing is all that can happen there, because
|
|
75
|
+
# building a form needs the form API itself.
|
|
76
|
+
raise if form
|
|
77
|
+
|
|
78
|
+
@multipart_form = form
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Enables or disables libcurl's error buffer (CURLOPT_ERRORBUFFER),
|
|
82
|
+
# which holds a more detailed error description than #return_message.
|
|
83
|
+
# Read it with #error_message.
|
|
84
|
+
#
|
|
85
|
+
# @example Enable the error buffer.
|
|
86
|
+
# easy.errorbuffer = true
|
|
87
|
+
#
|
|
88
|
+
# @param [ Boolean ] value True to enable, false to disable.
|
|
89
|
+
#
|
|
90
|
+
# @return [ Boolean ] The value.
|
|
91
|
+
def errorbuffer=(value)
|
|
92
|
+
if value
|
|
93
|
+
@error_buffer ||= FFI::MemoryPointer.new(:char, Curl.easy_options(nil)[:errorbuffer][:opts])
|
|
94
|
+
@error_buffer.put_char(0, 0)
|
|
95
|
+
Curl.set_option(:errorbuffer, @error_buffer, handle)
|
|
96
|
+
handle.error_buffer = @error_buffer
|
|
97
|
+
else
|
|
98
|
+
Curl.set_option(:errorbuffer, nil, handle)
|
|
99
|
+
handle.error_buffer = nil
|
|
100
|
+
end
|
|
101
|
+
@error_buffer_enabled = !!value
|
|
102
|
+
value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the error message libcurl stored in the error buffer during
|
|
106
|
+
# the last transfer, or nil if it is empty or #errorbuffer= isn't enabled.
|
|
107
|
+
#
|
|
108
|
+
# @example Get the error message.
|
|
109
|
+
# easy.error_message
|
|
110
|
+
#
|
|
111
|
+
# @return [ String, nil ] The error message.
|
|
112
|
+
def error_message
|
|
113
|
+
return unless @error_buffer_enabled
|
|
114
|
+
message = @error_buffer.read_string
|
|
115
|
+
message unless message.empty?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Enables or disables libcurl's progress callback invocation. libcurl
|
|
119
|
+
# requires CURLOPT_NOPROGRESS to be 0 before it calls a progress
|
|
120
|
+
# callback, so remember the value to restore it when the native handle
|
|
121
|
+
# is replaced.
|
|
122
|
+
#
|
|
123
|
+
# @example Enable progress callbacks.
|
|
124
|
+
# easy.noprogress = 0
|
|
125
|
+
#
|
|
126
|
+
# @param [ Boolean, Integer ] value The value to set.
|
|
127
|
+
#
|
|
128
|
+
# @return [ Boolean, Integer ] The value.
|
|
129
|
+
def noprogress=(value)
|
|
130
|
+
Curl.set_option(:noprogress, value, handle)
|
|
131
|
+
@noprogress_value = value
|
|
132
|
+
value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# CURLOPT_TLSAUTH_TYPE and CURLOPT_PROXY_TLSAUTH_TYPE take a string, not
|
|
136
|
+
# an enum. Normalize the historical symbol API and libcurl's required
|
|
137
|
+
# "SRP" spelling.
|
|
138
|
+
def tlsauth_type=(value)
|
|
139
|
+
Curl.set_option(:tlsauth_type, tlsauth_type_value(value), handle)
|
|
140
|
+
value
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def proxy_tlsauth_type=(value)
|
|
144
|
+
Curl.set_option(:proxy_tlsauth_type, tlsauth_type_value(value), handle)
|
|
145
|
+
value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Options whose value is used to bound a retained body. Their setters
|
|
149
|
+
# have to keep the body length and the native size in sync.
|
|
150
|
+
POSTFIELD_OPTIONS = [:postfields, :postfieldsize, :postfieldsize_large, :copypostfields].freeze
|
|
151
|
+
|
|
32
152
|
Curl.easy_options(nil).each do |opt, props|
|
|
153
|
+
# These setters are defined below because libcurl does not copy the
|
|
154
|
+
# CURLOPT_POSTFIELDS string and the size must stay inside its buffer.
|
|
155
|
+
next if POSTFIELD_OPTIONS.include?(opt)
|
|
156
|
+
|
|
33
157
|
method_name = "#{opt}=".freeze
|
|
34
158
|
unless method_defined? method_name
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
value
|
|
159
|
+
if CALLBACK_TYPES.include?(props[:type])
|
|
160
|
+
default_callback = CALLBACK_DEFAULTS[opt]
|
|
161
|
+
define_method(method_name) do |value|
|
|
162
|
+
native_value = value
|
|
163
|
+
native_value = send(default_callback) if value.nil? && default_callback
|
|
164
|
+
Curl.set_option(opt, native_value, handle)
|
|
165
|
+
# Do not retain Ethon's internal callback as if the caller had
|
|
166
|
+
# assigned it; a duplicate must install its own callback.
|
|
167
|
+
retain_callback(opt, value)
|
|
168
|
+
value
|
|
169
|
+
end
|
|
170
|
+
elsif props[:type] == :blob
|
|
171
|
+
define_method(method_name) do |value|
|
|
172
|
+
Curl.set_option(opt, value, handle)
|
|
173
|
+
if value
|
|
174
|
+
(@blobs ||= {})[opt] = value
|
|
175
|
+
elsif @blobs
|
|
176
|
+
@blobs.delete(opt)
|
|
177
|
+
end
|
|
178
|
+
value
|
|
179
|
+
end
|
|
180
|
+
elsif props[:type] == :curl_slist
|
|
181
|
+
# libcurl keeps the list pointer, so retain the wrapper in case it
|
|
182
|
+
# owns the native list. Clearing the option can release it.
|
|
183
|
+
define_method(method_name) do |value|
|
|
184
|
+
Curl.set_option(opt, value, handle)
|
|
185
|
+
if value
|
|
186
|
+
(@slists ||= {})[opt] = value
|
|
187
|
+
elsif @slists
|
|
188
|
+
@slists.delete(opt)
|
|
189
|
+
end
|
|
190
|
+
value
|
|
191
|
+
end
|
|
192
|
+
else
|
|
193
|
+
define_method(method_name) do |value|
|
|
194
|
+
Curl.set_option(opt, value, handle)
|
|
195
|
+
value
|
|
196
|
+
end
|
|
38
197
|
end
|
|
39
198
|
end
|
|
40
199
|
next if props[:type] != :callback || method_defined?(opt)
|
|
41
200
|
define_method(opt) do |&block|
|
|
42
|
-
|
|
43
|
-
@procs[opt.to_sym] = block
|
|
44
|
-
Curl.set_option(opt, block, handle)
|
|
201
|
+
send(method_name, block)
|
|
45
202
|
nil
|
|
46
203
|
end
|
|
47
204
|
end
|
|
205
|
+
|
|
206
|
+
# libcurl does not copy CURLOPT_POSTFIELDS, so keep an immutable snapshot
|
|
207
|
+
# of the bytes alive for as long as the handle may reference it. The
|
|
208
|
+
# native option takes a pointer, which lets the body contain NUL bytes.
|
|
209
|
+
# Retain the buffer only once the option was accepted. Mutating a caller's
|
|
210
|
+
# string afterwards may reallocate it, so libcurl must not point at that
|
|
211
|
+
# buffer.
|
|
212
|
+
#
|
|
213
|
+
# CURLOPT_POSTFIELDSIZE survives between requests. Updating it here keeps
|
|
214
|
+
# libcurl from reading an earlier, larger body length out of the new
|
|
215
|
+
# buffer.
|
|
216
|
+
def postfields=(value)
|
|
217
|
+
if value.nil?
|
|
218
|
+
Curl.set_option(:postfields, nil, handle)
|
|
219
|
+
@postfields = nil
|
|
220
|
+
@postfields_pointer = nil
|
|
221
|
+
@postfield_body_size = nil
|
|
222
|
+
# -1 is libcurl's default: the body ends at the first NUL.
|
|
223
|
+
Curl.set_option(:postfieldsize, -1, handle)
|
|
224
|
+
@postfieldsize_value = -1
|
|
225
|
+
else
|
|
226
|
+
string = value.to_s
|
|
227
|
+
string = string.dup.freeze unless string.frozen?
|
|
228
|
+
pointer = FFI::MemoryPointer.new(:char, string.bytesize + 1)
|
|
229
|
+
pointer.put_bytes(0, string)
|
|
230
|
+
pointer.put_char(string.bytesize, 0)
|
|
231
|
+
Curl.set_option(:postfields, pointer, handle)
|
|
232
|
+
@postfields = string
|
|
233
|
+
@postfields_pointer = pointer
|
|
234
|
+
@postfield_body_size = string.bytesize
|
|
235
|
+
Curl.set_option(:postfieldsize, string.bytesize, handle)
|
|
236
|
+
@postfieldsize_value = string.bytesize
|
|
237
|
+
end
|
|
238
|
+
value
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# libcurl copies CURLOPT_COPYPOSTFIELDS, so the Ruby string does not have
|
|
242
|
+
# to stay alive. An explicit, smaller CURLOPT_POSTFIELDSIZE still selects
|
|
243
|
+
# a prefix. A larger size would make libcurl read past the copied buffer,
|
|
244
|
+
# which is how a stale size from a previous body leaked memory into the
|
|
245
|
+
# request.
|
|
246
|
+
def copypostfields=(value)
|
|
247
|
+
if value.nil?
|
|
248
|
+
Curl.set_option(:copypostfields, nil, handle)
|
|
249
|
+
@postfields = nil
|
|
250
|
+
@postfields_pointer = nil
|
|
251
|
+
@postfield_body_size = nil
|
|
252
|
+
# Clear the size too: libcurl fails the request when a non-zero size
|
|
253
|
+
# is paired with the NULL body left by COPYPOSTFIELDS.
|
|
254
|
+
Curl.set_option(:postfieldsize, -1, handle)
|
|
255
|
+
@postfieldsize_value = -1
|
|
256
|
+
return value
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
string = value.to_s
|
|
260
|
+
size = @postfieldsize_value
|
|
261
|
+
if size && size >= 0
|
|
262
|
+
if @postfield_body_size == size
|
|
263
|
+
# The size still describes the body that is being replaced. Keeping
|
|
264
|
+
# it would truncate a longer copy or read past a shorter one.
|
|
265
|
+
size = nil
|
|
266
|
+
elsif size > string.bytesize
|
|
267
|
+
raise Errors::InvalidValue.new(:copypostfields, value)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if size.nil? || size < 0
|
|
272
|
+
Curl.set_option(:postfieldsize, string.bytesize, handle)
|
|
273
|
+
@postfieldsize_value = string.bytesize
|
|
274
|
+
end
|
|
275
|
+
Curl.set_option(:copypostfields, string, handle)
|
|
276
|
+
@postfields = nil
|
|
277
|
+
@postfields_pointer = nil
|
|
278
|
+
@postfield_body_size = string.bytesize
|
|
279
|
+
value
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# CURLOPT_POSTFIELDSIZE and CURLOPT_POSTFIELDSIZE_LARGE bound the bytes
|
|
283
|
+
# libcurl reads from CURLOPT_POSTFIELDS. Letting them exceed the retained
|
|
284
|
+
# buffer makes libcurl send adjacent process memory.
|
|
285
|
+
def postfieldsize=(value)
|
|
286
|
+
set_postfieldsize(:postfieldsize, value)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def postfieldsize_large=(value)
|
|
290
|
+
set_postfieldsize(:postfieldsize_large, value)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Observe libcurl debug messages without replacing Ethon's internal
|
|
294
|
+
# debug callback. The block receives the native CURL_DEBUGFUNCTION
|
|
295
|
+
# arguments and is called only when verbose mode is enabled.
|
|
296
|
+
#
|
|
297
|
+
# @example Observe debug messages.
|
|
298
|
+
# easy.debugfunction { |handle, type, data, size, user_data| }
|
|
299
|
+
#
|
|
300
|
+
# @param [ Block ] block The block to execute, or no block to clear it.
|
|
301
|
+
def debugfunction(&block)
|
|
302
|
+
@user_debug_callback = block
|
|
303
|
+
nil
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private
|
|
307
|
+
|
|
308
|
+
# Validate the size against the body Ethon currently knows about before
|
|
309
|
+
# handing it to libcurl.
|
|
310
|
+
def set_postfieldsize(option, value)
|
|
311
|
+
size = value.nil? ? nil : Ethon::Curls::Options.integer_value(option, value)
|
|
312
|
+
if @postfield_body_size && size && size > @postfield_body_size
|
|
313
|
+
raise Errors::InvalidValue.new(option, value)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
Curl.set_option(option, value, handle)
|
|
317
|
+
@postfieldsize_value = size unless value.nil?
|
|
318
|
+
value
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def initialize_copy(other)
|
|
322
|
+
super
|
|
323
|
+
@procs = @procs.dup if @procs
|
|
324
|
+
@blobs = @blobs.dup if @blobs
|
|
325
|
+
@slists = @slists.dup if @slists
|
|
326
|
+
@on_complete = @on_complete.dup if @on_complete
|
|
327
|
+
@on_headers = @on_headers.dup if @on_headers
|
|
328
|
+
@on_body = @on_body.dup if @on_body
|
|
329
|
+
@on_progress = @on_progress.dup if @on_progress
|
|
330
|
+
@headers = @headers.dup.freeze if @headers
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def tlsauth_type_value(value)
|
|
334
|
+
case value.to_s.downcase
|
|
335
|
+
when "", "none"
|
|
336
|
+
nil
|
|
337
|
+
when "srp"
|
|
338
|
+
"SRP"
|
|
339
|
+
else
|
|
340
|
+
value
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def retain_callback(option, callback)
|
|
345
|
+
if option == :readfunction
|
|
346
|
+
# A callback installed directly by the user does not describe the
|
|
347
|
+
# retained body and must not be replaced when the Easy is duplicated.
|
|
348
|
+
unless callback && callback.equal?(@read_callback)
|
|
349
|
+
@request_body = nil
|
|
350
|
+
@request_body_start = nil
|
|
351
|
+
end
|
|
352
|
+
@read_callback = callback
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Setting the option that backs on_progress replaces or clears the
|
|
356
|
+
# internal callback. Other progress options lose to XFERINFOFUNCTION.
|
|
357
|
+
if option == @internal_progress_option
|
|
358
|
+
@progress_source = callback ? :custom : nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if callback.nil?
|
|
362
|
+
@procs.delete(option) if @procs
|
|
363
|
+
else
|
|
364
|
+
(@procs ||= {})[option] = callback
|
|
365
|
+
end
|
|
366
|
+
end
|
|
48
367
|
end
|
|
49
368
|
end
|
|
50
369
|
end
|