patron 0.13.3 → 0.14.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.
@@ -1,7 +1,5 @@
1
1
  #include <ruby.h>
2
- #if defined(USE_TBR) && defined(HAVE_THREAD_H)
3
2
  #include <ruby/thread.h>
4
- #endif
5
3
  #include <sys/stat.h>
6
4
  #include <curl/curl.h>
7
5
  #include "membuffer.h"
@@ -26,10 +24,15 @@ static VALUE ePartialFileError = Qnil;
26
24
  static VALUE eTimeoutError = Qnil;
27
25
  static VALUE eTooManyRedirects = Qnil;
28
26
  static VALUE eAborted = Qnil;
27
+ static VALUE eClientCertificateError = Qnil;
29
28
 
30
29
  struct patron_curl_state {
31
30
  CURL* handle;
32
- char* upload_buf;
31
+ CURL* base_handle;
32
+ CURLSH* share;
33
+ /* The String libcurl reads the request body from. CURLOPT_POSTFIELDS does not copy,
34
+ so the String has to stay alive (and marked) until cleanup() */
35
+ VALUE upload_data;
33
36
  FILE* download_file;
34
37
  FILE* debug_file;
35
38
  FILE* request_body_file;
@@ -39,13 +42,21 @@ struct patron_curl_state {
39
42
  struct curl_httppost* last;
40
43
  membuffer header_buffer;
41
44
  membuffer body_buffer;
42
- size_t download_byte_limit;
45
+ curl_off_t download_byte_limit;
43
46
  VALUE user_progress_blk;
47
+ /* A non-local exit (raise, Thread#raise, Thread#kill...) from the progress proc can't
48
+ unwind through libcurl's frames, so it is parked here and resumed after perform() */
49
+ int pending_tag;
50
+ VALUE pending_exception;
51
+ /* Set while a request is in flight. There is one handle and one set of buffers per
52
+ Session, and the transfer runs without the GVL, so a second thread entering
53
+ handle_request would corrupt the first one's request */
54
+ volatile int busy;
44
55
  int interrupt;
45
- size_t dltotal;
46
- size_t dlnow;
47
- size_t ultotal;
48
- size_t ulnow;
56
+ curl_off_t dltotal;
57
+ curl_off_t dlnow;
58
+ curl_off_t ultotal;
59
+ curl_off_t ulnow;
49
60
  };
50
61
 
51
62
 
@@ -74,16 +85,33 @@ static size_t file_write_handler(void* stream, size_t size, size_t nmemb, FILE*
74
85
  }
75
86
  }
76
87
 
77
- static int call_user_rb_progress_blk(void* vd_curl_state) {
88
+ static VALUE call_user_rb_progress_blk_unprotected(VALUE vd_curl_state) {
78
89
  struct patron_curl_state* state = (struct patron_curl_state*)vd_curl_state;
79
- // Invoke the block with the array
80
- VALUE blk_result = rb_funcall(state->user_progress_blk,
90
+ return rb_funcall(state->user_progress_blk,
81
91
  rb_intern("call"), 4,
82
- LONG2NUM(state->dltotal),
83
- LONG2NUM(state->dlnow),
84
- LONG2NUM(state->ultotal),
85
- LONG2NUM(state->ulnow));
86
- return 0;
92
+ LL2NUM(state->dltotal),
93
+ LL2NUM(state->dlnow),
94
+ LL2NUM(state->ultotal),
95
+ LL2NUM(state->ulnow));
96
+ }
97
+
98
+ /* Runs with the GVL re-acquired, from inside a libcurl callback. Whatever the proc
99
+ throws must not propagate from here (see pending_tag), so park it and tell libcurl
100
+ to abort the transfer instead. */
101
+ static void *call_user_rb_progress_blk(void *vd_curl_state) {
102
+ struct patron_curl_state* state = (struct patron_curl_state*)vd_curl_state;
103
+ int tag = 0;
104
+
105
+ rb_protect(call_user_rb_progress_blk_unprotected, (VALUE)state, &tag);
106
+ if (tag) {
107
+ /* Whatever rb_protect caught stays in the errinfo of the thread: for a throw (which is
108
+ how Timeout.timeout unwinds on Rubies before 3.1) it is not an Exception and can't be
109
+ put back with rb_set_errinfo, so we only hold a reference to keep it marked */
110
+ state->pending_tag = tag;
111
+ state->pending_exception = rb_errinfo();
112
+ state->interrupt = INTERRUPT_ABORT;
113
+ }
114
+ return NULL;
87
115
  }
88
116
 
89
117
 
@@ -92,7 +120,7 @@ static int call_user_rb_progress_blk(void* vd_curl_state) {
92
120
  * user calls the "interrupt" method on the session or when the Ruby interpreter
93
121
  * is attempting to exit.
94
122
  */
95
- static int session_progress_handler(void* clientp, size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {
123
+ static int session_progress_handler(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
96
124
  struct patron_curl_state* state = (struct patron_curl_state*) clientp;
97
125
  state->dltotal = dltotal;
98
126
  state->dlnow = dlnow;
@@ -102,16 +130,8 @@ static int session_progress_handler(void* clientp, size_t dltotal, size_t dlnow,
102
130
  // If a progress proc has been set, re-acquire the GIL and call it using
103
131
  // `call_user_rb_progress_blk`. TODO: use the retval of that proc
104
132
  // to permit premature abort
105
- if(RTEST(state->user_progress_blk)) {
106
- // Even though it is not documented, rb_thread_call_with_gvl is available even when
107
- // rb_thread_call_without_gvl is not. See https://bugs.ruby-lang.org/issues/5543#note-4
108
- // > rb_thread_call_with_gvl() is globally-visible (but not in headers)
109
- // > for 1.9.3: https://bugs.ruby-lang.org/issues/4328
110
- #if (defined(HAVE_TBR) || defined(HAVE_TCWOGVL)) && defined(USE_TBR)
111
- rb_thread_call_with_gvl((void *(*)(void *)) call_user_rb_progress_blk, (void*)state);
112
- #else
113
- call_user_rb_progress_blk((void*)state);
114
- #endif
133
+ if(RTEST(state->user_progress_blk) && !state->pending_tag) {
134
+ rb_thread_call_with_gvl(call_user_rb_progress_blk, state);
115
135
  }
116
136
 
117
137
  // Set the interrupt if the download byte limit has been reached
@@ -128,10 +148,18 @@ static int session_progress_handler(void* clientp, size_t dltotal, size_t dlnow,
128
148
  return state->interrupt;
129
149
  }
130
150
 
151
+ #if LIBCURL_VERSION_NUM < 0x072000
152
+ /* Before 7.32.0 there is only CURLOPT_PROGRESSFUNCTION, which hands over doubles */
153
+ static int session_legacy_progress_handler(void* clientp, double dltotal, double dlnow, double ultotal, double ulnow) {
154
+ return session_progress_handler(clientp, (curl_off_t) dltotal, (curl_off_t) dlnow, (curl_off_t) ultotal, (curl_off_t) ulnow);
155
+ }
156
+ #endif
131
157
 
132
- /*----------------------------------------------------------------------------*/
133
- /* List of active curl sessions */
134
158
 
159
+ /*
160
+ List of active curl sessions, used exclusively to be able to set interrupts
161
+ for all of them if the Ruby interpreter gets shut down with libCURL requests still in flight.
162
+ */
135
163
  struct patron_curl_state_list {
136
164
  struct patron_curl_state *state;
137
165
  struct patron_curl_state_list *next;
@@ -184,29 +212,52 @@ static void session_close_debug_file(struct patron_curl_state *curl) {
184
212
  }
185
213
 
186
214
  /* Cleans up the patron_curl_state data when the Session object is garbage collected. */
187
- void session_free(struct patron_curl_state *curl) {
188
- if (curl->handle) {
189
- curl_easy_cleanup(curl->handle);
190
- curl->handle = NULL;
191
- }
215
+ static void session_free(void *ptr) {
216
+ struct patron_curl_state *state = ptr;
217
+
218
+ curl_easy_cleanup(state->base_handle);
219
+ curl_share_cleanup(state->share);
220
+
221
+ session_close_debug_file(state);
222
+
223
+ membuffer_destroy(&state->header_buffer);
224
+ membuffer_destroy(&state->body_buffer);
192
225
 
193
- session_close_debug_file(curl);
226
+ cs_list_remove(state);
194
227
 
195
- membuffer_destroy( &curl->header_buffer );
196
- membuffer_destroy( &curl->body_buffer );
228
+ ruby_xfree(state);
229
+ }
230
+
231
+ static void session_mark(void *ptr) {
232
+ struct patron_curl_state *state = ptr;
233
+
234
+ rb_gc_mark(state->user_progress_blk);
235
+ rb_gc_mark(state->upload_data);
236
+ rb_gc_mark(state->pending_exception);
237
+ }
197
238
 
198
- cs_list_remove(curl);
239
+ static size_t session_memsize(const void *ptr) {
240
+ const struct patron_curl_state *state = ptr;
199
241
 
200
- free(curl);
242
+ return sizeof(*state) + state->header_buffer.capacity + state->body_buffer.capacity;
201
243
  }
202
244
 
245
+ static const rb_data_type_t patron_session_data_type = {
246
+ "Patron::Session",
247
+ {session_mark, session_free, session_memsize,},
248
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
249
+ };
250
+
203
251
  /* Allocates patron_curl_state data needed for a new Session object. */
204
252
  VALUE session_alloc(VALUE klass) {
205
253
  struct patron_curl_state* state;
206
- VALUE obj = Data_Make_Struct(klass, struct patron_curl_state, NULL, session_free, state);
254
+ VALUE obj = TypedData_Make_Struct(klass, struct patron_curl_state, &patron_session_data_type, state);
207
255
 
208
256
  membuffer_init(&state->header_buffer);
209
257
  membuffer_init(&state->body_buffer);
258
+ state->user_progress_blk = Qnil;
259
+ state->upload_data = Qnil;
260
+ state->pending_exception = Qnil;
210
261
  cs_list_append(state);
211
262
 
212
263
  /*
@@ -215,16 +266,42 @@ VALUE session_alloc(VALUE klass) {
215
266
  reuse the TCP connection and can speed things up if the same resource - like a backend service -
216
267
  gets accessed over and over with requests.
217
268
  */
218
- state->handle = curl_easy_init();
219
- curl_easy_setopt(state->handle, CURLOPT_NOSIGNAL, 1);
220
- curl_easy_setopt(state->handle, CURLOPT_NOPROGRESS, 0);
221
- #if LIBCURL_VERSION_NUM >= 0x072000
222
- /* this is libCURLv7.32.0 or later, supports CURLOPT_XFERINFOFUNCTION */
223
- curl_easy_setopt(state->handle, CURLOPT_XFERINFOFUNCTION, &session_progress_handler);
224
- #else
225
- curl_easy_setopt(state->handle, CURLOPT_PROGRESSFUNCTION, &session_progress_handler);
226
- #endif
227
- curl_easy_setopt(state->handle, CURLOPT_PROGRESSDATA, state);
269
+ state->share = curl_share_init();
270
+ if (!state->share) {
271
+ rb_raise(rb_eNoMemError, "libcurl could not allocate a share handle");
272
+ }
273
+ curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
274
+ curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
275
+ curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
276
+ curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
277
+ curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
278
+ state->base_handle = curl_easy_init();
279
+ if (!state->base_handle) {
280
+ /* obj is already allocated, so session_free() will release the share; curl_share_cleanup(NULL)
281
+ and curl_easy_cleanup(NULL) are no-ops, which keeps that path safe for a half-built state */
282
+ rb_raise(rb_eNoMemError, "libcurl could not allocate an easy handle");
283
+ }
284
+ curl_easy_setopt(state->base_handle, CURLOPT_SHARE, state->share);
285
+ curl_easy_setopt(state->base_handle, CURLOPT_WRITEFUNCTION, &session_write_handler);
286
+ curl_easy_setopt(state->base_handle, CURLOPT_WRITEDATA, &state->body_buffer);
287
+ curl_easy_setopt(state->base_handle, CURLOPT_HEADERFUNCTION, &session_write_handler);
288
+ curl_easy_setopt(state->base_handle, CURLOPT_HEADERDATA, &state->header_buffer);
289
+ curl_easy_setopt(state->base_handle, CURLOPT_NOSIGNAL, 1);
290
+ curl_easy_setopt(state->base_handle, CURLOPT_NOPROGRESS, 0);
291
+ #if LIBCURL_VERSION_NUM >= 0x072000
292
+ /* this is libCURLv7.32.0 or later, supports CURLOPT_XFERINFOFUNCTION */
293
+ curl_easy_setopt(state->base_handle, CURLOPT_XFERINFOFUNCTION, &session_progress_handler);
294
+ #else
295
+ curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSFUNCTION, &session_legacy_progress_handler);
296
+ #endif
297
+ curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSDATA, state);
298
+ #ifdef CURLPROTO_HTTP
299
+ // Security: do not allow Curl to go looking on gopher/SMTP etc.
300
+ // Must prevent situations like this:
301
+ // https://hackerone.com/reports/115748
302
+ curl_easy_setopt(state->base_handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
303
+ curl_easy_setopt(state->base_handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
304
+ #endif
228
305
 
229
306
  return obj;
230
307
  }
@@ -232,7 +309,7 @@ VALUE session_alloc(VALUE klass) {
232
309
  /* Return the patron_curl_state from the ruby VALUE which is the Session instance. */
233
310
  static struct patron_curl_state* get_patron_curl_state(VALUE self) {
234
311
  struct patron_curl_state* state;
235
- Data_Get_Struct(self, struct patron_curl_state, state);
312
+ TypedData_Get_Struct(self, struct patron_curl_state, &patron_session_data_type, state);
236
313
  return state;
237
314
  }
238
315
 
@@ -276,13 +353,25 @@ static VALUE session_escape(VALUE self, VALUE value) {
276
353
  char* escaped = NULL;
277
354
  VALUE retval = Qnil;
278
355
 
279
- struct patron_curl_state* state = curl_easy_init();
280
- escaped = curl_easy_escape(state->handle,
356
+ #if LIBCURL_VERSION_NUM >= 0x075200
357
+ /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
358
+ CURL *curl = NULL;
359
+ #else
360
+ CURL* curl = curl_easy_init();
361
+ #endif
362
+ escaped = curl_easy_escape(curl,
281
363
  RSTRING_PTR(string),
282
- (int) RSTRING_LEN(string));
364
+ rb_long2int(RSTRING_LEN(string)));
365
+ #if LIBCURL_VERSION_NUM >= 0x075200
366
+ /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
367
+ #else
368
+ curl_easy_cleanup(curl);
369
+ #endif
370
+ if (!escaped) {
371
+ rb_raise(ePatronError, "libcurl could not escape the string");
372
+ }
283
373
 
284
374
  retval = rb_str_new2(escaped);
285
- curl_easy_cleanup(state);
286
375
  curl_free(escaped);
287
376
 
288
377
  return retval;
@@ -297,17 +386,31 @@ static VALUE session_escape(VALUE self, VALUE value) {
297
386
  static VALUE session_unescape(VALUE self, VALUE value) {
298
387
  VALUE string = StringValue(value);
299
388
  char* unescaped = NULL;
389
+ int unescaped_len = 0;
300
390
  VALUE retval = Qnil;
301
391
 
302
- struct patron_curl_state* state = curl_easy_init();
303
- unescaped = curl_easy_unescape(state->handle,
392
+ #if LIBCURL_VERSION_NUM >= 0x075200
393
+ /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
394
+ CURL *curl = NULL;
395
+ #else
396
+ CURL *curl = curl_easy_init();
397
+ #endif
398
+ /* Ask for the length: the decoded bytes may contain NUL ("%00"), so strlen would cut it short */
399
+ unescaped = curl_easy_unescape(curl,
304
400
  RSTRING_PTR(string),
305
- (int) RSTRING_LEN(string),
306
- NULL);
401
+ rb_long2int(RSTRING_LEN(string)),
402
+ &unescaped_len);
403
+ #if LIBCURL_VERSION_NUM >= 0x075200
404
+ /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
405
+ #else
406
+ curl_easy_cleanup(curl);
407
+ #endif
408
+ if (!unescaped) {
409
+ rb_raise(ePatronError, "libcurl could not unescape the string");
410
+ }
307
411
 
308
- retval = rb_str_new2(unescaped);
412
+ retval = rb_str_new(unescaped, unescaped_len);
309
413
  curl_free(unescaped);
310
- curl_easy_cleanup(state);
311
414
 
312
415
  return retval;
313
416
  }
@@ -348,8 +451,18 @@ static int formadd_values(VALUE data_key, VALUE data_value, VALUE self) {
348
451
  VALUE name = rb_obj_as_string(data_key);
349
452
  VALUE value = rb_obj_as_string(data_value);
350
453
 
351
- curl_formadd(&state->post, &state->last, CURLFORM_PTRNAME, RSTRING_PTR(name),
352
- CURLFORM_PTRCONTENTS, RSTRING_PTR(value), CURLFORM_END);
454
+ /* COPY* makes libcurl copy both strings: the PTR* variants would keep pointers into
455
+ Strings nothing holds on to (a Symbol key gets stringified into a temporary), and
456
+ PTRCONTENTS stops at the first NUL byte. Passing the length keeps binary values intact. */
457
+ curl_formadd(&state->post, &state->last,
458
+ CURLFORM_COPYNAME, RSTRING_PTR(name), CURLFORM_NAMELENGTH, (long) RSTRING_LEN(name),
459
+ CURLFORM_COPYCONTENTS, RSTRING_PTR(value),
460
+ #if LIBCURL_VERSION_NUM >= 0x072E00
461
+ CURLFORM_CONTENTLEN, (curl_off_t) RSTRING_LEN(value),
462
+ #else
463
+ CURLFORM_CONTENTSLENGTH, (long) RSTRING_LEN(value),
464
+ #endif
465
+ CURLFORM_END);
353
466
 
354
467
  return 0;
355
468
  }
@@ -359,19 +472,24 @@ static int formadd_files(VALUE data_key, VALUE data_value, VALUE self) {
359
472
  VALUE name = rb_obj_as_string(data_key);
360
473
  VALUE value = rb_obj_as_string(data_value);
361
474
 
362
- curl_formadd(&state->post, &state->last, CURLFORM_PTRNAME, RSTRING_PTR(name),
363
- CURLFORM_FILE, RSTRING_PTR(value), CURLFORM_END);
475
+ curl_formadd(&state->post, &state->last,
476
+ CURLFORM_COPYNAME, RSTRING_PTR(name), CURLFORM_NAMELENGTH, (long) RSTRING_LEN(name),
477
+ CURLFORM_FILE, StringValueCStr(value), CURLFORM_END);
364
478
 
365
479
  return 0;
366
480
  }
367
481
 
368
- // Set the given char pointer and it's length to be the CURL request body
369
- static void set_curl_request_body(CURL* curl, char* buf, curl_off_t len) {
370
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buf);
482
+ // Use the given String as the CURL request body. The String is kept in the state
483
+ // so that it outlives the (GVL-less) transfer which reads from its buffer.
484
+ static void set_curl_request_body(struct patron_curl_state* state, VALUE data) {
485
+ CURL* curl = state->handle;
486
+ StringValue(data);
487
+ state->upload_data = data;
488
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, RSTRING_PTR(state->upload_data));
371
489
  #ifdef CURLOPT_POSTFIELDSIZE_LARGE
372
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len);
490
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t) RSTRING_LEN(state->upload_data));
373
491
  #else
374
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
492
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long) RSTRING_LEN(state->upload_data));
375
493
  #endif
376
494
  }
377
495
 
@@ -380,7 +498,7 @@ static void set_chunked_encoding(struct patron_curl_state *state) {
380
498
  }
381
499
 
382
500
  static FILE* open_file(VALUE filename, const char* perms) {
383
- FILE* handle = fopen(StringValuePtr(filename), perms);
501
+ FILE* handle = fopen(StringValueCStr(filename), perms);
384
502
  if (!handle) {
385
503
  rb_raise(rb_eArgError, "Unable to open specified file.");
386
504
  }
@@ -410,7 +528,6 @@ static long floating_rb_seconds_to_milliseconds(VALUE r_seconds) {
410
528
  }
411
529
 
412
530
  static void set_request_body(struct patron_curl_state* state, VALUE stringable_or_file) {
413
- CURL* curl = state->handle;
414
531
  if(rb_respond_to(stringable_or_file, rb_intern("to_path"))) {
415
532
  // Set up a file read callback (read the entire request body from a file).
416
533
  // Instead of using the Ruby file reads, use #to_path to obtain the
@@ -420,10 +537,7 @@ static void set_request_body(struct patron_curl_state* state, VALUE stringable_o
420
537
  set_request_body_file(state, r_path_str);
421
538
  } else {
422
539
  // Set the request body from a String
423
- VALUE data = rb_funcall(stringable_or_file, rb_intern("to_s"), 0);
424
- long len = RSTRING_LEN(data);
425
- state->upload_buf = StringValuePtr(data);
426
- set_curl_request_body(curl, state->upload_buf, len);
540
+ set_curl_request_body(state, rb_funcall(stringable_or_file, rb_intern("to_s"), 0));
427
541
  }
428
542
  }
429
543
 
@@ -433,7 +547,7 @@ static void set_request_body(struct patron_curl_state* state, VALUE stringable_o
433
547
  */
434
548
  static void set_options_from_request(VALUE self, VALUE request) {
435
549
  struct patron_curl_state* state = get_patron_curl_state(self);
436
- CURL* curl = state->handle;
550
+ CURL* curl = curl_easy_duphandle(state->base_handle);
437
551
 
438
552
  ID action = Qnil;
439
553
  VALUE headers = Qnil;
@@ -447,15 +561,28 @@ static void set_options_from_request(VALUE self, VALUE request) {
447
561
  VALUE insecure = Qnil;
448
562
  VALUE cacert = Qnil;
449
563
  VALUE ssl_version = Qnil;
564
+ VALUE ssl_cert = Qnil;
565
+ VALUE ssl_cert_type = Qnil;
566
+ VALUE ssl_key = Qnil;
567
+ VALUE ssl_key_password = Qnil;
450
568
  VALUE http_version = Qnil;
451
569
  VALUE buffer_size = Qnil;
452
- VALUE action_name = rb_funcall(request, rb_intern("action"), 0);
453
- VALUE a_c_encoding = rb_funcall(request, rb_intern("automatic_content_encoding"), 0);
454
- VALUE download_byte_limit = rb_funcall(request, rb_intern("download_byte_limit"), 0);
455
- VALUE maybe_progress_proc = rb_funcall(request, rb_intern("progress_callback"), 0);
570
+ VALUE action_name = Qnil;
571
+ VALUE a_c_encoding = Qnil;
572
+ VALUE download_byte_limit = Qnil;
573
+ VALUE maybe_progress_proc = Qnil;
574
+
575
+ /* Assign before anything can raise, so that cleanup() frees this handle and not a stale one */
576
+ state->handle = curl;
577
+ action_name = rb_funcall(request, rb_intern("action"), 0);
578
+ a_c_encoding = rb_funcall(request, rb_intern("automatic_content_encoding"), 0);
579
+ download_byte_limit = rb_funcall(request, rb_intern("download_byte_limit"), 0);
580
+ maybe_progress_proc = rb_funcall(request, rb_intern("progress_callback"), 0);
581
+
582
+ curl_easy_setopt(curl, CURLOPT_SHARE, state->share);
456
583
 
457
584
  if (RTEST(download_byte_limit)) {
458
- state->download_byte_limit = FIX2INT(download_byte_limit);
585
+ state->download_byte_limit = (curl_off_t) NUM2LL(download_byte_limit);
459
586
  } else {
460
587
  state->download_byte_limit = 0;
461
588
  }
@@ -475,7 +602,7 @@ static void set_options_from_request(VALUE self, VALUE request) {
475
602
  }
476
603
 
477
604
  action = SYM2ID(action_name);
478
- if(rb_funcall(request, rb_intern("force_ipv4"), 0)) {
605
+ if (RTEST(rb_funcall(request, rb_intern("force_ipv4"), 0))) {
479
606
  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
480
607
  }
481
608
  if (action == rb_intern("get")) {
@@ -541,11 +668,8 @@ static void set_options_from_request(VALUE self, VALUE request) {
541
668
  } else if (action == rb_intern("delete")) {
542
669
  VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
543
670
  if (RTEST(data)) {
544
- long len = RSTRING_LEN(data);
545
- state->upload_buf = StringValuePtr(data);
546
671
  curl_easy_setopt(curl, CURLOPT_POST, 1);
547
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, state->upload_buf);
548
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
672
+ set_request_body(state, data);
549
673
  }
550
674
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
551
675
 
@@ -578,13 +702,6 @@ static void set_options_from_request(VALUE self, VALUE request) {
578
702
  }
579
703
  curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url));
580
704
 
581
- #ifdef CURLPROTO_HTTP
582
- // Security: do not allow Curl to go looking on gopher/SMTP etc.
583
- // Must prevent situations like this:
584
- // https://hackerone.com/reports/115748
585
- curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
586
- curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
587
- #endif
588
705
 
589
706
  timeout = rb_funcall(request, rb_intern("timeout"), 0);
590
707
  if (RTEST(timeout)) {
@@ -598,22 +715,22 @@ static void set_options_from_request(VALUE self, VALUE request) {
598
715
 
599
716
  timeout = rb_funcall(request, rb_intern("dns_cache_timeout"), 0);
600
717
  if (RTEST(timeout)) {
601
- curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, FIX2INT(timeout));
718
+ curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, NUM2LONG(timeout));
602
719
  }
603
720
 
604
721
  VALUE low_speed_time = rb_funcall(request, rb_intern("low_speed_time"), 0);
605
722
  if(RTEST(low_speed_time)) {
606
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, FIX2LONG(low_speed_time));
723
+ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, NUM2LONG(low_speed_time));
607
724
  }
608
725
 
609
726
  VALUE low_speed_limit_bytes_per_second = rb_funcall(request, rb_intern("low_speed_limit"), 0);
610
727
  if(RTEST(low_speed_limit_bytes_per_second)) {
611
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, FIX2LONG(low_speed_limit_bytes_per_second));
728
+ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, NUM2LONG(low_speed_limit_bytes_per_second));
612
729
  }
613
730
 
614
731
  redirects = rb_funcall(request, rb_intern("max_redirects"), 0);
615
732
  if (RTEST(redirects)) {
616
- int r = FIX2INT(redirects);
733
+ long r = NUM2LONG(redirects);
617
734
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, r == 0 ? 0 : 1);
618
735
  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, r);
619
736
  }
@@ -713,22 +830,63 @@ static void set_options_from_request(VALUE self, VALUE request) {
713
830
  }
714
831
  }
715
832
 
833
+ ssl_cert = rb_funcall(request, rb_intern("ssl_cert"), 0);
834
+ if(RTEST(ssl_cert)) {
835
+ ssl_cert = FilePathValue(ssl_cert);
836
+ curl_easy_setopt(curl, CURLOPT_SSLCERT, StringValueCStr(ssl_cert));
837
+ }
838
+
839
+ ssl_cert_type = rb_funcall(request, rb_intern("ssl_cert_type"), 0);
840
+ if(RTEST(ssl_cert_type)) {
841
+ ssl_cert_type = rb_obj_as_string(ssl_cert_type);
842
+ curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, StringValueCStr(ssl_cert_type));
843
+ }
844
+
845
+ ssl_key = rb_funcall(request, rb_intern("ssl_key"), 0);
846
+ if(RTEST(ssl_key)) {
847
+ ssl_key = FilePathValue(ssl_key);
848
+ curl_easy_setopt(curl, CURLOPT_SSLKEY, StringValueCStr(ssl_key));
849
+ }
850
+
851
+ ssl_key_password = rb_funcall(request, rb_intern("ssl_key_password"), 0);
852
+ if(RTEST(ssl_key_password)) {
853
+ curl_easy_setopt(curl, CURLOPT_KEYPASSWD, StringValueCStr(ssl_key_password));
854
+ }
855
+
716
856
  cacert = rb_funcall(request, rb_intern("cacert"), 0);
717
857
  if(RTEST(cacert)) {
718
- curl_easy_setopt(curl, CURLOPT_CAINFO, StringValuePtr(cacert));
858
+ cacert = FilePathValue(cacert);
859
+ curl_easy_setopt(curl, CURLOPT_CAINFO, StringValueCStr(cacert));
719
860
  }
720
861
 
721
862
  buffer_size = rb_funcall(request, rb_intern("buffer_size"), 0);
722
863
  if (RTEST(buffer_size)) {
723
864
  curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, NUM2LONG(buffer_size));
724
865
  }
866
+ }
725
867
 
726
- if(state->debug_file) {
727
- curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
728
- curl_easy_setopt(curl, CURLOPT_STDERR, state->debug_file);
868
+ /* Store one of curl's transfer marks (seconds since the request started) under +key+. */
869
+ static void set_timing(CURL* curl, VALUE timings, const char* key, CURLINFO info) {
870
+ double seconds = 0;
871
+ if (curl_easy_getinfo(curl, info, &seconds) == CURLE_OK) {
872
+ rb_hash_aset(timings, ID2SYM(rb_intern(key)), rb_float_new(seconds));
729
873
  }
730
874
  }
731
875
 
876
+ /* curl's cumulative transfer marks: namelookup <= connect <= appconnect <= pretransfer
877
+ * <= starttransfer <= total. They are read after the transfer, so they cost no GVL time. */
878
+ static VALUE create_timings(CURL* curl) {
879
+ VALUE timings = rb_hash_new();
880
+ set_timing(curl, timings, "namelookup", CURLINFO_NAMELOOKUP_TIME);
881
+ set_timing(curl, timings, "connect", CURLINFO_CONNECT_TIME);
882
+ set_timing(curl, timings, "appconnect", CURLINFO_APPCONNECT_TIME);
883
+ set_timing(curl, timings, "pretransfer", CURLINFO_PRETRANSFER_TIME);
884
+ set_timing(curl, timings, "starttransfer", CURLINFO_STARTTRANSFER_TIME);
885
+ set_timing(curl, timings, "redirect", CURLINFO_REDIRECT_TIME);
886
+ set_timing(curl, timings, "total", CURLINFO_TOTAL_TIME);
887
+ return timings;
888
+ }
889
+
732
890
  /* Use the info in a Curl handle to create a new Response object. */
733
891
  static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE body_buffer) {
734
892
  VALUE args[6] = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil };
@@ -736,22 +894,26 @@ static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE
736
894
  long code = 0;
737
895
  long count = 0;
738
896
  VALUE responseKlass = Qnil;
897
+ VALUE response = Qnil;
739
898
 
740
899
  curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url);
741
900
  args[0] = rb_str_new2(effective_url);
742
901
 
743
902
  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
744
- args[1] = INT2NUM(code);
903
+ args[1] = LONG2NUM(code);
745
904
 
746
905
  curl_easy_getinfo(curl, CURLINFO_REDIRECT_COUNT, &count);
747
- args[2] = INT2NUM(count);
906
+ args[2] = LONG2NUM(count);
748
907
 
749
908
  args[3] = header_buffer;
750
909
  args[4] = body_buffer;
751
910
  args[5] = rb_funcall(self, rb_intern("default_response_charset"), 0);
752
911
 
753
912
  responseKlass = rb_funcall(self, rb_intern("response_class"), 0);
754
- return rb_class_new_instance(6, args, responseKlass);
913
+ response = rb_class_new_instance(6, args, responseKlass);
914
+ /* Set after construction so custom response classes keep their #initialize signature. */
915
+ rb_iv_set(response, "@timings", create_timings(curl));
916
+ return response;
755
917
  }
756
918
 
757
919
  /* Raise an exception based on the Curl error code. */
@@ -766,6 +928,7 @@ static VALUE select_error(CURLcode code) {
766
928
  case CURLE_OPERATION_TIMEDOUT: error = eTimeoutError; break;
767
929
  case CURLE_TOO_MANY_REDIRECTS: error = eTooManyRedirects; break;
768
930
  case CURLE_ABORTED_BY_CALLBACK: error = eAborted; break;
931
+ case CURLE_SSL_CERTPROBLEM: error = eClientCertificateError; break;
769
932
  default: error = ePatronError;
770
933
  }
771
934
 
@@ -773,6 +936,18 @@ static VALUE select_error(CURLcode code) {
773
936
  }
774
937
 
775
938
 
939
+ struct perform_context {
940
+ CURL *curl;
941
+ CURLcode code;
942
+ };
943
+
944
+ static void *perform_without_gvl(void *ptr) {
945
+ struct perform_context *context = ptr;
946
+
947
+ context->code = curl_easy_perform(context->curl);
948
+ return NULL;
949
+ }
950
+
776
951
  /* Uses as the unblocking function when the thread running Patron gets
777
952
  signaled. The important difference with session_interrupt is that we
778
953
  are not allowed to touch any Ruby structures while outside the GIL,
@@ -787,72 +962,71 @@ void session_ubf_abort(void* patron_state) {
787
962
  static VALUE perform_request(VALUE self) {
788
963
  struct patron_curl_state *state = get_patron_curl_state(self);
789
964
  CURL* curl = state->handle;
790
- membuffer* header_buffer = NULL;
791
- membuffer* body_buffer = NULL;
792
- CURLcode ret = 0;
965
+ struct perform_context context = {curl, 0};
793
966
 
794
967
  state->interrupt = 0; /* clear the interrupt flag */
795
-
796
- header_buffer = &state->header_buffer;
797
- body_buffer = &state->body_buffer;
798
-
799
- membuffer_clear(header_buffer);
800
- membuffer_clear(body_buffer);
801
-
802
- /* headers */
803
- curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &session_write_handler);
804
- curl_easy_setopt(curl, CURLOPT_HEADERDATA, header_buffer);
805
-
806
- /* body */
807
- if (!state->download_file) {
808
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &session_write_handler);
809
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, body_buffer);
968
+ state->pending_tag = 0;
969
+ state->pending_exception = Qnil;
970
+
971
+ rb_thread_call_without_gvl(perform_without_gvl, &context, session_ubf_abort, state);
972
+
973
+ /* The progress proc threw: resume that exit now that libcurl is done with the handle.
974
+ cleanup() still runs, since we are inside the rb_ensure of session_handle_request. */
975
+ if (state->pending_tag) {
976
+ int tag = state->pending_tag;
977
+ VALUE exception = state->pending_exception;
978
+ state->pending_tag = 0;
979
+ state->pending_exception = Qnil;
980
+ /* errinfo is only an Exception for a raise; a throw leaves an internal object there and
981
+ Thread#kill a Fixnum, both of which rb_obj_is_kind_of must not be asked about */
982
+ if (RB_TYPE_P(exception, T_OBJECT) && rb_obj_is_kind_of(exception, rb_eException)) {
983
+ rb_exc_raise(exception);
984
+ }
985
+ rb_jump_tag(tag);
810
986
  }
811
987
 
812
- #if (defined(HAVE_TBR) || defined(HAVE_TCWOGVL)) && defined(USE_TBR)
813
- #if defined(HAVE_TCWOGVL)
814
- ret = (CURLcode) rb_thread_call_without_gvl(
815
- (void *(*)(void *)) curl_easy_perform, curl,
816
- session_ubf_abort, (void*)state
817
- );
818
- #else
819
- ret = (CURLcode) rb_thread_blocking_region(
820
- (rb_blocking_function_t*) curl_easy_perform, curl,
821
- session_ubf_abort, (void*)state
822
- );
823
- #endif
824
- #else
825
- ret = curl_easy_perform(curl);
826
- #endif
827
-
828
- if (CURLE_OK == ret) {
829
- VALUE header_str = membuffer_to_rb_str(header_buffer);
988
+ if (CURLE_OK == context.code) {
989
+ VALUE header_str = membuffer_to_rb_str(&state->header_buffer);
830
990
  VALUE body_str = Qnil;
831
- if (!state->download_file) { body_str = membuffer_to_rb_str(body_buffer); }
991
+ if (!state->download_file) { body_str = membuffer_to_rb_str(&state->body_buffer); }
832
992
 
833
993
  curl_easy_setopt(curl, CURLOPT_COOKIELIST, "FLUSH"); // Flush cookies to the cookie jar
834
994
 
835
995
  return create_response(self, curl, header_str, body_str);
836
996
  } else {
837
- rb_raise(select_error(ret), "%s", state->error_buf);
997
+ rb_raise(select_error(context.code), "%s", state->error_buf);
838
998
  }
839
999
  }
840
1000
 
1001
+ /* Setting the options can raise too (a Request attribute of the wrong type, say),
1002
+ * and this needs to run under rb_ensure so that the half-configured handle, headers and
1003
+ * open files get cleaned up instead of leaking into the next request on this Session.
1004
+ */
1005
+ static VALUE prepare_and_perform_request(VALUE args) {
1006
+ VALUE self = rb_ary_entry(args, 0);
1007
+ set_options_from_request(self, rb_ary_entry(args, 1));
1008
+ return perform_request(self);
1009
+ }
1010
+
841
1011
  /* Cleanup after each request by resetting the Curl handle and deallocating
842
1012
  * all request related objects such as the header slist.
843
1013
  */
844
1014
  static VALUE cleanup(VALUE self) {
845
1015
  struct patron_curl_state *state = get_patron_curl_state(self);
846
- curl_easy_reset(state->handle);
1016
+ curl_easy_cleanup(state->handle);
1017
+ state->handle = NULL;
847
1018
 
848
1019
  if (state->headers) {
849
1020
  curl_slist_free_all(state->headers);
850
1021
  state->headers = NULL;
851
1022
  }
1023
+ membuffer_clear(&state->header_buffer);
852
1024
 
853
1025
  if (state->download_file) {
854
1026
  fclose(state->download_file);
855
1027
  state->download_file = NULL;
1028
+ } else {
1029
+ membuffer_clear(&state->body_buffer);
856
1030
  }
857
1031
 
858
1032
  if (state->request_body_file) {
@@ -866,7 +1040,10 @@ static VALUE cleanup(VALUE self) {
866
1040
  state->last = NULL;
867
1041
  }
868
1042
 
869
- state->upload_buf = NULL;
1043
+ state->upload_data = Qnil;
1044
+ state->pending_tag = 0;
1045
+ state->pending_exception = Qnil;
1046
+ state->busy = 0;
870
1047
 
871
1048
  return Qnil;
872
1049
  }
@@ -885,8 +1062,16 @@ static VALUE cleanup(VALUE self) {
885
1062
  * @return [Patron::Response] the result of calling `response_class` on the Session
886
1063
  */
887
1064
  static VALUE session_handle_request(VALUE self, VALUE request) {
888
- set_options_from_request(self, request);
889
- return rb_ensure(&perform_request, self, &cleanup, self);
1065
+ struct patron_curl_state *state = get_patron_curl_state(self);
1066
+ VALUE args = Qnil;
1067
+
1068
+ if (state->busy) {
1069
+ rb_raise(ePatronError, "Session is already performing a request on another thread; use one Session per thread");
1070
+ }
1071
+ state->busy = 1;
1072
+
1073
+ args = rb_ary_new3(2, self, request);
1074
+ return rb_ensure(&prepare_and_perform_request, args, &cleanup, self);
890
1075
  }
891
1076
 
892
1077
  /* Interrupt any currently executing request. This will cause the current
@@ -912,12 +1097,12 @@ static VALUE session_interrupt(VALUE self) {
912
1097
  */
913
1098
  static VALUE add_cookie_file(VALUE self, VALUE file) {
914
1099
  struct patron_curl_state *state = get_patron_curl_state(self);
915
- CURL* curl = state->handle;
1100
+ CURL* curl = state->base_handle;
916
1101
  char* file_path = NULL;
917
1102
 
918
1103
  // FIXME: http://websystemsengineering.blogspot.nl/2013/03/curloptcookiefile-vs-curloptcookiejar.html
919
- file_path = RSTRING_PTR(file);
920
- if (file_path != NULL && strlen(file_path) != 0) {
1104
+ file_path = StringValueCStr(file);
1105
+ if (strlen(file_path) != 0) {
921
1106
  curl_easy_setopt(curl, CURLOPT_COOKIEJAR, file_path);
922
1107
  }
923
1108
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, file_path);
@@ -933,15 +1118,18 @@ static VALUE add_cookie_file(VALUE self, VALUE file) {
933
1118
  */
934
1119
  static VALUE set_debug_file(VALUE self, VALUE file) {
935
1120
  struct patron_curl_state *state = get_patron_curl_state(self);
936
- char* file_path = RSTRING_PTR(file);
1121
+ CURL *curl = state->base_handle;
1122
+ char *file_path = StringValueCStr(file);
937
1123
 
938
1124
  session_close_debug_file(state);
939
1125
 
940
- if(file_path != NULL && strlen(file_path) != 0) {
1126
+ if(strlen(file_path) != 0) {
941
1127
  state->debug_file = open_file(file, "wb");
942
1128
  } else {
943
1129
  state->debug_file = stderr;
944
1130
  }
1131
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
1132
+ curl_easy_setopt(curl, CURLOPT_STDERR, state->debug_file);
945
1133
 
946
1134
  return self;
947
1135
  }
@@ -958,6 +1146,22 @@ void Init_session_ext() {
958
1146
 
959
1147
  mPatron = rb_define_module("Patron");
960
1148
 
1149
+ /* These classes are defined in Ruby and only looked up here, so unlike the ones from
1150
+ rb_define_class the GC does not know we hold them: register them, or compaction moves
1151
+ them out from under us and rb_raise ends up instantiating garbage */
1152
+ rb_global_variable(&ePatronError);
1153
+ rb_global_variable(&eUnsupportedProtocol);
1154
+ rb_global_variable(&eUnsupportedSSLVersion);
1155
+ rb_global_variable(&eUnsupportedHTTPVersion);
1156
+ rb_global_variable(&eURLFormatError);
1157
+ rb_global_variable(&eHostResolutionError);
1158
+ rb_global_variable(&eConnectionFailed);
1159
+ rb_global_variable(&ePartialFileError);
1160
+ rb_global_variable(&eTimeoutError);
1161
+ rb_global_variable(&eTooManyRedirects);
1162
+ rb_global_variable(&eAborted);
1163
+ rb_global_variable(&eClientCertificateError);
1164
+
961
1165
  ePatronError = rb_const_get(mPatron, rb_intern("Error"));
962
1166
 
963
1167
  eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
@@ -970,6 +1174,7 @@ void Init_session_ext() {
970
1174
  eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
971
1175
  eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));
972
1176
  eAborted = rb_const_get(mPatron, rb_intern("Aborted"));
1177
+ eClientCertificateError = rb_const_get(mPatron, rb_intern("ClientCertificateError"));
973
1178
 
974
1179
  rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);
975
1180
  rb_define_module_function(mPatron, "libcurl_version_exact", libcurl_version_exact, 0);
@@ -986,11 +1191,11 @@ void Init_session_ext() {
986
1191
  rb_define_singleton_method(cSession, "unescape", session_unescape, 1);
987
1192
  rb_define_method(cSession, "unescape", session_unescape, 1);
988
1193
 
989
- rb_define_method(cSession, "handle_request", session_handle_request, 1);
1194
+ rb_define_private_method(cSession, "handle_request", session_handle_request, 1);
990
1195
  rb_define_method(cSession, "reset", session_interrupt, 0);
991
1196
  rb_define_method(cSession, "interrupt", session_interrupt, 0);
992
- rb_define_method(cSession, "add_cookie_file", add_cookie_file, 1);
993
- rb_define_method(cSession, "set_debug_file", set_debug_file, 1);
1197
+ rb_define_private_method(cSession, "add_cookie_file", add_cookie_file, 1);
1198
+ rb_define_private_method(cSession, "set_debug_file", set_debug_file, 1);
994
1199
  rb_define_alias(cSession, "urlencode", "escape");
995
1200
  rb_define_alias(cSession, "urldecode", "unescape");
996
1201