#include "util.h" #include "rhttp.h" #include /* * * Using ruby dispatch interface: * * require 'rev_dispatch' * * # create a new dispatch loop * d = Evdispatch::Loop.new * * # start the event loop thread * * # send a dispatch http request and store a handle to the request * google_id = d.request("http://www.google.com/") * * # do some processing and later on check for the response * * res = d.response( google_id ) * * puts res[:body] * * # sometime later you can stop the event loop * d.stop * * * You only get 1 background event loop, calling start multiple times will have no effect. * You typically don't need or want to stop the event loop after it's active. It will sit in the * background and happily wait for new requests using a minimal amount of cpu while waiting. * Everything in the background thread is written in C++ and has absolutely no hooks back into ruby. * The results of the work being processed in the background can be retrieved by ruby but that is it. * * the request method can be used passing it options will set specific libcurl options * */ static VALUE rb_Evdispatch; static VALUE rb_Loop; static VALUE rb_Response; static void Response_free( rResponse *res ); /** * call-seq: * loop.start -> loop * * Starts up the background event loop */ static VALUE Loop_start( VALUE self ) { EVD::Dispatch *d; Data_Get_Struct( self, EVD::Dispatch, d ); d->start(); return self; } /** * call-seq: * loop.request_http(url) -> request_id * * Notifies the background thread of a new request. Call this method to start a basic HTTP GET request. */ static VALUE Loop_request_http( VALUE self, VALUE url ) { EVD::Dispatch *d; Data_Get_Struct( self, EVD::Dispatch, d ); Check_Type( url, T_STRING ); EVD::request_t id = d->request( new EVD::HttpRequest( *d, RSTRING_PTR(url) ) ); return rb_int_new(id); } #define SET_LONG_VAL(name)\ {\ VALUE obj = rb_hash_aref( options, ID2SYM(rb_intern(name)) ); \ if( !NIL_P(obj) ) {\ snprintf( VALUE_BUFFER, VALUE_BUFFER_SIZE, "%ld", FIX2LONG(obj) );\ req->set_opt(name, VALUE_BUFFER);\ }\ } #define SET_STR_VAL(name)\ {\ VALUE obj = rb_hash_aref( options, ID2SYM(rb_intern(name)) ); \ if( !NIL_P(obj) ) {\ req->set_opt(name, RSTRING_PTR(obj));\ }\ } static int build_header_list(VALUE key, VALUE value, struct curl_slist **slist ) { if (key == Qundef) return ST_CONTINUE; VALUE str = rb_str_new(RSTRING_PTR(key),RSTRING_LEN(key)); str = rb_str_cat2(str,":"); *slist = curl_slist_append(*slist, RSTRING_PTR(rb_str_append(str,value)) ); return ST_CONTINUE; } static struct curl_slist *build_headers( VALUE hash ) { struct curl_slist *slist = NULL; rb_hash_foreach( hash, (int (*)(...))build_header_list, (VALUE)&slist ); return slist; } static void set_headers( VALUE options, EVD::HttpRequest *req ) { VALUE hash = rb_hash_aref( options, ID2SYM(rb_intern("headers")) ); if( !NIL_P(hash) && TYPE(hash) == T_HASH ) { struct curl_slist *headers = build_headers( hash ); req->set_opt( "headers", headers ); } } // set options #define SET_REQ_OPTS(options,req) \ SET_LONG_VAL("port"); \ SET_STR_VAL("autoreferer"); \ SET_LONG_VAL("followlocation"); \ SET_LONG_VAL("maxredirs"); \ SET_STR_VAL("referer"); \ SET_STR_VAL("useragent"); \ SET_STR_VAL("cookie"); \ SET_STR_VAL("post"); \ set_headers(options, req) /** * call-seq: * loop.request( url, options ) -> request_id * * Notify the background of a new request. Can send a few options to influence the request: * * options ( from libcurl ): * * :port: (Fixnum) port to connect to host with * :autoreferer: (Fixnum) Pass a non-zero parameter to enable this. When enabled, libcurl will automatically set the Referer: field in requests where it follows a Location: redirect. * :followlocation: (Fixnum) A non-zero parameter tells the library to follow any Location: header that the server sends as part of an HTTP header. * This means that the library will re-send the same request on the new location and follow new Location: headers all the way until no more such headers are returned. 'maxredirs' can be used to limit the number of redirects libcurl will follow. * :maxredirs: (Fixnum) Pass a long. The set number will be the redirection limit. If that many redirections have been followed, the next redirect will cause an error (CURLE_TOO_MANY_REDIRECTS). This option only makes sense if the CURLOPT_FOLLOWLOCATION is used at the same time. Added in 7.15.1: Setting the limit to 0 will make libcurl refuse any redirect. Set it to -1 for an infinite number of redirects (which is the default) * :referer: (String) Pass a pointer to a zero terminated string as parameter. It will be used to set the Referer: header in the http request sent to the remote server. This can be used to fool servers or scripts. You can also set any custom header with CURLOPT_HTTPHEADER. * :useragent: (String) Pass a pointer to a zero terminated string as parameter. It will be used to set the User-Agent: header in the http request sent to the remote server. This can be used to fool servers or scripts. You can also set any custom header with CURLOPT_HTTPHEADER. * :cookie: (String) Pass a pointer to a zero terminated string as parameter. It will be used to set a cookie in the http request. The format of the string should be NAME=CONTENTS, where NAME is the cookie name and CONTENTS is what the cookie should contain. * If you need to set multiple cookies, you need to set them all using a single option and thus you need to concatenate them all in one single string. Set multiple cookies in one string like this: "name1=content1; name2=content2;" etc. * Note that this option sets the cookie header explictly in the outgoing request(s). If multiple requests are done due to authentication, followed redirections or similar, they will all get this cookie passed on. * Using this option multiple times will only make the latest string override the previous ones. * :headers: (Hash) An arbitrary list of HTTP Headers. Set any HTTP header or override the defaults. see libcurl's documentation for more details. * * :post: (String) Pass a string and sets CURLOPT_POST, CURLOPT_POSTFIELDSIZE, and CURLOPT_COPYPOSTFIELDS * * :stream: (Evdispatch::Response) Send the response to the IO object. Ignore the response id returned it'd invalid */ static VALUE Loop_request( int argc, VALUE *argv, VALUE self ) { EVD::Dispatch *d; const int VALUE_BUFFER_SIZE = 1024; char VALUE_BUFFER[VALUE_BUFFER_SIZE]; Data_Get_Struct( self, EVD::Dispatch, d ); VALUE url, options; // required 1 argument the 'url' and 1 optional the hash of options if( rb_scan_args( argc, argv, "11", &url, &options ) == 1 ) { options = rb_hash_new(); } Check_Type(url, T_STRING ); Check_Type(options, T_HASH ); VALUE response = rb_hash_aref( options, ID2SYM(rb_intern("stream")) ); if( !NIL_P(response) && TYPE(response) == T_TRUE ) { rResponse *res = new rResponse; VALUE response_object = Data_Wrap_Struct( rb_Response, NULL, Response_free, res ); if( !res->init() ) { rb_raise(rb_eIOError, "Failed to create new pipe"); } EVD::HttpRequest *req = new EVD::HttpRequest( *d, RSTRING_PTR(url), res->m_pipe[1] ); SET_REQ_OPTS(options,req); res->m_response = req->m_response; d->request(req); return response_object; } else { EVD::HttpRequest *req = new EVD::HttpRequest( *d, RSTRING_PTR(url) ); SET_REQ_OPTS(options,req); return rb_int_new( d->request(req) ); } } static VALUE Loop_wait_for_response( VALUE self, VALUE id, VALUE timeout_seconds, VALUE timeout_mseconds ) { EVD::Dispatch *d; EVD::Queue::POP_STATE rstate; EVD::request_t rid = FIX2LONG(id); Data_Get_Struct( self, EVD::Dispatch, d ); Check_Type( id, T_FIXNUM ); Check_Type( timeout_seconds, T_FIXNUM ); Check_Type( timeout_mseconds, T_FIXNUM ); long seconds = FIX2LONG(timeout_seconds); long nanoseconds = FIX2LONG(timeout_mseconds)*1000*1000; //printf( "wait seconds %ld, nanoseconds %ld\n", seconds, nanoseconds ); rstate = d->wait_for_response_by_id( rid, EVD::Timer(seconds, nanoseconds) ); return rb_int_new(rstate); } static VALUE Loop_response_for( VALUE self, VALUE id ) { EVD::Dispatch *d; Data_Get_Struct( self, EVD::Dispatch, d ); Check_Type( id, T_FIXNUM ); EVD::HttpResponse *res = NULL; EVD::request_t rid = FIX2LONG(id); res = (EVD::HttpResponse*)d->response_for( rid ); if( res ) { VALUE result = rb_hash_new(); rb_hash_aset( result, ID2SYM(rb_intern("name")), rb_str_new( res->name.c_str(), res->name.length() ) ); rb_hash_aset( result, ID2SYM(rb_intern("body")), rb_str_new( res->body.c_str(), res->body.length() ) ); rb_hash_aset( result, ID2SYM(rb_intern("id")), rb_int_new( res->id ) ); rb_hash_aset( result, ID2SYM(rb_intern("response_time")), rb_float_new( res->response_time ) ); rb_hash_aset( result, ID2SYM(rb_intern("header")), rb_str_new( res->m_header.c_str(), res->m_header.length() ) ); delete res; return result; } return Qnil; } static VALUE Loop_blocking_response_for( int argc, VALUE *argv, VALUE self ) { VALUE req_id, options; EVD::Dispatch *dispatcher; Data_Get_Struct( self, EVD::Dispatch, dispatcher ); // required 1 argument the 'url' and 1 optional the hash of options if( rb_scan_args( argc, argv, "11", &req_id, &options ) == 1 ) { options = rb_hash_new(); rb_hash_aset( options, ID2SYM(rb_intern("timeout")), rb_float_new( 2.0 ) ); } Check_Type( req_id, T_FIXNUM ); Check_Type( options, T_HASH ); EVD::request_t id = FIX2LONG(req_id); VALUE timeout_value = rb_hash_aref( options, ID2SYM(rb_intern("timeout")) ); double timeout = RFLOAT( timeout_value )->value; struct timeval start; EVD::Timer::current_time(&start); long int secs = (int)timeout; long int msecs = (secs == 0) ? 500 : 0; // XXX: timer hack for small timeout //printf( "waiting on id: %d, %d:%d\n", id, secs, msecs ); while( dispatcher->wait_for_response_by_id( id, EVD::Timer(secs,msecs*1000*1000) ) ) { if( EVD::Timer::elapsed_time( &start ) > timeout ){ printf("exceeded max elasped time...\n"); break; } } EVD::HttpResponse *res = (EVD::HttpResponse *)dispatcher->response_for( id ); if( res ) { VALUE result = rb_hash_new(); rb_hash_aset( result, ID2SYM(rb_intern("name")), rb_str_new( res->name.c_str(), res->name.length() ) ); rb_hash_aset( result, ID2SYM(rb_intern("body")), rb_str_new( res->body.c_str(), res->body.length() ) ); rb_hash_aset( result, ID2SYM(rb_intern("id")), rb_int_new( res->id ) ); rb_hash_aset( result, ID2SYM(rb_intern("response_time")), rb_float_new( res->response_time ) ); rb_hash_aset( result, ID2SYM(rb_intern("header")), rb_str_new( res->m_header.c_str(), res->m_header.length() ) ); delete res; return result; } return Qnil; } static VALUE Loop_flush( VALUE self ) { EVD::Dispatch *d; Data_Get_Struct( self, EVD::Dispatch, d ); d->flush(); return Qnil; } static VALUE Loop_stop( VALUE self ) { EVD::Dispatch *d; Data_Get_Struct( self, EVD::Dispatch, d ); d->stop(); return Qnil; } static void Loop_free( EVD::Dispatch *d ) { delete d; } static VALUE Loop_alloc(VALUE klass) { VALUE object; EVD::Dispatch *d = new EVD::Dispatch(); object = Data_Wrap_Struct( klass, NULL, Loop_free, d ); return object; } static void Response_free( rResponse *res ) { delete res; } static VALUE Response_alloc(VALUE klass) { VALUE object; rResponse *res = new rResponse; object = Data_Wrap_Struct( klass, NULL, Response_free, res ); return object; } /* This code might be interesting later if we decide to wire in direct writing to a Ruby socket or file const char *klass_name = rb_class2name(CLASS_OF(stream)); if( !strcmp("IO", klass_name) || !strcmp("Socket", klass_name) || !strcmp("File", klass_name) ) { RFile *fstream = RFILE(stream); struct OpenFile *fptr = fstream->fptr; FILE *file = fptr->f; int fd = fileno(file); write(fd,"hi",2); }*/ static VALUE Response_read( VALUE self ) { rResponse *res; Data_Get_Struct( self, rResponse, res ); int status = res->read_body_partial(); if( status == -1 ) { rb_raise(rb_eIOError, "Failed to read pipe"); } return rb_int_new(status); } static VALUE Response_body( VALUE self ) { rResponse *res; Data_Get_Struct( self, rResponse, res ); return rb_str_new(res->m_buffer.c_str(), res->m_buffer.length()); } static VALUE Response_headers( VALUE self ) { rResponse *res; Data_Get_Struct( self, rResponse, res ); return rb_str_new(res->m_response->m_header.c_str(), res->m_response->m_header.length() ); } extern "C" void Init_revdispatch() { rb_Evdispatch = rb_define_module( "Evdispatch" ); rb_Loop = rb_define_class_under( rb_Evdispatch, "Loop", rb_cObject ); // setup the Loop object rb_define_alloc_func( rb_Loop, Loop_alloc ); rb_define_method( rb_Loop, "start", RB_METHOD(Loop_start), 0 ); rb_define_method( rb_Loop, "request_http", RB_METHOD(Loop_request_http), 1 ); rb_define_method( rb_Loop, "request", RB_METHOD(Loop_request), -1 ); rb_define_method( rb_Loop, "flush", RB_METHOD(Loop_flush), 0 ); rb_define_method( rb_Loop, "blocking_response_for", RB_METHOD(Loop_blocking_response_for), -1 ); rb_define_method( rb_Loop, "response_for", RB_METHOD(Loop_response_for), 1 ); rb_define_method( rb_Loop, "wait_for_response", RB_METHOD(Loop_wait_for_response), 3 ); rb_define_method( rb_Loop, "stop", RB_METHOD(Loop_stop), 0 ); // define the response object rb_Response = rb_define_class_under( rb_Evdispatch, "Response", rb_cObject ); // setup the Response object rb_define_alloc_func( rb_Response, Response_alloc ); rb_define_method( rb_Response, "read", RB_METHOD(Response_read), 0 ); rb_define_method( rb_Response, "body", RB_METHOD(Response_body), 0 ); rb_define_method( rb_Response, "headers", RB_METHOD(Response_headers), 0 ); }