patron 0.13.4 → 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.
@@ -24,12 +24,15 @@ static VALUE ePartialFileError = Qnil;
24
24
  static VALUE eTimeoutError = Qnil;
25
25
  static VALUE eTooManyRedirects = Qnil;
26
26
  static VALUE eAborted = Qnil;
27
+ static VALUE eClientCertificateError = Qnil;
27
28
 
28
29
  struct patron_curl_state {
29
30
  CURL* handle;
30
31
  CURL* base_handle;
31
32
  CURLSH* share;
32
- char* upload_buf;
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
- 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,8 +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
- rb_thread_call_with_gvl((void *(*)(void *)) call_user_rb_progress_blk, (void*)state);
133
+ if(RTEST(state->user_progress_blk) && !state->pending_tag) {
134
+ rb_thread_call_with_gvl(call_user_rb_progress_blk, state);
107
135
  }
108
136
 
109
137
  // Set the interrupt if the download byte limit has been reached
@@ -120,6 +148,13 @@ static int session_progress_handler(void* clientp, size_t dltotal, size_t dlnow,
120
148
  return state->interrupt;
121
149
  }
122
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
157
+
123
158
 
124
159
  /*
125
160
  List of active curl sessions, used exclusively to be able to set interrupts
@@ -177,7 +212,9 @@ static void session_close_debug_file(struct patron_curl_state *curl) {
177
212
  }
178
213
 
179
214
  /* Cleans up the patron_curl_state data when the Session object is garbage collected. */
180
- void session_free(struct patron_curl_state *state) {
215
+ static void session_free(void *ptr) {
216
+ struct patron_curl_state *state = ptr;
217
+
181
218
  curl_easy_cleanup(state->base_handle);
182
219
  curl_share_cleanup(state->share);
183
220
 
@@ -188,16 +225,39 @@ void session_free(struct patron_curl_state *state) {
188
225
 
189
226
  cs_list_remove(state);
190
227
 
191
- free(state);
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);
192
237
  }
193
238
 
239
+ static size_t session_memsize(const void *ptr) {
240
+ const struct patron_curl_state *state = ptr;
241
+
242
+ return sizeof(*state) + state->header_buffer.capacity + state->body_buffer.capacity;
243
+ }
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
+
194
251
  /* Allocates patron_curl_state data needed for a new Session object. */
195
252
  VALUE session_alloc(VALUE klass) {
196
253
  struct patron_curl_state* state;
197
- 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);
198
255
 
199
256
  membuffer_init(&state->header_buffer);
200
257
  membuffer_init(&state->body_buffer);
258
+ state->user_progress_blk = Qnil;
259
+ state->upload_data = Qnil;
260
+ state->pending_exception = Qnil;
201
261
  cs_list_append(state);
202
262
 
203
263
  /*
@@ -207,12 +267,20 @@ VALUE session_alloc(VALUE klass) {
207
267
  gets accessed over and over with requests.
208
268
  */
209
269
  state->share = curl_share_init();
270
+ if (!state->share) {
271
+ rb_raise(rb_eNoMemError, "libcurl could not allocate a share handle");
272
+ }
210
273
  curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
211
274
  curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
212
275
  curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
213
276
  curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
214
277
  curl_share_setopt(state->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
215
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
+ }
216
284
  curl_easy_setopt(state->base_handle, CURLOPT_SHARE, state->share);
217
285
  curl_easy_setopt(state->base_handle, CURLOPT_WRITEFUNCTION, &session_write_handler);
218
286
  curl_easy_setopt(state->base_handle, CURLOPT_WRITEDATA, &state->body_buffer);
@@ -224,7 +292,7 @@ VALUE session_alloc(VALUE klass) {
224
292
  /* this is libCURLv7.32.0 or later, supports CURLOPT_XFERINFOFUNCTION */
225
293
  curl_easy_setopt(state->base_handle, CURLOPT_XFERINFOFUNCTION, &session_progress_handler);
226
294
  #else
227
- curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSFUNCTION, &session_progress_handler);
295
+ curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSFUNCTION, &session_legacy_progress_handler);
228
296
  #endif
229
297
  curl_easy_setopt(state->base_handle, CURLOPT_PROGRESSDATA, state);
230
298
  #ifdef CURLPROTO_HTTP
@@ -241,7 +309,7 @@ VALUE session_alloc(VALUE klass) {
241
309
  /* Return the patron_curl_state from the ruby VALUE which is the Session instance. */
242
310
  static struct patron_curl_state* get_patron_curl_state(VALUE self) {
243
311
  struct patron_curl_state* state;
244
- Data_Get_Struct(self, struct patron_curl_state, state);
312
+ TypedData_Get_Struct(self, struct patron_curl_state, &patron_session_data_type, state);
245
313
  return state;
246
314
  }
247
315
 
@@ -293,14 +361,17 @@ static VALUE session_escape(VALUE self, VALUE value) {
293
361
  #endif
294
362
  escaped = curl_easy_escape(curl,
295
363
  RSTRING_PTR(string),
296
- (int) RSTRING_LEN(string));
297
-
298
- retval = rb_str_new2(escaped);
364
+ rb_long2int(RSTRING_LEN(string)));
299
365
  #if LIBCURL_VERSION_NUM >= 0x075200
300
366
  /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
301
367
  #else
302
368
  curl_easy_cleanup(curl);
303
369
  #endif
370
+ if (!escaped) {
371
+ rb_raise(ePatronError, "libcurl could not escape the string");
372
+ }
373
+
374
+ retval = rb_str_new2(escaped);
304
375
  curl_free(escaped);
305
376
 
306
377
  return retval;
@@ -315,6 +386,7 @@ static VALUE session_escape(VALUE self, VALUE value) {
315
386
  static VALUE session_unescape(VALUE self, VALUE value) {
316
387
  VALUE string = StringValue(value);
317
388
  char* unescaped = NULL;
389
+ int unescaped_len = 0;
318
390
  VALUE retval = Qnil;
319
391
 
320
392
  #if LIBCURL_VERSION_NUM >= 0x075200
@@ -323,18 +395,22 @@ static VALUE session_unescape(VALUE self, VALUE value) {
323
395
  #else
324
396
  CURL *curl = curl_easy_init();
325
397
  #endif
398
+ /* Ask for the length: the decoded bytes may contain NUL ("%00"), so strlen would cut it short */
326
399
  unescaped = curl_easy_unescape(curl,
327
400
  RSTRING_PTR(string),
328
- (int) RSTRING_LEN(string),
329
- NULL);
330
-
331
- retval = rb_str_new2(unescaped);
332
- curl_free(unescaped);
401
+ rb_long2int(RSTRING_LEN(string)),
402
+ &unescaped_len);
333
403
  #if LIBCURL_VERSION_NUM >= 0x075200
334
404
  /* this is libCURLv7.82.0 or later, the curl parameter is ignored */
335
405
  #else
336
406
  curl_easy_cleanup(curl);
337
407
  #endif
408
+ if (!unescaped) {
409
+ rb_raise(ePatronError, "libcurl could not unescape the string");
410
+ }
411
+
412
+ retval = rb_str_new(unescaped, unescaped_len);
413
+ curl_free(unescaped);
338
414
 
339
415
  return retval;
340
416
  }
@@ -375,8 +451,18 @@ static int formadd_values(VALUE data_key, VALUE data_value, VALUE self) {
375
451
  VALUE name = rb_obj_as_string(data_key);
376
452
  VALUE value = rb_obj_as_string(data_value);
377
453
 
378
- curl_formadd(&state->post, &state->last, CURLFORM_PTRNAME, RSTRING_PTR(name),
379
- 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);
380
466
 
381
467
  return 0;
382
468
  }
@@ -386,19 +472,24 @@ static int formadd_files(VALUE data_key, VALUE data_value, VALUE self) {
386
472
  VALUE name = rb_obj_as_string(data_key);
387
473
  VALUE value = rb_obj_as_string(data_value);
388
474
 
389
- curl_formadd(&state->post, &state->last, CURLFORM_PTRNAME, RSTRING_PTR(name),
390
- 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);
391
478
 
392
479
  return 0;
393
480
  }
394
481
 
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);
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));
398
489
  #ifdef CURLOPT_POSTFIELDSIZE_LARGE
399
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len);
490
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t) RSTRING_LEN(state->upload_data));
400
491
  #else
401
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
492
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long) RSTRING_LEN(state->upload_data));
402
493
  #endif
403
494
  }
404
495
 
@@ -407,7 +498,7 @@ static void set_chunked_encoding(struct patron_curl_state *state) {
407
498
  }
408
499
 
409
500
  static FILE* open_file(VALUE filename, const char* perms) {
410
- FILE* handle = fopen(StringValuePtr(filename), perms);
501
+ FILE* handle = fopen(StringValueCStr(filename), perms);
411
502
  if (!handle) {
412
503
  rb_raise(rb_eArgError, "Unable to open specified file.");
413
504
  }
@@ -437,7 +528,6 @@ static long floating_rb_seconds_to_milliseconds(VALUE r_seconds) {
437
528
  }
438
529
 
439
530
  static void set_request_body(struct patron_curl_state* state, VALUE stringable_or_file) {
440
- CURL* curl = state->handle;
441
531
  if(rb_respond_to(stringable_or_file, rb_intern("to_path"))) {
442
532
  // Set up a file read callback (read the entire request body from a file).
443
533
  // Instead of using the Ruby file reads, use #to_path to obtain the
@@ -447,10 +537,7 @@ static void set_request_body(struct patron_curl_state* state, VALUE stringable_o
447
537
  set_request_body_file(state, r_path_str);
448
538
  } else {
449
539
  // 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);
540
+ set_curl_request_body(state, rb_funcall(stringable_or_file, rb_intern("to_s"), 0));
454
541
  }
455
542
  }
456
543
 
@@ -474,18 +561,28 @@ static void set_options_from_request(VALUE self, VALUE request) {
474
561
  VALUE insecure = Qnil;
475
562
  VALUE cacert = Qnil;
476
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;
477
568
  VALUE http_version = Qnil;
478
569
  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);
570
+ VALUE action_name = Qnil;
571
+ VALUE a_c_encoding = Qnil;
572
+ VALUE download_byte_limit = Qnil;
573
+ VALUE maybe_progress_proc = Qnil;
483
574
 
575
+ /* Assign before anything can raise, so that cleanup() frees this handle and not a stale one */
484
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
+
485
582
  curl_easy_setopt(curl, CURLOPT_SHARE, state->share);
486
583
 
487
584
  if (RTEST(download_byte_limit)) {
488
- state->download_byte_limit = FIX2INT(download_byte_limit);
585
+ state->download_byte_limit = (curl_off_t) NUM2LL(download_byte_limit);
489
586
  } else {
490
587
  state->download_byte_limit = 0;
491
588
  }
@@ -571,11 +668,8 @@ static void set_options_from_request(VALUE self, VALUE request) {
571
668
  } else if (action == rb_intern("delete")) {
572
669
  VALUE data = rb_funcall(request, rb_intern("upload_data"), 0);
573
670
  if (RTEST(data)) {
574
- long len = RSTRING_LEN(data);
575
- state->upload_buf = StringValuePtr(data);
576
671
  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);
672
+ set_request_body(state, data);
579
673
  }
580
674
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
581
675
 
@@ -621,22 +715,22 @@ static void set_options_from_request(VALUE self, VALUE request) {
621
715
 
622
716
  timeout = rb_funcall(request, rb_intern("dns_cache_timeout"), 0);
623
717
  if (RTEST(timeout)) {
624
- curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, FIX2INT(timeout));
718
+ curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, NUM2LONG(timeout));
625
719
  }
626
720
 
627
721
  VALUE low_speed_time = rb_funcall(request, rb_intern("low_speed_time"), 0);
628
722
  if(RTEST(low_speed_time)) {
629
- 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));
630
724
  }
631
725
 
632
726
  VALUE low_speed_limit_bytes_per_second = rb_funcall(request, rb_intern("low_speed_limit"), 0);
633
727
  if(RTEST(low_speed_limit_bytes_per_second)) {
634
- 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));
635
729
  }
636
730
 
637
731
  redirects = rb_funcall(request, rb_intern("max_redirects"), 0);
638
732
  if (RTEST(redirects)) {
639
- int r = FIX2INT(redirects);
733
+ long r = NUM2LONG(redirects);
640
734
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, r == 0 ? 0 : 1);
641
735
  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, r);
642
736
  }
@@ -736,9 +830,33 @@ static void set_options_from_request(VALUE self, VALUE request) {
736
830
  }
737
831
  }
738
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
+
739
856
  cacert = rb_funcall(request, rb_intern("cacert"), 0);
740
857
  if(RTEST(cacert)) {
741
- curl_easy_setopt(curl, CURLOPT_CAINFO, StringValuePtr(cacert));
858
+ cacert = FilePathValue(cacert);
859
+ curl_easy_setopt(curl, CURLOPT_CAINFO, StringValueCStr(cacert));
742
860
  }
743
861
 
744
862
  buffer_size = rb_funcall(request, rb_intern("buffer_size"), 0);
@@ -747,6 +865,28 @@ static void set_options_from_request(VALUE self, VALUE request) {
747
865
  }
748
866
  }
749
867
 
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));
873
+ }
874
+ }
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
+
750
890
  /* Use the info in a Curl handle to create a new Response object. */
751
891
  static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE body_buffer) {
752
892
  VALUE args[6] = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil };
@@ -754,6 +894,7 @@ static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE
754
894
  long code = 0;
755
895
  long count = 0;
756
896
  VALUE responseKlass = Qnil;
897
+ VALUE response = Qnil;
757
898
 
758
899
  curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url);
759
900
  args[0] = rb_str_new2(effective_url);
@@ -769,7 +910,10 @@ static VALUE create_response(VALUE self, CURL* curl, VALUE header_buffer, VALUE
769
910
  args[5] = rb_funcall(self, rb_intern("default_response_charset"), 0);
770
911
 
771
912
  responseKlass = rb_funcall(self, rb_intern("response_class"), 0);
772
- 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;
773
917
  }
774
918
 
775
919
  /* Raise an exception based on the Curl error code. */
@@ -784,6 +928,7 @@ static VALUE select_error(CURLcode code) {
784
928
  case CURLE_OPERATION_TIMEDOUT: error = eTimeoutError; break;
785
929
  case CURLE_TOO_MANY_REDIRECTS: error = eTooManyRedirects; break;
786
930
  case CURLE_ABORTED_BY_CALLBACK: error = eAborted; break;
931
+ case CURLE_SSL_CERTPROBLEM: error = eClientCertificateError; break;
787
932
  default: error = ePatronError;
788
933
  }
789
934
 
@@ -791,6 +936,18 @@ static VALUE select_error(CURLcode code) {
791
936
  }
792
937
 
793
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
+
794
951
  /* Uses as the unblocking function when the thread running Patron gets
795
952
  signaled. The important difference with session_interrupt is that we
796
953
  are not allowed to touch any Ruby structures while outside the GIL,
@@ -805,16 +962,30 @@ void session_ubf_abort(void* patron_state) {
805
962
  static VALUE perform_request(VALUE self) {
806
963
  struct patron_curl_state *state = get_patron_curl_state(self);
807
964
  CURL* curl = state->handle;
808
- CURLcode ret = 0;
965
+ struct perform_context context = {curl, 0};
809
966
 
810
967
  state->interrupt = 0; /* clear the interrupt flag */
968
+ state->pending_tag = 0;
969
+ state->pending_exception = Qnil;
970
+
971
+ rb_thread_call_without_gvl(perform_without_gvl, &context, session_ubf_abort, state);
972
+
973
+ /* The progress proc threw: resume that exit now that libcurl is done with the handle.
974
+ cleanup() still runs, since we are inside the rb_ensure of session_handle_request. */
975
+ if (state->pending_tag) {
976
+ int tag = state->pending_tag;
977
+ VALUE exception = state->pending_exception;
978
+ state->pending_tag = 0;
979
+ state->pending_exception = Qnil;
980
+ /* errinfo is only an Exception for a raise; a throw leaves an internal object there and
981
+ Thread#kill a Fixnum, both of which rb_obj_is_kind_of must not be asked about */
982
+ if (RB_TYPE_P(exception, T_OBJECT) && rb_obj_is_kind_of(exception, rb_eException)) {
983
+ rb_exc_raise(exception);
984
+ }
985
+ rb_jump_tag(tag);
986
+ }
811
987
 
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) {
988
+ if (CURLE_OK == context.code) {
818
989
  VALUE header_str = membuffer_to_rb_str(&state->header_buffer);
819
990
  VALUE body_str = Qnil;
820
991
  if (!state->download_file) { body_str = membuffer_to_rb_str(&state->body_buffer); }
@@ -823,16 +994,27 @@ static VALUE perform_request(VALUE self) {
823
994
 
824
995
  return create_response(self, curl, header_str, body_str);
825
996
  } else {
826
- rb_raise(select_error(ret), "%s", state->error_buf);
997
+ rb_raise(select_error(context.code), "%s", state->error_buf);
827
998
  }
828
999
  }
829
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
+
830
1011
  /* Cleanup after each request by resetting the Curl handle and deallocating
831
1012
  * all request related objects such as the header slist.
832
1013
  */
833
1014
  static VALUE cleanup(VALUE self) {
834
1015
  struct patron_curl_state *state = get_patron_curl_state(self);
835
1016
  curl_easy_cleanup(state->handle);
1017
+ state->handle = NULL;
836
1018
 
837
1019
  if (state->headers) {
838
1020
  curl_slist_free_all(state->headers);
@@ -858,7 +1040,10 @@ static VALUE cleanup(VALUE self) {
858
1040
  state->last = NULL;
859
1041
  }
860
1042
 
861
- state->upload_buf = NULL;
1043
+ state->upload_data = Qnil;
1044
+ state->pending_tag = 0;
1045
+ state->pending_exception = Qnil;
1046
+ state->busy = 0;
862
1047
 
863
1048
  return Qnil;
864
1049
  }
@@ -877,8 +1062,16 @@ static VALUE cleanup(VALUE self) {
877
1062
  * @return [Patron::Response] the result of calling `response_class` on the Session
878
1063
  */
879
1064
  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);
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);
882
1075
  }
883
1076
 
884
1077
  /* Interrupt any currently executing request. This will cause the current
@@ -908,8 +1101,8 @@ static VALUE add_cookie_file(VALUE self, VALUE file) {
908
1101
  char* file_path = NULL;
909
1102
 
910
1103
  // 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) {
1104
+ file_path = StringValueCStr(file);
1105
+ if (strlen(file_path) != 0) {
913
1106
  curl_easy_setopt(curl, CURLOPT_COOKIEJAR, file_path);
914
1107
  }
915
1108
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, file_path);
@@ -926,11 +1119,11 @@ static VALUE add_cookie_file(VALUE self, VALUE file) {
926
1119
  static VALUE set_debug_file(VALUE self, VALUE file) {
927
1120
  struct patron_curl_state *state = get_patron_curl_state(self);
928
1121
  CURL *curl = state->base_handle;
929
- char *file_path = RSTRING_PTR(file);
1122
+ char *file_path = StringValueCStr(file);
930
1123
 
931
1124
  session_close_debug_file(state);
932
1125
 
933
- if(file_path != NULL && strlen(file_path) != 0) {
1126
+ if(strlen(file_path) != 0) {
934
1127
  state->debug_file = open_file(file, "wb");
935
1128
  } else {
936
1129
  state->debug_file = stderr;
@@ -953,6 +1146,22 @@ void Init_session_ext() {
953
1146
 
954
1147
  mPatron = rb_define_module("Patron");
955
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
+
956
1165
  ePatronError = rb_const_get(mPatron, rb_intern("Error"));
957
1166
 
958
1167
  eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
@@ -965,6 +1174,7 @@ void Init_session_ext() {
965
1174
  eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
966
1175
  eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));
967
1176
  eAborted = rb_const_get(mPatron, rb_intern("Aborted"));
1177
+ eClientCertificateError = rb_const_get(mPatron, rb_intern("ClientCertificateError"));
968
1178
 
969
1179
  rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);
970
1180
  rb_define_module_function(mPatron, "libcurl_version_exact", libcurl_version_exact, 0);