patron 0.13.4 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/README.md +113 -50
  4. data/ext/patron/extconf.rb +55 -20
  5. data/ext/patron/session_ext.c +1092 -761
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +25 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -142
  10. data/lib/patron/response.rb +33 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +127 -104
  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 -49
  17. data/.autotest +0 -15
  18. data/.github/workflows/ci.yml +0 -26
  19. data/.gitignore +0 -10
  20. data/.rspec +0 -0
  21. data/.travis.yml +0 -13
  22. data/.yardopts +0 -1
  23. data/Gemfile +0 -17
  24. data/Rakefile +0 -31
  25. data/ext/patron/.gitignore +0 -4
  26. data/ext/patron/membuffer.c +0 -85
  27. data/ext/patron/membuffer.h +0 -78
  28. data/ext/patron/sglib.h +0 -1952
  29. data/lib/patron/proxy_type.rb +0 -11
  30. data/patron.gemspec +0 -38
  31. data/pic.png +0 -0
  32. data/script/console +0 -11
  33. data/script/test_server +0 -7
  34. data/spec/header_parser_spec.rb +0 -101
  35. data/spec/patron_spec.rb +0 -28
  36. data/spec/request_spec.rb +0 -77
  37. data/spec/response_spec.rb +0 -145
  38. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  39. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  40. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  41. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  42. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  43. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  44. data/spec/session_spec.rb +0 -698
  45. data/spec/session_ssl_spec.rb +0 -313
  46. data/spec/spec_helper.rb +0 -34
  47. data/spec/support/certs/cacert.pem +0 -36
  48. data/spec/support/certs/privkey.pem +0 -51
  49. data/spec/support/config.ru +0 -146
  50. data/spec/support/fork.rb +0 -70
  51. data/spec/support/test_server.rb +0 -26
  52. data/spec/util_spec.rb +0 -71
@@ -1,960 +1,1268 @@
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
-
28
- struct patron_curl_state {
29
- CURL* handle;
30
- CURL* base_handle;
31
- CURLSH* share;
32
- char* upload_buf;
33
- FILE* download_file;
34
- FILE* debug_file;
35
- 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;
36
108
  char error_buf[CURL_ERROR_SIZE];
37
- struct curl_slist* headers;
38
- struct curl_httppost* post;
39
- struct curl_httppost* last;
40
- membuffer header_buffer;
41
- membuffer body_buffer;
42
- size_t download_byte_limit;
43
- VALUE user_progress_blk;
44
- int interrupt;
45
- size_t dltotal;
46
- size_t dlnow;
47
- size_t ultotal;
48
- size_t ulnow;
109
+ curl_off_t download_byte_limit;
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. */
117
+ int pending_tag;
118
+ VALUE pending_errinfo;
119
+
120
+ volatile sig_atomic_t interrupt;
121
+ volatile sig_atomic_t busy;
49
122
  };
50
123
 
51
-
52
124
  /*----------------------------------------------------------------------------*/
53
- /* Curl Callbacks */
125
+ /* TypedData */
126
+
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
+ }
54
138
 
55
- /* Takes data streamed from libcurl and writes it to a Ruby string buffer. */
56
- static size_t session_write_handler(char* stream, size_t size, size_t nmemb, membuffer* buf) {
57
- int rc = membuffer_append(buf, stream, 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);
151
+ }
58
152
 
59
- /* return 0 to signal that we could not append data to our buffer */
60
- if (MB_OK != rc) { return 0; }
153
+ static void session_compact(void *ptr) {
154
+ struct patron_session *s = ptr;
61
155
 
62
- /* otherwise, return the number of bytes appended */
63
- return size * nmemb;
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);
64
162
  }
65
163
 
66
- /* Used as WRITEFUNCTION for file downloads (required on Windows) */
67
- static size_t file_write_handler(void* stream, size_t size, size_t nmemb, FILE* fp) {
68
- fwrite(stream, size, nmemb, fp);
69
- if (ferror(fp)) {
70
- return 0;
71
- } else {
72
- /* Just always OK the write */
73
- return size * nmemb;
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);
167
+ }
168
+
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
+ };
187
+
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;
196
+ }
197
+
198
+ #define SESSION_WRITE(s, field, value) RB_OBJ_WRITE((s)->self, &(s)->field, (value))
199
+
200
+ /*----------------------------------------------------------------------------*/
201
+ /* Errors */
202
+
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;
74
215
  }
75
216
  }
76
217
 
77
- static int call_user_rb_progress_blk(void* vd_curl_state) {
78
- struct patron_curl_state* state = (struct patron_curl_state*)vd_curl_state;
79
- // Invoke the block with the array
80
- rb_funcall(state->user_progress_blk,
81
- rb_intern("call"), 4,
82
- LONG2NUM(state->dltotal),
83
- LONG2NUM(state->dlnow),
84
- LONG2NUM(state->ultotal),
85
- LONG2NUM(state->ulnow));
86
- return 0;
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);
87
226
  }
88
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)
89
234
 
90
- /* A non-zero return value from the progress handler will terminate the current
91
- * request. We use this fact in order to interrupt any request when either the
92
- * user calls the "interrupt" method on the session or when the Ruby interpreter
93
- * is attempting to exit.
94
- */
95
- static int session_progress_handler(void* clientp, size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {
96
- struct patron_curl_state* state = (struct patron_curl_state*) clientp;
97
- state->dltotal = dltotal;
98
- state->dlnow = dlnow;
99
- state->ultotal = ultotal;
100
- state->ulnow = ulnow;
235
+ /*----------------------------------------------------------------------------*/
236
+ /* Deferred exceptions from inside libcurl callbacks */
101
237
 
102
- // If a progress proc has been set, re-acquire the GIL and call it using
103
- // `call_user_rb_progress_blk`. TODO: use the retval of that proc
104
- // to permit premature abort
105
- if(RTEST(state->user_progress_blk)) {
106
- rb_thread_call_with_gvl((void *(*)(void *)) call_user_rb_progress_blk, (void*)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
+ }
244
+
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
+ }
258
+
259
+ /*----------------------------------------------------------------------------*/
260
+ /* libcurl callbacks. All of them run with the GVL held, from curl_multi_perform(). */
261
+
262
+ struct append_args {
263
+ VALUE str;
264
+ const char *ptr;
265
+ size_t len;
266
+ };
267
+
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;
272
+ }
273
+
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;
277
+
278
+ rb_protect(append_protected, (VALUE)&args, &tag);
279
+ if (tag) {
280
+ park_exception(s, tag);
281
+ return 0;
107
282
  }
283
+ return len;
284
+ }
285
+
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;
108
289
 
109
- // Set the interrupt if the download byte limit has been reached
110
- if(state->download_byte_limit != 0 && (dltotal > state->download_byte_limit)) {
111
- state->interrupt = INTERRUPT_DOWNLOAD_OVERFLOW;
290
+ if (s->download_file) {
291
+ if (fwrite(ptr, 1, len, s->download_file) != len) return 0;
292
+ return len;
112
293
  }
113
-
114
- // If the interrupt value is anything except 0, the perform() call
115
- // will be aborted by libCURL.
116
- // Note however that some older versions of libcurl have a bug which
117
- // makes the return value of this callback be ignored, and the request
118
- // will actually proceed undeterred.
119
- // See https://sourceforge.net/p/curl/bugs/1318/
120
- return state->interrupt;
294
+ return append_to_string(s, s->body_buf, ptr, len);
121
295
  }
122
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
+ }
123
301
 
124
- /*
125
- List of active curl sessions, used exclusively to be able to set interrupts
126
- for all of them if the Ruby interpreter gets shut down with libCURL requests still in flight.
127
- */
128
- struct patron_curl_state_list {
129
- struct patron_curl_state *state;
130
- struct patron_curl_state_list *next;
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);
305
+ }
306
+
307
+ struct progress_args {
308
+ struct patron_session *s;
309
+ curl_off_t dltotal, dlnow, ultotal, ulnow;
131
310
  };
132
311
 
133
- #define CS_LIST_COMPARATOR(p, _state_) (p->state - _state_)
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
+ }
317
+
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;
134
321
 
135
- static struct patron_curl_state_list *cs_list = NULL;
322
+ if (s->interrupt) return 1;
136
323
 
137
- static void cs_list_append( struct patron_curl_state *state ) {
138
- struct patron_curl_state_list *item = NULL;
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
+ }
139
328
 
140
- assert(state != NULL);
141
- item = ruby_xmalloc(sizeof(struct patron_curl_state_list));
142
- item->state = state;
143
- item->next = NULL;
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);
144
333
 
145
- SGLIB_LIST_ADD(struct patron_curl_state_list, cs_list, item, next);
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;
146
345
  }
147
346
 
148
- static void cs_list_remove(struct patron_curl_state *state) {
149
- struct patron_curl_state_list *item = NULL;
347
+ struct debug_args {
348
+ VALUE io;
349
+ const char *prefix;
350
+ const char *data;
351
+ size_t len;
352
+ };
150
353
 
151
- assert(state != NULL);
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;
361
+ }
152
362
 
153
- SGLIB_LIST_DELETE_IF_MEMBER(struct patron_curl_state_list, cs_list, state, CS_LIST_COMPARATOR, next, item);
154
- if (item) {
155
- ruby_xfree(item);
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;
156
378
  }
157
- }
158
379
 
159
- /* Gets attached to at_exit of the Ruby process to be able to abort all running libCURL requests and quit */
160
- static void cs_list_interrupt(VALUE data) {
161
- UNUSED_ARGUMENT(data);
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;
387
+ }
162
388
 
163
- SGLIB_LIST_MAP_ON_ELEMENTS(struct patron_curl_state_list, cs_list, item, next, {
164
- item->state->interrupt = INTERRUPT_ABORT;
165
- });
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]);
166
392
  }
167
393
 
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
+ }
168
398
 
169
399
  /*----------------------------------------------------------------------------*/
170
- /* Object allocation */
171
-
172
- static void session_close_debug_file(struct patron_curl_state *curl) {
173
- if (curl->debug_file && stderr != curl->debug_file) {
174
- fclose(curl->debug_file);
175
- }
176
- curl->debug_file = NULL;
177
- }
178
-
179
- /* Cleans up the patron_curl_state data when the Session object is garbage collected. */
180
- void session_free(struct patron_curl_state *state) {
181
- curl_easy_cleanup(state->base_handle);
182
- curl_share_cleanup(state->share);
183
-
184
- session_close_debug_file(state);
185
-
186
- membuffer_destroy(&state->header_buffer);
187
- membuffer_destroy(&state->body_buffer);
188
-
189
- cs_list_remove(state);
190
-
191
- free(state);
192
- }
193
-
194
- /* Allocates patron_curl_state data needed for a new Session object. */
195
- VALUE session_alloc(VALUE klass) {
196
- struct patron_curl_state* state;
197
- VALUE obj = Data_Make_Struct(klass, struct patron_curl_state, NULL, session_free, state);
198
-
199
- membuffer_init(&state->header_buffer);
200
- membuffer_init(&state->body_buffer);
201
- cs_list_append(state);
202
-
203
- /*
204
- Eagerly initialize the curl handle. We initialize it only once and store it
205
- in the struct until the Session object gets garbage-collected in session_free(). This allows libCURL to
206
- reuse the TCP connection and can speed things up if the same resource - like a backend service -
207
- gets accessed over and over with requests.
208
- */
209
- state->share = curl_share_init();
210
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
211
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
212
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
213
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
214
- curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
215
- state->base_handle = curl_easy_init();
216
- curl_easy_setopt(state->base_handle, CURLOPT_SHARE, state->share);
217
- curl_easy_setopt(state->base_handle, CURLOPT_WRITEFUNCTION, &session_write_handler);
218
- curl_easy_setopt(state->base_handle, CURLOPT_WRITEDATA, &state->body_buffer);
219
- curl_easy_setopt(state->base_handle, CURLOPT_HEADERFUNCTION, &session_write_handler);
220
- curl_easy_setopt(state->base_handle, CURLOPT_HEADERDATA, &state->header_buffer);
221
- curl_easy_setopt(state->base_handle, CURLOPT_NOSIGNAL, 1);
222
- curl_easy_setopt(state->base_handle, CURLOPT_NOPROGRESS, 0);
223
- #if LIBCURL_VERSION_NUM >= 0x072000
224
- /* this is libCURLv7.32.0 or later, supports CURLOPT_XFERINFOFUNCTION */
225
- curl_easy_setopt(state->base_handle, CURLOPT_XFERINFOFUNCTION, &session_progress_handler);
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();
416
+ }
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");
226
448
  #else
227
- curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSFUNCTION, &session_progress_handler);
228
- #endif
229
- curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSDATA, state);
230
- #ifdef CURLPROTO_HTTP
231
- // Security: do not allow Curl to go looking on gopher/SMTP etc.
232
- // Must prevent situations like this:
233
- // https://hackerone.com/reports/115748
234
- curl_easy_setopt(state->base_handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
235
- 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);
236
451
  #endif
452
+ }
237
453
 
238
- 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);
239
465
  }
240
466
 
241
- /* Return the patron_curl_state from the ruby VALUE which is the Session instance. */
242
- static struct patron_curl_state* get_patron_curl_state(VALUE self) {
243
- struct patron_curl_state* state;
244
- Data_Get_Struct(self, struct patron_curl_state, state);
245
- 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;
246
481
  }
247
482
 
248
483
  /*----------------------------------------------------------------------------*/
249
- /* Method implementations */
484
+ /* Module functions */
250
485
 
251
486
  /*
252
- * Returns the version of the embedded libcurl.
253
- *
254
- * @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]
255
490
  */
256
491
  static VALUE libcurl_version(VALUE klass) {
257
- char* value = curl_version();
258
- UNUSED_ARGUMENT(klass);
259
- return rb_str_new2(value);
492
+ return rb_str_new_cstr(curl_version());
260
493
  }
261
494
 
262
495
  /*
263
- * Returns the version of the embedded libcurl.
264
- *
265
- * @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
266
499
  */
267
500
  static VALUE libcurl_version_exact(VALUE klass) {
268
- UNUSED_ARGUMENT(klass);
269
- VALUE cv_major = INT2FIX(LIBCURL_VERSION_MAJOR);
270
- VALUE cv_minor = INT2FIX(LIBCURL_VERSION_MINOR);
271
- VALUE cv_patch = INT2FIX(LIBCURL_VERSION_PATCH);
272
- VALUE version_arr = rb_ary_new3(3, cv_major, cv_minor, cv_patch);
273
- return version_arr;
501
+ return rb_ary_new_from_args(3, INT2FIX(LIBCURL_VERSION_MAJOR), INT2FIX(LIBCURL_VERSION_MINOR), INT2FIX(LIBCURL_VERSION_PATCH));
502
+ }
503
+
504
+ /*
505
+ * Describes the libcurl in use at runtime.
506
+ *
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`
509
+ */
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");
526
+ #endif
527
+ #ifdef CURL_VERSION_THREADSAFE
528
+ FEATURE(CURL_VERSION_THREADSAFE, "threadsafe");
529
+ #endif
530
+ #undef FEATURE
531
+
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;
274
537
  }
275
538
 
276
539
  /*
277
- * Escapes the provided string using libCURL URL escaping functions.
540
+ * URL-escapes a String using libcurl.
278
541
  *
279
- * @param [String] value plain string to URL-escape
280
- * @return [String] the escaped string
542
+ * @param value [String]
543
+ * @return [String] the escaped string
281
544
  */
282
545
  static VALUE session_escape(VALUE self, VALUE value) {
283
-
284
546
  VALUE string = StringValue(value);
285
- char* escaped = NULL;
286
- VALUE retval = Qnil;
287
-
288
- #if LIBCURL_VERSION_NUM >= 0x075200
289
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
290
- CURL *curl = NULL;
291
- #else
292
- CURL* curl = curl_easy_init();
293
- #endif
294
- escaped = curl_easy_escape(curl,
295
- RSTRING_PTR(string),
296
- (int) RSTRING_LEN(string));
547
+ char *escaped = curl_easy_escape(NULL, RSTRING_PTR(string), rb_long2int(RSTRING_LEN(string)));
548
+ VALUE result;
297
549
 
298
- retval = rb_str_new2(escaped);
299
- #if LIBCURL_VERSION_NUM >= 0x075200
300
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
301
- #else
302
- curl_easy_cleanup(curl);
303
- #endif
550
+ if (!escaped) rb_raise(ePatronError, "libcurl could not escape the string");
551
+ result = rb_usascii_str_new_cstr(escaped);
304
552
  curl_free(escaped);
305
-
306
- return retval;
553
+ return result;
307
554
  }
308
555
 
309
556
  /*
310
- * 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.
311
559
  *
312
- * @param [String] value URL-encoded String to unescape
313
- * @return [String] unescaped (decoded) string
560
+ * @param value [String]
561
+ * @return [String] the unescaped string
314
562
  */
315
563
  static VALUE session_unescape(VALUE self, VALUE value) {
316
564
  VALUE string = StringValue(value);
317
- char* unescaped = NULL;
318
- VALUE retval = Qnil;
319
-
320
- #if LIBCURL_VERSION_NUM >= 0x075200
321
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
322
- CURL *curl = NULL;
323
- #else
324
- CURL *curl = curl_easy_init();
325
- #endif
326
- unescaped = curl_easy_unescape(curl,
327
- RSTRING_PTR(string),
328
- (int) RSTRING_LEN(string),
329
- NULL);
565
+ int outlen = 0;
566
+ char *unescaped = curl_easy_unescape(NULL, RSTRING_PTR(string), rb_long2int(RSTRING_LEN(string)), &outlen);
567
+ VALUE result;
330
568
 
331
- retval = rb_str_new2(unescaped);
569
+ if (!unescaped) rb_raise(ePatronError, "libcurl could not unescape the string");
570
+ result = rb_str_new(unescaped, outlen);
332
571
  curl_free(unescaped);
333
- #if LIBCURL_VERSION_NUM >= 0x075200
334
- /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
335
- #else
336
- curl_easy_cleanup(curl);
337
- #endif
572
+ return result;
573
+ }
338
574
 
339
- return retval;
340
- }
341
-
342
- /* Callback used to iterate over the HTTP headers and store them in an slist. */
343
- static int each_http_header(VALUE header_key, VALUE header_value, VALUE self) {
344
- struct patron_curl_state *state = get_patron_curl_state(self);
345
- CURL* curl = state->handle;
346
-
347
- VALUE name = rb_obj_as_string(header_key);
348
- VALUE value = rb_obj_as_string(header_value);
349
- VALUE header_str = Qnil;
350
-
351
- // TODO: see how to combine this with automatic_content_encoding
352
- if (rb_str_cmp(name, rb_str_new2("Accept-Encoding")) == 0) {
353
- if (rb_funcall(value, rb_intern("include?"), 1, rb_str_new2("gzip"))) {
354
- #ifdef CURLOPT_ACCEPT_ENCODING
355
- curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
356
- #elif defined CURLOPT_ENCODING
357
- curl_easy_setopt(curl, CURLOPT_ENCODING, "gzip");
358
- #else
359
- rb_raise(rb_eArgError,
360
- "The libcurl version installed doesn't support 'gzip'.");
361
- #endif
362
- }
363
- }
575
+ /*----------------------------------------------------------------------------*/
576
+ /* Request configuration */
364
577
 
365
- header_str = rb_str_plus(name, rb_str_new2(": "));
366
- 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
+ }
367
581
 
368
- 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;
369
586
 
370
- 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;
371
593
  }
372
594
 
373
- static int formadd_values(VALUE data_key, VALUE data_value, VALUE self) {
374
- struct patron_curl_state *state = get_patron_curl_state(self);
375
- VALUE name = rb_obj_as_string(data_key);
376
- VALUE value = rb_obj_as_string(data_value);
377
-
378
- curl_formadd(&state->post, &state->last, CURLFORM_PTRNAME, RSTRING_PTR(name),
379
- CURLFORM_PTRCONTENTS, RSTRING_PTR(value), 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;
380
600
 
601
+ for (i = 0; i + needle_len <= len; i++) {
602
+ if (memcmp(haystack + i, needle, (size_t)needle_len) == 0) return 1;
603
+ }
381
604
  return 0;
382
605
  }
383
606
 
384
- static int formadd_files(VALUE data_key, VALUE data_value, VALUE self) {
385
- struct patron_curl_state *state = get_patron_curl_state(self);
386
- VALUE name = rb_obj_as_string(data_key);
387
- 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;
388
611
 
389
- curl_formadd(&state->post, &state->last, CURLFORM_PTRNAME, RSTRING_PTR(name),
390
- CURLFORM_FILE, RSTRING_PTR(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
+ }
391
616
 
392
- return 0;
617
+ line = rb_sprintf("%"PRIsVALUE": %"PRIsVALUE, name, value);
618
+ s->headers = curl_slist_append(s->headers, StringValueCStr(line));
619
+ return ST_CONTINUE;
393
620
  }
394
621
 
395
- // Set the given char pointer and it's length to be the CURL request body
396
- static void set_curl_request_body(CURL* curl, char* buf, curl_off_t len) {
397
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buf);
398
- #ifdef CURLOPT_POSTFIELDSIZE_LARGE
399
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len);
400
- #else
401
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
402
- #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;
403
631
  }
404
632
 
405
- static void set_chunked_encoding(struct patron_curl_state *state) {
406
- 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;
407
641
  }
408
642
 
409
- static FILE* open_file(VALUE filename, const char* perms) {
410
- FILE* handle = fopen(StringValuePtr(filename), perms);
411
- if (!handle) {
412
- 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);
413
651
  }
652
+ return ST_CONTINUE;
653
+ }
654
+
655
+ static FILE *open_file(VALUE filename, const char *mode) {
656
+ FILE *handle;
414
657
 
658
+ FilePathValue(filename);
659
+ handle = fopen(StringValueCStr(filename), mode);
660
+ if (!handle) rb_sys_fail_str(filename);
415
661
  return handle;
416
662
  }
417
663
 
418
- static void set_request_body_file(struct patron_curl_state* state, VALUE r_path_str) {
419
- CURL* curl = state->handle;
420
-
421
- state->request_body_file = open_file(r_path_str, "rb");
422
- curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
423
- curl_easy_setopt(curl, CURLOPT_READDATA, state->request_body_file);
424
- struct stat stat_info;
425
- fstat(fileno(state->request_body_file), &stat_info);
426
- #ifdef CURLOPT_INFILESIZE_LARGE
427
- curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, stat_info.st_size);
428
- #else
429
- curl_easy_setopt(curl, CURLOPT_INFILESIZE, stat_info.st_size);
430
- #endif
431
- }
432
-
433
- static long floating_rb_seconds_to_milliseconds(VALUE r_seconds) {
434
- double seconds_f = NUM2DBL(r_seconds);
435
- long millis = seconds_f * (double)1000.0;
436
- return millis;
437
- }
438
-
439
- static void set_request_body(struct patron_curl_state* state, VALUE stringable_or_file) {
440
- CURL* curl = state->handle;
441
- if(rb_respond_to(stringable_or_file, rb_intern("to_path"))) {
442
- // Set up a file read callback (read the entire request body from a file).
443
- // Instead of using the Ruby file reads, use #to_path to obtain the
444
- // file path on the file system and open a file pointer to it
445
- VALUE r_path_str = rb_funcall(stringable_or_file, rb_intern("to_path"), 0);
446
- r_path_str = rb_funcall(r_path_str, rb_intern("to_s"), 0);
447
- set_request_body_file(state, r_path_str);
448
- } else {
449
- // Set the request body from a String
450
- VALUE data = rb_funcall(stringable_or_file, rb_intern("to_s"), 0);
451
- long len = RSTRING_LEN(data);
452
- state->upload_buf = StringValuePtr(data);
453
- set_curl_request_body(curl, state->upload_buf, len);
454
- }
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);
669
+
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);
455
674
  }
456
675
 
457
- /* Set the options on the Curl handle from a Request object. Takes each field
458
- * in the Request object and uses it to set the appropriate option on the Curl
459
- * handle.
460
- */
461
- static void set_options_from_request(VALUE self, VALUE request) {
462
- struct patron_curl_state* state = get_patron_curl_state(self);
463
- CURL* curl = curl_easy_duphandle(state->base_handle);
464
-
465
- ID action = Qnil;
466
- VALUE headers = Qnil;
467
- VALUE url = Qnil;
468
- VALUE timeout = Qnil;
469
- VALUE redirects = Qnil;
470
- VALUE proxy = Qnil;
471
- VALUE proxy_type = Qnil;
472
- VALUE credentials = Qnil;
473
- VALUE ignore_content_length = Qnil;
474
- VALUE insecure = Qnil;
475
- VALUE cacert = Qnil;
476
- VALUE ssl_version = Qnil;
477
- VALUE http_version = Qnil;
478
- VALUE buffer_size = Qnil;
479
- VALUE action_name = rb_funcall(request, rb_intern("action"), 0);
480
- VALUE a_c_encoding = rb_funcall(request, rb_intern("automatic_content_encoding"), 0);
481
- VALUE download_byte_limit = rb_funcall(request, rb_intern("download_byte_limit"), 0);
482
- VALUE maybe_progress_proc = rb_funcall(request, rb_intern("progress_callback"), 0);
483
-
484
- state->handle = curl;
485
- curl_easy_setopt(curl, CURLOPT_SHARE, state->share);
486
-
487
- if (RTEST(download_byte_limit)) {
488
- state->download_byte_limit = FIX2INT(download_byte_limit);
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));
489
680
  } else {
490
- state->download_byte_limit = 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));
491
685
  }
686
+ }
492
687
 
493
- if (rb_obj_is_proc(maybe_progress_proc)) {
494
- state->user_progress_blk = maybe_progress_proc;
495
- } else {
496
- state->user_progress_blk = Qnil;
497
- }
688
+ static long seconds_to_milliseconds(VALUE seconds) {
689
+ return (long)(NUM2DBL(seconds) * 1000.0);
690
+ }
691
+
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);
498
695
 
499
- headers = rb_funcall(request, rb_intern("headers"), 0);
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;
711
+
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);
500
727
  if (RTEST(headers)) {
501
- if (rb_type(headers) != T_HASH) {
502
- rb_raise(rb_eArgError, "Headers must be passed in a hash.");
503
- }
728
+ Check_Type(headers, T_HASH);
504
729
  rb_hash_foreach(headers, each_http_header, self);
505
730
  }
506
731
 
507
- action = SYM2ID(action_name);
508
- if (RTEST(rb_funcall(request, rb_intern("force_ipv4"), 0))) {
509
- 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);
510
734
  }
511
- if (action == rb_intern("get")) {
512
- VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
513
- VALUE download_file = rb_funcall(request, rb_intern("file_name"), 0);
514
735
 
515
- curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
516
- if (RTEST(data)) {
517
- set_request_body(state, data);
518
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
519
- }
520
- if (RTEST(download_file)) {
521
- // we need the WRITEDATA option for the file destination
522
- state->download_file = open_file(download_file, "wb");
523
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, state->download_file);
524
- // curl docs say that CURLOPT_WRITEFUNCTION must be set too
525
- // to avoid issues on Windows
526
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &file_write_handler);
527
- } else {
528
- state->download_file = NULL;
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");
529
746
  }
530
- } else if (action == rb_intern("post") || action == rb_intern("put") || action == rb_intern("patch")) {
531
- VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
532
- VALUE filename = rb_funcall(request, rb_intern("file_name"), 0);
533
- VALUE multipart = rb_funcall(request, rb_intern("multipart"), 0);
534
-
535
- if (action == rb_intern("post")) {
536
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
537
- } else if (action == rb_intern("put")) {
538
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
539
- } else if (action == rb_intern("patch")) {
540
- 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);
541
750
  }
542
-
543
- if (RTEST(data) && !RTEST(multipart)) {
544
- if (action == rb_intern("post")) {
545
- 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);
546
761
  }
547
- set_request_body(state, data);
548
- } else if (RTEST(filename) && !RTEST(multipart)) {
549
- set_chunked_encoding(state);
550
- set_request_body_file(state, filename);
551
- } else if (RTEST(multipart)) {
552
- if (action == rb_intern("post")) {
553
- if(RTEST(data) && RTEST(filename)) {
554
- if (rb_type(data) == T_HASH && rb_type(filename) == T_HASH) {
555
- rb_hash_foreach(data, formadd_values, self);
556
- rb_hash_foreach(filename, formadd_files, self);
557
- } else {
558
- rb_raise(rb_eArgError, "Data and Filename must be passed in a hash.");
559
- }
560
- }
561
- curl_easy_setopt(curl, CURLOPT_HTTPPOST, state->post);
562
- } else {
563
- 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);
564
765
  }
565
-
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));
566
774
  } else {
567
- 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");
568
776
  }
569
-
570
- // support for data passed with a DELETE request (e.g.: used by elasticsearch)
571
- } else if (action == rb_intern("delete")) {
572
- VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
573
- if (RTEST(data)) {
574
- long len = RSTRING_LEN(data);
575
- state->upload_buf = StringValuePtr(data);
576
- curl_easy_setopt(curl, CURLOPT_POST, 1);
577
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, state->upload_buf);
578
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
579
- }
580
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
581
-
582
- } else if (action == rb_intern("head")) {
583
- 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);
584
785
  } else {
585
- VALUE action_name = rb_funcall(request, rb_intern("action_name"), 0);
586
- 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));
587
788
  }
588
789
 
589
- curl_easy_setopt(curl, CURLOPT_HTTPHEADER, state->headers);
590
- curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, state->error_buf);
790
+ setopt_or_raise(easy, CURLOPT_HTTPHEADER, s->headers);
591
791
 
592
- // Enable automatic content-encoding support via gzip/deflate if set in the request,
593
- // see https://curl.haxx.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html
594
- if(RTEST(a_c_encoding)) {
595
- #ifdef CURLOPT_ACCEPT_ENCODING
596
- curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
597
- #elif defined CURLOPT_ENCODING
598
- curl_easy_setopt(curl, CURLOPT_ENCODING, "");
599
- #else
600
- rb_raise(rb_eArgError,
601
- "The libcurl version installed doesn't support automatic content negotiation");
602
- #endif
792
+ if (RTEST(request_attr(request, id_automatic_content_encoding))) {
793
+ setopt_or_raise(easy, CURLOPT_ACCEPT_ENCODING, "");
603
794
  }
604
-
605
- url = rb_funcall(request, rb_intern("url"), 0);
606
- if (!RTEST(url)) {
607
- rb_raise(rb_eArgError, "Must provide a URL");
795
+
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);
799
+
800
+ value = request_attr(request, id_timeout);
801
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_TIMEOUT_MS, seconds_to_milliseconds(value));
802
+
803
+ value = request_attr(request, id_connect_timeout);
804
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_CONNECTTIMEOUT_MS, seconds_to_milliseconds(value));
805
+
806
+ value = request_attr(request, id_dns_cache_timeout);
807
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_DNS_CACHE_TIMEOUT, NUM2LONG(value));
808
+
809
+ value = request_attr(request, id_low_speed_time);
810
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_LOW_SPEED_TIME, NUM2LONG(value));
811
+
812
+ value = request_attr(request, id_low_speed_limit);
813
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_LOW_SPEED_LIMIT, NUM2LONG(value));
814
+
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);
608
820
  }
609
- curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url));
610
-
611
-
612
- timeout = rb_funcall(request, rb_intern("timeout"), 0);
613
- if (RTEST(timeout)) {
614
- curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, floating_rb_seconds_to_milliseconds(timeout));
821
+
822
+ value = request_attr(request, id_proxy);
823
+ if (RTEST(value)) set_string_option(s, CURLOPT_PROXY, value, 0);
824
+
825
+ value = request_attr(request, id_proxy_type);
826
+ if (RTEST(value)) setopt_or_raise(easy, CURLOPT_PROXYTYPE, NUM2LONG(value));
827
+
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);
615
832
  }
616
833
 
617
- timeout = rb_funcall(request, rb_intern("connect_timeout"), 0);
618
- if (RTEST(timeout)) {
619
- curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, floating_rb_seconds_to_milliseconds(timeout));
834
+ if (RTEST(request_attr(request, id_ignore_content_length))) {
835
+ setopt_or_raise(easy, CURLOPT_IGNORE_CONTENT_LENGTH, 1L);
620
836
  }
621
837
 
622
- timeout = rb_funcall(request, rb_intern("dns_cache_timeout"), 0);
623
- if (RTEST(timeout)) {
624
- curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, FIX2INT(timeout));
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);
625
841
  }
626
842
 
627
- VALUE low_speed_time = rb_funcall(request, rb_intern("low_speed_time"), 0);
628
- if(RTEST(low_speed_time)) {
629
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, FIX2LONG(low_speed_time));
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"));
630
846
  }
631
847
 
632
- VALUE low_speed_limit_bytes_per_second = rb_funcall(request, rb_intern("low_speed_limit"), 0);
633
- if(RTEST(low_speed_limit_bytes_per_second)) {
634
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, FIX2LONG(low_speed_limit_bytes_per_second));
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"));
635
851
  }
636
852
 
637
- redirects = rb_funcall(request, rb_intern("max_redirects"), 0);
638
- if (RTEST(redirects)) {
639
- int r = FIX2INT(redirects);
640
- curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, r == 0 ? 0 : 1);
641
- curl_easy_setopt(curl, CURLOPT_MAXREDIRS, r);
853
+ value = request_attr(request, id_ssl_cert);
854
+ if (RTEST(value)) set_string_option(s, CURLOPT_SSLCERT, value, 1);
855
+
856
+ value = request_attr(request, id_ssl_cert_type);
857
+ if (RTEST(value)) set_string_option(s, CURLOPT_SSLCERTTYPE, value, 0);
858
+
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));
870
+ }
871
+
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));
642
879
  }
880
+ }
881
+
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) {
885
+ VALUE timings = rb_hash_new();
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);
893
+ return timings;
894
+ }
895
+
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;
901
+ long code = 0;
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);
919
+ }
920
+
921
+ /*----------------------------------------------------------------------------*/
922
+ /* Performing a request */
923
+
924
+ struct poll_context {
925
+ CURLM *multi;
926
+ CURLMcode rc;
927
+ };
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
+ }
934
+
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
+ }
942
+
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
+ }
951
+
952
+ static VALUE float_seconds(long milliseconds) {
953
+ return rb_float_new((double)milliseconds / 1000.0);
954
+ }
643
955
 
644
- proxy = rb_funcall(request, rb_intern("proxy"), 0);
645
- if (RTEST(proxy)) {
646
- curl_easy_setopt(curl, CURLOPT_PROXY, StringValuePtr(proxy));
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);
647
966
  }
967
+ return io;
968
+ }
648
969
 
649
- proxy_type = rb_funcall(request, rb_intern("proxy_type"), 0);
650
- if (RTEST(proxy_type)) {
651
- curl_easy_setopt(curl, CURLOPT_PROXYTYPE, NUM2LONG(proxy_type));
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++;
652
996
  }
653
997
 
654
- credentials = rb_funcall(request, rb_intern("credentials"), 0);
655
- if (RTEST(credentials)) {
656
- VALUE auth_type = rb_funcall(request, rb_intern("auth_type"), 0);
657
- curl_easy_setopt(curl, CURLOPT_HTTPAUTH, NUM2LONG(auth_type));
658
- curl_easy_setopt(curl, CURLOPT_USERPWD, StringValuePtr(credentials));
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;
659
1003
  }
660
1004
 
661
- ignore_content_length = rb_funcall(request, rb_intern("ignore_content_length"), 0);
662
- if (RTEST(ignore_content_length)) {
663
- curl_easy_setopt(curl, CURLOPT_IGNORE_CONTENT_LENGTH, 1);
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;
1011
+ }
664
1012
  }
665
1013
 
666
- insecure = rb_funcall(request, rb_intern("insecure"), 0);
667
- if(RTEST(insecure)) {
668
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
669
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
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;
670
1018
  }
671
1019
 
672
- ssl_version = rb_funcall(request, rb_intern("ssl_version"), 0);
673
- if(RTEST(ssl_version)) {
674
- VALUE ssl_version_str = rb_funcall(ssl_version, rb_intern("to_s"), 0);
675
- char* version = StringValuePtr(ssl_version_str);
1020
+ {
1021
+ VALUE readables = rb_ary_new(), writables = rb_ary_new(), exceptables = rb_ary_new();
1022
+ VALUE ready;
676
1023
 
677
- if(strcmp(version, "SSLv2") == 0) {
678
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_SSLv2);
679
- } else if(strcmp(version, "SSLv3") == 0) {
680
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_SSLv3);
681
- } else if(strcmp(version, "TLSv1") == 0) {
682
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
683
- }
684
- #if LIBCURL_VERSION_NUM >= 0x072200
685
- /* this is libCURLv7.34.0 or later */
686
- else if(strcmp(version, "TLSv1_0") == 0) {
687
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_0); //libcurl v7.34.0+
688
- } else if(strcmp(version, "TLSv1_1") == 0) {
689
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_1); //libcurl v7.34.0+
690
- } else if(strcmp(version, "TLSv1_2") == 0) {
691
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); //libcurl v7.34.0+
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);
692
1029
  }
693
- #endif
694
- #if LIBCURL_VERSION_NUM >= 0x073400
695
- /* this is libCURLv7.52.0 or later */
696
- else if(strcmp(version, "TLSv1_3") == 0) {
697
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3); //libcurl v7.52.0+
698
- }
699
- #endif
700
- else {
701
- rb_raise(eUnsupportedSSLVersion, "Unsupported SSL version: %s", version);
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));
702
1034
  }
703
1035
  }
1036
+ }
704
1037
 
705
- http_version = rb_funcall(request, rb_intern("http_version"), 0);
706
- if(RTEST(http_version)) {
707
- VALUE http_version_str = rb_funcall(http_version, rb_intern("to_s"), 0);
708
- char* version = StringValuePtr(http_version_str);
709
- if(strcmp(version, "None") == 0) {
710
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_NONE);
711
- } else if(strcmp(version, "HTTPv1_0") == 0) {
712
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
713
- } else if(strcmp(version, "HTTPv1_1") == 0) {
714
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
715
- }
716
- #if LIBCURL_VERSION_NUM >= 0x072100
717
- /* this is libCURLv7.33.0 or later */
718
- else if(strcmp(version, "HTTPv2_0") == 0) {
719
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); //libcurl v7.33.0+
720
- }
721
- #endif
722
- #if LIBCURL_VERSION_NUM >= 0x072F00
723
- /* this is libCURLv7.47.0 or later */
724
- else if(strcmp(version, "HTTPv2_TLS") == 0) {
725
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); //libcurl v7.47.0+
726
- }
727
- #endif
728
- #if LIBCURL_VERSION_NUM >= 0x073100
729
- /* this is libCURLv7.49.0 or later */
730
- else if(strcmp(version, "HTTPv2_PRIOR") == 0) {
731
- curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE); //libcurl v7.49.0+
732
- }
733
- #endif
734
- else {
735
- rb_raise(eUnsupportedHTTPVersion, "Unsupported HTTP version: %s", version);
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();
1043
+
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);
736
1057
  }
1058
+ if (s->interrupt) break;
737
1059
  }
1060
+ }
738
1061
 
739
- cacert = rb_funcall(request, rb_intern("cacert"), 0);
740
- if(RTEST(cacert)) {
741
- curl_easy_setopt(curl, CURLOPT_CAINFO, StringValuePtr(cacert));
742
- }
1062
+ static CURLcode transfer_result(struct patron_session *s) {
1063
+ CURLcode result = CURLE_OK;
1064
+ CURLMsg *msg;
1065
+ int remaining = 0;
743
1066
 
744
- buffer_size = rb_funcall(request, rb_intern("buffer_size"), 0);
745
- if (RTEST(buffer_size)) {
746
- curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, NUM2LONG(buffer_size));
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;
747
1069
  }
1070
+ return result;
748
1071
  }
749
1072
 
750
- /* Use the info in a Curl handle to create a new Response object. */
751
- static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE body_buffer) {
752
- VALUE args[6] = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil };
753
- char* effective_url = NULL;
754
- long code = 0;
755
- long count = 0;
756
- VALUE responseKlass = Qnil;
757
-
758
- curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url);
759
- args[0] = rb_str_new2(effective_url);
1073
+ struct perform_args {
1074
+ struct patron_session *s;
1075
+ VALUE request;
1076
+ };
760
1077
 
761
- curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
762
- args[1] = LONG2NUM(code);
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;
763
1082
 
764
- curl_easy_getinfo(curl, CURLINFO_REDIRECT_COUNT, &count);
765
- args[2] = LONG2NUM(count);
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));
766
1088
 
767
- args[3] = header_buffer;
768
- args[4] = body_buffer;
769
- args[5] = rb_funcall(self, rb_intern("default_response_charset"), 0);
770
-
771
- responseKlass = rb_funcall(self, rb_intern("response_class"), 0);
772
- return rb_class_new_instance(6, args, responseKlass);
773
- }
1089
+ set_options_from_request(s, args->request);
1090
+ drive_transfer(s);
774
1091
 
775
- /* Raise an exception based on the Curl error code. */
776
- static VALUE select_error(CURLcode code) {
777
- VALUE error = Qnil;
778
- switch (code) {
779
- case CURLE_UNSUPPORTED_PROTOCOL: error = eUnsupportedProtocol; break;
780
- case CURLE_URL_MALFORMAT: error = eURLFormatError; break;
781
- case CURLE_COULDNT_RESOLVE_HOST: error = eHostResolutionError; break;
782
- case CURLE_COULDNT_CONNECT: error = eConnectionFailed; break;
783
- case CURLE_PARTIAL_FILE: error = ePartialFileError; break;
784
- case CURLE_OPERATION_TIMEDOUT: error = eTimeoutError; break;
785
- case CURLE_TOO_MANY_REDIRECTS: error = eTooManyRedirects; break;
786
- case CURLE_ABORTED_BY_CALLBACK: error = eAborted; break;
787
- default: error = ePatronError;
788
- }
789
-
790
- return error;
791
- }
792
-
793
-
794
- /* Uses as the unblocking function when the thread running Patron gets
795
- signaled. The important difference with session_interrupt is that we
796
- are not allowed to touch any Ruby structures while outside the GIL,
797
- but we _are_ permitted to touch our internal curl state struct
798
- */
799
- void session_ubf_abort(void* patron_state) {
800
- struct patron_curl_state* state = (struct patron_curl_state*) patron_state;
801
- state->interrupt = INTERRUPT_ABORT;
802
- }
803
-
804
- /* Perform the actual HTTP request by calling libcurl. */
805
- static VALUE perform_request(VALUE self) {
806
- struct patron_curl_state *state = get_patron_curl_state(self);
807
- CURL* curl = state->handle;
808
- CURLcode ret = 0;
809
-
810
- state->interrupt = 0; /* clear the interrupt flag */
811
-
812
- ret = (CURLcode) rb_thread_call_without_gvl(
813
- (void *(*)(void *)) curl_easy_perform, curl,
814
- session_ubf_abort, (void*)state
815
- );
816
-
817
- if (CURLE_OK == ret) {
818
- VALUE header_str = membuffer_to_rb_str(&state->header_buffer);
819
- VALUE body_str = Qnil;
820
- if (!state->download_file) { body_str = membuffer_to_rb_str(&state->body_buffer); }
821
-
822
- curl_easy_setopt(curl, CURLOPT_COOKIELIST, "FLUSH"); // Flush cookies to the cookie jar
823
-
824
- return create_response(self, curl, header_str, body_str);
825
- } else {
826
- rb_raise(select_error(ret), "%s", state->error_buf);
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");
1096
+ }
1097
+ if (s->interrupt) {
1098
+ raise_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
827
1099
  }
1100
+
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");
1106
+
1107
+ return create_response(s);
828
1108
  }
829
1109
 
830
- /* Cleanup after each request by resetting the Curl handle and deallocating
831
- * all request related objects such as the header slist.
832
- */
1110
+ /* Runs under rb_ensure after every request, whether it finished, raised or was interrupted. */
833
1111
  static VALUE cleanup(VALUE self) {
834
- struct patron_curl_state *state = get_patron_curl_state(self);
835
- curl_easy_cleanup(state->handle);
1112
+ struct patron_session *s = get_session(self);
836
1113
 
837
- if (state->headers) {
838
- curl_slist_free_all(state->headers);
839
- state->headers = NULL;
1114
+ if (s->easy) {
1115
+ curl_multi_remove_handle(s->multi, s->easy);
1116
+ curl_easy_cleanup(s->easy);
1117
+ s->easy = NULL;
840
1118
  }
841
- membuffer_clear(&state->header_buffer);
842
-
843
- if (state->download_file) {
844
- fclose(state->download_file);
845
- state->download_file = NULL;
846
- } else {
847
- membuffer_clear(&state->body_buffer);
1119
+ if (s->mime) {
1120
+ curl_mime_free(s->mime);
1121
+ s->mime = NULL;
848
1122
  }
849
-
850
- if (state->request_body_file) {
851
- fclose(state->request_body_file);
852
- state->request_body_file = NULL;
1123
+ if (s->headers) {
1124
+ curl_slist_free_all(s->headers);
1125
+ s->headers = NULL;
853
1126
  }
854
-
855
- if (state->post) {
856
- curl_formfree(state->post);
857
- state->post = NULL;
858
- state->last = NULL;
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;
859
1134
  }
860
1135
 
861
- state->upload_buf = NULL;
862
-
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;
863
1142
  return Qnil;
864
1143
  }
865
1144
 
866
1145
  /*
867
- * Peform the actual HTTP request by calling libcurl. Each filed in the
868
- * +request+ object will be used to set the appropriate option on the libcurl
869
- * library. After the request completes, a Response object will be created and
870
- * 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.
871
1148
  *
872
- * In the event of an error in the libcurl library, a Ruby exception will be
873
- * created and raised. The exception will return the libcurl error code and
874
- * error message.
875
- *
876
- * @param request[Patron::Request] the request to use when filling the CURL options
877
- * @return [Patron::Response] the result of calling `response_class` on the Session
1149
+ * @param request [Patron::Request]
1150
+ * @return [Patron::Response]
878
1151
  */
879
1152
  static VALUE session_handle_request(VALUE self, VALUE request) {
880
- set_options_from_request(self, request);
881
- return rb_ensure(&perform_request, self, &cleanup, self);
1153
+ struct patron_session *s = get_session(self);
1154
+ struct perform_args args = { s, request };
1155
+
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");
1158
+ }
1159
+ ensure_handles_for_this_process(s);
1160
+ s->busy = 1;
1161
+ return rb_ensure(perform, (VALUE)&args, cleanup, self);
882
1162
  }
883
1163
 
884
- /* Interrupt any currently executing request. This will cause the current
885
- * request to error and raise an exception. The method can be called from another thread to
886
- * 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.
887
1167
  *
888
- * @return [void] This method always raises
1168
+ * @return self
889
1169
  */
890
1170
  static VALUE session_interrupt(VALUE self) {
891
- struct patron_curl_state *state = get_patron_curl_state(self);
892
- 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);
893
1175
  return self;
894
1176
  }
895
1177
 
896
1178
  /*
897
- * Turn on cookie handling for this session, storing them in memory by
898
- * default or in +file+ if specified. The `file` must be readable and
899
- * writable. Calling multiple times will add more files.
900
- * FIXME: what does the empty string actually do here?
901
- *
902
- * @param [String] file path to the existing cookie file, or nil to store in memory.
903
- * @return self
1179
+ * Enables cookie handling: in memory when +path+ is empty, otherwise read from and written
1180
+ * back to the file at +path+.
904
1181
  */
905
- static VALUE add_cookie_file(VALUE self, VALUE file) {
906
- struct patron_curl_state *state = get_patron_curl_state(self);
907
- CURL* curl = state->base_handle;
908
- char* file_path = NULL;
909
-
910
- // FIXME: http://websystemsengineering.blogspot.nl/2013/03/curloptcookiefile-vs-curloptcookiejar.html
911
- file_path = RSTRING_PTR(file);
912
- if (file_path != NULL && strlen(file_path) != 0) {
913
- curl_easy_setopt(curl, CURLOPT_COOKIEJAR, file_path);
914
- }
915
- 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);
916
1185
 
1186
+ if (*file_path) setopt_or_raise(s->base, CURLOPT_COOKIEJAR, file_path);
1187
+ setopt_or_raise(s->base, CURLOPT_COOKIEFILE, file_path);
917
1188
  return self;
918
1189
  }
919
1190
 
920
1191
  /*
921
- * Enable debug output to stderr or to specified +file+.
922
- *
923
- * @param [String, nil] file path to the debug file, or nil to write to STDERR
924
- * @return self
1192
+ * Sends libcurl's verbose output to +io+, or disables it when +io+ is nil.
925
1193
  */
926
- static VALUE set_debug_file(VALUE self, VALUE file) {
927
- struct patron_curl_state *state = get_patron_curl_state(self);
928
- CURL *curl = state->base_handle;
929
- char *file_path = RSTRING_PTR(file);
1194
+ static VALUE session_set_debug_io(VALUE self, VALUE io) {
1195
+ struct patron_session *s = get_session(self);
930
1196
 
931
- 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
+ }
932
1201
 
933
- if(file_path != NULL && strlen(file_path) != 0) {
934
- state->debug_file = open_file(file, "wb");
935
- } else {
936
- state->debug_file = stderr;
937
- }
938
- curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
939
- curl_easy_setopt(curl, CURLOPT_STDERR, state->debug_file);
1202
+ /*----------------------------------------------------------------------------*/
1203
+ /* Init */
940
1204
 
941
- return self;
1205
+ static VALUE shareable_table(void) {
1206
+ return rb_hash_new();
942
1207
  }
943
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
+ }
944
1212
 
945
- /*----------------------------------------------------------------------------*/
946
- /* 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);
947
1217
 
948
- void Init_session_ext() {
949
1218
  curl_global_init(CURL_GLOBAL_ALL);
950
1219
  rb_require("patron/error");
951
-
952
- 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");
953
1243
 
954
1244
  mPatron = rb_define_module("Patron");
955
1245
 
956
- ePatronError = rb_const_get(mPatron, rb_intern("Error"));
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);
1251
+ rb_global_variable(&ePatronError);
1252
+ rb_global_variable(&eUnsupportedProtocol);
1253
+ rb_global_variable(&eUnsupportedSSLVersion);
1254
+ rb_global_variable(&eUnsupportedHTTPVersion);
1255
+ rb_global_variable(&eURLFormatError);
1256
+ rb_global_variable(&eHostResolutionError);
1257
+ rb_global_variable(&eConnectionFailed);
1258
+ rb_global_variable(&ePartialFileError);
1259
+ rb_global_variable(&eTimeoutError);
1260
+ rb_global_variable(&eTooManyRedirects);
1261
+ rb_global_variable(&eAborted);
1262
+ rb_global_variable(&eDownloadLimitExceeded);
1263
+ rb_global_variable(&eClientCertificateError);
957
1264
 
1265
+ ePatronError = rb_const_get(mPatron, rb_intern("Error"));
958
1266
  eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
959
1267
  eUnsupportedSSLVersion = rb_const_get(mPatron, rb_intern("UnsupportedSSLVersion"));
960
1268
  eUnsupportedHTTPVersion = rb_const_get(mPatron, rb_intern("UnsupportedHTTPVersion"));
@@ -965,37 +1273,60 @@ void Init_session_ext() {
965
1273
  eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
966
1274
  eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));
967
1275
  eAborted = rb_const_get(mPatron, rb_intern("Aborted"));
1276
+ eDownloadLimitExceeded = rb_const_get(mPatron, rb_intern("DownloadLimitExceeded"));
1277
+ eClientCertificateError = rb_const_get(mPatron, rb_intern("ClientCertificateError"));
968
1278
 
969
- rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);
1279
+ rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);
970
1280
  rb_define_module_function(mPatron, "libcurl_version_exact", libcurl_version_exact, 0);
1281
+ rb_define_module_function(mPatron, "libcurl_features", libcurl_features, 0);
971
1282
 
972
1283
  cSession = rb_define_class_under(mPatron, "Session", rb_cObject);
973
- 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");
974
1286
  rb_define_alloc_func(cSession, session_alloc);
975
1287
 
976
- // Make "escape" available both as a class method and as an instance method,
977
- // to make it usable to the Util module which does not have access to
978
- // the Session object internally
979
- rb_define_singleton_method(cSession, "escape", session_escape, 1);
980
- rb_define_method(cSession, "escape", session_escape, 1);
981
- rb_define_singleton_method(cSession, "unescape", session_unescape, 1);
982
- rb_define_method(cSession, "unescape", session_unescape, 1);
983
-
984
- rb_define_private_method(cSession, "handle_request", session_handle_request, 1);
985
- rb_define_method(cSession, "reset", session_interrupt, 0);
986
- rb_define_method(cSession, "interrupt", session_interrupt, 0);
987
- rb_define_private_method(cSession, "add_cookie_file", add_cookie_file, 1);
988
- 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);
989
1293
  rb_define_alias(cSession, "urlencode", "escape");
990
1294
  rb_define_alias(cSession, "urldecode", "unescape");
991
1295
 
992
- 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));
993
1302
  rb_define_const(cRequest, "AuthDigest", LONG2NUM(CURLAUTH_DIGEST));
994
- 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));
995
1325
 
996
1326
  mProxyType = rb_define_module_under(mPatron, "ProxyType");
997
1327
  rb_define_const(mProxyType, "HTTP", LONG2NUM(CURLPROXY_HTTP));
998
1328
  rb_define_const(mProxyType, "HTTP_1_0", LONG2NUM(CURLPROXY_HTTP_1_0));
1329
+ rb_define_const(mProxyType, "HTTPS", LONG2NUM(CURLPROXY_HTTPS));
999
1330
  rb_define_const(mProxyType, "SOCKS4", LONG2NUM(CURLPROXY_SOCKS4));
1000
1331
  rb_define_const(mProxyType, "SOCKS5", LONG2NUM(CURLPROXY_SOCKS5));
1001
1332
  rb_define_const(mProxyType, "SOCKS4A", LONG2NUM(CURLPROXY_SOCKS4A));