patron 0.14.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +83 -49
  4. data/ext/patron/extconf.rb +54 -23
  5. data/ext/patron/session_ext.c +1044 -923
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +21 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -144
  10. data/lib/patron/response.rb +28 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +114 -108
  13. data/lib/patron/util.rb +14 -12
  14. data/lib/patron/version.rb +3 -1
  15. data/lib/patron.rb +9 -15
  16. metadata +12 -58
  17. data/.autotest +0 -15
  18. data/.github/dependabot.yml +0 -6
  19. data/.github/workflows/ci.yml +0 -79
  20. data/.gitignore +0 -10
  21. data/.rspec +0 -0
  22. data/.travis.yml +0 -13
  23. data/.yardopts +0 -1
  24. data/Gemfile +0 -18
  25. data/Rakefile +0 -31
  26. data/ext/patron/.gitignore +0 -4
  27. data/ext/patron/membuffer.c +0 -85
  28. data/ext/patron/membuffer.h +0 -78
  29. data/ext/patron/sglib.h +0 -1952
  30. data/lib/patron/proxy_type.rb +0 -11
  31. data/patron.gemspec +0 -39
  32. data/pic.png +0 -0
  33. data/script/console +0 -11
  34. data/script/generate_client_certs +0 -65
  35. data/script/test_server +0 -7
  36. data/spec/header_parser_spec.rb +0 -101
  37. data/spec/patron_spec.rb +0 -28
  38. data/spec/request_spec.rb +0 -89
  39. data/spec/response_spec.rb +0 -145
  40. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  41. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  42. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  43. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  44. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  45. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  46. data/spec/session_spec.rb +0 -865
  47. data/spec/session_ssl_spec.rb +0 -431
  48. data/spec/spec_helper.rb +0 -40
  49. data/spec/support/certs/cacert.pem +0 -36
  50. data/spec/support/certs/client.p12 +0 -0
  51. data/spec/support/certs/client_ca.pem +0 -20
  52. data/spec/support/certs/client_cert.pem +0 -20
  53. data/spec/support/certs/client_key.pem +0 -30
  54. data/spec/support/certs/privkey.pem +0 -51
  55. data/spec/support/certs/rogue_client.p12 +0 -0
  56. data/spec/support/config.ru +0 -153
  57. data/spec/support/fork.rb +0 -70
  58. data/spec/support/raw_request_capture.rb +0 -93
  59. data/spec/support/test_server.rb +0 -29
  60. data/spec/support/valgrind.supp +0 -2
  61. data/spec/util_spec.rb +0 -71
@@ -1,1154 +1,1253 @@
1
+ /*
2
+ * Patron: libcurl bindings for Ruby.
3
+ *
4
+ * One Patron::Session owns one CURLM (multi handle), one template easy handle that
5
+ * every request is duplicated from, and one CURLSH that carries cookies, DNS entries
6
+ * and TLS sessions across requests. The multi handle owns the connection cache, which
7
+ * is what gives a Session connection reuse between requests.
8
+ *
9
+ * A request is driven by curl_multi_perform() while holding the GVL, so every libcurl
10
+ * callback (write, header, progress, debug) is ordinary Ruby-safe code, and by
11
+ * curl_multi_poll() with the GVL released. Ruby interrupts the poll through the
12
+ * unblocking function, which calls curl_multi_wakeup(); that is what makes Thread#kill,
13
+ * Thread#raise, Timeout and interpreter exit return promptly from any phase of a
14
+ * transfer, including DNS and connect.
15
+ *
16
+ * Requires Ruby >= 3.3 and libcurl >= 7.81.0.
17
+ */
1
18
  #include <ruby.h>
2
19
  #include <ruby/thread.h>
20
+ #include <ruby/ractor.h>
21
+ #include <ruby/encoding.h>
22
+ #include <ruby/io.h>
23
+ #include <ruby/fiber/scheduler.h>
24
+
25
+ #include <stdio.h>
26
+ #include <string.h>
27
+ #include <signal.h>
28
+ #include <unistd.h>
3
29
  #include <sys/stat.h>
30
+ #include <sys/select.h>
31
+ #include <pthread.h>
32
+
4
33
  #include <curl/curl.h>
5
- #include "membuffer.h"
6
- #include "sglib.h" /* Simple Generic Library -> http://sglib.sourceforge.net */
7
34
 
8
- #define UNUSED_ARGUMENT(x) (void)x
35
+ #if LIBCURL_VERSION_NUM < 0x075100
36
+ #error "Patron requires libcurl 7.81.0 or newer"
37
+ #endif
38
+
9
39
  #define INTERRUPT_ABORT 1
10
40
  #define INTERRUPT_DOWNLOAD_OVERFLOW 2
11
41
 
12
- static VALUE mPatron = Qnil;
13
- static VALUE mProxyType = Qnil;
14
- static VALUE cSession = Qnil;
15
- static VALUE cRequest = Qnil;
16
- static VALUE ePatronError = Qnil;
17
- static VALUE eUnsupportedProtocol = Qnil;
18
- static VALUE eUnsupportedSSLVersion = Qnil;
19
- static VALUE eUnsupportedHTTPVersion = Qnil;
20
- static VALUE eURLFormatError = Qnil;
21
- static VALUE eHostResolutionError = Qnil;
22
- static VALUE eConnectionFailed = Qnil;
23
- static VALUE ePartialFileError = Qnil;
24
- static VALUE eTimeoutError = Qnil;
25
- static VALUE eTooManyRedirects = Qnil;
26
- static VALUE eAborted = Qnil;
27
- static VALUE eClientCertificateError = Qnil;
28
-
29
- struct patron_curl_state {
30
- CURL* handle;
31
- CURL* base_handle;
32
- CURLSH* share;
33
- /* The String libcurl reads the request body from. CURLOPT_POSTFIELDS does not copy,
34
- so the String has to stay alive (and marked) until cleanup() */
35
- VALUE upload_data;
36
- FILE* download_file;
37
- FILE* debug_file;
38
- FILE* request_body_file;
42
+ /* How long one curl_multi_poll() may sleep. libcurl wakes us earlier on socket activity,
43
+ * on its own timers, and on curl_multi_wakeup(); this is only the upper bound. */
44
+ #define POLL_TIMEOUT_MS 1000
45
+
46
+ #define BODY_BUFFER_INITIAL_CAPACITY (16 * 1024)
47
+ #define HEADER_BUFFER_INITIAL_CAPACITY 1024
48
+
49
+ static VALUE mPatron;
50
+ static VALUE mProxyType;
51
+ static VALUE cSession;
52
+ static VALUE cRequest;
53
+ static VALUE ePatronError;
54
+ static VALUE eUnsupportedProtocol;
55
+ static VALUE eUnsupportedSSLVersion;
56
+ static VALUE eUnsupportedHTTPVersion;
57
+ static VALUE eURLFormatError;
58
+ static VALUE eHostResolutionError;
59
+ static VALUE eConnectionFailed;
60
+ static VALUE ePartialFileError;
61
+ static VALUE eTimeoutError;
62
+ static VALUE eTooManyRedirects;
63
+ static VALUE eAborted;
64
+ static VALUE eDownloadLimitExceeded;
65
+ static VALUE eClientCertificateError;
66
+
67
+ /* Frozen, Ractor-shareable lookup tables exposed as Patron::Request::SSL_VERSIONS and
68
+ * Patron::Request::HTTP_VERSIONS: version name => libcurl constant. */
69
+ static VALUE hSSLVersions;
70
+ static VALUE hHTTPVersions;
71
+
72
+ #define REQUEST_ATTRIBUTES(X) \
73
+ X(action) X(action_name) X(headers) X(url) X(timeout) X(connect_timeout) \
74
+ X(dns_cache_timeout) X(low_speed_time) X(low_speed_limit) X(max_redirects) \
75
+ X(proxy) X(proxy_type) X(credentials) X(auth_type) X(ignore_content_length) \
76
+ X(insecure) X(ssl_version) X(http_version) X(ssl_cert) X(ssl_cert_type) \
77
+ X(ssl_key) X(ssl_key_password) X(cacert) X(buffer_size) X(download_byte_limit) \
78
+ X(progress_callback) X(automatic_content_encoding) X(force_ipv4) \
79
+ X(upload_data) X(file_name) X(multipart)
80
+
81
+ #define DECLARE_ID(name) static ID id_##name;
82
+ REQUEST_ATTRIBUTES(DECLARE_ID)
83
+ #undef DECLARE_ID
84
+
85
+ static ID id_to_path, id_to_s, id_response_class, id_default_response_charset;
86
+ static ID id_get, id_post, id_put, id_patch, id_delete, id_head;
87
+ static ID id_ivar_curl_code;
88
+ static ID id_url_kw, id_status_kw, id_redirect_count_kw, id_header_data_kw, id_body_kw,
89
+ id_default_charset_kw, id_timings_kw;
90
+
91
+ struct patron_session {
92
+ /* Lives as long as the Session object */
93
+ VALUE self; /* back-pointer for write barriers, never marked */
94
+ CURLM *multi;
95
+ CURL *base; /* template; every request is a curl_easy_duphandle() of it */
96
+ CURLSH *share;
97
+ pthread_mutex_t share_locks[CURL_LOCK_DATA_LAST];
98
+ pid_t pid; /* handles must not be reused across fork() */
99
+ VALUE debug_io; /* IO receiving CURLOPT_VERBOSE output, or nil */
100
+ VALUE socket_ios; /* fd => IO wrapping one of libcurl's sockets, for the Fiber scheduler */
101
+
102
+ /* Per request, torn down in cleanup() */
103
+ CURL *easy;
104
+ curl_mime *mime;
105
+ struct curl_slist *headers;
106
+ FILE *download_file;
107
+ FILE *request_body_file;
39
108
  char error_buf[CURL_ERROR_SIZE];
40
- struct curl_slist* headers;
41
- struct curl_httppost* post;
42
- struct curl_httppost* last;
43
- membuffer header_buffer;
44
- membuffer body_buffer;
45
109
  curl_off_t download_byte_limit;
46
- VALUE user_progress_blk;
47
- /* A non-local exit (raise, Thread#raise, Thread#kill...) from the progress proc can't
48
- unwind through libcurl's frames, so it is parked here and resumed after perform() */
110
+ VALUE progress_proc;
111
+ VALUE upload_data; /* pinned while libcurl reads it through CURLOPT_POSTFIELDS */
112
+ VALUE header_buf; /* binary String receiving the raw headers */
113
+ VALUE body_buf; /* binary String receiving the body, or nil when downloading to a file */
114
+
115
+ /* A callback may not let a Ruby exception unwind through libcurl's frames. The tag and
116
+ * errinfo are parked here, the transfer is aborted, and perform() re-raises afterwards. */
49
117
  int pending_tag;
50
- VALUE pending_exception;
51
- /* Set while a request is in flight. There is one handle and one set of buffers per
52
- Session, and the transfer runs without the GVL, so a second thread entering
53
- handle_request would corrupt the first one's request */
54
- volatile int busy;
55
- int interrupt;
56
- curl_off_t dltotal;
57
- curl_off_t dlnow;
58
- curl_off_t ultotal;
59
- curl_off_t ulnow;
60
- };
118
+ VALUE pending_errinfo;
61
119
 
120
+ volatile sig_atomic_t interrupt;
121
+ volatile sig_atomic_t busy;
122
+ };
62
123
 
63
124
  /*----------------------------------------------------------------------------*/
64
- /* Curl Callbacks */
65
-
66
- /* Takes data streamed from libcurl and writes it to a Ruby string buffer. */
67
- static size_t session_write_handler(char* stream, size_t size, size_t nmemb, membuffer* buf) {
68
- int rc = membuffer_append(buf, stream, size * nmemb);
125
+ /* TypedData */
69
126
 
70
- /* return 0 to signal that we could not append data to our buffer */
71
- if (MB_OK != rc) { return 0; }
127
+ static void session_free(void *ptr) {
128
+ struct patron_session *s = ptr;
129
+ int i;
130
+
131
+ /* A transfer cannot be in flight here: perform() keeps self on the stack. */
132
+ if (s->multi) curl_multi_cleanup(s->multi);
133
+ if (s->base) curl_easy_cleanup(s->base);
134
+ if (s->share) curl_share_cleanup(s->share);
135
+ for (i = 0; i < CURL_LOCK_DATA_LAST; i++) pthread_mutex_destroy(&s->share_locks[i]);
136
+ ruby_xfree(s);
137
+ }
72
138
 
73
- /* otherwise, return the number of bytes appended */
74
- return size * nmemb;
139
+ static void session_mark(void *ptr) {
140
+ struct patron_session *s = ptr;
141
+
142
+ rb_gc_mark_movable(s->debug_io);
143
+ rb_gc_mark_movable(s->socket_ios);
144
+ rb_gc_mark_movable(s->progress_proc);
145
+ rb_gc_mark_movable(s->header_buf);
146
+ rb_gc_mark_movable(s->body_buf);
147
+ rb_gc_mark_movable(s->pending_errinfo);
148
+ /* libcurl holds RSTRING_PTR() of this one for the whole transfer, and an embedded
149
+ * String keeps its bytes inside the object slot, so it must not be moved. */
150
+ rb_gc_mark(s->upload_data);
75
151
  }
76
152
 
77
- /* Used as WRITEFUNCTION for file downloads (required on Windows) */
78
- static size_t file_write_handler(void* stream, size_t size, size_t nmemb, FILE* fp) {
79
- fwrite(stream, size, nmemb, fp);
80
- if (ferror(fp)) {
81
- return 0;
82
- } else {
83
- /* Just always OK the write */
84
- return size * nmemb;
85
- }
153
+ static void session_compact(void *ptr) {
154
+ struct patron_session *s = ptr;
155
+
156
+ s->debug_io = rb_gc_location(s->debug_io);
157
+ s->socket_ios = rb_gc_location(s->socket_ios);
158
+ s->progress_proc = rb_gc_location(s->progress_proc);
159
+ s->header_buf = rb_gc_location(s->header_buf);
160
+ s->body_buf = rb_gc_location(s->body_buf);
161
+ s->pending_errinfo = rb_gc_location(s->pending_errinfo);
86
162
  }
87
163
 
88
- static VALUE call_user_rb_progress_blk_unprotected(VALUE vd_curl_state) {
89
- struct patron_curl_state* state = (struct patron_curl_state*)vd_curl_state;
90
- return rb_funcall(state->user_progress_blk,
91
- rb_intern("call"), 4,
92
- LL2NUM(state->dltotal),
93
- LL2NUM(state->dlnow),
94
- LL2NUM(state->ultotal),
95
- LL2NUM(state->ulnow));
164
+ static size_t session_memsize(const void *ptr) {
165
+ /* The Strings account for themselves; libcurl's own allocations are not visible. */
166
+ return sizeof(struct patron_session);
96
167
  }
97
168
 
98
- /* Runs with the GVL re-acquired, from inside a libcurl callback. Whatever the proc
99
- throws must not propagate from here (see pending_tag), so park it and tell libcurl
100
- to abort the transfer instead. */
101
- static void *call_user_rb_progress_blk(void *vd_curl_state) {
102
- struct patron_curl_state* state = (struct patron_curl_state*)vd_curl_state;
103
- int tag = 0;
169
+ /* RUBY_TYPED_EMBEDDABLE is deliberately not used: libcurl keeps pointers into this struct
170
+ * (CURLOPT_XFERINFODATA, CURLOPT_ERRORBUFFER, CURLOPT_DEBUGDATA, the write/header userdata)
171
+ * and an embedded struct moves under compaction. */
172
+ static const rb_data_type_t patron_session_data_type = {
173
+ .wrap_struct_name = "Patron::Session",
174
+ .function = {
175
+ .dmark = session_mark,
176
+ .dfree = session_free,
177
+ .dsize = session_memsize,
178
+ .dcompact = session_compact,
179
+ },
180
+ .flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY
181
+ #ifdef RUBY_TYPED_THREAD_SAFE_FREE
182
+ /* dfree only touches handles owned by this object */
183
+ | RUBY_TYPED_THREAD_SAFE_FREE
184
+ #endif
185
+ ,
186
+ };
104
187
 
105
- rb_protect(call_user_rb_progress_blk_unprotected, (VALUE)state, &tag);
106
- if (tag) {
107
- /* Whatever rb_protect caught stays in the errinfo of the thread: for a throw (which is
108
- how Timeout.timeout unwinds on Rubies before 3.1) it is not an Exception and can't be
109
- put back with rb_set_errinfo, so we only hold a reference to keep it marked */
110
- state->pending_tag = tag;
111
- state->pending_exception = rb_errinfo();
112
- state->interrupt = INTERRUPT_ABORT;
113
- }
114
- return NULL;
188
+ /* Every entry point refreshes the back-pointer: the Session object itself may have been
189
+ * moved by compaction since the last call. Within a call it is on the C stack and pinned,
190
+ * which is what makes `s->self` safe to use from the libcurl callbacks. */
191
+ static struct patron_session *get_session(VALUE self) {
192
+ struct patron_session *s;
193
+ TypedData_Get_Struct(self, struct patron_session, &patron_session_data_type, s);
194
+ s->self = self;
195
+ return s;
115
196
  }
116
197
 
198
+ #define SESSION_WRITE(s, field, value) RB_OBJ_WRITE((s)->self, &(s)->field, (value))
117
199
 
118
- /* A non-zero return value from the progress handler will terminate the current
119
- * request. We use this fact in order to interrupt any request when either the
120
- * user calls the "interrupt" method on the session or when the Ruby interpreter
121
- * is attempting to exit.
122
- */
123
- static int session_progress_handler(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
124
- struct patron_curl_state* state = (struct patron_curl_state*) clientp;
125
- state->dltotal = dltotal;
126
- state->dlnow = dlnow;
127
- state->ultotal = ultotal;
128
- state->ulnow = ulnow;
129
-
130
- // If a progress proc has been set, re-acquire the GIL and call it using
131
- // `call_user_rb_progress_blk`. TODO: use the retval of that proc
132
- // to permit premature abort
133
- if(RTEST(state->user_progress_blk) && !state->pending_tag) {
134
- rb_thread_call_with_gvl(call_user_rb_progress_blk, state);
135
- }
200
+ /*----------------------------------------------------------------------------*/
201
+ /* Errors */
136
202
 
137
- // Set the interrupt if the download byte limit has been reached
138
- if(state->download_byte_limit != 0 && (dltotal > state->download_byte_limit)) {
139
- state->interrupt = INTERRUPT_DOWNLOAD_OVERFLOW;
203
+ static VALUE error_class_for(CURLcode code) {
204
+ switch (code) {
205
+ case CURLE_UNSUPPORTED_PROTOCOL: return eUnsupportedProtocol;
206
+ case CURLE_URL_MALFORMAT: return eURLFormatError;
207
+ case CURLE_COULDNT_RESOLVE_HOST: return eHostResolutionError;
208
+ case CURLE_COULDNT_CONNECT: return eConnectionFailed;
209
+ case CURLE_PARTIAL_FILE: return ePartialFileError;
210
+ case CURLE_OPERATION_TIMEDOUT: return eTimeoutError;
211
+ case CURLE_TOO_MANY_REDIRECTS: return eTooManyRedirects;
212
+ case CURLE_ABORTED_BY_CALLBACK: return eAborted;
213
+ case CURLE_SSL_CERTPROBLEM: return eClientCertificateError;
214
+ default: return ePatronError;
140
215
  }
141
-
142
- // If the interrupt value is anything except 0, the perform() call
143
- // will be aborted by libCURL.
144
- // Note however that some older versions of libcurl have a bug which
145
- // makes the return value of this callback be ignored, and the request
146
- // will actually proceed undeterred.
147
- // See https://sourceforge.net/p/curl/bugs/1318/
148
- return state->interrupt;
149
216
  }
150
217
 
151
- #if LIBCURL_VERSION_NUM < 0x072000
152
- /* Before 7.32.0 there is only CURLOPT_PROGRESSFUNCTION, which hands over doubles */
153
- static int session_legacy_progress_handler(void* clientp, double dltotal, double dlnow, double ultotal, double ulnow) {
154
- return session_progress_handler(clientp, (curl_off_t) dltotal, (curl_off_t) dlnow, (curl_off_t) ultotal, (curl_off_t) ulnow);
218
+ NORETURN(static void raise_curl_error(VALUE klass, CURLcode code, const char *message));
219
+ static void raise_curl_error(VALUE klass, CURLcode code, const char *message) {
220
+ VALUE exc;
221
+
222
+ if (!message || !*message) message = curl_easy_strerror(code);
223
+ exc = rb_exc_new_cstr(klass, message);
224
+ rb_ivar_set(exc, id_ivar_curl_code, INT2NUM((int)code));
225
+ rb_exc_raise(exc);
155
226
  }
156
- #endif
157
227
 
228
+ /* curl_easy_setopt() only fails for options the installed libcurl cannot honour; that must
229
+ * surface instead of silently running the request with defaults. */
230
+ #define setopt_or_raise(handle, option, value) do { \
231
+ CURLcode setopt_rc_ = curl_easy_setopt((handle), (option), (value)); \
232
+ if (setopt_rc_ != CURLE_OK) raise_curl_error(ePatronError, setopt_rc_, NULL); \
233
+ } while (0)
158
234
 
159
- /*
160
- List of active curl sessions, used exclusively to be able to set interrupts
161
- for all of them if the Ruby interpreter gets shut down with libCURL requests still in flight.
162
- */
163
- struct patron_curl_state_list {
164
- struct patron_curl_state *state;
165
- struct patron_curl_state_list *next;
166
- };
235
+ /*----------------------------------------------------------------------------*/
236
+ /* Deferred exceptions from inside libcurl callbacks */
167
237
 
168
- #define CS_LIST_COMPARATOR(p, _state_) (p->state - _state_)
238
+ static void park_exception(struct patron_session *s, int tag) {
239
+ if (s->pending_tag) return;
240
+ s->pending_tag = tag;
241
+ SESSION_WRITE(s, pending_errinfo, rb_errinfo());
242
+ s->interrupt = INTERRUPT_ABORT;
243
+ }
169
244
 
170
- static struct patron_curl_state_list *cs_list = NULL;
245
+ /* A raise can be re-raised as such. A throw or a Thread#kill leaves a non-exception in
246
+ * errinfo, which rb_set_errinfo refuses, so those jump by tag and rely on errinfo being
247
+ * untouched since the callback parked it (nothing in between rescues). */
248
+ NORETURN(static void reraise_parked(struct patron_session *s));
249
+ static void reraise_parked(struct patron_session *s) {
250
+ int tag = s->pending_tag;
251
+ VALUE errinfo = s->pending_errinfo;
252
+
253
+ s->pending_tag = 0;
254
+ SESSION_WRITE(s, pending_errinfo, Qnil);
255
+ if (RB_TYPE_P(errinfo, T_OBJECT) && rb_obj_is_kind_of(errinfo, rb_eException)) rb_exc_raise(errinfo);
256
+ rb_jump_tag(tag);
257
+ }
171
258
 
172
- static void cs_list_append( struct patron_curl_state *state ) {
173
- struct patron_curl_state_list *item = NULL;
259
+ /*----------------------------------------------------------------------------*/
260
+ /* libcurl callbacks. All of them run with the GVL held, from curl_multi_perform(). */
174
261
 
175
- assert(state != NULL);
176
- item = ruby_xmalloc(sizeof(struct patron_curl_state_list));
177
- item->state = state;
178
- item->next = NULL;
262
+ struct append_args {
263
+ VALUE str;
264
+ const char *ptr;
265
+ size_t len;
266
+ };
179
267
 
180
- SGLIB_LIST_ADD(struct patron_curl_state_list, cs_list, item, next);
268
+ static VALUE append_protected(VALUE arg) {
269
+ struct append_args *a = (struct append_args *)arg;
270
+ rb_str_cat(a->str, a->ptr, (long)a->len);
271
+ return Qnil;
181
272
  }
182
273
 
183
- static void cs_list_remove(struct patron_curl_state *state) {
184
- struct patron_curl_state_list *item = NULL;
185
-
186
- assert(state != NULL);
274
+ static size_t append_to_string(struct patron_session *s, VALUE str, const char *ptr, size_t len) {
275
+ struct append_args args = { str, ptr, len };
276
+ int tag = 0;
187
277
 
188
- SGLIB_LIST_DELETE_IF_MEMBER(struct patron_curl_state_list, cs_list, state, CS_LIST_COMPARATOR, next, item);
189
- if (item) {
190
- ruby_xfree(item);
278
+ rb_protect(append_protected, (VALUE)&args, &tag);
279
+ if (tag) {
280
+ park_exception(s, tag);
281
+ return 0;
191
282
  }
283
+ return len;
192
284
  }
193
285
 
194
- /* Gets attached to at_exit of the Ruby process to be able to abort all running libCURL requests and quit */
195
- static void cs_list_interrupt(VALUE data) {
196
- UNUSED_ARGUMENT(data);
286
+ static size_t body_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
287
+ struct patron_session *s = userdata;
288
+ size_t len = size * nmemb;
197
289
 
198
- SGLIB_LIST_MAP_ON_ELEMENTS(struct patron_curl_state_list, cs_list, item, next, {
199
- item->state->interrupt = INTERRUPT_ABORT;
200
- });
290
+ if (s->download_file) {
291
+ if (fwrite(ptr, 1, len, s->download_file) != len) return 0;
292
+ return len;
293
+ }
294
+ return append_to_string(s, s->body_buf, ptr, len);
201
295
  }
202
296
 
297
+ static size_t header_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
298
+ struct patron_session *s = userdata;
299
+ return append_to_string(s, s->header_buf, ptr, size * nmemb);
300
+ }
203
301
 
204
- /*----------------------------------------------------------------------------*/
205
- /* Object allocation */
206
-
207
- static void session_close_debug_file(struct patron_curl_state *curl) {
208
- if (curl->debug_file && stderr != curl->debug_file) {
209
- fclose(curl->debug_file);
210
- }
211
- curl->debug_file = NULL;
302
+ static size_t file_read_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
303
+ FILE *fp = userdata;
304
+ return fread(buffer, 1, size * nitems, fp);
212
305
  }
213
306
 
214
- /* Cleans up the patron_curl_state data when the Session object is garbage collected. */
215
- static void session_free(void *ptr) {
216
- struct patron_curl_state *state = ptr;
307
+ struct progress_args {
308
+ struct patron_session *s;
309
+ curl_off_t dltotal, dlnow, ultotal, ulnow;
310
+ };
311
+
312
+ static VALUE progress_protected(VALUE arg) {
313
+ struct progress_args *a = (struct progress_args *)arg;
314
+ VALUE argv[4] = { LL2NUM(a->dltotal), LL2NUM(a->dlnow), LL2NUM(a->ultotal), LL2NUM(a->ulnow) };
315
+ return rb_proc_call_with_block(a->s->progress_proc, 4, argv, Qnil);
316
+ }
217
317
 
218
- curl_easy_cleanup(state->base_handle);
219
- curl_share_cleanup(state->share);
318
+ /* Non-zero return aborts the transfer with CURLE_ABORTED_BY_CALLBACK. */
319
+ static int xferinfo_cb(void *userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
320
+ struct patron_session *s = userdata;
220
321
 
221
- session_close_debug_file(state);
322
+ if (s->interrupt) return 1;
222
323
 
223
- membuffer_destroy(&state->header_buffer);
224
- membuffer_destroy(&state->body_buffer);
324
+ if (s->download_byte_limit > 0 && (dltotal > s->download_byte_limit || dlnow > s->download_byte_limit)) {
325
+ s->interrupt = INTERRUPT_DOWNLOAD_OVERFLOW;
326
+ return 1;
327
+ }
225
328
 
226
- cs_list_remove(state);
329
+ if (!NIL_P(s->progress_proc)) {
330
+ struct progress_args args = { s, dltotal, dlnow, ultotal, ulnow };
331
+ int tag = 0;
332
+ VALUE result = rb_protect(progress_protected, (VALUE)&args, &tag);
227
333
 
228
- ruby_xfree(state);
334
+ if (tag) {
335
+ park_exception(s, tag);
336
+ return 1;
337
+ }
338
+ /* Only an explicit false aborts; a callback that happens to end in nil keeps going. */
339
+ if (result == Qfalse) {
340
+ s->interrupt = INTERRUPT_ABORT;
341
+ return 1;
342
+ }
343
+ }
344
+ return 0;
229
345
  }
230
346
 
231
- static void session_mark(void *ptr) {
232
- struct patron_curl_state *state = ptr;
347
+ struct debug_args {
348
+ VALUE io;
349
+ const char *prefix;
350
+ const char *data;
351
+ size_t len;
352
+ };
233
353
 
234
- rb_gc_mark(state->user_progress_blk);
235
- rb_gc_mark(state->upload_data);
236
- rb_gc_mark(state->pending_exception);
354
+ static VALUE debug_protected(VALUE arg) {
355
+ struct debug_args *a = (struct debug_args *)arg;
356
+ VALUE line = rb_str_buf_new((long)(a->len + 2));
357
+ rb_str_cat_cstr(line, a->prefix);
358
+ rb_str_cat(line, a->data, (long)a->len);
359
+ rb_io_write(a->io, line);
360
+ return Qnil;
237
361
  }
238
362
 
239
- static size_t session_memsize(const void *ptr) {
240
- const struct patron_curl_state *state = ptr;
363
+ /* Mirrors what libcurl itself prints with CURLOPT_VERBOSE: informational text and the
364
+ * headers in both directions, never the payload. */
365
+ static int debug_cb(CURL *handle, curl_infotype type, char *data, size_t len, void *userdata) {
366
+ struct patron_session *s = userdata;
367
+ const char *prefix;
368
+ struct debug_args args;
369
+ int tag = 0;
370
+
371
+ if (NIL_P(s->debug_io)) return 0;
372
+
373
+ switch (type) {
374
+ case CURLINFO_TEXT: prefix = "* "; break;
375
+ case CURLINFO_HEADER_IN: prefix = "< "; break;
376
+ case CURLINFO_HEADER_OUT: prefix = "> "; break;
377
+ default: return 0;
378
+ }
241
379
 
242
- return sizeof(*state) + state->header_buffer.capacity + state->body_buffer.capacity;
380
+ args.io = s->debug_io;
381
+ args.prefix = prefix;
382
+ args.data = data;
383
+ args.len = len;
384
+ rb_protect(debug_protected, (VALUE)&args, &tag);
385
+ if (tag) park_exception(s, tag);
386
+ return 0;
243
387
  }
244
388
 
245
- static const rb_data_type_t patron_session_data_type = {
246
- "Patron::Session",
247
- {session_mark, session_free, session_memsize,},
248
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
249
- };
389
+ static void share_lock_cb(CURL *handle, curl_lock_data data, curl_lock_access access, void *userptr) {
390
+ struct patron_session *s = userptr;
391
+ pthread_mutex_lock(&s->share_locks[data]);
392
+ }
250
393
 
251
- /* Allocates patron_curl_state data needed for a new Session object. */
252
- VALUE session_alloc(VALUE klass) {
253
- struct patron_curl_state* state;
254
- VALUE obj = TypedData_Make_Struct(klass, struct patron_curl_state, &patron_session_data_type, state);
255
-
256
- membuffer_init(&state->header_buffer);
257
- membuffer_init(&state->body_buffer);
258
- state->user_progress_blk = Qnil;
259
- state->upload_data = Qnil;
260
- state->pending_exception = Qnil;
261
- cs_list_append(state);
262
-
263
- /*
264
- Eagerly initialize the curl handle. We initialize it only once and store it
265
- in the struct until the Session object gets garbage-collected in session_free(). This allows libCURL to
266
- reuse the TCP connection and can speed things up if the same resource - like a backend service -
267
- gets accessed over and over with requests.
268
- */
269
- state->share = curl_share_init();
270
- if (!state->share) {
271
- rb_raise(rb_eNoMemError, "libcurl could not allocate a share handle");
272
- }
273
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
274
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
275
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
276
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
277
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
278
- state->base_handle = curl_easy_init();
279
- if (!state->base_handle) {
280
- /* obj is already allocated, so session_free() will release the share; curl_share_cleanup(NULL)
281
- and curl_easy_cleanup(NULL) are no-ops, which keeps that path safe for a half-built state */
282
- rb_raise(rb_eNoMemError, "libcurl could not allocate an easy handle");
394
+ static void share_unlock_cb(CURL *handle, curl_lock_data data, void *userptr) {
395
+ struct patron_session *s = userptr;
396
+ pthread_mutex_unlock(&s->share_locks[data]);
397
+ }
398
+
399
+ /*----------------------------------------------------------------------------*/
400
+ /* Handle lifecycle */
401
+
402
+ static void init_handles(struct patron_session *s) {
403
+ int i;
404
+
405
+ for (i = 0; i < CURL_LOCK_DATA_LAST; i++) pthread_mutex_init(&s->share_locks[i], NULL);
406
+
407
+ s->share = curl_share_init();
408
+ s->multi = curl_multi_init();
409
+ s->base = curl_easy_init();
410
+ if (!s->share || !s->multi || !s->base) {
411
+ if (s->base) curl_easy_cleanup(s->base);
412
+ if (s->multi) curl_multi_cleanup(s->multi);
413
+ if (s->share) curl_share_cleanup(s->share);
414
+ s->base = NULL; s->multi = NULL; s->share = NULL;
415
+ rb_memerror();
283
416
  }
284
- curl_easy_setopt(state->base_handle, CURLOPT_SHARE, state->share);
285
- curl_easy_setopt(state->base_handle, CURLOPT_WRITEFUNCTION, &session_write_handler);
286
- curl_easy_setopt(state->base_handle, CURLOPT_WRITEDATA, &state->body_buffer);
287
- curl_easy_setopt(state->base_handle, CURLOPT_HEADERFUNCTION, &session_write_handler);
288
- curl_easy_setopt(state->base_handle, CURLOPT_HEADERDATA, &state->header_buffer);
289
- curl_easy_setopt(state->base_handle, CURLOPT_NOSIGNAL, 1);
290
- curl_easy_setopt(state->base_handle, CURLOPT_NOPROGRESS, 0);
291
- #if LIBCURL_VERSION_NUM >= 0x072000
292
- /* this is libCURLv7.32.0 or later, supports CURLOPT_XFERINFOFUNCTION */
293
- curl_easy_setopt(state->base_handle, CURLOPT_XFERINFOFUNCTION, &session_progress_handler);
417
+ s->pid = getpid();
418
+
419
+ curl_share_setopt(s->share, CURLSHOPT_LOCKFUNC, share_lock_cb);
420
+ curl_share_setopt(s->share, CURLSHOPT_UNLOCKFUNC, share_unlock_cb);
421
+ curl_share_setopt(s->share, CURLSHOPT_USERDATA, s);
422
+ curl_share_setopt(s->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
423
+ curl_share_setopt(s->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
424
+ curl_share_setopt(s->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
425
+ curl_share_setopt(s->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
426
+
427
+ /* The multi handle owns the connection cache. Multiplexing costs nothing with one
428
+ * transfer at a time and is ready for concurrent transfers later. */
429
+ curl_multi_setopt(s->multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
430
+
431
+ curl_easy_setopt(s->base, CURLOPT_SHARE, s->share);
432
+ curl_easy_setopt(s->base, CURLOPT_WRITEFUNCTION, body_write_cb);
433
+ curl_easy_setopt(s->base, CURLOPT_WRITEDATA, s);
434
+ curl_easy_setopt(s->base, CURLOPT_HEADERFUNCTION, header_write_cb);
435
+ curl_easy_setopt(s->base, CURLOPT_HEADERDATA, s);
436
+ curl_easy_setopt(s->base, CURLOPT_NOSIGNAL, 1L);
437
+ curl_easy_setopt(s->base, CURLOPT_NOPROGRESS, 0L);
438
+ curl_easy_setopt(s->base, CURLOPT_XFERINFOFUNCTION, xferinfo_cb);
439
+ curl_easy_setopt(s->base, CURLOPT_XFERINFODATA, s);
440
+ curl_easy_setopt(s->base, CURLOPT_DEBUGFUNCTION, debug_cb);
441
+ curl_easy_setopt(s->base, CURLOPT_DEBUGDATA, s);
442
+ curl_easy_setopt(s->base, CURLOPT_ERRORBUFFER, s->error_buf);
443
+
444
+ /* Never let a URL or a redirect take us to gopher, smtp, file and friends. */
445
+ #if LIBCURL_VERSION_NUM >= 0x075500
446
+ curl_easy_setopt(s->base, CURLOPT_PROTOCOLS_STR, "http,https");
447
+ curl_easy_setopt(s->base, CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
294
448
  #else
295
- curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSFUNCTION, &session_legacy_progress_handler);
296
- #endif
297
- curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSDATA, state);
298
- #ifdef CURLPROTO_HTTP
299
- // Security: do not allow Curl to go looking on gopher/SMTP etc.
300
- // Must prevent situations like this:
301
- // https://hackerone.com/reports/115748
302
- curl_easy_setopt(state->base_handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
303
- curl_easy_setopt(state->base_handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
449
+ curl_easy_setopt(s->base, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
450
+ curl_easy_setopt(s->base, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
304
451
  #endif
452
+ }
305
453
 
306
- return obj;
454
+ /* A forked child must not use connections it shares with the parent. The inherited
455
+ * handles are abandoned rather than cleaned up, since cleaning up would shut down
456
+ * TLS sessions on sockets the parent is still using. */
457
+ static void ensure_handles_for_this_process(struct patron_session *s) {
458
+ if (s->pid == getpid()) return;
459
+ s->multi = NULL;
460
+ s->base = NULL;
461
+ s->share = NULL;
462
+ init_handles(s);
463
+ /* Cookie jar and debug settings live on the base handle; the Ruby side re-applies them. */
464
+ rb_funcall(s->self, rb_intern("reapply_base_options"), 0);
307
465
  }
308
466
 
309
- /* Return the patron_curl_state from the ruby VALUE which is the Session instance. */
310
- static struct patron_curl_state* get_patron_curl_state(VALUE self) {
311
- struct patron_curl_state* state;
312
- TypedData_Get_Struct(self, struct patron_curl_state, &patron_session_data_type, state);
313
- return state;
467
+ static VALUE session_alloc(VALUE klass) {
468
+ struct patron_session *s;
469
+ VALUE obj = TypedData_Make_Struct(klass, struct patron_session, &patron_session_data_type, s);
470
+
471
+ s->self = obj;
472
+ s->debug_io = Qnil;
473
+ s->socket_ios = Qnil;
474
+ s->progress_proc = Qnil;
475
+ s->upload_data = Qnil;
476
+ s->header_buf = Qnil;
477
+ s->body_buf = Qnil;
478
+ s->pending_errinfo = Qnil;
479
+ init_handles(s);
480
+ return obj;
314
481
  }
315
482
 
316
483
  /*----------------------------------------------------------------------------*/
317
- /* Method implementations */
484
+ /* Module functions */
318
485
 
319
486
  /*
320
- * Returns the version of the embedded libcurl.
321
- *
322
- * @return [String] libcurl version string
487
+ * Returns the version string of the libcurl in use, e.g. "libcurl/8.7.1 OpenSSL/3.3.0".
488
+ *
489
+ * @return [String]
323
490
  */
324
491
  static VALUE libcurl_version(VALUE klass) {
325
- char* value = curl_version();
326
- UNUSED_ARGUMENT(klass);
327
- return rb_str_new2(value);
492
+ return rb_str_new_cstr(curl_version());
328
493
  }
329
494
 
330
495
  /*
331
- * Returns the version of the embedded libcurl.
332
- *
333
- * @return [Array] an array of MAJOR, MINOR, PATCH integers
496
+ * Returns the version of the libcurl Patron was compiled against.
497
+ *
498
+ * @return [Array<Integer>] major, minor and patch
334
499
  */
335
500
  static VALUE libcurl_version_exact(VALUE klass) {
336
- UNUSED_ARGUMENT(klass);
337
- VALUE cv_major = INT2FIX(LIBCURL_VERSION_MAJOR);
338
- VALUE cv_minor = INT2FIX(LIBCURL_VERSION_MINOR);
339
- VALUE cv_patch = INT2FIX(LIBCURL_VERSION_PATCH);
340
- VALUE version_arr = rb_ary_new3(3, cv_major, cv_minor, cv_patch);
341
- return version_arr;
501
+ return rb_ary_new_from_args(3, INT2FIX(LIBCURL_VERSION_MAJOR), INT2FIX(LIBCURL_VERSION_MINOR), INT2FIX(LIBCURL_VERSION_PATCH));
342
502
  }
343
503
 
344
504
  /*
345
- * Escapes the provided string using libCURL URL escaping functions.
505
+ * Describes the libcurl in use at runtime.
346
506
  *
347
- * @param [String] value plain string to URL-escape
348
- * @return [String] the escaped string
507
+ * @return [Hash] `:version`, `:ssl_version`, `:libz_version` Strings (the latter two may be nil)
508
+ * and `:features`, an Array of Symbols such as `:ssl`, `:http2`, `:http3`, `:asynchdns`, `:threadsafe`
349
509
  */
350
- static VALUE session_escape(VALUE self, VALUE value) {
351
-
352
- VALUE string = StringValue(value);
353
- char* escaped = NULL;
354
- VALUE retval = Qnil;
355
-
356
- #if LIBCURL_VERSION_NUM >= 0x075200
357
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
358
- CURL *curl = NULL;
359
- #else
360
- CURL* curl = curl_easy_init();
510
+ static VALUE libcurl_features(VALUE klass) {
511
+ curl_version_info_data *info = curl_version_info(CURLVERSION_NOW);
512
+ VALUE features = rb_ary_new();
513
+ VALUE result = rb_hash_new();
514
+
515
+ #define FEATURE(flag, name) if (info->features & (flag)) rb_ary_push(features, ID2SYM(rb_intern(name)))
516
+ FEATURE(CURL_VERSION_SSL, "ssl");
517
+ FEATURE(CURL_VERSION_IPV6, "ipv6");
518
+ FEATURE(CURL_VERSION_LIBZ, "libz");
519
+ FEATURE(CURL_VERSION_ASYNCHDNS, "asynchdns");
520
+ FEATURE(CURL_VERSION_HTTP2, "http2");
521
+ FEATURE(CURL_VERSION_HTTP3, "http3");
522
+ FEATURE(CURL_VERSION_BROTLI, "brotli");
523
+ FEATURE(CURL_VERSION_UNIX_SOCKETS, "unix_sockets");
524
+ #ifdef CURL_VERSION_ZSTD
525
+ FEATURE(CURL_VERSION_ZSTD, "zstd");
361
526
  #endif
362
- escaped = curl_easy_escape(curl,
363
- RSTRING_PTR(string),
364
- rb_long2int(RSTRING_LEN(string)));
365
- #if LIBCURL_VERSION_NUM >= 0x075200
366
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
367
- #else
368
- curl_easy_cleanup(curl);
527
+ #ifdef CURL_VERSION_THREADSAFE
528
+ FEATURE(CURL_VERSION_THREADSAFE, "threadsafe");
369
529
  #endif
370
- if (!escaped) {
371
- rb_raise(ePatronError, "libcurl could not escape the string");
372
- }
530
+ #undef FEATURE
373
531
 
374
- retval = rb_str_new2(escaped);
375
- curl_free(escaped);
532
+ rb_hash_aset(result, ID2SYM(rb_intern("version")), rb_str_new_cstr(info->version));
533
+ rb_hash_aset(result, ID2SYM(rb_intern("ssl_version")), info->ssl_version ? rb_str_new_cstr(info->ssl_version) : Qnil);
534
+ rb_hash_aset(result, ID2SYM(rb_intern("libz_version")), info->libz_version ? rb_str_new_cstr(info->libz_version) : Qnil);
535
+ rb_hash_aset(result, ID2SYM(rb_intern("features")), features);
536
+ return result;
537
+ }
538
+
539
+ /*
540
+ * URL-escapes a String using libcurl.
541
+ *
542
+ * @param value [String]
543
+ * @return [String] the escaped string
544
+ */
545
+ static VALUE session_escape(VALUE self, VALUE value) {
546
+ VALUE string = StringValue(value);
547
+ char *escaped = curl_easy_escape(NULL, RSTRING_PTR(string), rb_long2int(RSTRING_LEN(string)));
548
+ VALUE result;
376
549
 
377
- return retval;
550
+ if (!escaped) rb_raise(ePatronError, "libcurl could not escape the string");
551
+ result = rb_usascii_str_new_cstr(escaped);
552
+ curl_free(escaped);
553
+ return result;
378
554
  }
379
555
 
380
556
  /*
381
- * Unescapes the provided string using libCURL URL escaping functions.
557
+ * URL-unescapes a String using libcurl. The result is binary, since percent-escapes can
558
+ * encode arbitrary bytes.
382
559
  *
383
- * @param [String] value URL-encoded String to unescape
384
- * @return [String] unescaped (decoded) string
560
+ * @param value [String]
561
+ * @return [String] the unescaped string
385
562
  */
386
563
  static VALUE session_unescape(VALUE self, VALUE value) {
387
564
  VALUE string = StringValue(value);
388
- char* unescaped = NULL;
389
- int unescaped_len = 0;
390
- VALUE retval = Qnil;
391
-
392
- #if LIBCURL_VERSION_NUM >= 0x075200
393
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
394
- CURL *curl = NULL;
395
- #else
396
- CURL *curl = curl_easy_init();
397
- #endif
398
- /* Ask for the length: the decoded bytes may contain NUL ("%00"), so strlen would cut it short */
399
- unescaped = curl_easy_unescape(curl,
400
- RSTRING_PTR(string),
401
- rb_long2int(RSTRING_LEN(string)),
402
- &unescaped_len);
403
- #if LIBCURL_VERSION_NUM >= 0x075200
404
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
405
- #else
406
- curl_easy_cleanup(curl);
407
- #endif
408
- if (!unescaped) {
409
- rb_raise(ePatronError, "libcurl could not unescape the string");
410
- }
565
+ int outlen = 0;
566
+ char *unescaped = curl_easy_unescape(NULL, RSTRING_PTR(string), rb_long2int(RSTRING_LEN(string)), &outlen);
567
+ VALUE result;
411
568
 
412
- retval = rb_str_new(unescaped, unescaped_len);
569
+ if (!unescaped) rb_raise(ePatronError, "libcurl could not unescape the string");
570
+ result = rb_str_new(unescaped, outlen);
413
571
  curl_free(unescaped);
414
-
415
- return retval;
572
+ return result;
416
573
  }
417
574
 
418
- /* Callback used to iterate over the HTTP headers and store them in an slist. */
419
- static int each_http_header(VALUE header_key, VALUE header_value, VALUE self) {
420
- struct patron_curl_state *state = get_patron_curl_state(self);
421
- CURL* curl = state->handle;
422
-
423
- VALUE name = rb_obj_as_string(header_key);
424
- VALUE value = rb_obj_as_string(header_value);
425
- VALUE header_str = Qnil;
426
-
427
- // TODO: see how to combine this with automatic_content_encoding
428
- if (rb_str_cmp(name, rb_str_new2("Accept-Encoding")) == 0) {
429
- if (rb_funcall(value, rb_intern("include?"), 1, rb_str_new2("gzip"))) {
430
- #ifdef CURLOPT_ACCEPT_ENCODING
431
- curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
432
- #elif defined CURLOPT_ENCODING
433
- curl_easy_setopt(curl, CURLOPT_ENCODING, "gzip");
434
- #else
435
- rb_raise(rb_eArgError,
436
- "The libcurl version installed doesn't support 'gzip'.");
437
- #endif
438
- }
439
- }
575
+ /*----------------------------------------------------------------------------*/
576
+ /* Request configuration */
440
577
 
441
- header_str = rb_str_plus(name, rb_str_new2(": "));
442
- header_str = rb_str_plus(header_str, value);
578
+ static VALUE request_attr(VALUE request, ID id) {
579
+ return rb_funcallv(request, id, 0, NULL);
580
+ }
443
581
 
444
- state->headers = curl_slist_append(state->headers, StringValuePtr(header_str));
582
+ static int header_name_is(VALUE name, const char *expected) {
583
+ long len = (long)strlen(expected);
584
+ long i;
585
+ const char *p;
445
586
 
446
- return 0;
587
+ if (RSTRING_LEN(name) != len) return 0;
588
+ p = RSTRING_PTR(name);
589
+ for (i = 0; i < len; i++) {
590
+ if (rb_tolower((unsigned char)p[i]) != rb_tolower((unsigned char)expected[i])) return 0;
591
+ }
592
+ return 1;
447
593
  }
448
594
 
449
- static int formadd_values(VALUE data_key, VALUE data_value, VALUE self) {
450
- struct patron_curl_state *state = get_patron_curl_state(self);
451
- VALUE name = rb_obj_as_string(data_key);
452
- VALUE value = rb_obj_as_string(data_value);
453
-
454
- /* COPY* makes libcurl copy both strings: the PTR* variants would keep pointers into
455
- Strings nothing holds on to (a Symbol key gets stringified into a temporary), and
456
- PTRCONTENTS stops at the first NUL byte. Passing the length keeps binary values intact. */
457
- curl_formadd(&state->post, &state->last,
458
- CURLFORM_COPYNAME, RSTRING_PTR(name), CURLFORM_NAMELENGTH, (long) RSTRING_LEN(name),
459
- CURLFORM_COPYCONTENTS, RSTRING_PTR(value),
460
- #if LIBCURL_VERSION_NUM >= 0x072E00
461
- CURLFORM_CONTENTLEN, (curl_off_t) RSTRING_LEN(value),
462
- #else
463
- CURLFORM_CONTENTSLENGTH, (long) RSTRING_LEN(value),
464
- #endif
465
- CURLFORM_END);
595
+ static int string_contains(VALUE str, const char *needle) {
596
+ const char *haystack = RSTRING_PTR(str);
597
+ long len = RSTRING_LEN(str);
598
+ long needle_len = (long)strlen(needle);
599
+ long i;
466
600
 
601
+ for (i = 0; i + needle_len <= len; i++) {
602
+ if (memcmp(haystack + i, needle, (size_t)needle_len) == 0) return 1;
603
+ }
467
604
  return 0;
468
605
  }
469
606
 
470
- static int formadd_files(VALUE data_key, VALUE data_value, VALUE self) {
471
- struct patron_curl_state *state = get_patron_curl_state(self);
472
- VALUE name = rb_obj_as_string(data_key);
473
- VALUE value = rb_obj_as_string(data_value);
607
+ static int each_http_header(VALUE key, VALUE value, VALUE self) {
608
+ struct patron_session *s = get_session(self);
609
+ VALUE name = rb_obj_as_string(key);
610
+ VALUE line;
474
611
 
475
- curl_formadd(&state->post, &state->last,
476
- CURLFORM_COPYNAME, RSTRING_PTR(name), CURLFORM_NAMELENGTH, (long) RSTRING_LEN(name),
477
- CURLFORM_FILE, StringValueCStr(value), CURLFORM_END);
612
+ /* "Accept-Encoding: gzip" also asks libcurl to inflate the response for us. */
613
+ if (header_name_is(name, "Accept-Encoding") && string_contains(rb_obj_as_string(value), "gzip")) {
614
+ setopt_or_raise(s->easy, CURLOPT_ACCEPT_ENCODING, "gzip");
615
+ }
478
616
 
479
- return 0;
617
+ line = rb_sprintf("%"PRIsVALUE": %"PRIsVALUE, name, value);
618
+ s->headers = curl_slist_append(s->headers, StringValueCStr(line));
619
+ return ST_CONTINUE;
480
620
  }
481
621
 
482
- // Use the given String as the CURL request body. The String is kept in the state
483
- // so that it outlives the (GVL-less) transfer which reads from its buffer.
484
- static void set_curl_request_body(struct patron_curl_state* state, VALUE data) {
485
- CURL* curl = state->handle;
486
- StringValue(data);
487
- state->upload_data = data;
488
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, RSTRING_PTR(state->upload_data));
489
- #ifdef CURLOPT_POSTFIELDSIZE_LARGE
490
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t) RSTRING_LEN(state->upload_data));
491
- #else
492
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long) RSTRING_LEN(state->upload_data));
493
- #endif
622
+ static curl_mimepart *add_mime_part(struct patron_session *s, VALUE key) {
623
+ curl_mimepart *part;
624
+ VALUE name = rb_obj_as_string(key);
625
+
626
+ if (!s->mime) s->mime = curl_mime_init(s->easy);
627
+ part = curl_mime_addpart(s->mime);
628
+ if (!part) rb_memerror();
629
+ curl_mime_name(part, StringValueCStr(name));
630
+ return part;
494
631
  }
495
632
 
496
- static void set_chunked_encoding(struct patron_curl_state *state) {
497
- state->headers = curl_slist_append(state->headers, "Transfer-Encoding: chunked");
633
+ static int each_mime_value(VALUE key, VALUE value, VALUE self) {
634
+ struct patron_session *s = get_session(self);
635
+ VALUE str = rb_obj_as_string(value);
636
+ curl_mimepart *part = add_mime_part(s, key);
637
+
638
+ /* curl_mime_data() copies, so the String may be collected right after this call. */
639
+ curl_mime_data(part, RSTRING_PTR(str), (size_t)RSTRING_LEN(str));
640
+ return ST_CONTINUE;
498
641
  }
499
642
 
500
- static FILE* open_file(VALUE filename, const char* perms) {
501
- FILE* handle = fopen(StringValueCStr(filename), perms);
502
- if (!handle) {
503
- rb_raise(rb_eArgError, "Unable to open specified file.");
643
+ static int each_mime_file(VALUE key, VALUE value, VALUE self) {
644
+ struct patron_session *s = get_session(self);
645
+ VALUE path = rb_obj_as_string(value);
646
+ curl_mimepart *part = add_mime_part(s, key);
647
+
648
+ FilePathValue(path);
649
+ if (curl_mime_filedata(part, StringValueCStr(path)) != CURLE_OK) {
650
+ rb_raise(rb_eArgError, "Unable to read file for multipart upload: %"PRIsVALUE, path);
504
651
  }
652
+ return ST_CONTINUE;
653
+ }
654
+
655
+ static FILE *open_file(VALUE filename, const char *mode) {
656
+ FILE *handle;
505
657
 
658
+ FilePathValue(filename);
659
+ handle = fopen(StringValueCStr(filename), mode);
660
+ if (!handle) rb_sys_fail_str(filename);
506
661
  return handle;
507
662
  }
508
663
 
509
- static void set_request_body_file(struct patron_curl_state* state, VALUE r_path_str) {
510
- CURL* curl = state->handle;
511
-
512
- state->request_body_file = open_file(r_path_str, "rb");
513
- curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
514
- curl_easy_setopt(curl, CURLOPT_READDATA, state->request_body_file);
515
- struct stat stat_info;
516
- fstat(fileno(state->request_body_file), &stat_info);
517
- #ifdef CURLOPT_INFILESIZE_LARGE
518
- curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, stat_info.st_size);
519
- #else
520
- curl_easy_setopt(curl, CURLOPT_INFILESIZE, stat_info.st_size);
521
- #endif
522
- }
664
+ static void set_request_body_from_file(struct patron_session *s, VALUE path) {
665
+ struct stat st;
666
+
667
+ s->request_body_file = open_file(path, "rb");
668
+ if (fstat(fileno(s->request_body_file), &st) != 0) rb_sys_fail_str(path);
523
669
 
524
- static long floating_rb_seconds_to_milliseconds(VALUE r_seconds) {
525
- double seconds_f = NUM2DBL(r_seconds);
526
- long millis = seconds_f * (double)1000.0;
527
- return millis;
670
+ setopt_or_raise(s->easy, CURLOPT_UPLOAD, 1L);
671
+ setopt_or_raise(s->easy, CURLOPT_READFUNCTION, file_read_cb);
672
+ setopt_or_raise(s->easy, CURLOPT_READDATA, s->request_body_file);
673
+ setopt_or_raise(s->easy, CURLOPT_INFILESIZE_LARGE, (curl_off_t)st.st_size);
528
674
  }
529
675
 
530
- static void set_request_body(struct patron_curl_state* state, VALUE stringable_or_file) {
531
- if(rb_respond_to(stringable_or_file, rb_intern("to_path"))) {
532
- // Set up a file read callback (read the entire request body from a file).
533
- // Instead of using the Ruby file reads, use #to_path to obtain the
534
- // file path on the file system and open a file pointer to it
535
- VALUE r_path_str = rb_funcall(stringable_or_file, rb_intern("to_path"), 0);
536
- r_path_str = rb_funcall(r_path_str, rb_intern("to_s"), 0);
537
- set_request_body_file(state, r_path_str);
676
+ static void set_request_body(struct patron_session *s, VALUE stringable_or_file) {
677
+ if (rb_respond_to(stringable_or_file, id_to_path)) {
678
+ VALUE path = rb_funcallv(stringable_or_file, id_to_path, 0, NULL);
679
+ set_request_body_from_file(s, rb_obj_as_string(path));
538
680
  } else {
539
- // Set the request body from a String
540
- set_curl_request_body(state, rb_funcall(stringable_or_file, rb_intern("to_s"), 0));
681
+ VALUE data = rb_obj_as_string(stringable_or_file);
682
+ SESSION_WRITE(s, upload_data, data);
683
+ setopt_or_raise(s->easy, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)RSTRING_LEN(data));
684
+ setopt_or_raise(s->easy, CURLOPT_POSTFIELDS, RSTRING_PTR(data));
541
685
  }
542
686
  }
543
687
 
544
- /* Set the options on the Curl handle from a Request object. Takes each field
545
- * in the Request object and uses it to set the appropriate option on the Curl
546
- * handle.
547
- */
548
- static void set_options_from_request(VALUE self, VALUE request) {
549
- struct patron_curl_state* state = get_patron_curl_state(self);
550
- CURL* curl = curl_easy_duphandle(state->base_handle);
551
-
552
- ID action = Qnil;
553
- VALUE headers = Qnil;
554
- VALUE url = Qnil;
555
- VALUE timeout = Qnil;
556
- VALUE redirects = Qnil;
557
- VALUE proxy = Qnil;
558
- VALUE proxy_type = Qnil;
559
- VALUE credentials = Qnil;
560
- VALUE ignore_content_length = Qnil;
561
- VALUE insecure = Qnil;
562
- VALUE cacert = Qnil;
563
- VALUE ssl_version = Qnil;
564
- VALUE ssl_cert = Qnil;
565
- VALUE ssl_cert_type = Qnil;
566
- VALUE ssl_key = Qnil;
567
- VALUE ssl_key_password = Qnil;
568
- VALUE http_version = Qnil;
569
- VALUE buffer_size = Qnil;
570
- VALUE action_name = Qnil;
571
- VALUE a_c_encoding = Qnil;
572
- VALUE download_byte_limit = Qnil;
573
- VALUE maybe_progress_proc = Qnil;
574
-
575
- /* Assign before anything can raise, so that cleanup() frees this handle and not a stale one */
576
- state->handle = curl;
577
- action_name = rb_funcall(request, rb_intern("action"), 0);
578
- a_c_encoding = rb_funcall(request, rb_intern("automatic_content_encoding"), 0);
579
- download_byte_limit = rb_funcall(request, rb_intern("download_byte_limit"), 0);
580
- maybe_progress_proc = rb_funcall(request, rb_intern("progress_callback"), 0);
581
-
582
- curl_easy_setopt(curl, CURLOPT_SHARE, state->share);
583
-
584
- if (RTEST(download_byte_limit)) {
585
- state->download_byte_limit = (curl_off_t) NUM2LL(download_byte_limit);
586
- } else {
587
- state->download_byte_limit = 0;
588
- }
688
+ static long seconds_to_milliseconds(VALUE seconds) {
689
+ return (long)(NUM2DBL(seconds) * 1000.0);
690
+ }
589
691
 
590
- if (rb_obj_is_proc(maybe_progress_proc)) {
591
- state->user_progress_blk = maybe_progress_proc;
592
- } else {
593
- state->user_progress_blk = Qnil;
594
- }
692
+ static long lookup_version(VALUE table, VALUE requested, VALUE error_class, const char *what) {
693
+ VALUE key = rb_obj_as_string(requested);
694
+ VALUE found = rb_hash_lookup2(table, key, Qundef);
695
+
696
+ if (found == Qundef) rb_raise(error_class, "Unsupported %s: %"PRIsVALUE, what, key);
697
+ return NUM2LONG(found);
698
+ }
699
+
700
+ static void set_string_option(struct patron_session *s, CURLoption option, VALUE value, int is_path) {
701
+ VALUE str = is_path ? rb_funcallv(value, id_to_s, 0, NULL) : rb_obj_as_string(value);
702
+ if (is_path) FilePathValue(str);
703
+ setopt_or_raise(s->easy, option, StringValueCStr(str));
704
+ }
705
+
706
+ static void set_options_from_request(struct patron_session *s, VALUE request) {
707
+ VALUE self = s->self;
708
+ CURL *easy;
709
+ VALUE action_name, headers, url, value, upload_data, file_name;
710
+ ID action;
595
711
 
596
- headers = rb_funcall(request, rb_intern("headers"), 0);
712
+ easy = curl_easy_duphandle(s->base);
713
+ if (!easy) rb_memerror();
714
+ /* Assigned before anything can raise so that cleanup() releases it. */
715
+ s->easy = easy;
716
+ /* duphandle() hands back a handle "as if CURLOPT_SHARE was unset" (its man page), so the
717
+ * share with the cookies and TLS sessions must be attached to every request handle anew. */
718
+ curl_easy_setopt(easy, CURLOPT_SHARE, s->share);
719
+
720
+ value = request_attr(request, id_download_byte_limit);
721
+ s->download_byte_limit = RTEST(value) ? NUM2OFFT(value) : 0;
722
+
723
+ value = request_attr(request, id_progress_callback);
724
+ SESSION_WRITE(s, progress_proc, rb_obj_is_proc(value) ? value : Qnil);
725
+
726
+ headers = request_attr(request, id_headers);
597
727
  if (RTEST(headers)) {
598
- if (rb_type(headers) != T_HASH) {
599
- rb_raise(rb_eArgError, "Headers must be passed in a hash.");
600
- }
728
+ Check_Type(headers, T_HASH);
601
729
  rb_hash_foreach(headers, each_http_header, self);
602
730
  }
603
731
 
604
- action = SYM2ID(action_name);
605
- if (RTEST(rb_funcall(request, rb_intern("force_ipv4"), 0))) {
606
- curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
732
+ if (RTEST(request_attr(request, id_force_ipv4))) {
733
+ setopt_or_raise(easy, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
607
734
  }
608
- if (action == rb_intern("get")) {
609
- VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
610
- VALUE download_file = rb_funcall(request, rb_intern("file_name"), 0);
611
-
612
- curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
613
- if (RTEST(data)) {
614
- set_request_body(state, data);
615
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
616
- }
617
- if (RTEST(download_file)) {
618
- // we need the WRITEDATA option for the file destination
619
- state->download_file = open_file(download_file, "wb");
620
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, state->download_file);
621
- // curl docs say that CURLOPT_WRITEFUNCTION must be set too
622
- // to avoid issues on Windows
623
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &file_write_handler);
624
- } else {
625
- state->download_file = NULL;
735
+
736
+ action_name = request_attr(request, id_action);
737
+ action = SYM2ID(action_name);
738
+ upload_data = request_attr(request, id_upload_data);
739
+ file_name = request_attr(request, id_file_name);
740
+
741
+ if (action == id_get) {
742
+ setopt_or_raise(easy, CURLOPT_HTTPGET, 1L);
743
+ if (RTEST(upload_data)) {
744
+ set_request_body(s, upload_data);
745
+ setopt_or_raise(easy, CURLOPT_CUSTOMREQUEST, "GET");
626
746
  }
627
- } else if (action == rb_intern("post") || action == rb_intern("put") || action == rb_intern("patch")) {
628
- VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
629
- VALUE filename = rb_funcall(request, rb_intern("file_name"), 0);
630
- VALUE multipart = rb_funcall(request, rb_intern("multipart"), 0);
631
-
632
- if (action == rb_intern("post")) {
633
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
634
- } else if (action == rb_intern("put")) {
635
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
636
- } else if (action == rb_intern("patch")) {
637
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
747
+ if (RTEST(file_name)) {
748
+ s->download_file = open_file(file_name, "wb");
749
+ SESSION_WRITE(s, body_buf, Qnil);
638
750
  }
639
-
640
- if (RTEST(data) && !RTEST(multipart)) {
641
- if (action == rb_intern("post")) {
642
- curl_easy_setopt(curl, CURLOPT_POST, 1);
751
+ } else if (action == id_post || action == id_put || action == id_patch) {
752
+ VALUE multipart = request_attr(request, id_multipart);
753
+ const char *verb = action == id_post ? "POST" : action == id_put ? "PUT" : "PATCH";
754
+
755
+ setopt_or_raise(easy, CURLOPT_CUSTOMREQUEST, verb);
756
+
757
+ if (RTEST(multipart)) {
758
+ if (RTEST(upload_data)) {
759
+ Check_Type(upload_data, T_HASH);
760
+ rb_hash_foreach(upload_data, each_mime_value, self);
643
761
  }
644
- set_request_body(state, data);
645
- } else if (RTEST(filename) && !RTEST(multipart)) {
646
- set_chunked_encoding(state);
647
- set_request_body_file(state, filename);
648
- } else if (RTEST(multipart)) {
649
- if (action == rb_intern("post")) {
650
- if(RTEST(data) && RTEST(filename)) {
651
- if (rb_type(data) == T_HASH && rb_type(filename) == T_HASH) {
652
- rb_hash_foreach(data, formadd_values, self);
653
- rb_hash_foreach(filename, formadd_files, self);
654
- } else {
655
- rb_raise(rb_eArgError, "Data and Filename must be passed in a hash.");
656
- }
657
- }
658
- curl_easy_setopt(curl, CURLOPT_HTTPPOST, state->post);
659
- } else {
660
- rb_raise(rb_eArgError, "Multipart PUT not supported");
762
+ if (RTEST(file_name)) {
763
+ Check_Type(file_name, T_HASH);
764
+ rb_hash_foreach(file_name, each_mime_file, self);
661
765
  }
662
-
766
+ if (!s->mime) s->mime = curl_mime_init(easy);
767
+ setopt_or_raise(easy, CURLOPT_MIMEPOST, s->mime);
768
+ } else if (RTEST(upload_data)) {
769
+ if (action == id_post) setopt_or_raise(easy, CURLOPT_POST, 1L);
770
+ set_request_body(s, upload_data);
771
+ } else if (RTEST(file_name)) {
772
+ s->headers = curl_slist_append(s->headers, "Transfer-Encoding: chunked");
773
+ set_request_body_from_file(s, rb_obj_as_string(file_name));
663
774
  } else {
664
- rb_raise(rb_eArgError, "Must provide either data or a filename when doing a PUT or POST");
775
+ rb_raise(rb_eArgError, "Must provide either data or a filename when doing a PUT, POST or PATCH");
665
776
  }
666
-
667
- // support for data passed with a DELETE request (e.g.: used by elasticsearch)
668
- } else if (action == rb_intern("delete")) {
669
- VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
670
- if (RTEST(data)) {
671
- curl_easy_setopt(curl, CURLOPT_POST, 1);
672
- set_request_body(state, data);
673
- }
674
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
675
-
676
- } else if (action == rb_intern("head")) {
677
- curl_easy_setopt(curl, CURLOPT_NOBODY, 1);
777
+ } else if (action == id_delete) {
778
+ if (RTEST(upload_data)) {
779
+ setopt_or_raise(easy, CURLOPT_POST, 1L);
780
+ set_request_body(s, upload_data);
781
+ }
782
+ setopt_or_raise(easy, CURLOPT_CUSTOMREQUEST, "DELETE");
783
+ } else if (action == id_head) {
784
+ setopt_or_raise(easy, CURLOPT_NOBODY, 1L);
678
785
  } else {
679
- VALUE action_name = rb_funcall(request, rb_intern("action_name"), 0);
680
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, StringValuePtr(action_name));
786
+ VALUE verb = request_attr(request, id_action_name);
787
+ setopt_or_raise(easy, CURLOPT_CUSTOMREQUEST, StringValueCStr(verb));
681
788
  }
682
789
 
683
- curl_easy_setopt(curl, CURLOPT_HTTPHEADER, state->headers);
684
- curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, state->error_buf);
685
-
686
- // Enable automatic content-encoding support via gzip/deflate if set in the request,
687
- // see https://curl.haxx.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html
688
- if(RTEST(a_c_encoding)) {
689
- #ifdef CURLOPT_ACCEPT_ENCODING
690
- curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
691
- #elif defined CURLOPT_ENCODING
692
- curl_easy_setopt(curl, CURLOPT_ENCODING, "");
693
- #else
694
- rb_raise(rb_eArgError,
695
- "The libcurl version installed doesn't support automatic content negotiation");
696
- #endif
697
- }
698
-
699
- url = rb_funcall(request, rb_intern("url"), 0);
700
- if (!RTEST(url)) {
701
- rb_raise(rb_eArgError, "Must provide a URL");
702
- }
703
- curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url));
704
-
705
-
706
- timeout = rb_funcall(request, rb_intern("timeout"), 0);
707
- if (RTEST(timeout)) {
708
- curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, floating_rb_seconds_to_milliseconds(timeout));
709
- }
790
+ setopt_or_raise(easy, CURLOPT_HTTPHEADER, s->headers);
710
791
 
711
- timeout = rb_funcall(request, rb_intern("connect_timeout"), 0);
712
- if (RTEST(timeout)) {
713
- curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, floating_rb_seconds_to_milliseconds(timeout));
792
+ if (RTEST(request_attr(request, id_automatic_content_encoding))) {
793
+ setopt_or_raise(easy, CURLOPT_ACCEPT_ENCODING, "");
714
794
  }
715
795
 
716
- timeout = rb_funcall(request, rb_intern("dns_cache_timeout"), 0);
717
- if (RTEST(timeout)) {
718
- curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, NUM2LONG(timeout));
719
- }
796
+ url = request_attr(request, id_url);
797
+ if (!RTEST(url)) rb_raise(rb_eArgError, "Must provide a URL");
798
+ set_string_option(s, CURLOPT_URL, url, 0);
720
799
 
721
- VALUE low_speed_time = rb_funcall(request, rb_intern("low_speed_time"), 0);
722
- if(RTEST(low_speed_time)) {
723
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, NUM2LONG(low_speed_time));
724
- }
800
+ value = request_attr(request, id_timeout);
801
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_TIMEOUT_MS, seconds_to_milliseconds(value));
725
802
 
726
- VALUE low_speed_limit_bytes_per_second = rb_funcall(request, rb_intern("low_speed_limit"), 0);
727
- if(RTEST(low_speed_limit_bytes_per_second)) {
728
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, NUM2LONG(low_speed_limit_bytes_per_second));
729
- }
803
+ value = request_attr(request, id_connect_timeout);
804
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_CONNECTTIMEOUT_MS, seconds_to_milliseconds(value));
730
805
 
731
- redirects = rb_funcall(request, rb_intern("max_redirects"), 0);
732
- if (RTEST(redirects)) {
733
- long r = NUM2LONG(redirects);
734
- curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, r == 0 ? 0 : 1);
735
- curl_easy_setopt(curl, CURLOPT_MAXREDIRS, r);
736
- }
806
+ value = request_attr(request, id_dns_cache_timeout);
807
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_DNS_CACHE_TIMEOUT, NUM2LONG(value));
737
808
 
738
- proxy = rb_funcall(request, rb_intern("proxy"), 0);
739
- if (RTEST(proxy)) {
740
- curl_easy_setopt(curl, CURLOPT_PROXY, StringValuePtr(proxy));
741
- }
809
+ value = request_attr(request, id_low_speed_time);
810
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_LOW_SPEED_TIME, NUM2LONG(value));
742
811
 
743
- proxy_type = rb_funcall(request, rb_intern("proxy_type"), 0);
744
- if (RTEST(proxy_type)) {
745
- curl_easy_setopt(curl, CURLOPT_PROXYTYPE, NUM2LONG(proxy_type));
746
- }
812
+ value = request_attr(request, id_low_speed_limit);
813
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_LOW_SPEED_LIMIT, NUM2LONG(value));
747
814
 
748
- credentials = rb_funcall(request, rb_intern("credentials"), 0);
749
- if (RTEST(credentials)) {
750
- VALUE auth_type = rb_funcall(request, rb_intern("auth_type"), 0);
751
- curl_easy_setopt(curl, CURLOPT_HTTPAUTH, NUM2LONG(auth_type));
752
- curl_easy_setopt(curl, CURLOPT_USERPWD, StringValuePtr(credentials));
815
+ value = request_attr(request, id_max_redirects);
816
+ if (RTEST(value)) {
817
+ long redirects = NUM2LONG(value);
818
+ setopt_or_raise(easy, CURLOPT_FOLLOWLOCATION, redirects == 0 ? 0L : 1L);
819
+ setopt_or_raise(easy, CURLOPT_MAXREDIRS, redirects);
753
820
  }
754
821
 
755
- ignore_content_length = rb_funcall(request, rb_intern("ignore_content_length"), 0);
756
- if (RTEST(ignore_content_length)) {
757
- curl_easy_setopt(curl, CURLOPT_IGNORE_CONTENT_LENGTH, 1);
758
- }
822
+ value = request_attr(request, id_proxy);
823
+ if (RTEST(value)) set_string_option(s, CURLOPT_PROXY, value, 0);
759
824
 
760
- insecure = rb_funcall(request, rb_intern("insecure"), 0);
761
- if(RTEST(insecure)) {
762
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
763
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
764
- }
825
+ value = request_attr(request, id_proxy_type);
826
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_PROXYTYPE, NUM2LONG(value));
765
827
 
766
- ssl_version = rb_funcall(request, rb_intern("ssl_version"), 0);
767
- if(RTEST(ssl_version)) {
768
- VALUE ssl_version_str = rb_funcall(ssl_version, rb_intern("to_s"), 0);
769
- char* version = StringValuePtr(ssl_version_str);
770
-
771
- if(strcmp(version, "SSLv2") == 0) {
772
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_SSLv2);
773
- } else if(strcmp(version, "SSLv3") == 0) {
774
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_SSLv3);
775
- } else if(strcmp(version, "TLSv1") == 0) {
776
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
777
- }
778
- #if LIBCURL_VERSION_NUM >= 0x072200
779
- /* this is libCURLv7.34.0 or later */
780
- else if(strcmp(version, "TLSv1_0") == 0) {
781
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_0); //libcurl v7.34.0+
782
- } else if(strcmp(version, "TLSv1_1") == 0) {
783
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_1); //libcurl v7.34.0+
784
- } else if(strcmp(version, "TLSv1_2") == 0) {
785
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); //libcurl v7.34.0+
786
- }
787
- #endif
788
- #if LIBCURL_VERSION_NUM >= 0x073400
789
- /* this is libCURLv7.52.0 or later */
790
- else if(strcmp(version, "TLSv1_3") == 0) {
791
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3); //libcurl v7.52.0+
792
- }
793
- #endif
794
- else {
795
- rb_raise(eUnsupportedSSLVersion, "Unsupported SSL version: %s", version);
796
- }
828
+ value = request_attr(request, id_credentials);
829
+ if (RTEST(value)) {
830
+ setopt_or_raise(easy, CURLOPT_HTTPAUTH, NUM2LONG(request_attr(request, id_auth_type)));
831
+ set_string_option(s, CURLOPT_USERPWD, value, 0);
797
832
  }
798
833
 
799
- http_version = rb_funcall(request, rb_intern("http_version"), 0);
800
- if(RTEST(http_version)) {
801
- VALUE http_version_str = rb_funcall(http_version, rb_intern("to_s"), 0);
802
- char* version = StringValuePtr(http_version_str);
803
- if(strcmp(version, "None") == 0) {
804
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_NONE);
805
- } else if(strcmp(version, "HTTPv1_0") == 0) {
806
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
807
- } else if(strcmp(version, "HTTPv1_1") == 0) {
808
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
809
- }
810
- #if LIBCURL_VERSION_NUM >= 0x072100
811
- /* this is libCURLv7.33.0 or later */
812
- else if(strcmp(version, "HTTPv2_0") == 0) {
813
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); //libcurl v7.33.0+
814
- }
815
- #endif
816
- #if LIBCURL_VERSION_NUM >= 0x072F00
817
- /* this is libCURLv7.47.0 or later */
818
- else if(strcmp(version, "HTTPv2_TLS") == 0) {
819
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); //libcurl v7.47.0+
820
- }
821
- #endif
822
- #if LIBCURL_VERSION_NUM >= 0x073100
823
- /* this is libCURLv7.49.0 or later */
824
- else if(strcmp(version, "HTTPv2_PRIOR") == 0) {
825
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE); //libcurl v7.49.0+
826
- }
827
- #endif
828
- else {
829
- rb_raise(eUnsupportedHTTPVersion, "Unsupported HTTP version: %s", version);
830
- }
834
+ if (RTEST(request_attr(request, id_ignore_content_length))) {
835
+ setopt_or_raise(easy, CURLOPT_IGNORE_CONTENT_LENGTH, 1L);
831
836
  }
832
837
 
833
- ssl_cert = rb_funcall(request, rb_intern("ssl_cert"), 0);
834
- if(RTEST(ssl_cert)) {
835
- ssl_cert = FilePathValue(ssl_cert);
836
- curl_easy_setopt(curl, CURLOPT_SSLCERT, StringValueCStr(ssl_cert));
838
+ if (RTEST(request_attr(request, id_insecure))) {
839
+ setopt_or_raise(easy, CURLOPT_SSL_VERIFYPEER, 0L);
840
+ setopt_or_raise(easy, CURLOPT_SSL_VERIFYHOST, 0L);
837
841
  }
838
842
 
839
- ssl_cert_type = rb_funcall(request, rb_intern("ssl_cert_type"), 0);
840
- if(RTEST(ssl_cert_type)) {
841
- ssl_cert_type = rb_obj_as_string(ssl_cert_type);
842
- curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, StringValueCStr(ssl_cert_type));
843
+ value = request_attr(request, id_ssl_version);
844
+ if (RTEST(value)) {
845
+ setopt_or_raise(easy, CURLOPT_SSLVERSION, lookup_version(hSSLVersions, value, eUnsupportedSSLVersion, "SSL version"));
843
846
  }
844
847
 
845
- ssl_key = rb_funcall(request, rb_intern("ssl_key"), 0);
846
- if(RTEST(ssl_key)) {
847
- ssl_key = FilePathValue(ssl_key);
848
- curl_easy_setopt(curl, CURLOPT_SSLKEY, StringValueCStr(ssl_key));
848
+ value = request_attr(request, id_http_version);
849
+ if (RTEST(value)) {
850
+ setopt_or_raise(easy, CURLOPT_HTTP_VERSION, lookup_version(hHTTPVersions, value, eUnsupportedHTTPVersion, "HTTP version"));
849
851
  }
850
852
 
851
- ssl_key_password = rb_funcall(request, rb_intern("ssl_key_password"), 0);
852
- if(RTEST(ssl_key_password)) {
853
- curl_easy_setopt(curl, CURLOPT_KEYPASSWD, StringValueCStr(ssl_key_password));
854
- }
853
+ value = request_attr(request, id_ssl_cert);
854
+ if (RTEST(value)) set_string_option(s, CURLOPT_SSLCERT, value, 1);
855
855
 
856
- cacert = rb_funcall(request, rb_intern("cacert"), 0);
857
- if(RTEST(cacert)) {
858
- cacert = FilePathValue(cacert);
859
- curl_easy_setopt(curl, CURLOPT_CAINFO, StringValueCStr(cacert));
860
- }
856
+ value = request_attr(request, id_ssl_cert_type);
857
+ if (RTEST(value)) set_string_option(s, CURLOPT_SSLCERTTYPE, value, 0);
861
858
 
862
- buffer_size = rb_funcall(request, rb_intern("buffer_size"), 0);
863
- if (RTEST(buffer_size)) {
864
- curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, NUM2LONG(buffer_size));
865
- }
859
+ value = request_attr(request, id_ssl_key);
860
+ if (RTEST(value)) set_string_option(s, CURLOPT_SSLKEY, value, 1);
861
+
862
+ value = request_attr(request, id_ssl_key_password);
863
+ if (RTEST(value)) set_string_option(s, CURLOPT_KEYPASSWD, value, 0);
864
+
865
+ value = request_attr(request, id_cacert);
866
+ if (RTEST(value)) set_string_option(s, CURLOPT_CAINFO, value, 1);
867
+
868
+ value = request_attr(request, id_buffer_size);
869
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_BUFFERSIZE, NUM2LONG(value));
866
870
  }
867
871
 
868
- /* Store one of curl's transfer marks (seconds since the request started) under +key+. */
869
- static void set_timing(CURL* curl, VALUE timings, const char* key, CURLINFO info) {
870
- double seconds = 0;
871
- if (curl_easy_getinfo(curl, info, &seconds) == CURLE_OK) {
872
- rb_hash_aset(timings, ID2SYM(rb_intern(key)), rb_float_new(seconds));
872
+ /*----------------------------------------------------------------------------*/
873
+ /* Response */
874
+
875
+ static void set_timing(CURL *easy, VALUE timings, const char *key, CURLINFO info) {
876
+ curl_off_t microseconds = 0;
877
+ if (curl_easy_getinfo(easy, info, &microseconds) == CURLE_OK) {
878
+ rb_hash_aset(timings, ID2SYM(rb_intern(key)), rb_float_new((double)microseconds / 1000000.0));
873
879
  }
874
880
  }
875
881
 
876
- /* curl's cumulative transfer marks: namelookup <= connect <= appconnect <= pretransfer
877
- * <= starttransfer <= total. They are read after the transfer, so they cost no GVL time. */
878
- static VALUE create_timings(CURL* curl) {
882
+ /* libcurl's cumulative transfer marks in seconds since the request started:
883
+ * namelookup <= connect <= appconnect <= pretransfer <= starttransfer <= total. */
884
+ static VALUE create_timings(CURL *easy) {
879
885
  VALUE timings = rb_hash_new();
880
- set_timing(curl, timings, "namelookup", CURLINFO_NAMELOOKUP_TIME);
881
- set_timing(curl, timings, "connect", CURLINFO_CONNECT_TIME);
882
- set_timing(curl, timings, "appconnect", CURLINFO_APPCONNECT_TIME);
883
- set_timing(curl, timings, "pretransfer", CURLINFO_PRETRANSFER_TIME);
884
- set_timing(curl, timings, "starttransfer", CURLINFO_STARTTRANSFER_TIME);
885
- set_timing(curl, timings, "redirect", CURLINFO_REDIRECT_TIME);
886
- set_timing(curl, timings, "total", CURLINFO_TOTAL_TIME);
886
+ set_timing(easy, timings, "namelookup", CURLINFO_NAMELOOKUP_TIME_T);
887
+ set_timing(easy, timings, "connect", CURLINFO_CONNECT_TIME_T);
888
+ set_timing(easy, timings, "appconnect", CURLINFO_APPCONNECT_TIME_T);
889
+ set_timing(easy, timings, "pretransfer", CURLINFO_PRETRANSFER_TIME_T);
890
+ set_timing(easy, timings, "starttransfer", CURLINFO_STARTTRANSFER_TIME_T);
891
+ set_timing(easy, timings, "redirect", CURLINFO_REDIRECT_TIME_T);
892
+ set_timing(easy, timings, "total", CURLINFO_TOTAL_TIME_T);
887
893
  return timings;
888
894
  }
889
895
 
890
- /* Use the info in a Curl handle to create a new Response object. */
891
- static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE body_buffer) {
892
- VALUE args[6] = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil };
893
- char* effective_url = NULL;
896
+ static VALUE create_response(struct patron_session *s) {
897
+ VALUE self = s->self;
898
+ CURL *easy = s->easy;
899
+ VALUE kwargs = rb_hash_new();
900
+ char *effective_url = NULL;
894
901
  long code = 0;
895
- long count = 0;
896
- VALUE responseKlass = Qnil;
897
- VALUE response = Qnil;
898
-
899
- curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url);
900
- args[0] = rb_str_new2(effective_url);
901
-
902
- curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
903
- args[1] = LONG2NUM(code);
904
-
905
- curl_easy_getinfo(curl, CURLINFO_REDIRECT_COUNT, &count);
906
- args[2] = LONG2NUM(count);
907
-
908
- args[3] = header_buffer;
909
- args[4] = body_buffer;
910
- args[5] = rb_funcall(self, rb_intern("default_response_charset"), 0);
911
-
912
- responseKlass = rb_funcall(self, rb_intern("response_class"), 0);
913
- response = rb_class_new_instance(6, args, responseKlass);
914
- /* Set after construction so custom response classes keep their #initialize signature. */
915
- rb_iv_set(response, "@timings", create_timings(curl));
916
- return response;
902
+ long redirect_count = 0;
903
+ VALUE klass;
904
+
905
+ curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &effective_url);
906
+ curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &code);
907
+ curl_easy_getinfo(easy, CURLINFO_REDIRECT_COUNT, &redirect_count);
908
+
909
+ rb_hash_aset(kwargs, ID2SYM(id_url_kw), effective_url ? rb_usascii_str_new_cstr(effective_url) : Qnil);
910
+ rb_hash_aset(kwargs, ID2SYM(id_status_kw), LONG2NUM(code));
911
+ rb_hash_aset(kwargs, ID2SYM(id_redirect_count_kw), LONG2NUM(redirect_count));
912
+ rb_hash_aset(kwargs, ID2SYM(id_header_data_kw), s->header_buf);
913
+ rb_hash_aset(kwargs, ID2SYM(id_body_kw), s->body_buf);
914
+ rb_hash_aset(kwargs, ID2SYM(id_default_charset_kw), rb_funcallv(self, id_default_response_charset, 0, NULL));
915
+ rb_hash_aset(kwargs, ID2SYM(id_timings_kw), create_timings(easy));
916
+
917
+ klass = rb_funcallv(self, id_response_class, 0, NULL);
918
+ return rb_class_new_instance_kw(1, &kwargs, klass, RB_PASS_KEYWORDS);
917
919
  }
918
920
 
919
- /* Raise an exception based on the Curl error code. */
920
- static VALUE select_error(CURLcode code) {
921
- VALUE error = Qnil;
922
- switch (code) {
923
- case CURLE_UNSUPPORTED_PROTOCOL: error = eUnsupportedProtocol; break;
924
- case CURLE_URL_MALFORMAT: error = eURLFormatError; break;
925
- case CURLE_COULDNT_RESOLVE_HOST: error = eHostResolutionError; break;
926
- case CURLE_COULDNT_CONNECT: error = eConnectionFailed; break;
927
- case CURLE_PARTIAL_FILE: error = ePartialFileError; break;
928
- case CURLE_OPERATION_TIMEDOUT: error = eTimeoutError; break;
929
- case CURLE_TOO_MANY_REDIRECTS: error = eTooManyRedirects; break;
930
- case CURLE_ABORTED_BY_CALLBACK: error = eAborted; break;
931
- case CURLE_SSL_CERTPROBLEM: error = eClientCertificateError; break;
932
- default: error = ePatronError;
933
- }
921
+ /*----------------------------------------------------------------------------*/
922
+ /* Performing a request */
934
923
 
935
- return error;
936
- }
924
+ struct poll_context {
925
+ CURLM *multi;
926
+ CURLMcode rc;
927
+ };
937
928
 
929
+ static void *poll_without_gvl(void *ptr) {
930
+ struct poll_context *ctx = ptr;
931
+ ctx->rc = curl_multi_poll(ctx->multi, NULL, 0, POLL_TIMEOUT_MS, NULL);
932
+ return NULL;
933
+ }
938
934
 
939
- struct perform_context {
940
- CURL *curl;
941
- CURLcode code;
942
- };
935
+ /* Runs without the GVL whenever Ruby needs this thread back (Thread#kill, Thread#raise,
936
+ * signals, interpreter exit). Only wakes the poll; Ruby decides after it returns whether
937
+ * there is an exception to raise. */
938
+ static void wakeup_poll(void *ptr) {
939
+ struct patron_session *s = ptr;
940
+ curl_multi_wakeup(s->multi);
941
+ }
943
942
 
944
- static void *perform_without_gvl(void *ptr) {
945
- struct perform_context *context = ptr;
943
+ /* Blocks the whole thread until libcurl has something to do, with the GVL released */
944
+ static void wait_without_scheduler(struct patron_session *s) {
945
+ struct poll_context ctx = { s->multi, CURLM_OK };
946
+ /* Pending Ruby interrupts are raised right here as rb_nogvl returns; the rb_ensure
947
+ * around the request takes care of the half-finished transfer. */
948
+ rb_nogvl(poll_without_gvl, &ctx, wakeup_poll, s, RB_NOGVL_UBF_ASYNC_SAFE);
949
+ if (ctx.rc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(ctx.rc));
950
+ }
946
951
 
947
- context->code = curl_easy_perform(context->curl);
948
- return NULL;
952
+ static VALUE float_seconds(long milliseconds) {
953
+ return rb_float_new((double)milliseconds / 1000.0);
949
954
  }
950
955
 
951
- /* Uses as the unblocking function when the thread running Patron gets
952
- signaled. The important difference with session_interrupt is that we
953
- are not allowed to touch any Ruby structures while outside the GIL,
954
- but we _are_ permitted to touch our internal curl state struct
955
- */
956
- void session_ubf_abort(void* patron_state) {
957
- struct patron_curl_state* state = (struct patron_curl_state*) patron_state;
958
- state->interrupt = INTERRUPT_ABORT;
956
+ /* An IO wrapping one of libcurl's sockets, for handing to the scheduler. FMODE_EXTERNAL keeps
957
+ * Ruby from ever closing the descriptor; libcurl owns it. The wrappers are dropped whenever the
958
+ * set of sockets changes, so a descriptor number the kernel reused gets a fresh one. */
959
+ static VALUE socket_io(struct patron_session *s, int fd) {
960
+ VALUE key = INT2FIX(fd);
961
+ VALUE io = rb_hash_lookup2(s->socket_ios, key, Qnil);
962
+
963
+ if (NIL_P(io)) {
964
+ io = rb_io_open_descriptor(rb_cIO, fd, FMODE_READWRITE | FMODE_EXTERNAL, Qnil, Qnil, NULL);
965
+ rb_hash_aset(s->socket_ios, key, io);
966
+ }
967
+ return io;
959
968
  }
960
969
 
961
- /* Perform the actual HTTP request by calling libcurl. */
962
- static VALUE perform_request(VALUE self) {
963
- struct patron_curl_state *state = get_patron_curl_state(self);
964
- CURL* curl = state->handle;
965
- struct perform_context context = {curl, 0};
966
-
967
- state->interrupt = 0; /* clear the interrupt flag */
968
- state->pending_tag = 0;
969
- state->pending_exception = Qnil;
970
-
971
- rb_thread_call_without_gvl(perform_without_gvl, &context, session_ubf_abort, state);
972
-
973
- /* The progress proc threw: resume that exit now that libcurl is done with the handle.
974
- cleanup() still runs, since we are inside the rb_ensure of session_handle_request. */
975
- if (state->pending_tag) {
976
- int tag = state->pending_tag;
977
- VALUE exception = state->pending_exception;
978
- state->pending_tag = 0;
979
- state->pending_exception = Qnil;
980
- /* errinfo is only an Exception for a raise; a throw leaves an internal object there and
981
- Thread#kill a Fixnum, both of which rb_obj_is_kind_of must not be asked about */
982
- if (RB_TYPE_P(exception, T_OBJECT) && rb_obj_is_kind_of(exception, rb_eException)) {
983
- rb_exc_raise(exception);
970
+ #define MAX_WAIT_FDS 16
971
+
972
+ /* Yields to the Fiber scheduler until libcurl has something to do, so other fibers on this thread
973
+ * run meanwhile. Uses io_wait for the usual single socket, io_select when libcurl juggles several
974
+ * (happy eyeballs), and a plain sleep while it has none (threaded resolver at work). */
975
+ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
976
+ fd_set rd, wr, ex;
977
+ int maxfd = -1, fd, nfds = 0, i;
978
+ int fds[MAX_WAIT_FDS], events[MAX_WAIT_FDS];
979
+ long timeout_ms = -1;
980
+ VALUE timeout;
981
+
982
+ FD_ZERO(&rd); FD_ZERO(&wr); FD_ZERO(&ex);
983
+ curl_multi_fdset(s->multi, &rd, &wr, &ex, &maxfd);
984
+ curl_multi_timeout(s->multi, &timeout_ms);
985
+ if (timeout_ms < 0 || timeout_ms > POLL_TIMEOUT_MS) timeout_ms = POLL_TIMEOUT_MS;
986
+
987
+ for (fd = 0; fd <= maxfd && nfds < MAX_WAIT_FDS; fd++) {
988
+ int ev = 0;
989
+ if (FD_ISSET(fd, &rd)) ev |= RUBY_IO_READABLE;
990
+ if (FD_ISSET(fd, &wr)) ev |= RUBY_IO_WRITABLE;
991
+ if (FD_ISSET(fd, &ex)) ev |= RUBY_IO_PRIORITY;
992
+ if (!ev) continue;
993
+ fds[nfds] = fd;
994
+ events[nfds] = ev;
995
+ nfds++;
996
+ }
997
+
998
+ if (nfds == 0) {
999
+ /* libcurl asks to be called back within 100 ms when it has no socket to wait on */
1000
+ if (timeout_ms > 100) timeout_ms = 100;
1001
+ rb_fiber_scheduler_kernel_sleep(scheduler, float_seconds(timeout_ms));
1002
+ return;
1003
+ }
1004
+
1005
+ if (NIL_P(s->socket_ios)) SESSION_WRITE(s, socket_ios, rb_hash_new());
1006
+ if (RHASH_SIZE(s->socket_ios) != (st_index_t)nfds) rb_hash_clear(s->socket_ios);
1007
+ for (i = 0; i < nfds; i++) {
1008
+ if (rb_hash_lookup2(s->socket_ios, INT2FIX(fds[i]), Qundef) == Qundef) {
1009
+ rb_hash_clear(s->socket_ios);
1010
+ break;
984
1011
  }
985
- rb_jump_tag(tag);
986
1012
  }
987
1013
 
988
- if (CURLE_OK == context.code) {
989
- VALUE header_str = membuffer_to_rb_str(&state->header_buffer);
990
- VALUE body_str = Qnil;
991
- if (!state->download_file) { body_str = membuffer_to_rb_str(&state->body_buffer); }
992
-
993
- curl_easy_setopt(curl, CURLOPT_COOKIELIST, "FLUSH"); // Flush cookies to the cookie jar
994
-
995
- return create_response(self, curl, header_str, body_str);
996
- } else {
997
- rb_raise(select_error(context.code), "%s", state->error_buf);
1014
+ timeout = float_seconds(timeout_ms);
1015
+ if (nfds == 1) {
1016
+ rb_fiber_scheduler_io_wait(scheduler, socket_io(s, fds[0]), INT2NUM(events[0]), timeout);
1017
+ return;
998
1018
  }
999
- }
1000
1019
 
1001
- /* Setting the options can raise too (a Request attribute of the wrong type, say),
1002
- * and this needs to run under rb_ensure so that the half-configured handle, headers and
1003
- * open files get cleaned up instead of leaking into the next request on this Session.
1004
- */
1005
- static VALUE prepare_and_perform_request(VALUE args) {
1006
- VALUE self = rb_ary_entry(args, 0);
1007
- set_options_from_request(self, rb_ary_entry(args, 1));
1008
- return perform_request(self);
1020
+ {
1021
+ VALUE readables = rb_ary_new(), writables = rb_ary_new(), exceptables = rb_ary_new();
1022
+ VALUE ready;
1023
+
1024
+ for (i = 0; i < nfds; i++) {
1025
+ VALUE io = socket_io(s, fds[i]);
1026
+ if (events[i] & RUBY_IO_READABLE) rb_ary_push(readables, io);
1027
+ if (events[i] & RUBY_IO_WRITABLE) rb_ary_push(writables, io);
1028
+ if (events[i] & RUBY_IO_PRIORITY) rb_ary_push(exceptables, io);
1029
+ }
1030
+ ready = rb_fiber_scheduler_io_select(scheduler, readables, writables, exceptables, timeout);
1031
+ if (ready == Qundef) {
1032
+ /* A scheduler without io_select; poll instead. Only ever happens while connecting. */
1033
+ rb_fiber_scheduler_kernel_sleep(scheduler, float_seconds(timeout_ms > 50 ? 50 : timeout_ms));
1034
+ }
1035
+ }
1009
1036
  }
1010
1037
 
1011
- /* Cleanup after each request by resetting the Curl handle and deallocating
1012
- * all request related objects such as the header slist.
1013
- */
1014
- static VALUE cleanup(VALUE self) {
1015
- struct patron_curl_state *state = get_patron_curl_state(self);
1016
- curl_easy_cleanup(state->handle);
1017
- state->handle = NULL;
1038
+ static void drive_transfer(struct patron_session *s) {
1039
+ int running = 1;
1040
+ CURLMcode mc;
1041
+ /* Set per thread; a Session inside Async { } yields to it instead of blocking the thread */
1042
+ VALUE scheduler = rb_fiber_scheduler_current();
1018
1043
 
1019
- if (state->headers) {
1020
- curl_slist_free_all(state->headers);
1021
- state->headers = NULL;
1044
+ mc = curl_multi_add_handle(s->multi, s->easy);
1045
+ if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
1046
+
1047
+ while (running) {
1048
+ mc = curl_multi_perform(s->multi, &running);
1049
+ if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
1050
+ if (s->pending_tag || s->interrupt) break;
1051
+ if (!running) break;
1052
+
1053
+ if (NIL_P(scheduler)) {
1054
+ wait_without_scheduler(s);
1055
+ } else {
1056
+ wait_with_scheduler(s, scheduler);
1057
+ }
1058
+ if (s->interrupt) break;
1022
1059
  }
1023
- membuffer_clear(&state->header_buffer);
1060
+ }
1024
1061
 
1025
- if (state->download_file) {
1026
- fclose(state->download_file);
1027
- state->download_file = NULL;
1028
- } else {
1029
- membuffer_clear(&state->body_buffer);
1062
+ static CURLcode transfer_result(struct patron_session *s) {
1063
+ CURLcode result = CURLE_OK;
1064
+ CURLMsg *msg;
1065
+ int remaining = 0;
1066
+
1067
+ while ((msg = curl_multi_info_read(s->multi, &remaining))) {
1068
+ if (msg->msg == CURLMSG_DONE && msg->easy_handle == s->easy) result = msg->data.result;
1030
1069
  }
1070
+ return result;
1071
+ }
1031
1072
 
1032
- if (state->request_body_file) {
1033
- fclose(state->request_body_file);
1034
- state->request_body_file = NULL;
1073
+ struct perform_args {
1074
+ struct patron_session *s;
1075
+ VALUE request;
1076
+ };
1077
+
1078
+ static VALUE perform(VALUE ptr) {
1079
+ struct perform_args *args = (struct perform_args *)ptr;
1080
+ struct patron_session *s = args->s;
1081
+ CURLcode result;
1082
+
1083
+ s->interrupt = 0;
1084
+ s->pending_tag = 0;
1085
+ s->error_buf[0] = '\0';
1086
+ SESSION_WRITE(s, header_buf, rb_str_buf_new(HEADER_BUFFER_INITIAL_CAPACITY));
1087
+ SESSION_WRITE(s, body_buf, rb_str_buf_new(BODY_BUFFER_INITIAL_CAPACITY));
1088
+
1089
+ set_options_from_request(s, args->request);
1090
+ drive_transfer(s);
1091
+
1092
+ if (s->pending_tag) reraise_parked(s);
1093
+
1094
+ if (s->interrupt == INTERRUPT_DOWNLOAD_OVERFLOW) {
1095
+ raise_curl_error(eDownloadLimitExceeded, CURLE_ABORTED_BY_CALLBACK, "Download byte limit exceeded");
1035
1096
  }
1036
-
1037
- if (state->post) {
1038
- curl_formfree(state->post);
1039
- state->post = NULL;
1040
- state->last = NULL;
1097
+ if (s->interrupt) {
1098
+ raise_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
1041
1099
  }
1042
1100
 
1043
- state->upload_data = Qnil;
1044
- state->pending_tag = 0;
1045
- state->pending_exception = Qnil;
1046
- state->busy = 0;
1101
+ result = transfer_result(s);
1102
+ if (result != CURLE_OK) raise_curl_error(error_class_for(result), result, s->error_buf);
1103
+
1104
+ /* Write the cookies accumulated in the share out to the jar, if one is configured */
1105
+ curl_easy_setopt(s->easy, CURLOPT_COOKIELIST, "FLUSH");
1047
1106
 
1107
+ return create_response(s);
1108
+ }
1109
+
1110
+ /* Runs under rb_ensure after every request, whether it finished, raised or was interrupted. */
1111
+ static VALUE cleanup(VALUE self) {
1112
+ struct patron_session *s = get_session(self);
1113
+
1114
+ if (s->easy) {
1115
+ curl_multi_remove_handle(s->multi, s->easy);
1116
+ curl_easy_cleanup(s->easy);
1117
+ s->easy = NULL;
1118
+ }
1119
+ if (s->mime) {
1120
+ curl_mime_free(s->mime);
1121
+ s->mime = NULL;
1122
+ }
1123
+ if (s->headers) {
1124
+ curl_slist_free_all(s->headers);
1125
+ s->headers = NULL;
1126
+ }
1127
+ if (s->download_file) {
1128
+ fclose(s->download_file);
1129
+ s->download_file = NULL;
1130
+ }
1131
+ if (s->request_body_file) {
1132
+ fclose(s->request_body_file);
1133
+ s->request_body_file = NULL;
1134
+ }
1135
+
1136
+ SESSION_WRITE(s, progress_proc, Qnil);
1137
+ SESSION_WRITE(s, upload_data, Qnil);
1138
+ SESSION_WRITE(s, header_buf, Qnil);
1139
+ SESSION_WRITE(s, body_buf, Qnil);
1140
+ s->download_byte_limit = 0;
1141
+ s->busy = 0;
1048
1142
  return Qnil;
1049
1143
  }
1050
1144
 
1051
1145
  /*
1052
- * Peform the actual HTTP request by calling libcurl. Each filed in the
1053
- * +request+ object will be used to set the appropriate option on the libcurl
1054
- * library. After the request completes, a Response object will be created and
1055
- * returned.
1146
+ * Performs the HTTP request described by +request+ and returns a Response built with
1147
+ * #response_class. Raises a Patron::Error subclass when libcurl reports a failure.
1056
1148
  *
1057
- * In the event of an error in the libcurl library, a Ruby exception will be
1058
- * created and raised. The exception will return the libcurl error code and
1059
- * error message.
1060
- *
1061
- * @param request[Patron::Request] the request to use when filling the CURL options
1062
- * @return [Patron::Response] the result of calling `response_class` on the Session
1149
+ * @param request [Patron::Request]
1150
+ * @return [Patron::Response]
1063
1151
  */
1064
1152
  static VALUE session_handle_request(VALUE self, VALUE request) {
1065
- struct patron_curl_state *state = get_patron_curl_state(self);
1066
- VALUE args = Qnil;
1153
+ struct patron_session *s = get_session(self);
1154
+ struct perform_args args = { s, request };
1067
1155
 
1068
- if (state->busy) {
1069
- rb_raise(ePatronError, "Session is already performing a request on another thread; use one Session per thread");
1156
+ if (s->busy) {
1157
+ rb_raise(ePatronError, "This Session is already performing a request in another thread or fiber; use one Session per thread or fiber");
1070
1158
  }
1071
- state->busy = 1;
1072
-
1073
- args = rb_ary_new3(2, self, request);
1074
- return rb_ensure(&prepare_and_perform_request, args, &cleanup, self);
1159
+ ensure_handles_for_this_process(s);
1160
+ s->busy = 1;
1161
+ return rb_ensure(perform, (VALUE)&args, cleanup, self);
1075
1162
  }
1076
1163
 
1077
- /* Interrupt any currently executing request. This will cause the current
1078
- * request to error and raise an exception. The method can be called from another thread to
1079
- * abort the request in-flight.
1164
+ /*
1165
+ * Aborts the request currently in flight on this Session, which then raises Patron::Aborted.
1166
+ * Safe to call from another thread.
1080
1167
  *
1081
- * @return [void] This method always raises
1168
+ * @return self
1082
1169
  */
1083
1170
  static VALUE session_interrupt(VALUE self) {
1084
- struct patron_curl_state *state = get_patron_curl_state(self);
1085
- state->interrupt = INTERRUPT_ABORT;
1171
+ struct patron_session *s = get_session(self);
1172
+
1173
+ s->interrupt = INTERRUPT_ABORT;
1174
+ if (s->multi) curl_multi_wakeup(s->multi);
1086
1175
  return self;
1087
1176
  }
1088
1177
 
1089
1178
  /*
1090
- * Turn on cookie handling for this session, storing them in memory by
1091
- * default or in +file+ if specified. The `file` must be readable and
1092
- * writable. Calling multiple times will add more files.
1093
- * FIXME: what does the empty string actually do here?
1094
- *
1095
- * @param [String] file path to the existing cookie file, or nil to store in memory.
1096
- * @return self
1179
+ * Enables cookie handling: in memory when +path+ is empty, otherwise read from and written
1180
+ * back to the file at +path+.
1097
1181
  */
1098
- static VALUE add_cookie_file(VALUE self, VALUE file) {
1099
- struct patron_curl_state *state = get_patron_curl_state(self);
1100
- CURL* curl = state->base_handle;
1101
- char* file_path = NULL;
1102
-
1103
- // FIXME: http://websystemsengineering.blogspot.nl/2013/03/curloptcookiefile-vs-curloptcookiejar.html
1104
- file_path = StringValueCStr(file);
1105
- if (strlen(file_path) != 0) {
1106
- curl_easy_setopt(curl, CURLOPT_COOKIEJAR, file_path);
1107
- }
1108
- curl_easy_setopt(curl, CURLOPT_COOKIEFILE, file_path);
1182
+ static VALUE session_set_cookie_file(VALUE self, VALUE path) {
1183
+ struct patron_session *s = get_session(self);
1184
+ const char *file_path = StringValueCStr(path);
1109
1185
 
1186
+ if (*file_path) setopt_or_raise(s->base, CURLOPT_COOKIEJAR, file_path);
1187
+ setopt_or_raise(s->base, CURLOPT_COOKIEFILE, file_path);
1110
1188
  return self;
1111
1189
  }
1112
1190
 
1113
1191
  /*
1114
- * Enable debug output to stderr or to specified +file+.
1115
- *
1116
- * @param [String, nil] file path to the debug file, or nil to write to STDERR
1117
- * @return self
1192
+ * Sends libcurl's verbose output to +io+, or disables it when +io+ is nil.
1118
1193
  */
1119
- static VALUE set_debug_file(VALUE self, VALUE file) {
1120
- struct patron_curl_state *state = get_patron_curl_state(self);
1121
- CURL *curl = state->base_handle;
1122
- char *file_path = StringValueCStr(file);
1194
+ static VALUE session_set_debug_io(VALUE self, VALUE io) {
1195
+ struct patron_session *s = get_session(self);
1123
1196
 
1124
- session_close_debug_file(state);
1197
+ SESSION_WRITE(s, debug_io, io);
1198
+ setopt_or_raise(s->base, CURLOPT_VERBOSE, NIL_P(io) ? 0L : 1L);
1199
+ return self;
1200
+ }
1125
1201
 
1126
- if(strlen(file_path) != 0) {
1127
- state->debug_file = open_file(file, "wb");
1128
- } else {
1129
- state->debug_file = stderr;
1130
- }
1131
- curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
1132
- curl_easy_setopt(curl, CURLOPT_STDERR, state->debug_file);
1202
+ /*----------------------------------------------------------------------------*/
1203
+ /* Init */
1133
1204
 
1134
- return self;
1205
+ static VALUE shareable_table(void) {
1206
+ return rb_hash_new();
1135
1207
  }
1136
1208
 
1209
+ static void table_entry(VALUE table, const char *name, long value) {
1210
+ rb_hash_aset(table, rb_interned_str_cstr(name), LONG2NUM(value));
1211
+ }
1137
1212
 
1138
- /*----------------------------------------------------------------------------*/
1139
- /* Extension initialization */
1213
+ void Init_session_ext(void) {
1214
+ /* Must precede the method definitions it applies to. There is no mutable global state:
1215
+ * every handle belongs to one Session, and libcurl's own globals are initialised once here. */
1216
+ rb_ext_ractor_safe(true);
1140
1217
 
1141
- void Init_session_ext() {
1142
1218
  curl_global_init(CURL_GLOBAL_ALL);
1143
1219
  rb_require("patron/error");
1144
-
1145
- rb_set_end_proc(&cs_list_interrupt, Qnil);
1220
+ rb_require("patron/request");
1221
+
1222
+ #define DEFINE_ID(name) id_##name = rb_intern_const(#name);
1223
+ REQUEST_ATTRIBUTES(DEFINE_ID)
1224
+ #undef DEFINE_ID
1225
+ id_to_path = rb_intern_const("to_path");
1226
+ id_to_s = rb_intern_const("to_s");
1227
+ id_response_class = rb_intern_const("response_class");
1228
+ id_default_response_charset = rb_intern_const("default_response_charset");
1229
+ id_get = rb_intern_const("get");
1230
+ id_post = rb_intern_const("post");
1231
+ id_put = rb_intern_const("put");
1232
+ id_patch = rb_intern_const("patch");
1233
+ id_delete = rb_intern_const("delete");
1234
+ id_head = rb_intern_const("head");
1235
+ id_ivar_curl_code = rb_intern_const("@curl_code");
1236
+ id_url_kw = rb_intern_const("url");
1237
+ id_status_kw = rb_intern_const("status");
1238
+ id_redirect_count_kw = rb_intern_const("redirect_count");
1239
+ id_header_data_kw = rb_intern_const("header_data");
1240
+ id_body_kw = rb_intern_const("body");
1241
+ id_default_charset_kw = rb_intern_const("default_charset");
1242
+ id_timings_kw = rb_intern_const("timings");
1146
1243
 
1147
1244
  mPatron = rb_define_module("Patron");
1148
1245
 
1149
- /* These classes are defined in Ruby and only looked up here, so unlike the ones from
1150
- rb_define_class the GC does not know we hold them: register them, or compaction moves
1151
- them out from under us and rb_raise ends up instantiating garbage */
1246
+ /* These are Ruby-defined classes and tables, not immortal rb_define_class() ones,
1247
+ * so the GC has to know about the slots or compaction leaves them dangling */
1248
+ rb_global_variable(&cRequest);
1249
+ rb_global_variable(&hSSLVersions);
1250
+ rb_global_variable(&hHTTPVersions);
1152
1251
  rb_global_variable(&ePatronError);
1153
1252
  rb_global_variable(&eUnsupportedProtocol);
1154
1253
  rb_global_variable(&eUnsupportedSSLVersion);
@@ -1160,10 +1259,10 @@ void Init_session_ext() {
1160
1259
  rb_global_variable(&eTimeoutError);
1161
1260
  rb_global_variable(&eTooManyRedirects);
1162
1261
  rb_global_variable(&eAborted);
1262
+ rb_global_variable(&eDownloadLimitExceeded);
1163
1263
  rb_global_variable(&eClientCertificateError);
1164
1264
 
1165
1265
  ePatronError = rb_const_get(mPatron, rb_intern("Error"));
1166
-
1167
1266
  eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
1168
1267
  eUnsupportedSSLVersion = rb_const_get(mPatron, rb_intern("UnsupportedSSLVersion"));
1169
1268
  eUnsupportedHTTPVersion = rb_const_get(mPatron, rb_intern("UnsupportedHTTPVersion"));
@@ -1174,38 +1273,60 @@ void Init_session_ext() {
1174
1273
  eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
1175
1274
  eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));
1176
1275
  eAborted = rb_const_get(mPatron, rb_intern("Aborted"));
1276
+ eDownloadLimitExceeded = rb_const_get(mPatron, rb_intern("DownloadLimitExceeded"));
1177
1277
  eClientCertificateError = rb_const_get(mPatron, rb_intern("ClientCertificateError"));
1178
1278
 
1179
- rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);
1279
+ rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);
1180
1280
  rb_define_module_function(mPatron, "libcurl_version_exact", libcurl_version_exact, 0);
1281
+ rb_define_module_function(mPatron, "libcurl_features", libcurl_features, 0);
1181
1282
 
1182
1283
  cSession = rb_define_class_under(mPatron, "Session", rb_cObject);
1183
- cRequest = rb_define_class_under(mPatron, "Request", rb_cObject);
1284
+ /* Defined in Ruby; we only hang constants off it */
1285
+ cRequest = rb_path2class("Patron::Request");
1184
1286
  rb_define_alloc_func(cSession, session_alloc);
1185
1287
 
1186
- // Make "escape" available both as a class method and as an instance method,
1187
- // to make it usable to the Util module which does not have access to
1188
- // the Session object internally
1189
- rb_define_singleton_method(cSession, "escape", session_escape, 1);
1190
- rb_define_method(cSession, "escape", session_escape, 1);
1191
- rb_define_singleton_method(cSession, "unescape", session_unescape, 1);
1192
- rb_define_method(cSession, "unescape", session_unescape, 1);
1193
-
1194
- rb_define_private_method(cSession, "handle_request", session_handle_request, 1);
1195
- rb_define_method(cSession, "reset", session_interrupt, 0);
1196
- rb_define_method(cSession, "interrupt", session_interrupt, 0);
1197
- rb_define_private_method(cSession, "add_cookie_file", add_cookie_file, 1);
1198
- rb_define_private_method(cSession, "set_debug_file", set_debug_file, 1);
1288
+ /* escape/unescape are available on the class as well, for Patron::Util */
1289
+ rb_define_singleton_method(cSession, "escape", session_escape, 1);
1290
+ rb_define_method(cSession, "escape", session_escape, 1);
1291
+ rb_define_singleton_method(cSession, "unescape", session_unescape, 1);
1292
+ rb_define_method(cSession, "unescape", session_unescape, 1);
1199
1293
  rb_define_alias(cSession, "urlencode", "escape");
1200
1294
  rb_define_alias(cSession, "urldecode", "unescape");
1201
1295
 
1202
- rb_define_const(cRequest, "AuthBasic", LONG2NUM(CURLAUTH_BASIC));
1296
+ rb_define_private_method(cSession, "handle_request", session_handle_request, 1);
1297
+ rb_define_private_method(cSession, "set_cookie_file", session_set_cookie_file, 1);
1298
+ rb_define_private_method(cSession, "set_debug_io", session_set_debug_io, 1);
1299
+ rb_define_method(cSession, "interrupt", session_interrupt, 0);
1300
+
1301
+ rb_define_const(cRequest, "AuthBasic", LONG2NUM(CURLAUTH_BASIC));
1203
1302
  rb_define_const(cRequest, "AuthDigest", LONG2NUM(CURLAUTH_DIGEST));
1204
- rb_define_const(cRequest, "AuthAny", LONG2NUM(CURLAUTH_ANY));
1303
+ rb_define_const(cRequest, "AuthAny", LONG2NUM(CURLAUTH_ANY));
1304
+
1305
+ hSSLVersions = shareable_table();
1306
+ table_entry(hSSLVersions, "TLSv1", CURL_SSLVERSION_TLSv1);
1307
+ table_entry(hSSLVersions, "TLSv1_0", CURL_SSLVERSION_TLSv1_0);
1308
+ table_entry(hSSLVersions, "TLSv1_1", CURL_SSLVERSION_TLSv1_1);
1309
+ table_entry(hSSLVersions, "TLSv1_2", CURL_SSLVERSION_TLSv1_2);
1310
+ table_entry(hSSLVersions, "TLSv1_3", CURL_SSLVERSION_TLSv1_3);
1311
+ rb_define_const(cRequest, "SSL_VERSIONS", rb_ractor_make_shareable(hSSLVersions));
1312
+
1313
+ hHTTPVersions = shareable_table();
1314
+ table_entry(hHTTPVersions, "None", CURL_HTTP_VERSION_NONE);
1315
+ table_entry(hHTTPVersions, "HTTPv1_0", CURL_HTTP_VERSION_1_0);
1316
+ table_entry(hHTTPVersions, "HTTPv1_1", CURL_HTTP_VERSION_1_1);
1317
+ table_entry(hHTTPVersions, "HTTPv2_0", CURL_HTTP_VERSION_2_0);
1318
+ table_entry(hHTTPVersions, "HTTPv2_TLS", CURL_HTTP_VERSION_2TLS);
1319
+ table_entry(hHTTPVersions, "HTTPv2_PRIOR", CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE);
1320
+ table_entry(hHTTPVersions, "HTTPv3", CURL_HTTP_VERSION_3);
1321
+ #if LIBCURL_VERSION_NUM >= 0x075800
1322
+ table_entry(hHTTPVersions, "HTTPv3_ONLY", CURL_HTTP_VERSION_3ONLY);
1323
+ #endif
1324
+ rb_define_const(cRequest, "HTTP_VERSIONS", rb_ractor_make_shareable(hHTTPVersions));
1205
1325
 
1206
1326
  mProxyType = rb_define_module_under(mPatron, "ProxyType");
1207
1327
  rb_define_const(mProxyType, "HTTP", LONG2NUM(CURLPROXY_HTTP));
1208
1328
  rb_define_const(mProxyType, "HTTP_1_0", LONG2NUM(CURLPROXY_HTTP_1_0));
1329
+ rb_define_const(mProxyType, "HTTPS", LONG2NUM(CURLPROXY_HTTPS));
1209
1330
  rb_define_const(mProxyType, "SOCKS4", LONG2NUM(CURLPROXY_SOCKS4));
1210
1331
  rb_define_const(mProxyType, "SOCKS5", LONG2NUM(CURLPROXY_SOCKS5));
1211
1332
  rb_define_const(mProxyType, "SOCKS4A", LONG2NUM(CURLPROXY_SOCKS4A));