ethon 0.19.0 → 0.19.2
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 +69 -1
- data/README.md +55 -8
- data/lib/ethon/curl.rb +44 -0
- data/lib/ethon/easy/form.rb +129 -34
- data/lib/ethon/easy/informations.rb +30 -0
- data/lib/ethon/easy/mirror.rb +7 -1
- data/lib/ethon/easy/operations.rb +6 -1
- data/lib/ethon/easy/options.rb +11 -7
- data/lib/ethon/easy/queryable.rb +5 -0
- data/lib/ethon/errors/fork_safety.rb +2 -2
- data/lib/ethon/libc.rb +5 -0
- data/lib/ethon/multi/options.rb +29 -1
- data/lib/ethon/multi/stack.rb +6 -0
- data/lib/ethon/multi.rb +7 -3
- data/lib/ethon/multipart.rb +115 -0
- data/lib/ethon/version.rb +1 -1
- data/lib/ethon.rb +1 -0
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5174f0935f97734b2720d7a3445117049a2651cf2796bdfe0b85cee8742037c0
|
|
4
|
+
data.tar.gz: 14ec1a62317eff2251a2658dffb7cf8350071dee462fd1fb86c15888a78ffe31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44c508f1dff76285672f55f80a69390ee9f5e472bba9894a0b74328af214a1c9eae9288114fcfd1dd79aa20a70911c0fa864982a9726711fb7e8757d8e4cec08
|
|
7
|
+
data.tar.gz: 85dcaebb702402a4fc45c6db9de5412496aa4cf2b0705f1d09ca702a6ad2f77a065b4abbb1654da1705a22f5fc5a6315e734b3a0cadb374a702cac4265845f85
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
## Master
|
|
4
4
|
|
|
5
|
-
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.19.
|
|
5
|
+
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.19.2...master)
|
|
6
|
+
|
|
7
|
+
## 0.19.2
|
|
8
|
+
|
|
9
|
+
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.19.1...v0.19.2)
|
|
10
|
+
|
|
11
|
+
### Fixes and hardening
|
|
12
|
+
|
|
13
|
+
* Clear `CURLOPT_POSTFIELDSIZE` to 0 instead of -1 when `Easy#postfields=` or
|
|
14
|
+
`Easy#copypostfields=` clears the body. With a size of -1 libcurl treats the
|
|
15
|
+
body as indeterminate, sends the POST chunked, and reads it from the default
|
|
16
|
+
read callback, which is stdin, so the request waited until stdin was closed.
|
|
17
|
+
([#463](https://github.com/typhoeus/ethon/issues/463))
|
|
18
|
+
|
|
19
|
+
### Enhancements
|
|
20
|
+
|
|
21
|
+
* Mirror `certinfo`, so the certificate chain read with `certinfo: true`
|
|
22
|
+
survives the handle reset and reaches Typhoeus responses
|
|
23
|
+
([typhoeus/typhoeus#427](https://github.com/typhoeus/typhoeus/issues/427))
|
|
24
|
+
|
|
25
|
+
* Expose `Easy#primary_port`, `Easy#local_ip`, `Easy#local_port`,
|
|
26
|
+
`Easy#ssl_verifyresult`, `Easy#num_connects`, `Easy#os_errno`,
|
|
27
|
+
`Easy#http_connectcode`, `Easy#proxyauth_avail`, `Easy#condition_unmet`,
|
|
28
|
+
`Easy#header_size` and `Easy#content_type`. They are also included in the
|
|
29
|
+
mirror, so they survive the handle reset and show up in response options.
|
|
30
|
+
Everything in the mirror is read for every response, so the deprecated
|
|
31
|
+
`filetime`, `lastsocket` and `content_length_*` values and the RTSP and FTP
|
|
32
|
+
entry informations are left out in favour of the `_t` and `activesocket`
|
|
33
|
+
variants that are already exposed.
|
|
34
|
+
([#460](https://github.com/typhoeus/ethon/pull/460))
|
|
35
|
+
|
|
36
|
+
* Add `Ethon::Multipart::Part` for setting the content type and, optionally,
|
|
37
|
+
the filename of a single `multipart/form-data` part. Plain body values keep
|
|
38
|
+
their existing behavior.
|
|
39
|
+
([typhoeus/typhoeus#494](https://github.com/typhoeus/typhoeus/issues/494))
|
|
40
|
+
|
|
41
|
+
* Add `Ethon::Curl.allow_handles_after_fork!`, an opt-in for servers that fork
|
|
42
|
+
workers from a process that has already used libcurl and cannot `exec`.
|
|
43
|
+
Called in the child before its first handle, it lets the child create new
|
|
44
|
+
handles on the libcurl state it inherited instead of raising
|
|
45
|
+
`Ethon::Errors::ForkSafety`. Handles created before the fork are still
|
|
46
|
+
rejected, as is `Ethon::Curl.cleanup` in the child, and the default is
|
|
47
|
+
unchanged. The opt-in is not supported on macOS, where it raises
|
|
48
|
+
`Ethon::Errors::ForkSafety` in a child whose parent initialized libcurl.
|
|
49
|
+
([#462](https://github.com/typhoeus/ethon/issues/462))
|
|
50
|
+
|
|
51
|
+
## 0.19.1
|
|
52
|
+
|
|
53
|
+
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.19.0...v0.19.1)
|
|
54
|
+
|
|
55
|
+
### Behavior changes
|
|
56
|
+
|
|
57
|
+
* Size a Multi's connection cache to twice the largest number of easy handles
|
|
58
|
+
attached at once. libcurl derives its default `CURLMOPT_MAXCONNECTS` from the
|
|
59
|
+
number of handles attached when a connection is returned, but Ethon detaches
|
|
60
|
+
finished handles immediately, which made the default shrink during a run and
|
|
61
|
+
close connections that later requests could have reused. An explicit
|
|
62
|
+
`maxconnects:` still takes precedence.
|
|
63
|
+
([#455](https://github.com/typhoeus/ethon/pull/455))
|
|
64
|
+
|
|
65
|
+
### Fixes and hardening
|
|
66
|
+
|
|
67
|
+
* Allow `Easy#cleanup` from an `on_complete` callback. The transfer has
|
|
68
|
+
finished and a Multi detaches the handle before its callbacks run, so freeing
|
|
69
|
+
it is safe. Cleanup from body, header, and progress callbacks is still
|
|
70
|
+
rejected. This fixes `:resolve` requests in Typhoeus 1.7.0, which clean up
|
|
71
|
+
instead of pooling to avoid leaking `CURLOPT_RESOLVE` entries.
|
|
72
|
+
([#456](https://github.com/typhoeus/ethon/pull/456),
|
|
73
|
+
[typhoeus#824](https://github.com/typhoeus/typhoeus/issues/824))
|
|
6
74
|
|
|
7
75
|
## 0.19.0
|
|
8
76
|
|
data/README.md
CHANGED
|
@@ -75,7 +75,29 @@ easy.perform
|
|
|
75
75
|
|
|
76
76
|
File values in a `:body` hash are sent as `multipart/form-data`. Ethon loads
|
|
77
77
|
the optional `mime-types` gem on the first upload to detect each part's content
|
|
78
|
-
type;
|
|
78
|
+
type; with it absent, files fall back to `application/octet-stream`.
|
|
79
|
+
|
|
80
|
+
Wrap a value in `Ethon::Multipart::Part` to set the content type for that part,
|
|
81
|
+
and optionally the filename. A String with a filename is sent as an in-memory
|
|
82
|
+
upload.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
easy.http_request(
|
|
86
|
+
"www.example.com",
|
|
87
|
+
:post,
|
|
88
|
+
body: {
|
|
89
|
+
metadata: Ethon::Multipart::Part.new(
|
|
90
|
+
'{"a":1}',
|
|
91
|
+
content_type: "application/json"
|
|
92
|
+
),
|
|
93
|
+
file: Ethon::Multipart::Part.new(
|
|
94
|
+
File.open("photo.jpg"),
|
|
95
|
+
filename: "avatar.png",
|
|
96
|
+
content_type: "image/png"
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
```
|
|
79
101
|
|
|
80
102
|
## Parallel requests
|
|
81
103
|
|
|
@@ -228,13 +250,38 @@ With libcurl older than 7.84.0, create an Ethon handle before starting other
|
|
|
228
250
|
application threads so libcurl's global initialization happens while the
|
|
229
251
|
process is still single-threaded.
|
|
230
252
|
|
|
231
|
-
Ethon handles and initialized libcurl state must not be inherited
|
|
232
|
-
`fork`. If any `Easy` or `Multi` was created before forking, libcurl
|
|
233
|
-
initialized and the child cannot create new handles either; use
|
|
234
|
-
by `exec` in that case
|
|
235
|
-
all `Easy` and `Multi`
|
|
236
|
-
`Ethon::Errors::ForkSafety` when a child
|
|
237
|
-
state
|
|
253
|
+
Ethon handles and, by default, initialized libcurl state must not be inherited
|
|
254
|
+
across a `fork`. If any `Easy` or `Multi` was created before forking, libcurl
|
|
255
|
+
is already initialized and the child cannot create new handles either; use
|
|
256
|
+
`fork` followed by `exec` in that case, or opt in as described below. If
|
|
257
|
+
libcurl was not initialized before forking, create all `Easy` and `Multi`
|
|
258
|
+
instances after the fork. Ethon raises `Ethon::Errors::ForkSafety` when a child
|
|
259
|
+
process tries to use an inherited handle, or to use inherited libcurl state
|
|
260
|
+
without opting in.
|
|
261
|
+
|
|
262
|
+
A server that forks workers from a process that has already used libcurl, such
|
|
263
|
+
as Pitchfork with reforking or Puma with `fork_worker`, cannot `exec` without
|
|
264
|
+
losing the memory its workers share with that process. Instead, each worker can
|
|
265
|
+
opt in to creating new handles on the libcurl state it inherited. Call
|
|
266
|
+
`Ethon::Curl.allow_handles_after_fork!` in the worker before it creates its
|
|
267
|
+
first handle and before any other thread uses Ethon, for example from
|
|
268
|
+
Pitchfork's `after_worker_fork` hook:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
after_worker_fork do |_server, _worker|
|
|
272
|
+
Ethon::Curl.allow_handles_after_fork!
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Puma's equivalent hook is `before_worker_boot` (`on_worker_boot` before
|
|
277
|
+
Puma 7). Repeat calls in the same worker do nothing. The worker does not
|
|
278
|
+
initialize libcurl again, and it cannot clean up the libcurl state it
|
|
279
|
+
inherited: `Ethon::Curl.cleanup` raises `Ethon::Errors::ForkSafety` there, as
|
|
280
|
+
handles created before the fork still do. On macOS, system frameworks are not
|
|
281
|
+
safe to use after `fork` without `exec`, and libcurl can use them, for example
|
|
282
|
+
for TLS. The opt-in is not supported there and raises
|
|
283
|
+
`Ethon::Errors::ForkSafety` in a worker whose parent initialized libcurl, so
|
|
284
|
+
use `fork` followed by `exec` on macOS.
|
|
238
285
|
|
|
239
286
|
## License
|
|
240
287
|
|
data/lib/ethon/curl.rb
CHANGED
|
@@ -26,6 +26,11 @@ module Ethon
|
|
|
26
26
|
Libc.windows?
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# :nodoc:
|
|
30
|
+
def self.macos?
|
|
31
|
+
Libc.macos?
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
require 'ethon/curls/constants'
|
|
30
35
|
require 'ethon/curls/introspection_constants'
|
|
31
36
|
require 'ethon/curls/settings'
|
|
@@ -36,6 +41,7 @@ module Ethon
|
|
|
36
41
|
@blocking = true
|
|
37
42
|
|
|
38
43
|
@@initialized = false
|
|
44
|
+
@@initialized_pid = nil
|
|
39
45
|
@@loaded_pid = Process.pid
|
|
40
46
|
@@curl_mutex = Mutex.new
|
|
41
47
|
|
|
@@ -70,6 +76,7 @@ module Ethon
|
|
|
70
76
|
if not @@initialized
|
|
71
77
|
raise Errors::GlobalInit.new if Curl.global_init(GLOBAL_ALL) != 0
|
|
72
78
|
@@initialized = true
|
|
79
|
+
@@initialized_pid = Process.pid
|
|
73
80
|
Ethon.logger.debug("ETHON: Libcurl initialized") if Ethon.logger
|
|
74
81
|
end
|
|
75
82
|
}
|
|
@@ -85,10 +92,19 @@ module Ethon
|
|
|
85
92
|
# any other thread that uses these other libraries.
|
|
86
93
|
# See the description in libcurl of global environment requirements
|
|
87
94
|
# for details of how to use this function.
|
|
95
|
+
#
|
|
96
|
+
# @raise [ Ethon::Errors::ForkSafety ] In a child that opted in with
|
|
97
|
+
# allow_handles_after_fork! and inherited libcurl initialized by its
|
|
98
|
+
# parent.
|
|
88
99
|
def cleanup
|
|
89
100
|
check_pid!
|
|
90
101
|
@@curl_mutex.synchronize {
|
|
91
102
|
if @@initialized
|
|
103
|
+
if @@initialized_pid != Process.pid
|
|
104
|
+
raise Errors::ForkSafety.new(
|
|
105
|
+
"Ethon cannot clean up libcurl initialized before a fork; clean it up in the process that initialized it"
|
|
106
|
+
)
|
|
107
|
+
end
|
|
92
108
|
Curl.global_cleanup()
|
|
93
109
|
@@initialized = false
|
|
94
110
|
Ethon.logger.debug("ETHON: Libcurl cleanup") if Ethon.logger
|
|
@@ -105,6 +121,34 @@ module Ethon
|
|
|
105
121
|
@@curl_mutex = Mutex.new
|
|
106
122
|
end
|
|
107
123
|
|
|
124
|
+
# Let a forked child create new handles on the libcurl state it
|
|
125
|
+
# inherited. Call it in the child before its first handle; repeat calls
|
|
126
|
+
# do nothing. Handles created before the fork still raise ForkSafety,
|
|
127
|
+
# and so does cleanup, because the child did not initialize libcurl.
|
|
128
|
+
#
|
|
129
|
+
# @example Opt in from a Pitchfork worker's after_worker_fork hook.
|
|
130
|
+
# Ethon::Curl.allow_handles_after_fork!
|
|
131
|
+
#
|
|
132
|
+
# @raise [ Ethon::Errors::ForkSafety ] On macOS, in a child whose parent
|
|
133
|
+
# initialized libcurl.
|
|
134
|
+
#
|
|
135
|
+
# @api public
|
|
136
|
+
def allow_handles_after_fork!
|
|
137
|
+
return if @@loaded_pid == Process.pid
|
|
138
|
+
|
|
139
|
+
if @@initialized && macos?
|
|
140
|
+
raise Errors::ForkSafety.new(
|
|
141
|
+
"Ethon cannot use libcurl inherited across a fork on macOS; use fork+exec"
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Leave @@initialized set: the child inherited the parent's libcurl
|
|
146
|
+
# global state, so it must not run curl_global_init again.
|
|
147
|
+
@@loaded_pid = Process.pid
|
|
148
|
+
@@curl_mutex = Mutex.new
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
108
152
|
end
|
|
109
153
|
end
|
|
110
154
|
end
|
data/lib/ethon/easy/form.rb
CHANGED
|
@@ -48,8 +48,10 @@ module Ethon
|
|
|
48
48
|
@last ||= FFI::MemoryPointer.new(:pointer)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Return if form is multipart. The form is multipart
|
|
52
|
-
# when it contains a file or
|
|
51
|
+
# Return if form is multipart. The form is multipart when the multipart
|
|
52
|
+
# option is set, when it contains a file, or when a value is wrapped in a
|
|
53
|
+
# Multipart::Part that carries a content type or a filename. A wrapped
|
|
54
|
+
# plain value is encoded like an unwrapped one.
|
|
53
55
|
#
|
|
54
56
|
# @example Return if form is multipart.
|
|
55
57
|
# form.multipart?
|
|
@@ -57,7 +59,7 @@ module Ethon
|
|
|
57
59
|
# @return [ Boolean ] True if form is multipart, else false.
|
|
58
60
|
def multipart?
|
|
59
61
|
return true if @multipart
|
|
60
|
-
|
|
62
|
+
contains_multipart_part?(@params)
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
# Add form elements to libcurl.
|
|
@@ -88,13 +90,7 @@ module Ethon
|
|
|
88
90
|
part = structure.addpart
|
|
89
91
|
name, content = pair
|
|
90
92
|
part.name = name.to_s
|
|
91
|
-
|
|
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
|
|
93
|
+
add_mime_part(part, content)
|
|
98
94
|
end
|
|
99
95
|
structure
|
|
100
96
|
end
|
|
@@ -111,48 +107,147 @@ module Ethon
|
|
|
111
107
|
raise ArgumentError, "multipart bodies require a Hash of fields"
|
|
112
108
|
end
|
|
113
109
|
|
|
114
|
-
def
|
|
110
|
+
def contains_multipart_part?(value)
|
|
115
111
|
case value
|
|
112
|
+
when Multipart::Part
|
|
113
|
+
value.upload? || !value.content_type.nil?
|
|
116
114
|
when Hash
|
|
117
|
-
value.values.any? { |item|
|
|
115
|
+
value.values.any? { |item| contains_multipart_part?(item) }
|
|
118
116
|
when Array
|
|
119
|
-
value.any? { |item|
|
|
117
|
+
value.any? { |item| contains_multipart_part?(item) }
|
|
120
118
|
else
|
|
121
|
-
|
|
119
|
+
Multipart::Part.file?(value)
|
|
122
120
|
end
|
|
123
121
|
end
|
|
124
122
|
|
|
123
|
+
def add_mime_part(part, content)
|
|
124
|
+
case content
|
|
125
|
+
when Multipart::Part
|
|
126
|
+
add_mime_part_from_part(part, content)
|
|
127
|
+
when Queryable::FileInfo
|
|
128
|
+
part.file = content[2]
|
|
129
|
+
part.filename = content[0]
|
|
130
|
+
part.type = content[1]
|
|
131
|
+
else
|
|
132
|
+
part.data = content.to_s
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def add_mime_part_from_part(part, content)
|
|
137
|
+
unless content.upload?
|
|
138
|
+
part.data = content.value.to_s
|
|
139
|
+
part.type = content.content_type unless content.content_type.nil?
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
filename = part_filename(content)
|
|
144
|
+
content_type = part_content_type(content, filename)
|
|
145
|
+
|
|
146
|
+
if content.file?
|
|
147
|
+
part.file = part_path(content)
|
|
148
|
+
else
|
|
149
|
+
part.data = content.value.to_s
|
|
150
|
+
end
|
|
151
|
+
part.filename = filename
|
|
152
|
+
part.type = content_type
|
|
153
|
+
end
|
|
154
|
+
|
|
125
155
|
def form_add(name, content)
|
|
126
156
|
name = validate_header_line(:name, name.to_s)
|
|
127
|
-
|
|
157
|
+
options = [
|
|
158
|
+
:form_option, :copyname, :pointer, name,
|
|
159
|
+
:form_option, :namelength, :long, name.bytesize
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
case content
|
|
163
|
+
when Multipart::Part
|
|
164
|
+
add_form_options_for_part(options, content)
|
|
128
165
|
when Queryable::FileInfo
|
|
129
|
-
|
|
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)
|
|
133
|
-
Curl.formadd(first, last,
|
|
134
|
-
:form_option, :copyname, :pointer, name,
|
|
135
|
-
:form_option, :namelength, :long, name.bytesize,
|
|
136
|
-
:form_option, :file, :string, content[2],
|
|
137
|
-
:form_option, :filename, :string, filename,
|
|
138
|
-
:form_option, :contenttype, :string, content_type,
|
|
139
|
-
:form_option, :end
|
|
140
|
-
)
|
|
166
|
+
add_form_file_options(options, content[2], content[0], content[1])
|
|
141
167
|
else
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
:form_option, :end
|
|
148
|
-
)
|
|
168
|
+
data = content.to_s
|
|
169
|
+
options.concat([
|
|
170
|
+
:form_option, :copycontents, :pointer, data,
|
|
171
|
+
:form_option, :contentslength, :long, data.bytesize
|
|
172
|
+
])
|
|
149
173
|
end
|
|
150
174
|
|
|
175
|
+
options << :form_option << :end
|
|
176
|
+
code = Curl.formadd(first, last, *options)
|
|
177
|
+
|
|
151
178
|
raise Errors::FormAdd.new(code) unless code == 0
|
|
152
179
|
|
|
153
180
|
setup_garbage_collection
|
|
154
181
|
end
|
|
155
182
|
|
|
183
|
+
def add_form_options_for_part(options, content)
|
|
184
|
+
unless content.upload?
|
|
185
|
+
data = content.value.to_s
|
|
186
|
+
options.concat([
|
|
187
|
+
:form_option, :copycontents, :pointer, data,
|
|
188
|
+
:form_option, :contentslength, :long, data.bytesize
|
|
189
|
+
])
|
|
190
|
+
return if content.content_type.nil?
|
|
191
|
+
|
|
192
|
+
content_type = validate_header_line(:content_type, content.content_type.to_s)
|
|
193
|
+
options.concat([:form_option, :contenttype, :string, content_type])
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
filename = part_filename(content)
|
|
198
|
+
content_type = part_content_type(content, filename)
|
|
199
|
+
|
|
200
|
+
if content.file?
|
|
201
|
+
add_form_file_options(options, part_path(content), filename, content_type)
|
|
202
|
+
else
|
|
203
|
+
add_form_buffer_options(options, content.value.to_s, filename, content_type)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# libcurl keeps the path and opens the file when the form is sent, so
|
|
208
|
+
# make it absolute in case the process changes directory in between.
|
|
209
|
+
def part_path(part)
|
|
210
|
+
File.expand_path(part.value.path)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def part_filename(part)
|
|
214
|
+
part.filename || File.basename(part.value.path)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def part_content_type(part, filename)
|
|
218
|
+
part.content_type || mime_type(filename)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def add_form_file_options(options, path, filename, content_type)
|
|
222
|
+
# The form API writes these into Content-Disposition headers. Older
|
|
223
|
+
# libcurl versions do not escape them.
|
|
224
|
+
filename = validate_header_line(:filename, filename.to_s)
|
|
225
|
+
content_type = validate_header_line(:content_type, content_type.to_s)
|
|
226
|
+
options.concat([
|
|
227
|
+
:form_option, :file, :string, path,
|
|
228
|
+
:form_option, :filename, :string, filename,
|
|
229
|
+
:form_option, :contenttype, :string, content_type
|
|
230
|
+
])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def add_form_buffer_options(options, data, filename, content_type)
|
|
234
|
+
filename = validate_header_line(:filename, filename.to_s)
|
|
235
|
+
content_type = validate_header_line(:content_type, content_type.to_s)
|
|
236
|
+
|
|
237
|
+
# CURLFORM_BUFFERPTR does not copy the data, so keep a stable copy
|
|
238
|
+
# alive until libcurl has read the form during perform. The caller's
|
|
239
|
+
# String can be changed after setup, and the GC can move it.
|
|
240
|
+
buffer = FFI::MemoryPointer.from_string(data)
|
|
241
|
+
(@form_buffers ||= []) << buffer
|
|
242
|
+
|
|
243
|
+
options.concat([
|
|
244
|
+
:form_option, :buffer, :string, filename,
|
|
245
|
+
:form_option, :bufferptr, :pointer, buffer,
|
|
246
|
+
:form_option, :bufferlength, :long, data.bytesize,
|
|
247
|
+
:form_option, :contenttype, :string, content_type
|
|
248
|
+
])
|
|
249
|
+
end
|
|
250
|
+
|
|
156
251
|
def setup_garbage_collection
|
|
157
252
|
# first is a pointer to a pointer. Since it's a MemoryPointer it will
|
|
158
253
|
# auto clean itself up, but we need to clean up the object it points
|
|
@@ -9,6 +9,19 @@ module Ethon
|
|
|
9
9
|
|
|
10
10
|
# Holds available informations and their type, which is needed to
|
|
11
11
|
# request the informations from libcurl.
|
|
12
|
+
#
|
|
13
|
+
# Everything listed here is read for every response that builds a
|
|
14
|
+
# Mirror, so it stays limited to values that are cheap, not deprecated,
|
|
15
|
+
# and not tied to a single protocol. Deliberately left out:
|
|
16
|
+
#
|
|
17
|
+
# * CURLINFO_PRIVATE returns the pointer set with CURLOPT_PRIVATE, not a
|
|
18
|
+
# C string, so a string getter would read freed memory.
|
|
19
|
+
# * CURLINFO_FILETIME, CURLINFO_CONTENT_LENGTH_DOWNLOAD,
|
|
20
|
+
# CURLINFO_CONTENT_LENGTH_UPLOAD and CURLINFO_LASTSOCKET are superseded
|
|
21
|
+
# by filetime_t, content_length_*_t and activesocket, which are listed
|
|
22
|
+
# above.
|
|
23
|
+
# * The RTSP informations and CURLINFO_FTP_ENTRY_PATH are only set for
|
|
24
|
+
# those protocols.
|
|
12
25
|
AVAILABLE_INFORMATIONS = {
|
|
13
26
|
# The _T variants use curl_off_t and do not lose precision for large
|
|
14
27
|
# values or long-running transfers.
|
|
@@ -39,13 +52,24 @@ module Ethon
|
|
|
39
52
|
:httpauth_used => :long,
|
|
40
53
|
:proxyauth_used => :long,
|
|
41
54
|
:proxy_ssl_verifyresult => :long,
|
|
55
|
+
# Result of the SSL peer certificate verification. Zero means the
|
|
56
|
+
# certificate was verified successfully. (Added in 7.19.0)
|
|
57
|
+
:ssl_verifyresult => :long,
|
|
42
58
|
:proxy_error => :long,
|
|
43
59
|
:used_proxy => :long,
|
|
60
|
+
:local_port => :long,
|
|
61
|
+
:num_connects => :long,
|
|
62
|
+
:os_errno => :long,
|
|
63
|
+
:http_connectcode => :long,
|
|
64
|
+
:proxyauth_avail => :long,
|
|
65
|
+
:condition_unmet => :long,
|
|
66
|
+
:header_size => :long,
|
|
44
67
|
:scheme => :string,
|
|
45
68
|
:effective_method => :string,
|
|
46
69
|
:referer => :string,
|
|
47
70
|
:cainfo => :string,
|
|
48
71
|
:capath => :string,
|
|
72
|
+
:content_type => :string,
|
|
49
73
|
:activesocket => :socket,
|
|
50
74
|
:tls_ssl_ptr => :ptr,
|
|
51
75
|
|
|
@@ -100,6 +124,12 @@ module Ethon
|
|
|
100
124
|
# may be IPv6 if that's enabled.
|
|
101
125
|
:primary_ip => :string,
|
|
102
126
|
|
|
127
|
+
# Return the local IP address of the most recent connection.
|
|
128
|
+
:local_ip => :string,
|
|
129
|
+
|
|
130
|
+
# Return the destination port of the most recent connection.
|
|
131
|
+
:primary_port => :long,
|
|
132
|
+
|
|
103
133
|
# Return the last received HTTP, FTP or SMTP response code.
|
|
104
134
|
# The value will be zero if no server response code has
|
|
105
135
|
# been received. Note that a proxy's CONNECT response should
|
data/lib/ethon/easy/mirror.rb
CHANGED
|
@@ -8,9 +8,15 @@ module Ethon
|
|
|
8
8
|
# Raw pointers cannot be marshaled and point into native state that can
|
|
9
9
|
# go away when the handle is reset. Applications serialize a mirror (for
|
|
10
10
|
# example as a cached Typhoeus response), so leave them on the Easy.
|
|
11
|
+
#
|
|
12
|
+
# certinfo is appended by hand because libcurl returns a list of lists
|
|
13
|
+
# for it rather than a scalar, so it is not part of
|
|
14
|
+
# AVAILABLE_INFORMATIONS. It is read on every mirror like the rest,
|
|
15
|
+
# which costs one getinfo call that returns NULL when the Easy was not
|
|
16
|
+
# asked for the chain.
|
|
11
17
|
INFORMATIONS_TO_MIRROR = Informations::AVAILABLE_INFORMATIONS.
|
|
12
18
|
reject { |_, type| type == :ptr }.keys +
|
|
13
|
-
[:return_code, :response_headers, :response_body, :debug_info, :error_message]
|
|
19
|
+
[:return_code, :response_headers, :response_body, :debug_info, :error_message, :certinfo]
|
|
14
20
|
|
|
15
21
|
INFORMATIONS_TO_LOG = [:effective_url, :response_code, :return_code, :total_time]
|
|
16
22
|
|
|
@@ -103,12 +103,17 @@ module Ethon
|
|
|
103
103
|
# Cleanup is terminal: the native handle is freed and the Easy cannot be
|
|
104
104
|
# used again. Reusing it raises Ethon::Errors::EthonError.
|
|
105
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
|
+
#
|
|
106
111
|
# @example Perform clean up.
|
|
107
112
|
# easy.cleanup
|
|
108
113
|
#
|
|
109
114
|
# @return the result of the free which is nil
|
|
110
115
|
def cleanup
|
|
111
|
-
|
|
116
|
+
ensure_not_in_transfer!
|
|
112
117
|
ensure_not_attached_to_multi!
|
|
113
118
|
|
|
114
119
|
# Resolve the handle first: cleanup must not clear the response body if
|
data/lib/ethon/easy/options.rb
CHANGED
|
@@ -219,9 +219,12 @@ module Ethon
|
|
|
219
219
|
@postfields = nil
|
|
220
220
|
@postfields_pointer = nil
|
|
221
221
|
@postfield_body_size = nil
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
# A zero size keeps libcurl on the null reader, which sends
|
|
223
|
+
# Content-Length: 0. With the size left at -1 libcurl treats the
|
|
224
|
+
# body as indeterminate, sends it chunked, and reads it from the
|
|
225
|
+
# default read callback, which is stdin.
|
|
226
|
+
Curl.set_option(:postfieldsize, 0, handle)
|
|
227
|
+
@postfieldsize_value = nil
|
|
225
228
|
else
|
|
226
229
|
string = value.to_s
|
|
227
230
|
string = string.dup.freeze unless string.frozen?
|
|
@@ -249,10 +252,11 @@ module Ethon
|
|
|
249
252
|
@postfields = nil
|
|
250
253
|
@postfields_pointer = nil
|
|
251
254
|
@postfield_body_size = nil
|
|
252
|
-
# Clear the size too
|
|
253
|
-
# is
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
# Clear the size too, otherwise libcurl keeps describing the copy
|
|
256
|
+
# that is being removed. Zero rather than -1 for the same reason as
|
|
257
|
+
# in #postfields=.
|
|
258
|
+
Curl.set_option(:postfieldsize, 0, handle)
|
|
259
|
+
@postfieldsize_value = nil
|
|
256
260
|
return value
|
|
257
261
|
end
|
|
258
262
|
|
data/lib/ethon/easy/queryable.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'ethon/multipart'
|
|
3
|
+
|
|
2
4
|
module Ethon
|
|
3
5
|
class Easy
|
|
4
6
|
|
|
@@ -186,6 +188,9 @@ module Ethon
|
|
|
186
188
|
|
|
187
189
|
def pairs_for(v, key, pairs)
|
|
188
190
|
case v
|
|
191
|
+
when Multipart::Part
|
|
192
|
+
# The form builders need the metadata, so keep the part intact.
|
|
193
|
+
pairs << [key, v]
|
|
189
194
|
when Hash, Array
|
|
190
195
|
recursively_generate_pairs(v, key, pairs)
|
|
191
196
|
when File, Tempfile
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
module Ethon
|
|
3
3
|
module Errors
|
|
4
4
|
class ForkSafety < EthonError
|
|
5
|
-
def initialize
|
|
6
|
-
super(
|
|
5
|
+
def initialize(message = "Ethon cannot use libcurl initialized before a fork; use fork+exec or initialize libcurl after forking")
|
|
6
|
+
super(message)
|
|
7
7
|
end
|
|
8
8
|
end
|
|
9
9
|
end
|
data/lib/ethon/libc.rb
CHANGED
data/lib/ethon/multi/options.rb
CHANGED
|
@@ -32,6 +32,9 @@ module Ethon
|
|
|
32
32
|
|
|
33
33
|
# Sets maxconnects option.
|
|
34
34
|
#
|
|
35
|
+
# An explicit value also disables the automatic sizing described in
|
|
36
|
+
# #raise_default_maxconnects.
|
|
37
|
+
#
|
|
35
38
|
# @example Set maxconnects option.
|
|
36
39
|
# multi.maxconnects = $value
|
|
37
40
|
#
|
|
@@ -39,7 +42,8 @@ module Ethon
|
|
|
39
42
|
#
|
|
40
43
|
# @return [ void ]
|
|
41
44
|
def maxconnects=(value)
|
|
42
|
-
|
|
45
|
+
@maxconnects_configured = true unless value.nil?
|
|
46
|
+
set_maxconnects(value)
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# Sets max_host_connections option.
|
|
@@ -156,6 +160,30 @@ module Ethon
|
|
|
156
160
|
|
|
157
161
|
private
|
|
158
162
|
|
|
163
|
+
# Raise the default connection cache size to twice the largest number
|
|
164
|
+
# of easy handles ever attached at once.
|
|
165
|
+
#
|
|
166
|
+
# libcurl derives its default from the number of handles attached when
|
|
167
|
+
# a connection is returned. Ethon detaches each handle as soon as its
|
|
168
|
+
# transfer finishes, so that number falls during a run and libcurl
|
|
169
|
+
# closes connections that later requests could have reused.
|
|
170
|
+
#
|
|
171
|
+
# @param [ Integer ] count The number of attached handles after the
|
|
172
|
+
# pending add.
|
|
173
|
+
#
|
|
174
|
+
# @return [ void ]
|
|
175
|
+
def raise_default_maxconnects(count)
|
|
176
|
+
return if @maxconnects_configured
|
|
177
|
+
return if count <= (@maxconnects_high_water ||= 0)
|
|
178
|
+
|
|
179
|
+
@maxconnects_high_water = count
|
|
180
|
+
set_maxconnects(2 * count)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def set_maxconnects(value)
|
|
184
|
+
Curl.set_option(:maxconnects, value_for(value, :int, :maxconnects), handle, :multi)
|
|
185
|
+
end
|
|
186
|
+
|
|
159
187
|
def retain_callback(option, callback)
|
|
160
188
|
if callback.nil?
|
|
161
189
|
@procs.delete(option) if @procs
|
data/lib/ethon/multi/stack.rb
CHANGED
|
@@ -48,6 +48,12 @@ module Ethon
|
|
|
48
48
|
multi_handle = handle
|
|
49
49
|
easy_handle = easy.handle
|
|
50
50
|
|
|
51
|
+
# Pin libcurl's default connection cache size before the handle is
|
|
52
|
+
# attached. Finished handles are detached immediately, which would
|
|
53
|
+
# otherwise make the default shrink during a run and close reusable
|
|
54
|
+
# connections.
|
|
55
|
+
raise_default_maxconnects(stack.size + 1)
|
|
56
|
+
|
|
51
57
|
stack << easy
|
|
52
58
|
begin
|
|
53
59
|
code = Curl.multi_add_handle(multi_handle, easy_handle)
|
data/lib/ethon/multi.rb
CHANGED
|
@@ -55,9 +55,13 @@ module Ethon
|
|
|
55
55
|
# as the Multi may invoke the callback. (Added in 7.16.0)
|
|
56
56
|
# @option options :maxconnects [Integer]
|
|
57
57
|
# Pass a long. The set number will be used as the maximum amount of
|
|
58
|
-
# simultaneously open connections that libcurl may cache.
|
|
59
|
-
#
|
|
60
|
-
#
|
|
58
|
+
# simultaneously open connections that libcurl may cache. When this is
|
|
59
|
+
# not set, Ethon sizes the cache to twice the largest number of easy
|
|
60
|
+
# handles attached to this Multi at once. libcurl derives its own default
|
|
61
|
+
# from the handles attached when a connection is returned, but Ethon
|
|
62
|
+
# detaches finished handles, which would otherwise shrink the cache during
|
|
63
|
+
# a run. Pass an explicit value to cap the cache yourself, or 0 to use
|
|
64
|
+
# libcurl's dynamic default.
|
|
61
65
|
# By setting this option, you can prevent the cache size from growing
|
|
62
66
|
# beyond the limit set by you.
|
|
63
67
|
# When the cache is full, curl closes the oldest one in the cache to
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ethon
|
|
4
|
+
# Multipart form data support.
|
|
5
|
+
module Multipart
|
|
6
|
+
# A single part of a multipart form body.
|
|
7
|
+
#
|
|
8
|
+
# Wrap a value to control the content type, and optionally the filename,
|
|
9
|
+
# that Ethon sends for that field. Values that are not wrapped keep the
|
|
10
|
+
# existing behaviour: files are detected with mime-types and plain values
|
|
11
|
+
# have no content type.
|
|
12
|
+
#
|
|
13
|
+
# @example A JSON field.
|
|
14
|
+
# Ethon::Multipart::Part.new('{"a":1}', content_type: "application/json")
|
|
15
|
+
#
|
|
16
|
+
# @example A file with an explicit filename and content type.
|
|
17
|
+
# Ethon::Multipart::Part.new(
|
|
18
|
+
# File.open("photo.jpg"),
|
|
19
|
+
# filename: "avatar.png",
|
|
20
|
+
# content_type: "image/png"
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# @example An in-memory upload.
|
|
24
|
+
# Ethon::Multipart::Part.new(
|
|
25
|
+
# "file contents",
|
|
26
|
+
# filename: "notes.txt",
|
|
27
|
+
# content_type: "text/plain"
|
|
28
|
+
# )
|
|
29
|
+
class Part
|
|
30
|
+
# @return [ String, File, Tempfile ] The value sent for this field.
|
|
31
|
+
attr_reader :value
|
|
32
|
+
|
|
33
|
+
# @return [ String, nil ] The filename sent for an upload.
|
|
34
|
+
attr_reader :filename
|
|
35
|
+
|
|
36
|
+
# @return [ String, nil ] The content type sent for this part.
|
|
37
|
+
attr_reader :content_type
|
|
38
|
+
|
|
39
|
+
# Create a new multipart part.
|
|
40
|
+
#
|
|
41
|
+
# A value that responds to #read must be a File or Tempfile. Ethon
|
|
42
|
+
# sends files by path; other IO objects have to be read by the caller.
|
|
43
|
+
#
|
|
44
|
+
# @param [ String, File, Tempfile ] value The value of the part.
|
|
45
|
+
# @param [ String, nil ] filename The filename to send. Setting it turns
|
|
46
|
+
# a String value into a file part.
|
|
47
|
+
# @param [ String, nil ] content_type The content type for this part.
|
|
48
|
+
#
|
|
49
|
+
# @raise [ ArgumentError ] If value is an IO that Ethon cannot upload.
|
|
50
|
+
def initialize(value, filename: nil, content_type: nil)
|
|
51
|
+
if value.respond_to?(:read) && !self.class.file?(value)
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
"multipart parts take a String or a File, not #{value.class}; read the IO first"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@value = value
|
|
57
|
+
@filename = filename
|
|
58
|
+
@content_type = content_type
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return whether the value is a file Ethon can read by path.
|
|
62
|
+
#
|
|
63
|
+
# @return [ Boolean ] True for File and Tempfile values.
|
|
64
|
+
def file?
|
|
65
|
+
self.class.file?(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Return whether this part is an upload rather than a regular field.
|
|
69
|
+
# A filename turns in-memory data into an upload, matching curl's
|
|
70
|
+
# "-F name=value;filename=..." behaviour.
|
|
71
|
+
#
|
|
72
|
+
# @return [ Boolean ] True when the part has a file or a filename.
|
|
73
|
+
def upload?
|
|
74
|
+
file? || !filename.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
def self.file?(value)
|
|
79
|
+
value.is_a?(File) || (defined?(Tempfile) && value.is_a?(Tempfile))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Return the value as a string, so a part can still be encoded like a
|
|
83
|
+
# plain form value when a caller asks for a non-multipart body.
|
|
84
|
+
#
|
|
85
|
+
# @return [ String ] The string representation of the value.
|
|
86
|
+
def to_s
|
|
87
|
+
value.to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ==(other)
|
|
91
|
+
other.is_a?(Part) &&
|
|
92
|
+
value_key == other.value_key &&
|
|
93
|
+
filename == other.filename &&
|
|
94
|
+
content_type == other.content_type
|
|
95
|
+
end
|
|
96
|
+
alias eql? ==
|
|
97
|
+
|
|
98
|
+
def hash
|
|
99
|
+
[Part, value_key, filename, content_type].hash
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The value used by #== and #hash. Files compare by path because File
|
|
103
|
+
# itself compares by identity.
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
def value_key
|
|
107
|
+
file? ? value.path : value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspect
|
|
111
|
+
"#<#{self.class} value=#{value.inspect} filename=#{filename.inspect} content_type=#{content_type.inspect}>"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/ethon/version.rb
CHANGED
data/lib/ethon.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ethon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.19.
|
|
4
|
+
version: 0.19.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hans Hasselberg
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -128,16 +128,17 @@ files:
|
|
|
128
128
|
- lib/ethon/multi/operations.rb
|
|
129
129
|
- lib/ethon/multi/options.rb
|
|
130
130
|
- lib/ethon/multi/stack.rb
|
|
131
|
+
- lib/ethon/multipart.rb
|
|
131
132
|
- lib/ethon/version.rb
|
|
132
133
|
homepage: https://github.com/typhoeus/ethon
|
|
133
134
|
licenses:
|
|
134
135
|
- MIT
|
|
135
136
|
metadata:
|
|
136
137
|
bug_tracker_uri: https://github.com/typhoeus/ethon/issues
|
|
137
|
-
changelog_uri: https://github.com/typhoeus/ethon/blob/v0.19.
|
|
138
|
-
documentation_uri: https://www.rubydoc.info/gems/ethon/0.19.
|
|
138
|
+
changelog_uri: https://github.com/typhoeus/ethon/blob/v0.19.2/CHANGELOG.md
|
|
139
|
+
documentation_uri: https://www.rubydoc.info/gems/ethon/0.19.2
|
|
139
140
|
rubygems_mfa_required: 'true'
|
|
140
|
-
source_code_uri: https://github.com/typhoeus/ethon/tree/v0.19.
|
|
141
|
+
source_code_uri: https://github.com/typhoeus/ethon/tree/v0.19.2
|
|
141
142
|
post_install_message:
|
|
142
143
|
rdoc_options: []
|
|
143
144
|
require_paths:
|