ethon 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +681 -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 +259 -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 +86 -12
- data/lib/ethon/multi/stack.rb +175 -10
- data/lib/ethon/multi.rb +44 -12
- data/lib/ethon/version.rb +1 -1
- data/lib/ethon.rb +8 -9
- metadata +35 -6
data/README.md
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
[](https://badge.fury.io/rb/ethon)
|
|
2
2
|
[](https://github.com/typhoeus/ethon/actions/workflows/ruby.yml)
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# Ethon
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Ethon is a lightweight Ruby wrapper around [libcurl](https://curl.se/libcurl/),
|
|
7
|
+
built with [FFI](https://github.com/ffi/ffi). It exposes:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- `Ethon::Easy` for a single transfer.
|
|
10
|
+
- `Ethon::Multi` for concurrent transfers.
|
|
11
|
+
- libcurl options, callbacks, response streaming, debug output, and optional
|
|
12
|
+
lower-level URL, MIME, share, and header APIs.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
With bundler:
|
|
14
|
+
The name comes from Greek mythology: Ethon was a gigantic eagle and the son of
|
|
15
|
+
Typhoeus and Echidna.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
* [API documentation](https://rubydoc.info/github/typhoeus/ethon)
|
|
18
|
+
* [Changelog](https://github.com/typhoeus/ethon/blob/master/CHANGELOG.md)
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
## Installation
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Add the gem to your bundle:
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
```sh
|
|
25
|
+
bundle add ethon
|
|
26
|
+
```
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
Or install it directly:
|
|
26
29
|
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
easy.perform
|
|
30
|
-
#=> :ok
|
|
30
|
+
```sh
|
|
31
|
+
gem install ethon
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
Ethon requires Ruby 2.6 or newer and a libcurl shared library. Install libcurl
|
|
35
|
+
with your system package manager, or point `ETHON_CURL_LIBS` at a specific
|
|
36
|
+
build (see [Older libcurl versions](#older-libcurl-versions)).
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Create an `Easy` handle for a single transfer. The constructor accepts libcurl
|
|
41
|
+
options such as `followlocation`:
|
|
34
42
|
|
|
35
43
|
```ruby
|
|
36
44
|
easy = Ethon::Easy.new(url: "www.example.com", followlocation: true)
|
|
@@ -38,24 +46,22 @@ easy.perform
|
|
|
38
46
|
#=> :ok
|
|
39
47
|
```
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
After `perform` returns, read the response:
|
|
42
50
|
|
|
43
51
|
```ruby
|
|
44
|
-
easy = Ethon::Easy.new(url: "www.example.com", followlocation: true)
|
|
45
|
-
easy.perform
|
|
46
52
|
easy.response_code
|
|
47
53
|
#=> 200
|
|
48
54
|
easy.response_body
|
|
49
55
|
#=> "<!doctype html><html ..."
|
|
50
56
|
```
|
|
51
57
|
|
|
52
|
-
##
|
|
58
|
+
## HTTP helpers
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
`http_request` configures the URL, method, query parameters, and body:
|
|
55
61
|
|
|
56
62
|
```ruby
|
|
57
63
|
easy = Ethon::Easy.new
|
|
58
|
-
easy.http_request("www.example.com", :get, { params: {a: 1} })
|
|
64
|
+
easy.http_request("www.example.com", :get, { params: { a: 1 } })
|
|
59
65
|
easy.perform
|
|
60
66
|
#=> :ok
|
|
61
67
|
```
|
|
@@ -67,33 +73,170 @@ easy.perform
|
|
|
67
73
|
#=> :ok
|
|
68
74
|
```
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
File values in a `:body` hash are sent as `multipart/form-data`. Ethon loads
|
|
77
|
+
the optional `mime-types` gem on the first upload to detect each part's content
|
|
78
|
+
type; without it, parts use `application/octet-stream`.
|
|
79
|
+
|
|
80
|
+
## Parallel requests
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
Standard http2 servers require the client to connect once and create a session (multi) and then add simple requests to the multi handler.
|
|
75
|
-
The `perform` method then takes all the requests in the multi handler and sends them to the server.
|
|
82
|
+
Add `Easy` handles to a `Multi` and call `perform`:
|
|
76
83
|
|
|
77
|
-
See the following example
|
|
78
84
|
```ruby
|
|
79
85
|
multi = Ethon::Multi.new
|
|
80
|
-
|
|
86
|
+
2.times do
|
|
87
|
+
easy = Ethon::Easy.new(url: "www.example.com")
|
|
88
|
+
multi.add(easy)
|
|
89
|
+
end
|
|
90
|
+
multi.perform
|
|
91
|
+
```
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
## HTTP/2
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
Use `:http_version` to request HTTP/2. `:httpv2_tls` negotiates HTTP/2 over
|
|
96
|
+
TLS and uses HTTP/1.1 for plain HTTP. `:httpv2_0` also attempts the HTTP/2
|
|
97
|
+
Upgrade handshake on plain HTTP, which many older servers do not support.
|
|
98
|
+
`:httpv2_prior_knowledge` starts HTTP/2 without negotiation. See the
|
|
99
|
+
[libcurl HTTP/2 documentation](https://everything.curl.dev/http/versions/http2.html)
|
|
100
|
+
for details.
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
HTTP/2 works with a single easy handle. A multi handle lets several requests
|
|
103
|
+
share the connection and multiplex them:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
multi = Ethon::Multi.new
|
|
107
|
+
easy = Ethon::Easy.new
|
|
108
|
+
|
|
109
|
+
easy.http_request(
|
|
110
|
+
"https://www.example.com/get",
|
|
111
|
+
:get,
|
|
112
|
+
{ http_version: :httpv2_tls }
|
|
113
|
+
)
|
|
91
114
|
|
|
92
115
|
multi.add(easy)
|
|
93
116
|
multi.perform
|
|
94
117
|
```
|
|
95
118
|
|
|
96
|
-
##
|
|
119
|
+
## Lower-level libcurl APIs
|
|
120
|
+
|
|
121
|
+
Ethon exposes wrappers for some of libcurl's newer APIs. Optional bindings are
|
|
122
|
+
attached only when the loaded libcurl exports them. Wrappers for the URL, MIME,
|
|
123
|
+
share, header, and transfer helpers raise `NotImplementedError` when the
|
|
124
|
+
underlying function is missing; introspection helpers return `nil` or `[]`.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# URL API
|
|
128
|
+
url = Ethon::Curl::Url.new("https://example.com/a")
|
|
129
|
+
url.get(:host) #=> "example.com"
|
|
130
|
+
url.set(:path, "/b")
|
|
131
|
+
url.to_s #=> "https://example.com/b"
|
|
132
|
+
|
|
133
|
+
# MIME API
|
|
134
|
+
easy = Ethon::Easy.new(url: "http://example.com/upload")
|
|
135
|
+
mime = Ethon::Curl::Mime.new(easy)
|
|
136
|
+
part = mime.addpart
|
|
137
|
+
part.name = "file"
|
|
138
|
+
part.file = "/tmp/example.txt"
|
|
139
|
+
part.headers = { "X-Checksum" => "abc123" }
|
|
140
|
+
easy.mime = mime
|
|
141
|
+
|
|
142
|
+
# Share cookies or DNS data between easy handles
|
|
143
|
+
share = Ethon::Curl::Share.new.share(:cookie)
|
|
144
|
+
easy.share = share
|
|
145
|
+
|
|
146
|
+
# Structured response headers and option metadata
|
|
147
|
+
easy.perform
|
|
148
|
+
easy.header("Content-Type")
|
|
149
|
+
easy.each_header { |header| puts "#{header.name}: #{header.value}" }
|
|
150
|
+
Ethon::Curl.easy_option("URL")
|
|
151
|
+
Ethon::Curl.protocols
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
When a share handle is used from more than one thread, set
|
|
155
|
+
`Share#lockfunction=` and `Share#unlockfunction=` so libcurl can synchronize
|
|
156
|
+
access to the shared data.
|
|
157
|
+
|
|
158
|
+
## Older libcurl versions
|
|
159
|
+
|
|
160
|
+
Ethon attaches some functions only when the loaded libcurl exports them.
|
|
161
|
+
Wrappers that check for a missing function raise `NotImplementedError`; the
|
|
162
|
+
introspection helpers return `nil` or `[]`. This keeps the gem loadable with
|
|
163
|
+
libcurl versions that predate the URL, MIME, share, or structured-header APIs.
|
|
164
|
+
|
|
165
|
+
libcurl silently ignores unknown options, which can make a request run with a
|
|
166
|
+
different configuration than intended. Ethon therefore raises
|
|
167
|
+
`Ethon::Errors::UnsupportedOption` when `curl_easy_setopt` reports
|
|
168
|
+
`CURLE_UNKNOWN_OPTION`, and `Ethon::Errors::SetOption` for other rejected
|
|
169
|
+
values. Guard optional settings when supporting older runtimes:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
easy.tcp_keepcnt = 30 if Ethon::Curl.supports_option?(:tcp_keepcnt)
|
|
173
|
+
easy.httpsig_key = key if Ethon::Curl.version_at_least?("8.23.0")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`Curl.supports_option?` asks libcurl's own option table on 7.73.0 and newer.
|
|
177
|
+
Older versions cannot answer that question, so it returns `true` for every
|
|
178
|
+
option Ethon knows; setting an unsupported option still raises. Multi options
|
|
179
|
+
are only checked against Ethon's table, because libcurl does not expose their
|
|
180
|
+
metadata.
|
|
181
|
+
|
|
182
|
+
The deprecated form API is attached only when the loaded libcurl exports it,
|
|
183
|
+
because libcurl can be built with `--disable-form-api`. Multipart bodies use the
|
|
184
|
+
MIME API when available and fall back to the form API otherwise.
|
|
185
|
+
|
|
186
|
+
Point `ETHON_CURL_LIBS` at a specific libcurl build to test against it:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
ETHON_CURL_LIBS=/opt/curl/lib/libcurl.so bundle exec rake
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
CI builds libcurl 7.88 in addition to the system package and 8.23 to catch
|
|
193
|
+
accidental use of newer symbols and options. The 8.23 build uses
|
|
194
|
+
`--disable-form-api`, so the missing form API path is exercised too.
|
|
195
|
+
|
|
196
|
+
## Debugging
|
|
197
|
+
|
|
198
|
+
Set `verbose: true` on an `Easy` to collect libcurl's debug messages.
|
|
199
|
+
`Easy#debug_info` returns a `DebugInfo` object; use `#to_a` for every message
|
|
200
|
+
or `#to_h` to group them by type (such as `:text`, `:header_in`, and
|
|
201
|
+
`:header_out`). `Easy#debugfunction` registers an observer. The messages are
|
|
202
|
+
also written through `Ethon.logger` at the debug level, so configure it to see
|
|
203
|
+
them:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
Ethon.logger.level = Logger::DEBUG
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Response body data is not written to the log; it stays available through
|
|
210
|
+
`Easy#debug_info` and the `debugfunction` observer.
|
|
211
|
+
|
|
212
|
+
## Thread and fork safety
|
|
213
|
+
|
|
214
|
+
Ethon can be used from multiple threads, but it does not synchronize access to
|
|
215
|
+
individual handles. Follow [libcurl's thread-safety rules](https://curl.se/libcurl/c/threadsafe.html):
|
|
216
|
+
|
|
217
|
+
* Separate `Ethon::Easy` and `Ethon::Multi` instances may run concurrently in
|
|
218
|
+
different threads.
|
|
219
|
+
* Do not use the same `Easy` or `Multi` instance from more than one thread at a
|
|
220
|
+
time. This includes changing its options while a transfer is running.
|
|
221
|
+
* Callbacks run on the thread performing the transfer. Synchronize any shared
|
|
222
|
+
application state accessed by a callback.
|
|
223
|
+
* When using threads, set `nosignal: true` unless your libcurl uses a threaded
|
|
224
|
+
or c-ares resolver. See libcurl's signal guidance for the associated DNS
|
|
225
|
+
timeout and `SIGPIPE` considerations.
|
|
226
|
+
|
|
227
|
+
With libcurl older than 7.84.0, create an Ethon handle before starting other
|
|
228
|
+
application threads so libcurl's global initialization happens while the
|
|
229
|
+
process is still single-threaded.
|
|
230
|
+
|
|
231
|
+
Ethon handles and initialized libcurl state must not be inherited across a
|
|
232
|
+
`fork`. If any `Easy` or `Multi` was created before forking, libcurl is already
|
|
233
|
+
initialized and the child cannot create new handles either; use `fork` followed
|
|
234
|
+
by `exec` in that case. If libcurl was not initialized before forking, create
|
|
235
|
+
all `Easy` and `Multi` instances after the fork. Ethon raises
|
|
236
|
+
`Ethon::Errors::ForkSafety` when a child process tries to use inherited libcurl
|
|
237
|
+
state.
|
|
238
|
+
|
|
239
|
+
## License
|
|
97
240
|
|
|
98
241
|
(The MIT License)
|
|
99
242
|
|
data/lib/ethon/curl.rb
CHANGED
|
@@ -19,22 +19,31 @@ module Ethon
|
|
|
19
19
|
extend Ethon::Curls::FormOptions
|
|
20
20
|
extend Ethon::Curls::Messages
|
|
21
21
|
|
|
22
|
+
require 'ethon/curls/share_constants'
|
|
23
|
+
|
|
22
24
|
# :nodoc:
|
|
23
25
|
def self.windows?
|
|
24
26
|
Libc.windows?
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
require 'ethon/curls/constants'
|
|
30
|
+
require 'ethon/curls/introspection_constants'
|
|
28
31
|
require 'ethon/curls/settings'
|
|
29
32
|
require 'ethon/curls/classes'
|
|
33
|
+
require 'ethon/curls/url_constants'
|
|
30
34
|
extend Ethon::Curls::Functions
|
|
31
35
|
|
|
32
36
|
@blocking = true
|
|
33
37
|
|
|
34
38
|
@@initialized = false
|
|
39
|
+
@@loaded_pid = Process.pid
|
|
35
40
|
@@curl_mutex = Mutex.new
|
|
36
41
|
|
|
37
42
|
class << self
|
|
43
|
+
def version_info
|
|
44
|
+
curl_version_info(VERSION_NOW)
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
# This function sets up the program environment that libcurl needs.
|
|
39
48
|
# Think of it as an extension of the library loader.
|
|
40
49
|
#
|
|
@@ -56,6 +65,7 @@ module Ethon
|
|
|
56
65
|
#
|
|
57
66
|
# @raise [ Ethon::Errors::GlobalInit ] If Curl.global_init fails.
|
|
58
67
|
def init
|
|
68
|
+
check_pid!
|
|
59
69
|
@@curl_mutex.synchronize {
|
|
60
70
|
if not @@initialized
|
|
61
71
|
raise Errors::GlobalInit.new if Curl.global_init(GLOBAL_ALL) != 0
|
|
@@ -76,6 +86,7 @@ module Ethon
|
|
|
76
86
|
# See the description in libcurl of global environment requirements
|
|
77
87
|
# for details of how to use this function.
|
|
78
88
|
def cleanup
|
|
89
|
+
check_pid!
|
|
79
90
|
@@curl_mutex.synchronize {
|
|
80
91
|
if @@initialized
|
|
81
92
|
Curl.global_cleanup()
|
|
@@ -85,6 +96,17 @@ module Ethon
|
|
|
85
96
|
}
|
|
86
97
|
end
|
|
87
98
|
|
|
99
|
+
def check_pid!
|
|
100
|
+
return if @@loaded_pid == Process.pid
|
|
101
|
+
|
|
102
|
+
raise Errors::ForkSafety.new if @@initialized
|
|
103
|
+
|
|
104
|
+
@@loaded_pid = Process.pid
|
|
105
|
+
@@curl_mutex = Mutex.new
|
|
106
|
+
end
|
|
107
|
+
|
|
88
108
|
end
|
|
89
109
|
end
|
|
90
110
|
end
|
|
111
|
+
|
|
112
|
+
require 'ethon/curls/url'
|
data/lib/ethon/curls/classes.rb
CHANGED
|
@@ -1,6 +1,193 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'ethon/easy/util'
|
|
3
|
+
|
|
2
4
|
module Ethon
|
|
3
5
|
module Curl
|
|
6
|
+
class OwnedPointer < ::FFI::AutoPointer
|
|
7
|
+
OWNERS_MUTEX = Mutex.new
|
|
8
|
+
OWNERS = {}
|
|
9
|
+
|
|
10
|
+
def initialize(pointer)
|
|
11
|
+
@pid = Process.pid
|
|
12
|
+
registered = false
|
|
13
|
+
# Wrapping the same pointer twice would give two finalizers ownership
|
|
14
|
+
# of one native allocation. The first free marks it freed only for its
|
|
15
|
+
# own wrapper, so the other can still be used after free.
|
|
16
|
+
if pointer && !pointer.null?
|
|
17
|
+
OWNERS_MUTEX.synchronize do
|
|
18
|
+
if owner_pid = self.class.owners[pointer.address]
|
|
19
|
+
raise Errors::ForkSafety.new if owner_pid != @pid
|
|
20
|
+
|
|
21
|
+
raise ArgumentError, "native pointer is already owned by an Ethon handle"
|
|
22
|
+
end
|
|
23
|
+
self.class.owners[pointer.address] = @pid
|
|
24
|
+
registered = true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
super
|
|
29
|
+
rescue Exception
|
|
30
|
+
# Do not reserve an address for a wrapper that never got created.
|
|
31
|
+
if registered
|
|
32
|
+
OWNERS_MUTEX.synchronize do
|
|
33
|
+
self.class.owners.delete(pointer.address) if self.class.owners[pointer.address] == @pid
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
raise
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Explicitly free the native pointer. The wrapper must not be used
|
|
40
|
+
# afterwards, because FFI keeps the original address instead of
|
|
41
|
+
# clearing it.
|
|
42
|
+
def free
|
|
43
|
+
@freed = true
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_pid!
|
|
48
|
+
raise Errors::EthonError, "#{self.class} has already been freed" if @freed
|
|
49
|
+
raise Errors::ForkSafety.new if @pid != Process.pid
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Every owned pointer registers in the same table so callers can inspect
|
|
53
|
+
# all live native handles, regardless of their wrapper class.
|
|
54
|
+
def self.owners
|
|
55
|
+
OWNERS
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.release(pointer)
|
|
59
|
+
owner_pid = begin
|
|
60
|
+
OWNERS_MUTEX.synchronize { owners.delete(pointer.address) }
|
|
61
|
+
rescue ThreadError, SignalException
|
|
62
|
+
# Finalizers can run in a trap context where locking is forbidden,
|
|
63
|
+
# or be interrupted by a signal. The process is exiting at this
|
|
64
|
+
# point, so the unsynchronized delete is best effort.
|
|
65
|
+
owners.delete(pointer.address)
|
|
66
|
+
end
|
|
67
|
+
native_release(pointer) if owner_pid == Process.pid
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class EasyHandle < OwnedPointer
|
|
72
|
+
attr_accessor :error_buffer, :owner
|
|
73
|
+
|
|
74
|
+
def self.native_release(pointer)
|
|
75
|
+
Curl.easy_cleanup(pointer)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class MultiHandle < OwnedPointer
|
|
80
|
+
CALLBACKS_MUTEX = Mutex.new
|
|
81
|
+
|
|
82
|
+
# libcurl may invoke the socket callback from curl_multi_cleanup. Keep
|
|
83
|
+
# callbacks referenced until the native handle is done with them.
|
|
84
|
+
def self.callbacks
|
|
85
|
+
@callbacks ||= {}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.retain_callback(pointer, option, callback)
|
|
89
|
+
key = pointer.address
|
|
90
|
+
CALLBACKS_MUTEX.synchronize do
|
|
91
|
+
if callback
|
|
92
|
+
(callbacks[key] ||= {})[option] = callback
|
|
93
|
+
elsif (registry = callbacks[key])
|
|
94
|
+
registry.delete(option)
|
|
95
|
+
callbacks.delete(key) if registry.empty?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.native_release(pointer)
|
|
101
|
+
Curl.multi_cleanup(pointer)
|
|
102
|
+
ensure
|
|
103
|
+
begin
|
|
104
|
+
CALLBACKS_MUTEX.synchronize { callbacks.delete(pointer.address) }
|
|
105
|
+
rescue ThreadError, SignalException
|
|
106
|
+
# Finalizers can run in a trap context where locking is forbidden.
|
|
107
|
+
callbacks.delete(pointer.address)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class Slist < OwnedPointer
|
|
113
|
+
# Build a curl_slist from lines. Returns the raw pointer, which is owned
|
|
114
|
+
# by the caller; nil when there are no lines.
|
|
115
|
+
def self.build(lines)
|
|
116
|
+
list = nil
|
|
117
|
+
begin
|
|
118
|
+
lines.each do |line|
|
|
119
|
+
# libcurl writes slist entries verbatim. For list options such as
|
|
120
|
+
# FTP quotes and SMTP recipients, CR or LF would inject another
|
|
121
|
+
# protocol command, so reject them at the Ethon helper boundary.
|
|
122
|
+
value = Ethon::Easy::Util.validate_header_line(:slist, line.to_s)
|
|
123
|
+
appended = Curl.slist_append(list, value)
|
|
124
|
+
# libcurl returns NULL (not nil) and leaves the old list intact.
|
|
125
|
+
if appended.nil? || appended.null?
|
|
126
|
+
raise NoMemoryError, "curl_slist_append failed"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
list = appended
|
|
130
|
+
end
|
|
131
|
+
list
|
|
132
|
+
rescue Exception
|
|
133
|
+
Curl.slist_free_all(list) if list
|
|
134
|
+
raise
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def to_a
|
|
139
|
+
check_pid!
|
|
140
|
+
SlistNode.to_a(self)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.native_release(pointer)
|
|
144
|
+
Curl.slist_free_all(pointer)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class SlistNode < ::FFI::Struct
|
|
149
|
+
layout :data, :pointer,
|
|
150
|
+
:next, :pointer
|
|
151
|
+
|
|
152
|
+
def self.to_a(pointer)
|
|
153
|
+
values = []
|
|
154
|
+
until pointer.null?
|
|
155
|
+
node = new(pointer)
|
|
156
|
+
values << node[:data].read_string unless node[:data].null?
|
|
157
|
+
pointer = node[:next]
|
|
158
|
+
end
|
|
159
|
+
values
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class FormData < OwnedPointer
|
|
164
|
+
def self.native_release(pointer)
|
|
165
|
+
Curl.formfree(pointer) if Curl.respond_to?(:formfree)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# struct curl_blob, used by the CURLOPT_*_BLOB options.
|
|
170
|
+
class Blob < ::FFI::Struct
|
|
171
|
+
layout :data, :pointer,
|
|
172
|
+
:len, :size_t,
|
|
173
|
+
:flags, :uint
|
|
174
|
+
|
|
175
|
+
# Copy a Ruby String into native memory and return a blob that keeps the
|
|
176
|
+
# copied buffer alive for as long as the blob is retained.
|
|
177
|
+
def self.from_string(value, flags = 0)
|
|
178
|
+
string = value.to_s
|
|
179
|
+
data = ::FFI::MemoryPointer.new(:char, string.bytesize)
|
|
180
|
+
data.put_bytes(0, string)
|
|
181
|
+
|
|
182
|
+
blob = new
|
|
183
|
+
blob[:data] = data
|
|
184
|
+
blob[:len] = string.bytesize
|
|
185
|
+
blob[:flags] = flags
|
|
186
|
+
blob.instance_variable_set(:@data, data)
|
|
187
|
+
blob
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
4
191
|
# :nodoc:
|
|
5
192
|
class MsgData < ::FFI::Union
|
|
6
193
|
layout :whatever, :pointer, :code, :easy_code
|
|
@@ -12,7 +199,7 @@ module Ethon
|
|
|
12
199
|
end
|
|
13
200
|
|
|
14
201
|
class VersionInfoData < ::FFI::Struct
|
|
15
|
-
layout :curl_version, :
|
|
202
|
+
layout :curl_version, :int,
|
|
16
203
|
:version, :string,
|
|
17
204
|
:version_num, :int,
|
|
18
205
|
:host, :string,
|
|
@@ -23,26 +210,98 @@ module Ethon
|
|
|
23
210
|
:protocols, :pointer
|
|
24
211
|
end
|
|
25
212
|
|
|
213
|
+
# curl_version_info_data through CURLVERSION_ELEVENTH. Only safe to use
|
|
214
|
+
# when the runtime age is at least CURLVERSION_ELEVENTH.
|
|
215
|
+
class VersionInfoDataFull < ::FFI::Struct
|
|
216
|
+
layout :curl_version, :int,
|
|
217
|
+
:version, :string,
|
|
218
|
+
:version_num, :int,
|
|
219
|
+
:host, :string,
|
|
220
|
+
:features, :int,
|
|
221
|
+
:ssl_version, :string,
|
|
222
|
+
:ssl_version_num, :long,
|
|
223
|
+
:libz_version, :string,
|
|
224
|
+
:protocols, :pointer,
|
|
225
|
+
:ares, :string,
|
|
226
|
+
:ares_num, :int,
|
|
227
|
+
:libidn, :string,
|
|
228
|
+
:iconv_ver_num, :int,
|
|
229
|
+
:libssh_version, :string,
|
|
230
|
+
:brotli_ver_num, :uint,
|
|
231
|
+
:brotli_version, :string,
|
|
232
|
+
:nghttp2_ver_num, :uint,
|
|
233
|
+
:nghttp2_version, :string,
|
|
234
|
+
:quic_version, :string,
|
|
235
|
+
:cainfo, :string,
|
|
236
|
+
:capath, :string,
|
|
237
|
+
:zstd_ver_num, :uint,
|
|
238
|
+
:zstd_version, :string,
|
|
239
|
+
:hyper_version, :string,
|
|
240
|
+
:gsasl_version, :string,
|
|
241
|
+
:feature_names, :pointer
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# struct curl_header.
|
|
245
|
+
class HeaderData < ::FFI::Struct
|
|
246
|
+
layout :name, :string,
|
|
247
|
+
:value, :string,
|
|
248
|
+
:amount, :size_t,
|
|
249
|
+
:index, :size_t,
|
|
250
|
+
:origin, :uint,
|
|
251
|
+
:anchor, :pointer
|
|
252
|
+
|
|
253
|
+
def name; self[:name]; end
|
|
254
|
+
def value; self[:value]; end
|
|
255
|
+
def amount; self[:amount]; end
|
|
256
|
+
def index; self[:index]; end
|
|
257
|
+
def origin; self[:origin]; end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Immutable copy of a curl_header. libcurl reuses the backing struct
|
|
261
|
+
# between curl_easy_nextheader calls, so values must be copied eagerly.
|
|
262
|
+
class Header
|
|
263
|
+
# libcurl documents the five low origin bits and reserves the rest. Mask
|
|
264
|
+
# the whole reserved range so callers see exactly the CURLH_* bits that
|
|
265
|
+
# HeaderOrigin describes, including bits libcurl might set later.
|
|
266
|
+
DOCUMENTED_ORIGIN_BITS = (1 << 5) - 1
|
|
267
|
+
|
|
268
|
+
attr_reader :name, :value, :amount, :index, :origin
|
|
269
|
+
|
|
270
|
+
def initialize(data)
|
|
271
|
+
@name = data.name
|
|
272
|
+
@value = data.value
|
|
273
|
+
@amount = data.amount
|
|
274
|
+
@index = data.index
|
|
275
|
+
@origin = data.origin & DOCUMENTED_ORIGIN_BITS
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# struct curl_easyoption.
|
|
280
|
+
class EasyOptionInfo < ::FFI::Struct
|
|
281
|
+
layout :name, :string,
|
|
282
|
+
:id, :easy_option,
|
|
283
|
+
:type, :easy_type,
|
|
284
|
+
:flags, :uint
|
|
285
|
+
|
|
286
|
+
def name; self[:name]; end
|
|
287
|
+
def id; self[:id]; end
|
|
288
|
+
def type; self[:type]; end
|
|
289
|
+
def flags; self[:flags]; end
|
|
290
|
+
def alias?; (flags & 1) != 0; end
|
|
291
|
+
end
|
|
292
|
+
|
|
26
293
|
# :nodoc:
|
|
27
294
|
class FDSet < ::FFI::Struct
|
|
28
295
|
if Curl.windows?
|
|
29
296
|
layout :fd_count, :uint,
|
|
297
|
+
# SOCKET is UINT_PTR, so use the platform pointer-sized integer.
|
|
30
298
|
# TODO: Make it future proof by dynamically grabbing FD_SETSIZE.
|
|
31
|
-
:fd_array, [:
|
|
299
|
+
:fd_array, [:size_t, 2048]
|
|
32
300
|
|
|
33
301
|
def clear; self[:fd_count] = 0; end
|
|
34
302
|
else
|
|
35
303
|
# https://github.com/typhoeus/ethon/issues/182
|
|
36
|
-
FD_SETSIZE =
|
|
37
|
-
# Allow to override the (new) default cap
|
|
38
|
-
if ENV['ETHON_FD_SIZE']
|
|
39
|
-
ENV['ETHON_FD_SIZE']
|
|
40
|
-
|
|
41
|
-
# auto-detect ulimit, but cap at 2^16
|
|
42
|
-
else
|
|
43
|
-
[::Ethon::Libc.getdtablesize, 65_536].min
|
|
44
|
-
end
|
|
45
|
-
end
|
|
304
|
+
FD_SETSIZE = ::Ethon::Libc.fd_size
|
|
46
305
|
|
|
47
306
|
layout :fds_bits, [:long, FD_SETSIZE / ::FFI::Type::LONG.size]
|
|
48
307
|
|
|
@@ -61,5 +320,18 @@ module Ethon
|
|
|
61
320
|
:usec, :suseconds_t
|
|
62
321
|
end
|
|
63
322
|
end
|
|
323
|
+
|
|
324
|
+
# :nodoc:
|
|
325
|
+
class Certinfo < ::FFI::Struct
|
|
326
|
+
layout :num_of_certs, :int,
|
|
327
|
+
:certinfo, :pointer
|
|
328
|
+
|
|
329
|
+
def to_a
|
|
330
|
+
return [] if self[:num_of_certs] == 0
|
|
331
|
+
self[:certinfo].read_array_of_pointer(self[:num_of_certs]).map do |pointer|
|
|
332
|
+
SlistNode.to_a(pointer)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
64
336
|
end
|
|
65
337
|
end
|