streamly_ffi 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/ext/streamly.c ADDED
@@ -0,0 +1,427 @@
1
+ // static size_t upload_data_handler(char * stream, size_t size, size_t nmemb, VALUE upload_stream) {
2
+ // size_t result = 0;
3
+ //
4
+ // // TODO
5
+ // // Change upload_stream back to a VALUE
6
+ // // if TYPE(upload_stream) == T_STRING - read at most "len" continuously
7
+ // // if upload_stream is IO-like, read chunks of it
8
+ // // OR
9
+ // // if upload_stream responds to "each", use that?
10
+ //
11
+ // TRAP_BEG;
12
+ // // if (upload_stream != NULL && *upload_stream != NULL) {
13
+ // // int len = size * nmemb;
14
+ // // char *s1 = strncpy(stream, *upload_stream, len);
15
+ // // result = strlen(s1);
16
+ // // *upload_stream += result;
17
+ // // }
18
+ // TRAP_END;
19
+ //
20
+ // return result;
21
+ // }
22
+
23
+ #include "streamly.h"
24
+ #ifdef HAVE_RUBY_ENCODING_H
25
+ #include <ruby/encoding.h>
26
+ static rb_encoding *utf8Encoding;
27
+ #endif
28
+
29
+ static size_t header_handler(char * stream, size_t size, size_t nmemb, VALUE handler) {
30
+ size_t str_len = size * nmemb;
31
+
32
+ if(TYPE(handler) == T_STRING) {
33
+ #ifdef HAVE_RUBY_ENCODING_H
34
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
35
+ if (default_internal_enc) {
36
+ handler = rb_str_export_to_enc(handler, default_internal_enc);
37
+ } else {
38
+ handler = rb_str_export_to_enc(handler, utf8Encoding);
39
+ }
40
+ #endif
41
+ rb_str_buf_cat(handler, stream, str_len);
42
+ } else {
43
+ VALUE chunk = rb_str_new(stream, str_len);
44
+ #ifdef HAVE_RUBY_ENCODING_H
45
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
46
+ if (default_internal_enc) {
47
+ chunk = rb_str_export_to_enc(chunk, default_internal_enc);
48
+ } else {
49
+ chunk = rb_str_export_to_enc(chunk, utf8Encoding);
50
+ }
51
+ #endif
52
+ rb_funcall(handler, rb_intern("call"), 1, chunk);
53
+ }
54
+ return str_len;
55
+ }
56
+
57
+ static size_t data_handler(char * stream, size_t size, size_t nmemb, VALUE handler) {
58
+ size_t str_len = size * nmemb;
59
+
60
+ if(TYPE(handler) == T_STRING) {
61
+ #ifdef HAVE_RUBY_ENCODING_H
62
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
63
+ if (default_internal_enc) {
64
+ handler = rb_str_export_to_enc(handler, default_internal_enc);
65
+ } else {
66
+ handler = rb_str_export_to_enc(handler, utf8Encoding);
67
+ }
68
+ #endif
69
+ rb_str_buf_cat(handler, stream, str_len);
70
+ } else {
71
+ VALUE chunk = rb_str_new(stream, str_len);
72
+ #ifdef HAVE_RUBY_ENCODING_H
73
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
74
+ if (default_internal_enc) {
75
+ chunk = rb_str_export_to_enc(chunk, default_internal_enc);
76
+ } else {
77
+ chunk = rb_str_export_to_enc(chunk, utf8Encoding);
78
+ }
79
+ #endif
80
+ rb_funcall(handler, rb_intern("call"), 1, chunk);
81
+ }
82
+ return str_len;
83
+ }
84
+
85
+ static void streamly_instance_mark(struct curl_instance * instance) {
86
+ rb_gc_mark(instance->request_method);
87
+ rb_gc_mark(instance->request_payload_handler);
88
+ rb_gc_mark(instance->response_header_handler);
89
+ rb_gc_mark(instance->response_body_handler);
90
+ rb_gc_mark(instance->options);
91
+ }
92
+
93
+ static void streamly_instance_free(struct curl_instance * instance) {
94
+ curl_easy_cleanup(instance->handle);
95
+ free(instance);
96
+ }
97
+
98
+ // Initially borrowed from Patron - http://github.com/toland/patron
99
+ // slightly modified for Streamly
100
+ static VALUE each_http_header(VALUE header, VALUE self) {
101
+ struct curl_instance * instance;
102
+ GetInstance(self, instance);
103
+ size_t key_len, val_len, header_str_len;
104
+ VALUE key, val;
105
+
106
+ key = rb_ary_entry(header, 0);
107
+ key_len = RSTRING_LEN(key);
108
+
109
+ val = rb_ary_entry(header, 1);
110
+ val_len = RSTRING_LEN(val);
111
+
112
+ header_str_len = (key_len + val_len + 3);
113
+ unsigned char header_str[header_str_len];
114
+
115
+ memcpy(header_str, RSTRING_PTR(key), key_len);
116
+ memcpy(header_str+2, ": ", 2);
117
+ memcpy(header_str+val_len, RSTRING_PTR(val), val_len);
118
+
119
+ header_str[header_str_len+1] = '\0';
120
+ instance->request_headers = curl_slist_append(instance->request_headers, (char *)header_str);
121
+ return Qnil;
122
+ }
123
+
124
+ // Initially borrowed from Patron - http://github.com/toland/patron
125
+ // slightly modified for Streamly
126
+ static VALUE select_error(CURLcode code) {
127
+ VALUE error = Qnil;
128
+
129
+ switch (code) {
130
+ case CURLE_UNSUPPORTED_PROTOCOL:
131
+ error = eUnsupportedProtocol;
132
+ break;
133
+ case CURLE_URL_MALFORMAT:
134
+ error = eURLFormatError;
135
+ break;
136
+ case CURLE_COULDNT_RESOLVE_HOST:
137
+ error = eHostResolutionError;
138
+ break;
139
+ case CURLE_COULDNT_CONNECT:
140
+ error = eConnectionFailed;
141
+ break;
142
+ case CURLE_PARTIAL_FILE:
143
+ error = ePartialFileError;
144
+ break;
145
+ case CURLE_OPERATION_TIMEDOUT:
146
+ error = eTimeoutError;
147
+ break;
148
+ case CURLE_TOO_MANY_REDIRECTS:
149
+ error = eTooManyRedirects;
150
+ break;
151
+ default:
152
+ error = eStreamlyError;
153
+ }
154
+
155
+ return error;
156
+ }
157
+
158
+ /*
159
+ * Document-class: Streamly::Request
160
+ *
161
+ * A streaming REST client for Ruby that uses libcurl to do the heavy lifting.
162
+ * The API is almost exactly like rest-client, so users of that library should find it very familiar.
163
+ */
164
+ /*
165
+ * Document-method: new
166
+ *
167
+ * call-seq: new(args)
168
+ *
169
+ * +args+ should be a Hash and is required
170
+ * This Hash should at least contain +:url+ and +:method+ keys.
171
+ * You may also provide the following optional keys:
172
+ * +:headers+ - should be a Hash of name/value pairs
173
+ * +:response_header_handler+ - can be a string or object that responds to #call
174
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
175
+ * +:response_body_handler+ - can be a string or object that responds to #call
176
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
177
+ * +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
178
+ *
179
+ */
180
+ static VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass) {
181
+ struct curl_instance * instance;
182
+ VALUE obj = Data_Make_Struct(klass, struct curl_instance, streamly_instance_mark, streamly_instance_free, instance);
183
+ rb_obj_call_init(obj, argc, argv);
184
+ return obj;
185
+ }
186
+
187
+ /*
188
+ * Document-method: initialize
189
+ *
190
+ * call-seq: initialize(args)
191
+ *
192
+ * +args+ should be a Hash and is required
193
+ * This Hash should at least contain +:url+ and +:method+ keys.
194
+ * You may also provide the following optional keys:
195
+ * +:headers+ - should be a Hash of name/value pairs
196
+ * +:response_header_handler+ - can be a string or object that responds to #call
197
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
198
+ * +:response_body_handler+ - can be a string or object that responds to #call
199
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
200
+ * +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
201
+ *
202
+ */
203
+ static VALUE rb_streamly_init(int argc, VALUE * argv, VALUE self) {
204
+ struct curl_instance * instance;
205
+ VALUE args, url, payload, headers, username, password, credentials;
206
+
207
+ GetInstance(self, instance);
208
+ instance->handle = curl_easy_init();
209
+ instance->request_headers = NULL;
210
+ instance->request_method = Qnil;
211
+ instance->request_payload_handler = Qnil;
212
+ instance->response_header_handler = Qnil;
213
+ instance->response_body_handler = Qnil;
214
+ instance->options = Qnil;
215
+
216
+ rb_scan_args(argc, argv, "10", &args);
217
+
218
+ // Ensure our args parameter is a hash
219
+ Check_Type(args, T_HASH);
220
+
221
+ instance->request_method = rb_hash_aref(args, sym_method);
222
+ url = rb_hash_aref(args, sym_url);
223
+ payload = rb_hash_aref(args, sym_payload);
224
+ headers = rb_hash_aref(args, sym_headers);
225
+ username = rb_hash_aref(args, sym_username);
226
+ password = rb_hash_aref(args, sym_password);
227
+ instance->response_header_handler = rb_hash_aref(args, sym_response_header_handler);
228
+ instance->response_body_handler = rb_hash_aref(args, sym_response_body_handler);
229
+
230
+ // First lets verify we have a :method key
231
+ if (NIL_P(instance->request_method)) {
232
+ rb_raise(eStreamlyError, "You must specify a :method");
233
+ } else {
234
+ // OK, a :method was specified, but if it's POST or PUT we require a :payload
235
+ if (instance->request_method == sym_post || instance->request_method == sym_put) {
236
+ if (NIL_P(payload)) {
237
+ rb_raise(eStreamlyError, "You must specify a :payload for POST and PUT requests");
238
+ }
239
+ }
240
+ }
241
+
242
+ // Now verify a :url was provided
243
+ if (NIL_P(url)) {
244
+ rb_raise(eStreamlyError, "You must specify a :url to request");
245
+ }
246
+
247
+ if (NIL_P(instance->response_header_handler)) {
248
+ instance->response_header_handler = rb_str_new2("");
249
+ #ifdef HAVE_RUBY_ENCODING_H
250
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
251
+ if (default_internal_enc) {
252
+ instance->response_header_handler = rb_str_export_to_enc(instance->response_header_handler, default_internal_enc);
253
+ } else {
254
+ instance->response_header_handler = rb_str_export_to_enc(instance->response_header_handler, utf8Encoding);
255
+ }
256
+ #endif
257
+ }
258
+ if (instance->request_method != sym_head && NIL_P(instance->response_body_handler)) {
259
+ instance->response_body_handler = rb_str_new2("");
260
+ #ifdef HAVE_RUBY_ENCODING_H
261
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
262
+ if (default_internal_enc) {
263
+ instance->response_body_handler = rb_str_export_to_enc(instance->response_body_handler, default_internal_enc);
264
+ } else {
265
+ instance->response_body_handler = rb_str_export_to_enc(instance->response_body_handler, utf8Encoding);
266
+ }
267
+ #endif
268
+ }
269
+
270
+ if (!NIL_P(headers)) {
271
+ Check_Type(headers, T_HASH);
272
+ rb_iterate(rb_each, headers, each_http_header, self);
273
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPHEADER, instance->request_headers);
274
+ }
275
+
276
+ // So far so good, lets start setting up our request
277
+
278
+ // Set the type of request
279
+ if (instance->request_method == sym_head) {
280
+ curl_easy_setopt(instance->handle, CURLOPT_NOBODY, 1);
281
+ } else if (instance->request_method == sym_get) {
282
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPGET, 1);
283
+ } else if (instance->request_method == sym_post) {
284
+ curl_easy_setopt(instance->handle, CURLOPT_POST, 1);
285
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
286
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
287
+
288
+ // (multipart)
289
+ // curl_easy_setopt(instance->handle, CURLOPT_HTTPPOST, 1);
290
+
291
+ // TODO: get streaming upload working
292
+ // curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &upload_data_handler);
293
+ // curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
294
+ // curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
295
+ } else if (instance->request_method == sym_put) {
296
+ curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "PUT");
297
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
298
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
299
+
300
+ // TODO: get streaming upload working
301
+ // curl_easy_setopt(instance->handle, CURLOPT_UPLOAD, 1);
302
+ // curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &upload_data_handler);
303
+ // curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
304
+ // curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
305
+ } else if (instance->request_method == sym_delete) {
306
+ curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "DELETE");
307
+ }
308
+
309
+ // Other common options
310
+ curl_easy_setopt(instance->handle, CURLOPT_URL, RSTRING_PTR(url));
311
+ curl_easy_setopt(instance->handle, CURLOPT_FOLLOWLOCATION, 1);
312
+ curl_easy_setopt(instance->handle, CURLOPT_MAXREDIRS, 3);
313
+
314
+ // Response header handling
315
+ curl_easy_setopt(instance->handle, CURLOPT_HEADERFUNCTION, &header_handler);
316
+ curl_easy_setopt(instance->handle, CURLOPT_HEADERDATA, instance->response_header_handler);
317
+
318
+ // Response body handling
319
+ if (instance->request_method != sym_head) {
320
+ curl_easy_setopt(instance->handle, CURLOPT_ENCODING, "identity, deflate, gzip");
321
+ curl_easy_setopt(instance->handle, CURLOPT_WRITEFUNCTION, &data_handler);
322
+ curl_easy_setopt(instance->handle, CURLOPT_WRITEDATA, instance->response_body_handler);
323
+ }
324
+
325
+ if (!NIL_P(username) || !NIL_P(password)) {
326
+ credentials = rb_str_new2("");
327
+ if (!NIL_P(username)) {
328
+ rb_str_buf_cat(credentials, RSTRING_PTR(username), RSTRING_LEN(username));
329
+ }
330
+ rb_str_buf_cat(credentials, ":", 1);
331
+ if (!NIL_P(password)) {
332
+ rb_str_buf_cat(credentials, RSTRING_PTR(password), RSTRING_LEN(password));
333
+ }
334
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST);
335
+ curl_easy_setopt(instance->handle, CURLOPT_USERPWD, RSTRING_PTR(credentials));
336
+ rb_gc_mark(credentials);
337
+ }
338
+
339
+ curl_easy_setopt(instance->handle, CURLOPT_SSL_VERIFYPEER, 0);
340
+ curl_easy_setopt(instance->handle, CURLOPT_SSL_VERIFYHOST, 0);
341
+
342
+ curl_easy_setopt(instance->handle, CURLOPT_ERRORBUFFER, instance->error_buffer);
343
+
344
+ return self;
345
+ }
346
+
347
+ static VALUE nogvl_perform(void *handle) {
348
+ CURLcode res;
349
+ VALUE status = Qnil;
350
+
351
+ res = curl_easy_perform(handle);
352
+ if (CURLE_OK != res) {
353
+ status = select_error(res);
354
+ }
355
+
356
+ return status;
357
+ }
358
+
359
+ /*
360
+ * Document-method: rb_streamly_execute
361
+ *
362
+ * call-seq: rb_streamly_execute
363
+ */
364
+ static VALUE rb_streamly_execute(RB_STREAMLY_UNUSED int argc, RB_STREAMLY_UNUSED VALUE * argv, VALUE self) {
365
+ VALUE status;
366
+ struct curl_instance * instance;
367
+ GetInstance(self, instance);
368
+
369
+ // Done setting up, lets do this!
370
+ status = rb_thread_blocking_region(nogvl_perform, instance->handle, RUBY_UBF_IO, 0);
371
+ if (!NIL_P(status)) {
372
+ rb_raise(status, "%s", instance->error_buffer);
373
+ }
374
+
375
+ // Cleanup
376
+ if (instance->request_headers != NULL) {
377
+ curl_slist_free_all(instance->request_headers);
378
+ instance->request_headers = NULL;
379
+ }
380
+ curl_easy_reset(instance->handle);
381
+ instance->request_payload_handler = Qnil;
382
+
383
+ if (instance->request_method == sym_head && TYPE(instance->response_header_handler) == T_STRING) {
384
+ return instance->response_header_handler;
385
+ } else if (TYPE(instance->response_body_handler) == T_STRING) {
386
+ return instance->response_body_handler;
387
+ } else {
388
+ return Qnil;
389
+ }
390
+ }
391
+
392
+ // Ruby Extension initializer
393
+ void Init_streamly_ext() {
394
+ mStreamly = rb_define_module("Streamly");
395
+
396
+ cRequest = rb_define_class_under(mStreamly, "Request", rb_cObject);
397
+ rb_define_singleton_method(cRequest, "new", rb_streamly_new, -1);
398
+ rb_define_method(cRequest, "initialize", rb_streamly_init, -1);
399
+ rb_define_method(cRequest, "execute", rb_streamly_execute, -1);
400
+
401
+ eStreamlyError = rb_define_class_under(mStreamly, "Error", rb_eStandardError);
402
+ eUnsupportedProtocol = rb_define_class_under(mStreamly, "UnsupportedProtocol", rb_eStandardError);
403
+ eURLFormatError = rb_define_class_under(mStreamly, "URLFormatError", rb_eStandardError);
404
+ eHostResolutionError = rb_define_class_under(mStreamly, "HostResolutionError", rb_eStandardError);
405
+ eConnectionFailed = rb_define_class_under(mStreamly, "ConnectionFailed", rb_eStandardError);
406
+ ePartialFileError = rb_define_class_under(mStreamly, "PartialFileError", rb_eStandardError);
407
+ eTimeoutError = rb_define_class_under(mStreamly, "TimeoutError", rb_eStandardError);
408
+ eTooManyRedirects = rb_define_class_under(mStreamly, "TooManyRedirects", rb_eStandardError);
409
+
410
+ sym_method = ID2SYM(rb_intern("method"));
411
+ sym_url = ID2SYM(rb_intern("url"));
412
+ sym_payload = ID2SYM(rb_intern("payload"));
413
+ sym_headers = ID2SYM(rb_intern("headers"));
414
+ sym_head = ID2SYM(rb_intern("head"));
415
+ sym_get = ID2SYM(rb_intern("get"));
416
+ sym_post = ID2SYM(rb_intern("post"));
417
+ sym_put = ID2SYM(rb_intern("put"));
418
+ sym_delete = ID2SYM(rb_intern("delete"));
419
+ sym_username = ID2SYM(rb_intern("username"));
420
+ sym_password = ID2SYM(rb_intern("password"));
421
+ sym_response_header_handler = ID2SYM(rb_intern("response_header_handler"));
422
+ sym_response_body_handler = ID2SYM(rb_intern("response_body_handler"));
423
+
424
+ #ifdef HAVE_RUBY_ENCODING_H
425
+ utf8Encoding = rb_utf8_encoding();
426
+ #endif
427
+ }
data/ext/streamly.h ADDED
@@ -0,0 +1,66 @@
1
+ #include <curl/curl.h>
2
+ #include <ruby.h>
3
+
4
+ VALUE mStreamly, cRequest, eStreamlyError, eUnsupportedProtocol, eURLFormatError, eHostResolutionError;
5
+ VALUE eConnectionFailed, ePartialFileError, eTimeoutError, eTooManyRedirects;
6
+ VALUE sym_method, sym_url, sym_payload, sym_headers, sym_head, sym_get, sym_post, sym_put, sym_delete;
7
+ VALUE sym_response_header_handler, sym_response_body_handler, sym_username, sym_password;
8
+
9
+ #define GetInstance(obj, sval) (sval = (struct curl_instance*)DATA_PTR(obj));
10
+
11
+ struct curl_instance {
12
+ CURL* handle;
13
+ char error_buffer[CURL_ERROR_SIZE];
14
+ struct curl_slist* request_headers;
15
+ VALUE request_payload_handler;
16
+ VALUE response_header_handler;
17
+ VALUE response_body_handler;
18
+ VALUE request_method;
19
+ VALUE options;
20
+ };
21
+
22
+ // libcurl callbacks
23
+ static size_t header_handler(char * stream, size_t size, size_t nmemb, VALUE handler);
24
+ static size_t data_handler(char * stream, size_t size, size_t nmemb, VALUE handler);
25
+ // static size_t put_data_handler(char * stream, size_t size, size_t nmemb, VALUE upload_stream);
26
+ //
27
+ static VALUE select_error(CURLcode code);
28
+ static VALUE each_http_header(VALUE header, VALUE header_array);
29
+ static void streamly_instance_mark(struct curl_instance * instance);
30
+ static void streamly_instance_free(struct curl_instance * instance);
31
+
32
+ static VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass);
33
+
34
+ #if defined(__GNUC__) && (__GNUC__ >= 3)
35
+ #define RB_STREAMLY_UNUSED __attribute__ ((unused))
36
+ #else
37
+ #define RB_STREAMLY_UNUSED
38
+ #endif
39
+
40
+ /*
41
+ * partial emulation of the 1.9 rb_thread_blocking_region under 1.8,
42
+ * this is enough for dealing with blocking I/O functions in the
43
+ * presence of threads.
44
+ */
45
+ #ifndef HAVE_RB_THREAD_BLOCKING_REGION
46
+
47
+ #include <rubysig.h>
48
+ #define RUBY_UBF_IO ((rb_unblock_function_t *)-1)
49
+ typedef void rb_unblock_function_t(void *);
50
+ typedef VALUE rb_blocking_function_t(void *);
51
+ static VALUE
52
+ rb_thread_blocking_region(
53
+ rb_blocking_function_t *func, void *data1,
54
+ RB_STREAMLY_UNUSED rb_unblock_function_t *ubf,
55
+ RB_STREAMLY_UNUSED void *data2)
56
+ {
57
+ VALUE rv;
58
+
59
+ TRAP_BEG;
60
+ rv = func(data1);
61
+ TRAP_END;
62
+
63
+ return rv;
64
+ }
65
+
66
+ #endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */