ethon 0.17.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +698 -2
  3. data/README.md +183 -40
  4. data/ethon.gemspec +9 -1
  5. data/lib/ethon/curl.rb +22 -0
  6. data/lib/ethon/curls/classes.rb +284 -12
  7. data/lib/ethon/curls/codes.rb +28 -9
  8. data/lib/ethon/curls/constants.rb +61 -4
  9. data/lib/ethon/curls/form_options.rb +2 -1
  10. data/lib/ethon/curls/functions.rb +60 -11
  11. data/lib/ethon/curls/infos.rb +104 -5
  12. data/lib/ethon/curls/introspection.rb +139 -0
  13. data/lib/ethon/curls/introspection_constants.rb +42 -0
  14. data/lib/ethon/curls/mime.rb +187 -0
  15. data/lib/ethon/curls/optional.rb +14 -0
  16. data/lib/ethon/curls/options.rb +298 -71
  17. data/lib/ethon/curls/settings.rb +12 -3
  18. data/lib/ethon/curls/share.rb +165 -0
  19. data/lib/ethon/curls/share_constants.rb +47 -0
  20. data/lib/ethon/curls/url.rb +171 -0
  21. data/lib/ethon/curls/url_constants.rb +78 -0
  22. data/lib/ethon/easy/callbacks.rb +178 -46
  23. data/lib/ethon/easy/features.rb +4 -1
  24. data/lib/ethon/easy/files.rb +113 -0
  25. data/lib/ethon/easy/form.rb +65 -6
  26. data/lib/ethon/easy/header.rb +57 -15
  27. data/lib/ethon/easy/headers_api.rb +79 -0
  28. data/lib/ethon/easy/http/actionable.rb +25 -3
  29. data/lib/ethon/easy/http/get.rb +5 -1
  30. data/lib/ethon/easy/http/post.rb +1 -0
  31. data/lib/ethon/easy/http/postable.rb +9 -2
  32. data/lib/ethon/easy/http/putable.rb +19 -4
  33. data/lib/ethon/easy/http.rb +16 -4
  34. data/lib/ethon/easy/informations.rb +51 -2
  35. data/lib/ethon/easy/mime.rb +29 -0
  36. data/lib/ethon/easy/mirror.rb +17 -4
  37. data/lib/ethon/easy/operations.rb +259 -11
  38. data/lib/ethon/easy/options.rb +327 -8
  39. data/lib/ethon/easy/queryable.rb +59 -14
  40. data/lib/ethon/easy/response_callbacks.rb +33 -12
  41. data/lib/ethon/easy/share.rb +28 -0
  42. data/lib/ethon/easy/url.rb +32 -0
  43. data/lib/ethon/easy/util.rb +19 -0
  44. data/lib/ethon/easy.rb +185 -10
  45. data/lib/ethon/errors/fork_safety.rb +10 -0
  46. data/lib/ethon/errors/form_add.rb +12 -0
  47. data/lib/ethon/errors/global_init.rb +1 -1
  48. data/lib/ethon/errors/header.rb +12 -0
  49. data/lib/ethon/errors/invalid_option.rb +3 -1
  50. data/lib/ethon/errors/invalid_value.rb +3 -1
  51. data/lib/ethon/errors/mime.rb +17 -0
  52. data/lib/ethon/errors/multi_add.rb +1 -1
  53. data/lib/ethon/errors/multi_assign.rb +12 -0
  54. data/lib/ethon/errors/multi_fdset.rb +1 -1
  55. data/lib/ethon/errors/multi_perform.rb +12 -0
  56. data/lib/ethon/errors/multi_poll.rb +12 -0
  57. data/lib/ethon/errors/multi_remove.rb +1 -1
  58. data/lib/ethon/errors/multi_timeout.rb +2 -2
  59. data/lib/ethon/errors/multi_wait.rb +12 -0
  60. data/lib/ethon/errors/multi_wakeup.rb +12 -0
  61. data/lib/ethon/errors/select.rb +1 -1
  62. data/lib/ethon/errors/set_option.rb +27 -0
  63. data/lib/ethon/errors/share.rb +17 -0
  64. data/lib/ethon/errors/unsupported_option.rb +23 -0
  65. data/lib/ethon/errors/url.rb +17 -0
  66. data/lib/ethon/errors.rb +13 -0
  67. data/lib/ethon/libc.rb +30 -1
  68. data/lib/ethon/loggable.rb +1 -2
  69. data/lib/ethon/multi/operations.rb +269 -25
  70. data/lib/ethon/multi/options.rb +86 -12
  71. data/lib/ethon/multi/stack.rb +175 -10
  72. data/lib/ethon/multi.rb +44 -12
  73. data/lib/ethon/version.rb +1 -1
  74. data/lib/ethon.rb +8 -9
  75. metadata +52 -4
@@ -9,15 +9,34 @@ module Ethon
9
9
  # @example Return the handle.
10
10
  # easy.handle
11
11
  #
12
- # @return [ FFI::Pointer ] A pointer to the curl easy handle.
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
- @handle ||= FFI::AutoPointer.new(Curl.easy_init, Curl.method(:easy_cleanup))
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
- # @param [ ::FFI::Pointer ] Easy handle that will be assigned.
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
- @handle = h
39
+ assign_handle(h, true)
21
40
  end
22
41
 
23
42
  # Perform the easy request.
@@ -25,24 +44,88 @@ module Ethon
25
44
  # @example Perform the request.
26
45
  # easy.perform
27
46
  #
28
- # @return [ Integer ] The return code.
47
+ # @return [ Symbol ] The return code.
29
48
  def perform
30
- @return_code = Curl.easy_perform(handle)
31
- if Ethon.logger.debug?
32
- Ethon.logger.debug { "ETHON: performed #{log_inspect}" }
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
+ #
40
106
  # @example Perform clean up.
41
107
  # easy.cleanup
42
108
  #
43
109
  # @return the result of the free which is nil
44
110
  def cleanup
45
- handle.free
111
+ ensure_not_performing!
112
+ ensure_not_attached_to_multi!
113
+
114
+ # Resolve the handle first: cleanup must not clear the response body if
115
+ # the handle is unusable (cleaned up or inherited across a fork).
116
+ handle_to_free = handle
117
+ begin
118
+ clear_response_body_io
119
+ ensure
120
+ # Free the native handle even when closing the response body fails.
121
+ begin
122
+ handle_to_free.free
123
+ ensure
124
+ clear_native_state
125
+ @handle = nil
126
+ @cleaned_up = true
127
+ end
128
+ end
46
129
  end
47
130
 
48
131
  # Prepare the easy. Options, headers and callbacks
@@ -59,6 +142,171 @@ module Ethon
59
142
  "in future versions."
60
143
  )
61
144
  end
145
+
146
+ private
147
+
148
+ # Allocate the native handle. Kept separate so #handle= can replace a
149
+ # handle and install the callbacks before the next option setter runs.
150
+ def create_handle
151
+ Curl.check_pid!
152
+ pointer = Curl.easy_init
153
+ raise NoMemoryError, "curl_easy_init failed" if pointer.null?
154
+ Curl::EasyHandle.new(pointer)
155
+ end
156
+
157
+ # Reapply options Ethon installed on the old handle. These are not user
158
+ # request options: without them #error_message and progress callbacks
159
+ # silently stop working on the replacement.
160
+ def restore_handle_options
161
+ if @error_buffer_enabled && @error_buffer
162
+ Curl.set_option(:errorbuffer, @error_buffer, @handle)
163
+ @handle.error_buffer = @error_buffer
164
+ # Do not report the previous transfer's error from the new handle.
165
+ @error_buffer.put_char(0, 0)
166
+ end
167
+ self.noprogress = @noprogress_value unless @noprogress_value.nil?
168
+ end
169
+
170
+ # Install a handle and, unless the native duplicate already carries the
171
+ # request options, clear the Ruby-side state that belonged to the handle
172
+ # it replaces.
173
+ def assign_handle(h, clear_state)
174
+ raise Errors::EthonError, "Easy has been cleaned up" if @cleaned_up
175
+
176
+ ensure_not_performing!
177
+ ensure_not_attached_to_multi!
178
+
179
+ previous = @handle
180
+ @handle =
181
+ case h
182
+ when nil
183
+ # Create the replacement now so it gets Ethon's callbacks even
184
+ # before an option setter or #perform touches it.
185
+ create_handle
186
+ when Curl::EasyHandle
187
+ h.check_pid!
188
+ # Two Easies must not share one owned wrapper: whichever cleans up
189
+ # first invalidates the handle for the other.
190
+ if h.owner && !h.owner.equal?(self)
191
+ raise ArgumentError, "easy handle is already owned by another Ethon handle"
192
+ end
193
+ h
194
+ when Curl::OwnedPointer
195
+ raise TypeError, "expected an easy handle, got #{h.class}"
196
+ when ::FFI::Pointer
197
+ raise ArgumentError, "cannot assign a null easy handle" if h.null?
198
+
199
+ Curl::EasyHandle.new(h)
200
+ else
201
+ raise TypeError, "expected an easy handle pointer, got #{h.class}"
202
+ end
203
+ @handle.owner = self if @handle
204
+
205
+ replaced = @handle && !@handle.equal?(previous)
206
+ if replaced
207
+ # A replacement handle has no Ethon callbacks or Ethon-managed
208
+ # options, so its response would otherwise not be collected. Retained
209
+ # user callbacks are replayed on top of the internal ones.
210
+ install_callbacks(@handle)
211
+ restore_handle_options
212
+ restore_callbacks_on(self)
213
+ end
214
+ release_replaced_handle(previous)
215
+ # Free the old handle before dropping the Ruby references it owned.
216
+ clear_replaced_handle_state if replaced && clear_state
217
+ @handle
218
+ end
219
+
220
+ # The replacement handle does not have the old handle's request options.
221
+ # Clear the Ruby-side state so the accessors do not describe options the
222
+ # new handle never received. Callbacks, the error buffer, and the
223
+ # progress setting are replayed by #assign_handle and stay in place.
224
+ def clear_replaced_handle_state
225
+ @url = nil
226
+ @headers = nil
227
+ @header_list = nil
228
+ @postfields = nil
229
+ @postfields_pointer = nil
230
+ @postfield_body_size = nil
231
+ @postfieldsize_value = nil
232
+ @curlu = nil
233
+ @mime = nil
234
+ @multipart_form = nil
235
+ @slists = nil
236
+ @blobs = nil
237
+ @share = nil
238
+ @escape = nil
239
+ @multipart = nil
240
+ reset_transfer_state
241
+ clear_callback_error
242
+ end
243
+
244
+ # Free the handle this Easy owned before the replacement. A handle owned
245
+ # by another Easy (the source of #dup) must stay alive for its owner.
246
+ def release_replaced_handle(previous)
247
+ return unless previous
248
+ return if previous.equal?(@handle)
249
+ return unless previous.owner.equal?(self)
250
+
251
+ previous.free
252
+ end
253
+
254
+ # Drop references to native data and callbacks once the handle is gone.
255
+ def clear_native_state
256
+ @body_write_callback = nil
257
+ @header_write_callback = nil
258
+ @debug_callback = nil
259
+ @progress_callback = nil
260
+ @read_callback = nil
261
+ @request_body = nil
262
+ @request_body_start = nil
263
+ @postfields = nil
264
+ @postfields_pointer = nil
265
+ @postfield_body_size = nil
266
+ @postfieldsize_value = nil
267
+ @header_list = nil
268
+ @multipart_form = nil
269
+ @mime = nil
270
+ @curlu = nil
271
+ @blobs = nil
272
+ @slists = nil
273
+ @procs = nil
274
+ @on_complete = nil
275
+ @on_headers = nil
276
+ @on_body = nil
277
+ @on_progress = nil
278
+ @progress_source = nil
279
+ @internal_progress_option = nil
280
+ @user_debug_callback = nil
281
+ @share = nil
282
+ @error_buffer = nil
283
+ @error_buffer_enabled = nil
284
+ @noprogress_value = nil
285
+ end
286
+
287
+ def ensure_not_performing!
288
+ if @completing
289
+ raise Errors::EthonError, "Easy is currently completing; wait for the callbacks to finish"
290
+ end
291
+ if @performing
292
+ raise Errors::EthonError, "Easy is currently performing; wait for the transfer to finish"
293
+ end
294
+ end
295
+
296
+ # Resetting is safe from a completion callback: the transfer has already
297
+ # finished and the handle no longer belongs to a Multi. A reset from a
298
+ # body, header, or progress callback still runs inside libcurl.
299
+ def ensure_not_in_transfer!
300
+ return if @completing
301
+
302
+ ensure_not_performing!
303
+ end
304
+
305
+ def ensure_not_attached_to_multi!
306
+ return unless defined?(@multi) && @multi
307
+
308
+ raise Errors::EthonError, "Easy is still attached to a Multi; remove it first"
309
+ end
62
310
  end
63
311
  end
64
312
  end
@@ -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
- @url = value
12
- Curl.set_option(:url, value, handle)
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
- define_method(method_name) do |value|
36
- Curl.set_option(opt, value, handle)
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
- @procs ||= {}
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