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
data/lib/ethon/easy/callbacks.rb
CHANGED
|
@@ -20,13 +20,20 @@ module Ethon
|
|
|
20
20
|
# @example Set callbacks.
|
|
21
21
|
# easy.set_callbacks
|
|
22
22
|
def set_callbacks
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
install_callbacks(handle)
|
|
24
|
+
reset_transfer_state
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reset the state that belongs to a single transfer. Reusing an Easy must
|
|
28
|
+
# replace the previous response instead of appending to it.
|
|
29
|
+
def reset_transfer_state
|
|
30
|
+
rewind_request_body
|
|
31
|
+
@response_body = @response_body_io ? nil : String.new
|
|
27
32
|
@response_headers = String.new
|
|
28
33
|
@headers_called = false
|
|
29
34
|
@debug_info = Ethon::Easy::DebugInfo.new
|
|
35
|
+
@request_body_read = 0
|
|
36
|
+
@return_code = nil
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
# Returns the body write callback.
|
|
@@ -37,10 +44,19 @@ module Ethon
|
|
|
37
44
|
# @return [ Proc ] The callback.
|
|
38
45
|
def body_write_callback
|
|
39
46
|
@body_write_callback ||= proc do |stream, size, num, object|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
begin
|
|
48
|
+
headers_user_callback_result = headers
|
|
49
|
+
result = body(chunk = stream.read_string(size * num))
|
|
50
|
+
if @response_body_io && result != :abort && headers_user_callback_result != :abort
|
|
51
|
+
write_response_body(@response_body_io, chunk)
|
|
52
|
+
elsif result == :unyielded && !@response_body_io && headers_user_callback_result != :abort
|
|
53
|
+
@response_body << chunk
|
|
54
|
+
end
|
|
55
|
+
(result != :abort && headers_user_callback_result != :abort) ? size * num : -1
|
|
56
|
+
rescue Exception => e
|
|
57
|
+
@callback_error ||= e
|
|
58
|
+
0
|
|
59
|
+
end
|
|
44
60
|
end
|
|
45
61
|
end
|
|
46
62
|
|
|
@@ -52,13 +68,21 @@ module Ethon
|
|
|
52
68
|
# @return [ Proc ] The callback.
|
|
53
69
|
def header_write_callback
|
|
54
70
|
@header_write_callback ||= proc {|stream, size, num, object|
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
begin
|
|
72
|
+
@response_headers << stream.read_string(size * num)
|
|
73
|
+
size * num
|
|
74
|
+
rescue Exception => e
|
|
75
|
+
@callback_error ||= e
|
|
76
|
+
0
|
|
77
|
+
end
|
|
57
78
|
}
|
|
58
79
|
end
|
|
59
80
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
81
|
+
# This callback is called for every debug message libcurl emits while
|
|
82
|
+
# verbose mode is enabled.
|
|
83
|
+
#
|
|
84
|
+
# Messages are written through +Ethon.logger+ at the debug level. The
|
|
85
|
+
# user observer registered with #debugfunction is called as well.
|
|
62
86
|
#
|
|
63
87
|
# @example Return the callback.
|
|
64
88
|
# easy.debug_callback
|
|
@@ -66,26 +90,37 @@ module Ethon
|
|
|
66
90
|
# @return [ Proc ] The callback.
|
|
67
91
|
def debug_callback
|
|
68
92
|
@debug_callback ||= proc {|handle, type, data, size, udata|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
begin
|
|
94
|
+
message = data.read_string(size)
|
|
95
|
+
@debug_info.add type, message
|
|
96
|
+
|
|
97
|
+
if [:ssl_data_in, :ssl_data_out].include?(type)
|
|
98
|
+
log_debug("[#{size} bytes data]")
|
|
99
|
+
elsif [:data_in, :data_out].include?(type)
|
|
100
|
+
# Body data stays in #debug_info; logging it would duplicate the
|
|
101
|
+
# response body in the application log.
|
|
102
|
+
else
|
|
103
|
+
log_debug(message)
|
|
104
|
+
end
|
|
105
|
+
@user_debug_callback.call(handle, type, data, size, udata) if @user_debug_callback
|
|
106
|
+
rescue Exception => e
|
|
107
|
+
@callback_error ||= e
|
|
78
108
|
end
|
|
79
109
|
0
|
|
80
110
|
}
|
|
81
111
|
end
|
|
82
112
|
|
|
83
113
|
def set_progress_callback
|
|
84
|
-
|
|
85
|
-
|
|
114
|
+
# CURLOPT_XFERINFOFUNCTION was added in libcurl 7.32.0.
|
|
115
|
+
option = if Curl.version_at_least?("7.32.0")
|
|
116
|
+
:xferinfofunction
|
|
86
117
|
else
|
|
87
|
-
|
|
118
|
+
:progressfunction
|
|
88
119
|
end
|
|
120
|
+
Curl.set_option(option, progress_callback, handle)
|
|
121
|
+
# Remember which callback is effective so a duplicate can restore it.
|
|
122
|
+
@progress_source = :internal
|
|
123
|
+
@internal_progress_option = option
|
|
89
124
|
end
|
|
90
125
|
|
|
91
126
|
# Returns the progress callback.
|
|
@@ -95,10 +130,34 @@ module Ethon
|
|
|
95
130
|
#
|
|
96
131
|
# @return [ Proc ] The callback.
|
|
97
132
|
def progress_callback
|
|
98
|
-
@progress_callback ||= proc
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
@progress_callback ||= proc do |_, dltotal, dlnow, ultotal, ulnow|
|
|
134
|
+
begin
|
|
135
|
+
progress(dltotal, dlnow, ultotal, ulnow)
|
|
136
|
+
0
|
|
137
|
+
rescue Exception => e
|
|
138
|
+
@callback_error ||= e
|
|
139
|
+
0
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A completion callback may start the next transfer on this Easy. Keep
|
|
145
|
+
# the finished transfer's callback error so the caller still sees it;
|
|
146
|
+
# the next transfer starts without it once the error is raised.
|
|
147
|
+
def clear_callback_error
|
|
148
|
+
@callback_error = nil unless @completing
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The transfer teardown in #perform, #complete, and Multi#check already
|
|
152
|
+
# finishes the response stream. Finishing it here again could close a
|
|
153
|
+
# stream a completion callback installed for the next transfer, or
|
|
154
|
+
# replace the callback error with a teardown error.
|
|
155
|
+
def raise_callback_error
|
|
156
|
+
return unless @callback_error
|
|
157
|
+
|
|
158
|
+
error = @callback_error
|
|
159
|
+
@callback_error = nil
|
|
160
|
+
raise error
|
|
102
161
|
end
|
|
103
162
|
|
|
104
163
|
# Set the read callback. This callback is used by libcurl to
|
|
@@ -110,35 +169,52 @@ module Ethon
|
|
|
110
169
|
# @param [ String ] body The body.
|
|
111
170
|
def set_read_callback(body)
|
|
112
171
|
@request_body_read = 0
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
172
|
+
@request_body = body
|
|
173
|
+
# Remember where a seekable body starts so reusing the Easy replays the
|
|
174
|
+
# same range instead of finding the IO at EOF.
|
|
175
|
+
@request_body_start = begin
|
|
176
|
+
body.pos if body.respond_to?(:seek) && body.respond_to?(:pos)
|
|
177
|
+
rescue IOError, SystemCallError
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
@read_callback = proc do |stream, size, num, object|
|
|
181
|
+
begin
|
|
182
|
+
size = size * num
|
|
183
|
+
remaining = if body.respond_to?(:byteslice)
|
|
184
|
+
body.bytesize - @request_body_read
|
|
185
|
+
elsif body.respond_to?(:read)
|
|
186
|
+
# IO-like bodies advance their own position.
|
|
187
|
+
body.size - body.pos if body.respond_to?(:size) && body.respond_to?(:pos)
|
|
188
|
+
elsif body.respond_to?(:size)
|
|
189
|
+
body.size - @request_body_read
|
|
190
|
+
end
|
|
122
191
|
|
|
123
|
-
|
|
124
|
-
size = left if size > left
|
|
192
|
+
size = remaining if remaining && size > remaining
|
|
125
193
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
chunk = if size > 0
|
|
195
|
+
if body.respond_to?(:byteslice)
|
|
196
|
+
body.byteslice(@request_body_read, size)
|
|
197
|
+
elsif body.respond_to?(:read)
|
|
198
|
+
body.read(size)
|
|
199
|
+
else
|
|
200
|
+
body[@request_body_read, size]
|
|
201
|
+
end
|
|
133
202
|
end
|
|
134
203
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
# libcurl expects the number of bytes actually copied into the
|
|
205
|
+
# buffer. IO-like bodies may return fewer bytes than requested.
|
|
206
|
+
chunk_size = chunk ? chunk.bytesize : 0
|
|
207
|
+
if chunk_size > 0
|
|
208
|
+
stream.write_string(chunk, chunk_size)
|
|
209
|
+
@request_body_read += chunk_size
|
|
210
|
+
end
|
|
211
|
+
chunk_size
|
|
212
|
+
rescue Exception => e
|
|
213
|
+
@callback_error ||= e
|
|
214
|
+
0
|
|
139
215
|
end
|
|
140
|
-
size
|
|
141
216
|
end
|
|
217
|
+
readfunction(&@read_callback)
|
|
142
218
|
end
|
|
143
219
|
|
|
144
220
|
# Returns the body read callback.
|
|
@@ -150,6 +226,56 @@ module Ethon
|
|
|
150
226
|
def read_callback
|
|
151
227
|
@read_callback
|
|
152
228
|
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# IO#write may accept fewer bytes than it was given, especially for
|
|
233
|
+
# nonblocking or wrapper IOs. libcurl must only see the chunk as handled
|
|
234
|
+
# once every byte reached the destination. IOs that return nil (or a
|
|
235
|
+
# non-integer) keep the historical "wrote everything" behavior.
|
|
236
|
+
def write_response_body(io, chunk)
|
|
237
|
+
offset = 0
|
|
238
|
+
total = chunk.bytesize
|
|
239
|
+
while offset < total
|
|
240
|
+
pending = offset.zero? ? chunk : chunk.byteslice(offset, total - offset)
|
|
241
|
+
written = io.write(pending)
|
|
242
|
+
return unless written.is_a?(Integer)
|
|
243
|
+
|
|
244
|
+
raise IOError, "response body write made no progress" if written <= 0
|
|
245
|
+
offset += written
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Install the callbacks that fill the Ruby-side response state. The
|
|
250
|
+
# handle is passed explicitly so callers do not have to go through
|
|
251
|
+
# #handle, which would create a handle as a side effect.
|
|
252
|
+
def install_callbacks(target)
|
|
253
|
+
Curl.set_option(:writefunction, body_write_callback, target)
|
|
254
|
+
Curl.set_option(:headerfunction, header_write_callback, target)
|
|
255
|
+
Curl.set_option(:debugfunction, debug_callback, target)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Log a libcurl debug message through Ethon.logger. The default level is
|
|
259
|
+
# INFO, so applications opt in by lowering the logger level.
|
|
260
|
+
def log_debug(message)
|
|
261
|
+
return if message.empty?
|
|
262
|
+
|
|
263
|
+
logger = Ethon.logger
|
|
264
|
+
return unless logger.debug?
|
|
265
|
+
|
|
266
|
+
logger.debug { message.sub(/\n+\z/, "") }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Seek a request body back to where it was when the callback was
|
|
270
|
+
# installed. Pipes and other non-seekable bodies are left alone.
|
|
271
|
+
def rewind_request_body
|
|
272
|
+
return unless @request_body_start
|
|
273
|
+
return unless @request_body.respond_to?(:seek)
|
|
274
|
+
|
|
275
|
+
@request_body.seek(@request_body_start)
|
|
276
|
+
rescue IOError, SystemCallError
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
153
279
|
end
|
|
154
280
|
end
|
|
155
281
|
end
|
data/lib/ethon/easy/features.rb
CHANGED
|
@@ -24,7 +24,10 @@ module Ethon
|
|
|
24
24
|
!!(Curl.version_info[:features] & Curl::VERSION_ASYNCHDNS)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
# CURLOPT_TIMEOUT_MS was added in libcurl 7.16.2.
|
|
28
|
+
def supports_timeout_ms?
|
|
29
|
+
Curl.version_at_least?("7.16.2")
|
|
30
|
+
end
|
|
28
31
|
|
|
29
32
|
end
|
|
30
33
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Ethon
|
|
3
|
+
class Easy
|
|
4
|
+
# This module contains logic for streaming response bodies to Ruby IOs.
|
|
5
|
+
module Files
|
|
6
|
+
# Stream the response body to a path or writable Ruby object. Ethon closes
|
|
7
|
+
# files it opens from paths; caller-provided objects remain caller-owned.
|
|
8
|
+
# Pass nil to restore in-memory response buffering.
|
|
9
|
+
def file=(destination)
|
|
10
|
+
# Resolve the handle first. Opening a path truncates it, so a cleaned
|
|
11
|
+
# up or fork-inherited handle must not touch the destination before the
|
|
12
|
+
# error is raised.
|
|
13
|
+
handle
|
|
14
|
+
|
|
15
|
+
# Install the new destination before releasing the old one. A failed
|
|
16
|
+
# open must leave the previous destination and its buffered response
|
|
17
|
+
# usable instead of leaving the Easy pointed at a path that cannot be
|
|
18
|
+
# opened.
|
|
19
|
+
# Pathname-like objects may also implement #write. Prefer the path when
|
|
20
|
+
# the object is not an IO (File and Tempfile expose #fileno).
|
|
21
|
+
if destination.respond_to?(:to_path) && !destination.respond_to?(:fileno)
|
|
22
|
+
replace_response_body_path(destination.to_path)
|
|
23
|
+
elsif destination.respond_to?(:write)
|
|
24
|
+
replace_response_body_io(destination)
|
|
25
|
+
elsif destination
|
|
26
|
+
replace_response_body_path(destination.to_s)
|
|
27
|
+
else
|
|
28
|
+
clear_response_body_io
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@response_body = @response_body_io ? nil : String.new
|
|
32
|
+
destination
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias writedata= file=
|
|
36
|
+
|
|
37
|
+
def prepare_response_body_io
|
|
38
|
+
return unless @response_body_path && !@response_body_io
|
|
39
|
+
|
|
40
|
+
@response_body_io = File.open(@response_body_path, "wb")
|
|
41
|
+
@owns_response_body_io = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def close_response_body_io
|
|
45
|
+
@response_body_io.close if @owns_response_body_io && @response_body_io && !@response_body_io.closed?
|
|
46
|
+
@response_body_io = nil
|
|
47
|
+
@owns_response_body_io = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear_response_body_io
|
|
51
|
+
close_response_body_io
|
|
52
|
+
@response_body_path = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Flush buffered writes without closing a caller-owned IO. Response
|
|
56
|
+
# callbacks and readers should see the complete body before the transfer
|
|
57
|
+
# is treated as finished.
|
|
58
|
+
def flush_response_body_io
|
|
59
|
+
io = @response_body_io
|
|
60
|
+
return unless io
|
|
61
|
+
return if io.respond_to?(:closed?) && io.closed?
|
|
62
|
+
|
|
63
|
+
io.flush if io.respond_to?(:flush)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Finish the destination used by one transfer. A completion callback may
|
|
67
|
+
# already have installed the destination for the next transfer; only the
|
|
68
|
+
# stream this transfer wrote to belongs to it.
|
|
69
|
+
#
|
|
70
|
+
# @param [ IO, nil ] io The destination that transfer used.
|
|
71
|
+
def finish_response_body_io(io = @response_body_io)
|
|
72
|
+
return unless @response_body_io && @response_body_io.equal?(io)
|
|
73
|
+
|
|
74
|
+
if @owns_response_body_io
|
|
75
|
+
close_response_body_io
|
|
76
|
+
else
|
|
77
|
+
flush_response_body_io
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Open the new path first. If that fails, or if the old destination
|
|
84
|
+
# cannot be released, the previous state stays in place.
|
|
85
|
+
def replace_response_body_path(path)
|
|
86
|
+
io = File.open(path, "wb")
|
|
87
|
+
begin
|
|
88
|
+
clear_response_body_io
|
|
89
|
+
rescue Exception
|
|
90
|
+
close_quietly(io)
|
|
91
|
+
raise
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@response_body_path = path
|
|
95
|
+
@response_body_io = io
|
|
96
|
+
@owns_response_body_io = true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def replace_response_body_io(io)
|
|
100
|
+
clear_response_body_io
|
|
101
|
+
@response_body_io = io
|
|
102
|
+
@owns_response_body_io = false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def close_quietly(io)
|
|
106
|
+
io.close unless io.closed?
|
|
107
|
+
rescue StandardError
|
|
108
|
+
# The caller is already handling the failure that kept us from
|
|
109
|
+
# installing the new destination.
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/ethon/easy/form.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Ethon
|
|
|
57
57
|
# @return [ Boolean ] True if form is multipart, else false.
|
|
58
58
|
def multipart?
|
|
59
59
|
return true if @multipart
|
|
60
|
-
|
|
60
|
+
contains_file?(@params)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Add form elements to libcurl.
|
|
@@ -65,20 +65,77 @@ module Ethon
|
|
|
65
65
|
# @example Add form to libcurl.
|
|
66
66
|
# form.materialize
|
|
67
67
|
def materialize
|
|
68
|
+
unless Curl.respond_to?(:formadd)
|
|
69
|
+
raise NotImplementedError,
|
|
70
|
+
"curl_formadd is not supported by this libcurl; use the MIME API instead"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
ensure_named_fields!
|
|
68
74
|
query_pairs.each { |pair| form_add(pair.first.to_s, pair.last) }
|
|
69
75
|
end
|
|
70
76
|
|
|
77
|
+
# Build a MIME structure for this form.
|
|
78
|
+
#
|
|
79
|
+
# @example Build a MIME structure.
|
|
80
|
+
# form.mime
|
|
81
|
+
#
|
|
82
|
+
# @return [ Ethon::Curl::Mime ] The MIME structure.
|
|
83
|
+
def mime
|
|
84
|
+
@mime ||= begin
|
|
85
|
+
ensure_named_fields!
|
|
86
|
+
structure = Ethon::Curl::Mime.new(@easy)
|
|
87
|
+
query_pairs.each do |pair|
|
|
88
|
+
part = structure.addpart
|
|
89
|
+
name, content = pair
|
|
90
|
+
part.name = name.to_s
|
|
91
|
+
if content.is_a?(Queryable::FileInfo)
|
|
92
|
+
part.file = content[2]
|
|
93
|
+
part.filename = content[0]
|
|
94
|
+
part.type = content[1]
|
|
95
|
+
else
|
|
96
|
+
part.data = content.to_s
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
structure
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
71
103
|
private
|
|
72
104
|
|
|
73
|
-
|
|
74
|
-
|
|
105
|
+
# A multipart part needs a field name. A raw String body is one pair and
|
|
106
|
+
# has no name, which used to produce an unnamed part through the MIME API
|
|
107
|
+
# and a NoMethodError through the form API.
|
|
108
|
+
def ensure_named_fields!
|
|
109
|
+
return unless query_pairs.any? { |pair| pair.is_a?(String) }
|
|
110
|
+
|
|
111
|
+
raise ArgumentError, "multipart bodies require a Hash of fields"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def contains_file?(value)
|
|
115
|
+
case value
|
|
116
|
+
when Hash
|
|
117
|
+
value.values.any? { |item| contains_file?(item) }
|
|
75
118
|
when Array
|
|
119
|
+
value.any? { |item| contains_file?(item) }
|
|
120
|
+
else
|
|
121
|
+
value.is_a?(File) || (defined?(Tempfile) && value.is_a?(Tempfile))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def form_add(name, content)
|
|
126
|
+
name = validate_header_line(:name, name.to_s)
|
|
127
|
+
code = case content
|
|
128
|
+
when Queryable::FileInfo
|
|
129
|
+
# The form API writes these into Content-Disposition headers. Older
|
|
130
|
+
# libcurl versions do not escape them.
|
|
131
|
+
filename = validate_header_line(:filename, content[0].to_s)
|
|
132
|
+
content_type = validate_header_line(:content_type, content[1].to_s)
|
|
76
133
|
Curl.formadd(first, last,
|
|
77
134
|
:form_option, :copyname, :pointer, name,
|
|
78
135
|
:form_option, :namelength, :long, name.bytesize,
|
|
79
136
|
:form_option, :file, :string, content[2],
|
|
80
|
-
:form_option, :filename, :string,
|
|
81
|
-
:form_option, :contenttype, :string,
|
|
137
|
+
:form_option, :filename, :string, filename,
|
|
138
|
+
:form_option, :contenttype, :string, content_type,
|
|
82
139
|
:form_option, :end
|
|
83
140
|
)
|
|
84
141
|
else
|
|
@@ -91,6 +148,8 @@ module Ethon
|
|
|
91
148
|
)
|
|
92
149
|
end
|
|
93
150
|
|
|
151
|
+
raise Errors::FormAdd.new(code) unless code == 0
|
|
152
|
+
|
|
94
153
|
setup_garbage_collection
|
|
95
154
|
end
|
|
96
155
|
|
|
@@ -100,7 +159,7 @@ module Ethon
|
|
|
100
159
|
# to. So this results in (pseudo-c):
|
|
101
160
|
# form_data_cleanup_handler = *first
|
|
102
161
|
# curl_form_free(form_data_cleanup_handler)
|
|
103
|
-
@form_data_cleanup_handler ||=
|
|
162
|
+
@form_data_cleanup_handler ||= Curl::FormData.new(@first.get_pointer(0))
|
|
104
163
|
end
|
|
105
164
|
end
|
|
106
165
|
end
|
data/lib/ethon/easy/header.rb
CHANGED
|
@@ -5,14 +5,19 @@ module Ethon
|
|
|
5
5
|
#
|
|
6
6
|
# @api private
|
|
7
7
|
module Header
|
|
8
|
-
# Return headers, return empty hash if none.
|
|
8
|
+
# Return the request headers, return empty hash if none.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# The returned hash is an independent snapshot of the headers that were
|
|
11
|
+
# sent to libcurl. Mutating the hash or a value passed to #headers=
|
|
12
|
+
# afterwards cannot change either the handle or this snapshot, and
|
|
13
|
+
# mutating the returned hash or its values does not change the request.
|
|
12
14
|
#
|
|
13
|
-
# @
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
# @example Return the request headers.
|
|
16
|
+
# easy.request_headers
|
|
17
|
+
#
|
|
18
|
+
# @return [ Hash ] The request headers.
|
|
19
|
+
def request_headers
|
|
20
|
+
copy_headers(@headers || {})
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
# Set the headers.
|
|
@@ -20,17 +25,25 @@ module Ethon
|
|
|
20
25
|
# @example Set the headers.
|
|
21
26
|
# easy.headers = {'User-Agent' => 'ethon'}
|
|
22
27
|
#
|
|
23
|
-
# @param [ Hash ]
|
|
24
|
-
def headers=(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
# @param [ Hash ] value The headers.
|
|
29
|
+
def headers=(value)
|
|
30
|
+
# Snapshot before building the list so the stored value is exactly the
|
|
31
|
+
# set that libcurl accepted. Copy the values too: mutating a
|
|
32
|
+
# caller-owned header value in place would otherwise change the
|
|
33
|
+
# snapshot.
|
|
34
|
+
headers = copy_headers(value || {})
|
|
35
|
+
list = build_slist(headers)
|
|
36
|
+
begin
|
|
37
|
+
Curl.set_option(:httpheader, list, handle)
|
|
38
|
+
rescue Exception
|
|
39
|
+
Curl.slist_free_all(list) if list
|
|
40
|
+
raise
|
|
29
41
|
end
|
|
30
|
-
Curl.set_option(:httpheader, header_list, handle)
|
|
31
42
|
|
|
32
|
-
@
|
|
43
|
+
@headers = headers.freeze
|
|
44
|
+
@header_list = list && Curl::Slist.new(list)
|
|
33
45
|
end
|
|
46
|
+
alias request_headers= headers=
|
|
34
47
|
|
|
35
48
|
# Return header_list.
|
|
36
49
|
#
|
|
@@ -54,7 +67,36 @@ module Ethon
|
|
|
54
67
|
#
|
|
55
68
|
# @return [ String ] The composed header.
|
|
56
69
|
def compose_header(key, value)
|
|
57
|
-
|
|
70
|
+
header = if key.to_s.end_with?(';') && value.to_s.empty?
|
|
71
|
+
key.to_s
|
|
72
|
+
else
|
|
73
|
+
"#{key}: #{value}"
|
|
74
|
+
end
|
|
75
|
+
header = Util.escape_zero_byte(header)
|
|
76
|
+
# libcurl sends custom headers verbatim, so CR/LF would inject
|
|
77
|
+
# additional headers into the request.
|
|
78
|
+
Util.validate_header_line(:headers, header)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Copy a header hash so callers and the Easy cannot change each other's
|
|
84
|
+
# header values in place. Values that cannot be copied are kept as they
|
|
85
|
+
# are; header values are normally strings.
|
|
86
|
+
def copy_headers(headers)
|
|
87
|
+
headers.each_with_object({}) do |(key, value), copy|
|
|
88
|
+
copy[key] = begin
|
|
89
|
+
value.dup
|
|
90
|
+
rescue TypeError
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Build a curl_slist for the headers. Compose first so an invalid header
|
|
97
|
+
# does not leave a partial native list behind.
|
|
98
|
+
def build_slist(headers)
|
|
99
|
+
Curl::Slist.build(headers.map { |key, value| compose_header(key, value) })
|
|
58
100
|
end
|
|
59
101
|
end
|
|
60
102
|
end
|