Index: ext/curb.h =================================================================== --- ext/curb.h (revision 56) +++ ext/curb.h (working copy) @@ -14,15 +14,16 @@ #include "curb_easy.h" #include "curb_errors.h" #include "curb_postfield.h" +#include "curb_multi.h" #include "curb_macros.h" // These should be managed from the Rake 'release' task. -#define CURB_VERSION "0.1.4" -#define CURB_VER_NUM 140 +#define CURB_VERSION "0.2.1" +#define CURB_VER_NUM 200 #define CURB_VER_MAJ 0 -#define CURB_VER_MIN 1 -#define CURB_VER_MIC 4 +#define CURB_VER_MIN 2 +#define CURB_VER_MIC 0 #define CURB_VER_PATCH 0 Index: ext/curb_easy.h =================================================================== --- ext/curb_easy.h (revision 56) +++ ext/curb_easy.h (working copy) @@ -24,8 +24,8 @@ VALUE body_proc; VALUE header_proc; - VALUE body_data; /* These have the result from the last curl_easy_perform */ - VALUE header_data; /* unless a block is supplied (when they'll be nil) */ + VALUE body_data; /* Holds the response body from the last call to curl_easy_perform */ + VALUE header_data; /* unless a block is supplied (they'll be nil) */ VALUE progress_proc; VALUE debug_proc; VALUE interface; @@ -33,7 +33,9 @@ VALUE proxypwd; VALUE headers; /* ruby array of strings with headers to set */ VALUE cookiejar; /* filename */ - + VALUE success_proc; + VALUE failure_proc; + /* Other opts */ unsigned short local_port; // 0 is no port unsigned short local_port_range; // " " " " @@ -65,10 +67,24 @@ * and in case it's asked for before the next call. */ VALUE postdata_buffer; + + /* when added to a multi handle these buffers are needed + * when the easy handle isn't supplied the body proc + * or a custom http header is passed. + */ + VALUE bodybuf; + VALUE headerbuf; + struct curl_slist *curl_headers; + + VALUE self; /* pointer to self, used by multi interface */ + } ruby_curl_easy; extern VALUE cCurlEasy; +VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce, VALUE *bodybuf, VALUE *headerbuf, struct curl_slist **headers); +VALUE ruby_curl_easy_cleanup(VALUE self, ruby_curl_easy *rbce, VALUE bodybuf, VALUE headerbuf, struct curl_slist *headers); + void init_curb_easy(); #endif Index: ext/curb_multi.c =================================================================== --- ext/curb_multi.c (revision 0) +++ ext/curb_multi.c (revision 0) @@ -0,0 +1,211 @@ +/* curb_easy.c - Curl easy mode + * Copyright (c)2008 Todd A. Fisher. + * Licensed under the Ruby License. See LICENSE for details. + * + * $Id$ + */ +#include "curb_easy.h" +#include "curb_errors.h" +#include "curb_postfield.h" +#include "curb_multi.h" + +#include + +extern VALUE mCurl; + +#ifdef RDOC_NEVER_DEFINED + mCurl = rb_define_module("Curl"); +#endif + +VALUE cCurlMulti; + +static VALUE ruby_curl_multi_alloc(VALUE klass) { + return Data_Wrap_Struct(klass, NULL, curl_multi_cleanup, curl_multi_init()); +} + +/* + * call-seq: + * multi = Curl::Multi.new + * easy = Curl::Easy.new('url') + * + * multi.add(easy) + * + * Add an easy handle to the multi handle + */ +static VALUE ruby_curl_multi_add(VALUE self, VALUE easy) { + CURLMcode mcode; + CURLM *mptr; + ruby_curl_easy *rbce; + + Data_Get_Struct(self, CURLM, mptr); + Data_Get_Struct(easy, ruby_curl_easy, rbce); + + mcode = curl_multi_add_handle(mptr, rbce->curl); + if (mcode != 0) { + raise_curl_multi_error_exception(mcode); + } + + /* save a pointer to self */ + rbce->self = easy; + + /* setup the easy handle */ + ruby_curl_easy_setup( rbce, &(rbce->bodybuf), &(rbce->headerbuf), &(rbce->curl_headers) ); + + return self; +} + +/* + * call-seq: + * multi = Curl::Multi.new + * easy = Curl::Easy.new('url') + * + * multi.add(easy) + * + * # sometime later + * multi.remove(easy) + * + * Remove an easy handle from a multi handle + * + * Will raise an exception if the easy handle is not found + */ +static VALUE ruby_curl_multi_remove(VALUE self, VALUE easy) { + CURLMcode result; + CURLM *mptr; + ruby_curl_easy *rbce; + + Data_Get_Struct(self, CURLM, mptr); + Data_Get_Struct(easy, ruby_curl_easy, rbce); + + result = curl_multi_remove_handle(mptr, rbce->curl); + if (result != 0) { + raise_curl_multi_error_exception(result); + } + + return self; +} + +/* called within ruby_curl_multi_perform */ +static void rb_curl_multi_run(CURLM *multi_handle, int *still_running) { + int msgs_left; + CURLMsg *msg; + CURLcode ecode; + CURLMcode mcode; + CURL *easy_handle; + ruby_curl_easy *rbce = NULL; + + do { + mcode = curl_multi_perform(multi_handle, still_running); + } while (mcode == CURLM_CALL_MULTI_PERFORM); + + if (mcode != CURLM_OK) { + raise_curl_multi_error_exception(mcode); + } + + /* check for finished easy handles and remove from the multi handle */ + while ((msg = curl_multi_info_read(multi_handle, &msgs_left))) { + + if (msg->msg != CURLMSG_DONE) { + continue; + } + + easy_handle = msg->easy_handle; + if (easy_handle) { + ecode = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, &rbce); + if (ecode != 0) { + raise_curl_easy_error_exception(ecode); + } + ruby_curl_easy_cleanup( rbce->self, rbce, rbce->bodybuf, rbce->headerbuf, rbce->curl_headers ); + mcode = curl_multi_remove_handle(multi_handle, easy_handle); + if (mcode != CURLM_OK) { + raise_curl_multi_error_exception(mcode); + } + } + } +} + +/* + * call-seq: + * multi = Curl::Multi.new + * easy1 = Curl::Easy.new('url') + * easy2 = Curl::Easy.new('url') + * + * multi.add(easy1) + * multi.add(easy2) + * + * multi.perform do + * # while idle other code my execute here + * end + * + * Run multi handles, looping selecting when data can be transfered + */ +static VALUE ruby_curl_multi_perform(VALUE self) { + CURLMcode mcode; + CURLM *multi_handle; + int still_running, maxfd, rc; + fd_set fdread, fdwrite, fdexcep; + + long timeout; + struct timeval tv = {0, 0}; + + Data_Get_Struct(self, CURLM, multi_handle); + + rb_curl_multi_run( multi_handle, &still_running ); + + while(still_running) { + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + /* load the fd sets from the multi handle */ + mcode = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd); + if (mcode != CURLM_OK) { + raise_curl_multi_error_exception(mcode); + } + + /* get the curl suggested time out */ + mcode = curl_multi_timeout(multi_handle, &timeout); + if (mcode != CURLM_OK) { + raise_curl_multi_error_exception(mcode); + } + + if (timeout == 0) { /* no delay */ + rb_curl_multi_run( multi_handle, &still_running ); + continue; + } + else if (timeout == -1) { + timeout = 1; /* You must not wait too long + (more than a few seconds perhaps) before + you call curl_multi_perform() again */ + } + + if (rb_block_given_p()) { + rb_yield(self); + } + + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout * 1000) % 1000000; + + rc = select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv); + if (rc < 0) { + rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno)); + } + + rb_curl_multi_run( multi_handle, &still_running ); + + } + + return Qnil; +} + +/* =================== INIT LIB =====================*/ +void init_curb_multi() { + cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject); + + /* Class methods */ + rb_define_alloc_func( cCurlMulti, ruby_curl_multi_alloc ); + + /* Instnace methods */ + rb_define_method(cCurlMulti, "add", ruby_curl_multi_add, 1); + rb_define_method(cCurlMulti, "remove", ruby_curl_multi_remove, 1); + rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, 0); +} Index: ext/curb_errors.c =================================================================== --- ext/curb_errors.c (revision 56) +++ ext/curb_errors.c (working copy) @@ -98,9 +98,19 @@ VALUE eCurlErrTFTPFileExists; VALUE eCurlErrTFTPNoSuchUser; +/* multi errors */ +VALUE mCurlErrCallMultiPerform; +VALUE mCurlErrBadHandle; +VALUE mCurlErrBadEasyHandle; +VALUE mCurlErrOutOfMemory; +VALUE mCurlErrInternalError; +VALUE mCurlErrBadSocket; +VALUE mCurlErrUnknownOption; + /* binding errors */ VALUE eCurlErrInvalidPostField; + /* rb_raise an approriate exception for the supplied CURLcode */ void raise_curl_easy_error_exception(CURLcode code) { VALUE exclz; @@ -356,6 +366,43 @@ rb_raise(exclz, exmsg); } +void raise_curl_multi_error_exception(CURLMcode code) { + VALUE exclz; + const char *exmsg = NULL; + + switch(code) { + case CURLM_CALL_MULTI_PERFORM: /* -1 */ + exclz = mCurlErrCallMultiPerform; + break; + case CURLM_BAD_HANDLE: /* 1 */ + exclz = mCurlErrBadHandle; + break; + case CURLM_BAD_EASY_HANDLE: /* 2 */ + exclz = mCurlErrBadEasyHandle; + break; + case CURLM_OUT_OF_MEMORY: /* 3 */ + exclz = mCurlErrOutOfMemory; + break; + case CURLM_INTERNAL_ERROR: /* 4 */ + exclz = mCurlErrInternalError; + break; + case CURLM_BAD_SOCKET: /* 5 */ + exclz = mCurlErrBadSocket; + break; + case CURLM_UNKNOWN_OPTION: /* 6 */ + exclz = mCurlErrUnknownOption; + break; + default: + exclz = eCurlErrError; + exmsg = "Unknown error result from libcurl"; + } + + if (!exmsg) { + exmsg = curl_multi_strerror(code); + } + + rb_raise(exclz, exmsg); +} void init_curb_errors() { mCurlErr = rb_define_module_under(mCurl, "Err"); Index: ext/curb_multi.h =================================================================== --- ext/curb_multi.h (revision 0) +++ ext/curb_multi.h (revision 0) @@ -0,0 +1,18 @@ +/* curb_multi.h - Curl easy mode + * Copyright (c)2008 Todd A. Fisher. + * Licensed under the Ruby License. See LICENSE for details. + * + * $Id$ + */ +#ifndef __CURB_MULTI_H +#define __CURB_MULTI_H + +#include "curb.h" +#include "curb_easy.h" +#include + +extern VALUE cCurlMulti; +void init_curb_multi(); + + +#endif Index: ext/curb.c =================================================================== --- ext/curb.c (revision 56) +++ ext/curb.c (working copy) @@ -333,4 +333,5 @@ init_curb_errors(); init_curb_easy(); init_curb_postfield(); + init_curb_multi(); } Index: ext/curb_easy.c =================================================================== --- ext/curb_easy.c (revision 56) +++ ext/curb_easy.c (working copy) @@ -92,11 +92,18 @@ rb_gc_mark(rbce->proxypwd); rb_gc_mark(rbce->headers); rb_gc_mark(rbce->cookiejar); + rb_gc_mark(rbce->success_proc); + rb_gc_mark(rbce->failure_proc); rb_gc_mark(rbce->postdata_buffer); + rb_gc_mark(rbce->bodybuf); + rb_gc_mark(rbce->headerbuf); } void curl_easy_free(ruby_curl_easy *rbce) { + if (rbce->curl_headers) { + curl_slist_free_all(rbce->curl_headers); + } curl_easy_cleanup(rbce->curl); free(rbce); } @@ -115,9 +122,10 @@ * the instance is returned. */ static VALUE ruby_curl_easy_new(int argc, VALUE *argv, VALUE klass) { + CURLcode ecode; VALUE url, blk; VALUE new_curl; - + rb_scan_args(argc, argv, "01&", &url, &blk); ruby_curl_easy *rbce = ALLOC(ruby_curl_easy); @@ -139,6 +147,8 @@ rbce->proxypwd = Qnil; rbce->headers = rb_hash_new(); rbce->cookiejar = Qnil; + rbce->success_proc = Qnil; + rbce->failure_proc = Qnil; /* various-typed opts */ rbce->local_port = 0; @@ -168,13 +178,22 @@ /* buffers */ rbce->postdata_buffer = Qnil; + rbce->bodybuf = Qnil; + rbce->headerbuf = Qnil; + rbce->curl_headers = NULL; new_curl = Data_Wrap_Struct(cCurlEasy, curl_easy_mark, curl_easy_free, rbce); if (blk != Qnil) { rb_funcall(blk, idCall, 1, new_curl); } - + + /* set the rbce pointer to the curl handle */ + ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce); + if (ecode != CURLE_OK) { + raise_curl_easy_error_exception(ecode); + } + return new_curl; } @@ -194,6 +213,7 @@ newrbce = ALLOC(ruby_curl_easy); memcpy(newrbce, rbce, sizeof(ruby_curl_easy)); newrbce->curl = curl_easy_duphandle(rbce->curl); + newrbce->curl_headers = NULL; return Data_Wrap_Struct(cCurlEasy, curl_easy_mark, curl_easy_free, newrbce); } @@ -955,6 +975,36 @@ /* * call-seq: + * easy.on_success { ... } => <old handler> + * + * Assign or remove the +on_success+ handler for this Curl::Easy instance. + * To remove a previously-supplied handler, call this method with no + * attached block. + * + * The +on_success+ handler is called whe the request is finished with a + * status of 20x + */ +static VALUE ruby_curl_easy_on_success_set(int argc, VALUE *argv, VALUE self) { + CURB_HANDLER_PROC_SETTER(ruby_curl_easy, success_proc); +} + +/* + * call-seq: + * easy.on_failure { ... } => <old handler> + * + * Assign or remove the +on_failure+ handler for this Curl::Easy instance. + * To remove a previously-supplied handler, call this method with no + * attached block. + * + * The +on_failure+ handler is called whe the request is finished with a + * status of 20x + */ +static VALUE ruby_curl_easy_on_failure_set(int argc, VALUE *argv, VALUE self) { + CURB_HANDLER_PROC_SETTER(ruby_curl_easy, failure_proc); +} + +/* + * call-seq: * easy.on_header { |header_data| ... } => <old handler> * * Assign or remove the +on_header+ handler for this Curl::Easy instance. @@ -1044,234 +1094,280 @@ } /*********************************************** - * - * This is the main worker for the perform methods (get, post, head, put). - * It's not surfaced as a Ruby method - instead, the individual request - * methods are responsible for setting up stuff specific to that type, - * then calling this to handle common stuff and do the perform. - * + * + * Setup a connection + * * Always returns Qtrue, rb_raise on error. - * */ -static VALUE handle_perform(ruby_curl_easy *rbce) { +VALUE ruby_curl_easy_setup( ruby_curl_easy *rbce, VALUE *body_buffer, VALUE *header_buffer, struct curl_slist **hdrs ) { // TODO this could do with a bit of refactoring... CURL *curl; - CURLcode result = -1; - struct curl_slist *headers = NULL; curl = rbce->curl; - if (rbce->url != Qnil) { - VALUE url = rb_check_string_type(rbce->url); - VALUE bodybuf, headerbuf; + if (rbce->url == Qnil) { + rb_raise(eCurlErrError, "No URL supplied"); + } + + VALUE url = rb_check_string_type(rbce->url); + + // Need to configure the handler as per settings in rbce + curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url)); - // Need to configure the handler as per settings in rbce - curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url)); - - // network stuff and auth - if (rbce->interface != Qnil) { - curl_easy_setopt(curl, CURLOPT_INTERFACE, StringValuePtr(rbce->interface)); - } else { - curl_easy_setopt(curl, CURLOPT_INTERFACE, NULL); - } - - if (rbce->userpwd != Qnil) { - curl_easy_setopt(curl, CURLOPT_USERPWD, StringValuePtr(rbce->userpwd)); - } else { - curl_easy_setopt(curl, CURLOPT_USERPWD, NULL); - } - - if (rbce->proxy_url != Qnil) { - curl_easy_setopt(curl, CURLOPT_PROXY, StringValuePtr(rbce->proxy_url)); - } else { - curl_easy_setopt(curl, CURLOPT_PROXY, NULL); - } - - if (rbce->proxypwd != Qnil) { - curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, StringValuePtr(rbce->proxypwd)); - } else { - curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, NULL); - } - - // body/header procs - if (rbce->body_proc != Qnil) { - bodybuf = Qnil; - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &proc_data_handler); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce->body_proc); - } else { - bodybuf = rb_str_buf_new(32768); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &default_data_handler); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, bodybuf); - } - - if (rbce->header_proc != Qnil) { - headerbuf = Qnil; - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &proc_data_handler); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce->header_proc); - } else { - headerbuf = rb_str_buf_new(32768); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &default_data_handler); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, headerbuf); - } + // network stuff and auth + if (rbce->interface != Qnil) { + curl_easy_setopt(curl, CURLOPT_INTERFACE, StringValuePtr(rbce->interface)); + } else { + curl_easy_setopt(curl, CURLOPT_INTERFACE, NULL); + } + + if (rbce->userpwd != Qnil) { + curl_easy_setopt(curl, CURLOPT_USERPWD, StringValuePtr(rbce->userpwd)); + } else { + curl_easy_setopt(curl, CURLOPT_USERPWD, NULL); + } + + if (rbce->proxy_url != Qnil) { + curl_easy_setopt(curl, CURLOPT_PROXY, StringValuePtr(rbce->proxy_url)); + } else { + curl_easy_setopt(curl, CURLOPT_PROXY, NULL); + } + + if (rbce->proxypwd != Qnil) { + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, StringValuePtr(rbce->proxypwd)); + } else { + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, NULL); + } + + // body/header procs + if (rbce->body_proc != Qnil) { + *body_buffer = Qnil; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &proc_data_handler); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce->body_proc); + } else { + *body_buffer = rb_str_buf_new(32768); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &default_data_handler); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, *body_buffer); + } + + if (rbce->header_proc != Qnil) { + *header_buffer = Qnil; + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &proc_data_handler); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce->header_proc); + } else { + *header_buffer = rb_str_buf_new(32768); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &default_data_handler); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, *header_buffer); + } - // progress and debug procs - if (rbce->progress_proc != Qnil) { - curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, &proc_progress_handler); - curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rbce->progress_proc); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); - } else { - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); - } - - if (rbce->debug_proc != Qnil) { - curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, &proc_debug_handler); - curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rbce->debug_proc); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); - } else { - // have to remove handler to re-enable standard verbosity - curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, NULL); - curl_easy_setopt(curl, CURLOPT_DEBUGDATA, NULL); - curl_easy_setopt(curl, CURLOPT_VERBOSE, rbce->verbose); - } - - /* general opts */ - - curl_easy_setopt(curl, CURLOPT_HEADER, rbce->header_in_body); + // progress and debug procs + if (rbce->progress_proc != Qnil) { + curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, &proc_progress_handler); + curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rbce->progress_proc); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); + } else { + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); + } + + if (rbce->debug_proc != Qnil) { + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, &proc_debug_handler); + curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rbce->debug_proc); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + } else { + // have to remove handler to re-enable standard verbosity + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, NULL); + curl_easy_setopt(curl, CURLOPT_DEBUGDATA, NULL); + curl_easy_setopt(curl, CURLOPT_VERBOSE, rbce->verbose); + } + + /* general opts */ + + curl_easy_setopt(curl, CURLOPT_HEADER, rbce->header_in_body); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, rbce->follow_location); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, rbce->follow_location); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs); - curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, rbce->proxy_tunnel); - curl_easy_setopt(curl, CURLOPT_FILETIME, rbce->fetch_file_time); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, rbce->ssl_verify_peer); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, rbce->ssl_verify_peer); - - if ((rbce->use_netrc != Qnil) && (rbce->use_netrc != Qfalse)) { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, CURL_NETRC_OPTIONAL); - } else { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, CURL_NETRC_IGNORED); - } + curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, rbce->proxy_tunnel); + curl_easy_setopt(curl, CURLOPT_FILETIME, rbce->fetch_file_time); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, rbce->ssl_verify_peer); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, rbce->ssl_verify_peer); + + if ((rbce->use_netrc != Qnil) && (rbce->use_netrc != Qfalse)) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, CURL_NETRC_OPTIONAL); + } else { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, CURL_NETRC_IGNORED); + } - curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, rbce->unrestricted_auth); - - curl_easy_setopt(curl, CURLOPT_TIMEOUT, rbce->timeout); - curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, rbce->connect_timeout); - curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, rbce->dns_cache_timeout); + curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, rbce->unrestricted_auth); + + curl_easy_setopt(curl, CURLOPT_TIMEOUT, rbce->timeout); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, rbce->connect_timeout); + curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, rbce->dns_cache_timeout); #if LIBCURL_VERSION_NUM >= 0x070a08 - curl_easy_setopt(curl, CURLOPT_FTP_RESPONSE_TIMEOUT, rbce->ftp_response_timeout); + curl_easy_setopt(curl, CURLOPT_FTP_RESPONSE_TIMEOUT, rbce->ftp_response_timeout); #else - if (rbce->ftp_response_timeout > 0) { - rb_warn("Installed libcurl is too old to support ftp_response_timeout"); - } + if (rbce->ftp_response_timeout > 0) { + rb_warn("Installed libcurl is too old to support ftp_response_timeout"); + } #endif + + // Set up localport / proxy port + // FIXME these won't get returned to default if they're unset Ruby + if (rbce->proxy_port > 0) { + curl_easy_setopt(curl, CURLOPT_PROXYPORT, rbce->proxy_port); + } + + if (rbce->local_port > 0) { +#if LIBCURL_VERSION_NUM >= 0x070f02 + curl_easy_setopt(curl, CURLOPT_LOCALPORT, rbce->local_port); - // Set up localport / proxy port - // FIXME these won't get returned to default if they're unset Ruby - if (rbce->proxy_port > 0) { - curl_easy_setopt(curl, CURLOPT_PROXYPORT, rbce->proxy_port); + if (rbce->local_port_range > 0) { + curl_easy_setopt(curl, CURLOPT_LOCALPORTRANGE, rbce->local_port_range); } - - if (rbce->local_port > 0) { -#if LIBCURL_VERSION_NUM >= 0x070f02 - curl_easy_setopt(curl, CURLOPT_LOCALPORT, rbce->local_port); - - if (rbce->local_port_range > 0) { - curl_easy_setopt(curl, CURLOPT_LOCALPORTRANGE, rbce->local_port_range); - } #else - rb_warn("Installed libcurl is too old to support local_port"); + rb_warn("Installed libcurl is too old to support local_port"); #endif - } - - if (rbce->proxy_type != -1) { + } + + if (rbce->proxy_type != -1) { #if LIBCURL_VERSION_NUM >= 0x070a00 - if (rbce->proxy_type == -2) { - rb_warn("Installed libcurl is too old to support the selected proxy type"); - } else { - curl_easy_setopt(curl, CURLOPT_PROXYTYPE, rbce->proxy_type); - } + if (rbce->proxy_type == -2) { + rb_warn("Installed libcurl is too old to support the selected proxy type"); } else { - curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, rbce->proxy_type); + } + } else { + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); #else - rb_warn("Installed libcurl is too old to support proxy_type"); + rb_warn("Installed libcurl is too old to support proxy_type"); #endif - } + } - if (rbce->http_auth_types > 0) { + if (rbce->http_auth_types > 0) { #if LIBCURL_VERSION_NUM >= 0x070a06 - curl_easy_setopt(curl, CURLOPT_HTTPAUTH, rbce->http_auth_types); - } else { - curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, rbce->http_auth_types); + } else { + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); #else - rb_warn("Installed libcurl is too old to support http_auth_types"); + rb_warn("Installed libcurl is too old to support http_auth_types"); #endif - } + } - if (rbce->proxy_auth_types > 0) { + if (rbce->proxy_auth_types > 0) { #if LIBCURL_VERSION_NUM >= 0x070a07 - curl_easy_setopt(curl, CURLOPT_PROXYAUTH, rbce->proxy_auth_types); - } else { - curl_easy_setopt(curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl, CURLOPT_PROXYAUTH, rbce->proxy_auth_types); + } else { + curl_easy_setopt(curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY); #else - rb_warn("Installed libcurl is too old to support proxy_auth_types"); + rb_warn("Installed libcurl is too old to support proxy_auth_types"); #endif + } + + // Set up HTTP cookie handling if necessary + // FIXME this may not get disabled if it's enabled, the disabled again from ruby. + if (rbce->enable_cookies) { + if (rbce->cookiejar != Qnil) { + curl_easy_setopt(curl, CURLOPT_COOKIEJAR, StringValuePtr(rbce->cookiejar)); + } else { + curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* "" = magic to just enable */ } - - // Set up HTTP cookie handling if necessary - // FIXME this may not get disabled if it's enabled, the disabled again from ruby. - if (rbce->enable_cookies) { - if (rbce->cookiejar != Qnil) { - curl_easy_setopt(curl, CURLOPT_COOKIEJAR, StringValuePtr(rbce->cookiejar)); - } else { - curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* "" = magic to just enable */ - } - } - - // Setup HTTP headers if necessary - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL); // clear - - if (rbce->headers != Qnil) { - if ((rb_type(rbce->headers) == T_ARRAY) || (rb_type(rbce->headers) == T_HASH)) { - rb_iterate(rb_each, rbce->headers, cb_each_http_header, (VALUE)&headers); - } else { - VALUE headers_str = rb_obj_as_string(rbce->headers); - headers = curl_slist_append(headers, StringValuePtr(headers_str)); - } - - if (headers) { - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - } - } - - // Okay, do it. - result = curl_easy_perform(curl); - - // Free everything up - if (headers) { - curl_slist_free_all(headers); + } + + // Setup HTTP headers if necessary + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL); // clear + + if (rbce->headers != Qnil) { + if ((rb_type(rbce->headers) == T_ARRAY) || (rb_type(rbce->headers) == T_HASH)) { + rb_iterate(rb_each, rbce->headers, cb_each_http_header, (VALUE)hdrs); + } else { + VALUE headers_str = rb_obj_as_string(rbce->headers); + *hdrs = curl_slist_append(*hdrs, StringValuePtr(headers_str)); } - // Sort out the built-in body/header data. - if (bodybuf != Qnil) { - rbce->body_data = rb_str_to_str(bodybuf); - } else { - rbce->body_data = Qnil; + if (*hdrs) { + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *hdrs); } + } - if (headerbuf != Qnil) { - rbce->header_data = rb_str_to_str(headerbuf); - } else { - rbce->header_data = Qnil; + return Qnil; +} +/*********************************************** + * + * Clean up a connection + * + * Always returns Qtrue. + */ +VALUE ruby_curl_easy_cleanup( VALUE self, ruby_curl_easy *rbce, VALUE bodybuf, VALUE headerbuf, struct curl_slist *headers ) { + + // Free everything up + if (headers) { + curl_slist_free_all(headers); + rbce->curl_headers = NULL; + } + + // Sort out the built-in body/header data. + if (bodybuf != Qnil) { + rbce->body_data = rb_str_to_str(bodybuf); + rbce->bodybuf = Qnil; + } else { + rbce->body_data = Qnil; + } + + if (headerbuf != Qnil) { + rbce->header_data = rb_str_to_str(headerbuf); + rbce->headerbuf = Qnil; + } else { + rbce->header_data = Qnil; + } + + /* check the request status and determine if on_success or on_failure should be called */ + if (rbce->success_proc != Qnil) { + long response_code = 0; + curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &response_code); + /* NOTE: we allow response_code == 0, in the case the file is being read from disk */ + if ((response_code >= 200 && response_code < 300) || response_code == 0) { + rb_funcall( rbce->success_proc, idCall, 1, self ); } + } - if (result != 0) { - raise_curl_easy_error_exception(result); + if (rbce->failure_proc != Qnil) { + long response_code = 0; + curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code >= 300 && response_code < 600) { + rb_funcall( rbce->failure_proc, idCall, 1, rbce ); } - } else { - rb_raise(eCurlErrError, "No URL supplied"); } - + + return Qnil; +} + +/*********************************************** + * + * This is the main worker for the perform methods (get, post, head, put). + * It's not surfaced as a Ruby method - instead, the individual request + * methods are responsible for setting up stuff specific to that type, + * then calling this to handle common stuff and do the perform. + * + * Always returns Qtrue, rb_raise on error. + * + */ +static VALUE handle_perform(VALUE self, ruby_curl_easy *rbce) { + + CURLcode result = -1; + struct curl_slist *headers = NULL; + VALUE bodybuf = Qnil, headerbuf = Qnil; + + ruby_curl_easy_setup(rbce, &bodybuf, &headerbuf, &headers); + + result = curl_easy_perform(rbce->curl); + + ruby_curl_easy_cleanup(self, rbce, bodybuf, headerbuf, headers); + + if (result != 0) { + raise_curl_easy_error_exception(result); + } + return Qtrue; } @@ -1292,7 +1388,7 @@ curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); - return handle_perform(rbce); + return handle_perform(self,rbce); } /* @@ -1359,7 +1455,7 @@ curl_easy_setopt(curl, CURLOPT_POST, 0); curl_easy_setopt(curl, CURLOPT_HTTPPOST, first); - ret = handle_perform(rbce); + ret = handle_perform(self,rbce); curl_formfree(first); return ret; @@ -1378,7 +1474,7 @@ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len); - return handle_perform(rbce); + return handle_perform(self,rbce); } } } @@ -2192,7 +2288,9 @@ rb_define_method(cCurlEasy, "on_header", ruby_curl_easy_on_header_set, -1); rb_define_method(cCurlEasy, "on_progress", ruby_curl_easy_on_progress_set, -1); rb_define_method(cCurlEasy, "on_debug", ruby_curl_easy_on_debug_set, -1); - + rb_define_method(cCurlEasy, "on_success", ruby_curl_easy_on_success_set, -1); + rb_define_method(cCurlEasy, "on_failure", ruby_curl_easy_on_failure_set, -1); + rb_define_method(cCurlEasy, "perform", ruby_curl_easy_perform, 0); rb_define_method(cCurlEasy, "http_get", ruby_curl_easy_perform_get, 0); rb_define_method(cCurlEasy, "http_post", ruby_curl_easy_perform_post, -1); Index: ext/curb_errors.h =================================================================== --- ext/curb_errors.h (revision 56) +++ ext/curb_errors.h (working copy) @@ -12,6 +12,7 @@ /* base errors */ extern VALUE cCurlErr; +/* easy errors */ extern VALUE mCurlErr; extern VALUE eCurlErrError; extern VALUE eCurlErrFTPError; @@ -97,10 +98,20 @@ extern VALUE eCurlErrTFTPFileExists; extern VALUE eCurlErrTFTPNoSuchUser; +/* multi errors */ +extern VALUE mCurlErrCallMultiPerform; +extern VALUE mCurlErrBadHandle; +extern VALUE mCurlErrBadEasyHandle; +extern VALUE mCurlErrOutOfMemory; +extern VALUE mCurlErrInternalError; +extern VALUE mCurlErrBadSocket; +extern VALUE mCurlErrUnknownOption; + /* binding errors */ extern VALUE eCurlErrInvalidPostField; void init_curb_errors(); void raise_curl_easy_error_exception(CURLcode code); +void raise_curl_multi_error_exception(CURLMcode code); #endif Index: tests/tc_curl_multi.rb =================================================================== --- tests/tc_curl_multi.rb (revision 0) +++ tests/tc_curl_multi.rb (revision 0) @@ -0,0 +1,137 @@ +require File.join(File.dirname(__FILE__), 'helper') + +class TestCurbCurlMulti < Test::Unit::TestCase + def teardown + # get a better read on memory loss when running in valgrind + ObjectSpace.garbage_collect + end + + def test_new_multi_01 + d1 = "" + c1 = Curl::Easy.new($TEST_URL) do |curl| + curl.headers["User-Agent"] = "myapp-0.0" + curl.on_body {|d| d1 << d; d.length } + end + + d2 = "" + c2 = Curl::Easy.new($TEST_URL) do |curl| + curl.headers["User-Agent"] = "myapp-0.0" + curl.on_body {|d| d2 << d; d.length } + end + + m = Curl::Multi.new + + m.add( c1 ) + m.add( c2 ) + + m.perform + + assert_match(/^# DO NOT REMOVE THIS COMMENT/, d1) + assert_match(/^# DO NOT REMOVE THIS COMMENT/, d2) + + m = nil + + end + + def test_perform_block + c1 = Curl::Easy.new($TEST_URL) + c2 = Curl::Easy.new($TEST_URL) + + m = Curl::Multi.new + + m.add( c1 ) + m.add( c2 ) + + m.perform do + # idle + puts "idling..." + end + + assert_match(/^# DO NOT REMOVE THIS COMMENT/, c1.body_str) + assert_match(/^# DO NOT REMOVE THIS COMMENT/, c2.body_str) + + m = nil + + end + + def test_n_requests + n = 100 + m = Curl::Multi.new + responses = [] + n.times do|i| + responses[i] = "" + c = Curl::Easy.new($TEST_URL) do|curl| + curl.on_body{|data| responses[i] << data; data.size } + end + m.add c + end + + m.perform + + assert n, responses.size + n.times do|i| + assert_match(/^# DO NOT REMOVE THIS COMMENT/, responses[i], "response #{i}") + end + + m = nil + end + + def test_with_success + c1 = Curl::Easy.new($TEST_URL) + c2 = Curl::Easy.new($TEST_URL) + success_called1 = false + success_called2 = false + + c1.on_success do|c| + success_called1 = true + assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str) + end + + c2.on_success do|c| + success_called2 = true + assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str) + end + + m = Curl::Multi.new + + m.add( c1 ) + m.add( c2 ) + + m.perform do + # idle + puts "idling..." + end + + assert success_called2 + assert success_called1 + + m = nil + end + +=begin + def test_remote_requests + responses = {} + requests = ["http://www.google.co.uk/", "http://www.ruby-lang.org/"] + m = Curl::Multi.new + # add a few easy handles + requests.each do |url| + responses[url] = "" + responses["#{url}-header"] = "" + c = Curl::Easy.new(url) do|curl| + curl.follow_location = true + curl.on_header{|data| responses["#{url}-header"] << data; data.size } + curl.on_body{|data| responses[url] << data; data.size } + end + m.add(c) + end + + m.perform + + requests.each do|url| + puts responses["#{url}-header"].split("\r\n").inspect + #puts responses[url].size + end + end +=end + +end Index: tests/tc_curl_easy.rb =================================================================== --- tests/tc_curl_easy.rb (revision 56) +++ tests/tc_curl_easy.rb (working copy) @@ -438,5 +438,18 @@ assert_equal "some.file", c.cookiejar = "some.file" assert_equal "some.file", c.cookiejar end + + def test_on_success + curl = Curl::Easy.new($TEST_URL) + on_success_called = false + curl.on_success {|c| + on_success_called = true + assert_not_nil c.body_str + assert_equal "", c.header_str + assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str) + } + curl.perform + assert on_success_called, "Success handler not called" + end -end \ No newline at end of file +end Index: Rakefile =================================================================== --- Rakefile (revision 56) +++ Rakefile (working copy) @@ -67,7 +67,7 @@ end desc "Compile the shared object" -task :compile => CURB_SO +task :compile => [CURB_SO] desc "Install to your site_ruby directory" task :install => :alltests do Index: README =================================================================== --- README (revision 56) +++ README (working copy) @@ -104,3 +104,25 @@ c = Curl::Easy.new("http://my.rails.box/files/upload") c.multipart_form_post = true c.http_post(Curl::PostField.file('myfile.rb')) + +Multi Interface: + responses = {} + requests = ["http://www.google.co.uk/", "http://www.ruby-lang.org/"] + m = Curl::Multi.new + # add a few easy handles + requests.each do |url| + responses[url] = "" + c = Curl::Easy.new(url) do|curl| + curl.follow_location = true + curl.on_body{|data| responses[url] << data; data.size } + end + m.add(c) + end + + m.perform do + puts "idling... can do some work here, including add new requests" + end + + requests.each do|url| + puts responses[url] + end