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.0"
+#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,7 @@
   VALUE proxypwd;
   VALUE headers;        /* ruby array of strings with headers to set */
   VALUE cookiejar;      /* filename */
-  
+
   /* Other opts */
   unsigned short local_port;       // 0 is no port
   unsigned short local_port_range; // "  "  " "
@@ -65,10 +65,22 @@
    * 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;
+
 } 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(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,209 @@
+/* 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 <errno.h>
+
+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);
+  }
+
+  /* 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, 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,7 +366,44 @@
   
   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");
   eCurlErrError = rb_define_class_under(mCurlErr, "CurlError", rb_eRuntimeError);
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 <curl/multi.h>
+
+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)
@@ -94,9 +94,14 @@
   rb_gc_mark(rbce->cookiejar);
 
   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 +120,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);
@@ -168,13 +174,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 +209,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);
 }
@@ -1044,234 +1060,262 @@
 }
 
 /***********************************************
- * 
- * 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)hdrs);        
+    } else {
+      VALUE headers_str = rb_obj_as_string(rbce->headers);
+      *hdrs = curl_slist_append(*hdrs, StringValuePtr(headers_str));
     }
     
-    // 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);
-      }
+    if (*hdrs) {
+      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *hdrs);
     }
-    
-    // Okay, do it.
-    result = curl_easy_perform(curl);
-    
-    // Free everything up
-    if (headers) {
-      curl_slist_free_all(headers);
-    }
-    
-    // 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 (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( ruby_curl_easy *rbce, VALUE bodybuf, VALUE headerbuf, struct curl_slist *headers ) {
 
-    if (result != 0) {
-      raise_curl_easy_error_exception(result);
-    }
+  // 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 {
-    rb_raise(eCurlErrError, "No URL supplied");
+    rbce->body_data = Qnil;
   }
+
+  if (headerbuf != Qnil) {
+    rbce->header_data = rb_str_to_str(headerbuf);
+    rbce->headerbuf = Qnil;
+  } else {
+    rbce->header_data = Qnil;
+  }
+
+  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(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(rbce, bodybuf, headerbuf, headers);
   
+  if (result != 0) {
+    raise_curl_easy_error_exception(result);
+  }
+
   return Qtrue;
 }
 
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,105 @@
+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
+
+=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)
@@ -439,4 +439,4 @@
     assert_equal "some.file", c.cookiejar        
   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 => [:clean,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
