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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +56 -3
- data/CHANGELOG.md +20 -0
- data/Gemfile +2 -1
- data/README.md +30 -1
- data/ext/patron/extconf.rb +4 -0
- data/ext/patron/session_ext.c +285 -75
- data/lib/patron/error.rb +4 -0
- data/lib/patron/request.rb +6 -4
- data/lib/patron/response.rb +5 -0
- data/lib/patron/session.rb +17 -0
- data/lib/patron/version.rb +1 -1
- data/patron.gemspec +2 -1
- data/script/generate_client_certs +65 -0
- data/spec/request_spec.rb +12 -0
- data/spec/session_spec.rb +178 -11
- data/spec/session_ssl_spec.rb +126 -8
- data/spec/spec_helper.rb +6 -0
- data/spec/support/certs/client.p12 +0 -0
- data/spec/support/certs/client_ca.pem +20 -0
- data/spec/support/certs/client_cert.pem +20 -0
- data/spec/support/certs/client_key.pem +30 -0
- data/spec/support/certs/rogue_client.p12 +0 -0
- data/spec/support/config.ru +7 -0
- data/spec/support/raw_request_capture.rb +93 -0
- data/spec/support/test_server.rb +6 -3
- data/spec/support/valgrind.supp +2 -0
- metadata +13 -4
data/ext/patron/session_ext.c
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
rb_funcall(state->user_progress_blk,
|
|
90
|
+
return rb_funcall(state->user_progress_blk,
|
|
81
91
|
rb_intern("call"), 4,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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, &
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
329
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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,
|
|
390
|
-
|
|
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
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
480
|
-
VALUE a_c_encoding =
|
|
481
|
-
VALUE download_byte_limit =
|
|
482
|
-
VALUE maybe_progress_proc =
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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->
|
|
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
|
-
|
|
881
|
-
|
|
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 =
|
|
912
|
-
if (
|
|
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 =
|
|
1122
|
+
char *file_path = StringValueCStr(file);
|
|
930
1123
|
|
|
931
1124
|
session_close_debug_file(state);
|
|
932
1125
|
|
|
933
|
-
if(
|
|
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);
|