Class Patron::Request
In: lib/patron/request.rb
ext/patron/session_ext.c
Parent: Object

// ——————————————————————- // // Patron HTTP Client: Interface to libcurl // Copyright (c) 2008 The Hive www.thehive.com/ // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // ——————————————————————- include <ruby.h> include <curl/curl.h>

static VALUE mPatron = Qnil; static VALUE cSession = Qnil; static VALUE cRequest = Qnil; static VALUE ePatronError = Qnil; static VALUE eUnsupportedProtocol = Qnil; static VALUE eURLFormatError = Qnil; static VALUE eHostResolutionError = Qnil; static VALUE eConnectionFailed = Qnil; static VALUE ePartialFileError = Qnil; static VALUE eTimeoutError = Qnil; static VALUE eTooManyRedirects = Qnil;

struct curl_state {

  CURL* handle;
  char* upload_buf;
  FILE* download_file;
  FILE* upload_file;
  char error_buf[CURL_ERROR_SIZE];
  struct curl_slist* headers;

};

//—————————————————————————— // Curl Callbacks //

// Takes data streamed from libcurl and writes it to a Ruby string buffer. static size_t session_write_handler(char* stream, size_t size, size_t nmemb, VALUE out) {

  rb_str_buf_cat(out, stream, size * nmemb);
  return size * nmemb;

}

static size_t session_read_handler(char* stream, size_t size, size_t nmemb, char **buffer) {

  size_t result = 0;

  if (buffer != NULL && *buffer != NULL) {
      int len = size * nmemb;
      char *s1 = strncpy(stream, *buffer, len);
      result = strlen(s1);
       buffer += result;
  }

  return result;

}

//—————————————————————————— // Object allocation //

// Cleans up the Curl handle when the Session object is garbage collected. void session_free(struct curl_state *curl) {

  curl_easy_cleanup(curl->handle);
  free(curl);

}

// Allocates curl_state data needed for a new Session object. VALUE session_alloc(VALUE klass) {

  struct curl_state* curl;
  VALUE obj = Data_Make_Struct(klass, struct curl_state, NULL, session_free, curl);
  return obj;

}

//—————————————————————————— // Method implementations //

// Returns the version of the embedded libcurl as a string. VALUE libcurl_version(VALUE klass) {

  char* value = curl_version();
  return rb_str_new2(value);

}

// Initializes the libcurl handle on object initialization. // NOTE: This must be called from Session#initialize. VALUE session_ext_initialize(VALUE self) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  state->handle = curl_easy_init();

  return self;

}

// URL escapes the provided string. VALUE session_escape(VALUE self, VALUE value) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  VALUE string = StringValue(value);
  char* escaped = curl_easy_escape(state->handle,
                                   RSTRING_PTR(string),
                                   RSTRING_LEN(string));

  VALUE retval = rb_str_new2(escaped);
  curl_free(escaped);

  return retval;

}

// Unescapes the provided string. VALUE session_unescape(VALUE self, VALUE value) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  VALUE string = StringValue(value);
  char* unescaped = curl_easy_unescape(state->handle,
                                       RSTRING_PTR(string),
                                       RSTRING_LEN(string),
                                       NULL);

  VALUE retval = rb_str_new2(unescaped);
  curl_free(unescaped);

  return retval;

}

// Callback used to iterate over the HTTP headers and store them in an slist. static int each_http_header(VALUE header_key, VALUE header_value, VALUE self) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  VALUE name = rb_obj_as_string(header_key);
  VALUE value = rb_obj_as_string(header_value);

  VALUE header_str = Qnil;
  header_str = rb_str_plus(name, rb_str_new2(": "));
  header_str = rb_str_plus(header_str, value);

  state->headers = curl_slist_append(state->headers, StringValuePtr(header_str));
  return 0;

}

static void set_chunked_encoding(struct curl_state *state) {

  state->headers = curl_slist_append(state->headers, "Transfer-Encoding: chunked");

}

static FILE* open_file(VALUE filename, char* perms) {

  FILE* handle = fopen(StringValuePtr(filename), perms);
  if (!handle) {
    rb_raise(rb_eArgError, "Unable to open specified file.");
  }

  return handle;

}

// Set the options on the Curl handle from a Request object. Takes each field // in the Request object and uses it to set the appropriate option on the Curl // handle. static void set_options_from_request(VALUE self, VALUE request) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  CURL* curl = state->handle;

  VALUE headers = rb_iv_get(request, "@headers");
  if (!NIL_P(headers)) {
    if (rb_type(headers) != T_HASH) {
      rb_raise(rb_eArgError, "Headers must be passed in a hash.");
    }

    rb_hash_foreach(headers, each_http_header, self);
  }

  ID action = SYM2ID(rb_iv_get(request, "@action"));
  if (action == rb_intern("get")) {
    curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);

    VALUE download_file = rb_iv_get(request, "@file_name");
    if (!NIL_P(download_file)) {
      state->download_file = open_file(download_file, "w");
      curl_easy_setopt(curl, CURLOPT_WRITEDATA, state->download_file);
    } else {
      state->download_file = NULL;
    }
  } else if (action == rb_intern("post") || action == rb_intern("put")) {
    VALUE data = rb_iv_get(request, "@upload_data");
    VALUE filename = rb_iv_get(request, "@file_name");

    if (!NIL_P(data)) {
      state->upload_buf = StringValuePtr(data);
      int len = RSTRING_LEN(data);

      if (action == rb_intern("post")) {
        curl_easy_setopt(curl, CURLOPT_POST, 1);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, state->upload_buf);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
      } else {
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, &session_read_handler);
        curl_easy_setopt(curl, CURLOPT_READDATA, &state->upload_buf);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE, len);
      }
    } else if (!NIL_P(filename)) {
      set_chunked_encoding(state);

      curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);

      if (action == rb_intern("post")) {
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
      }

      state->upload_file = open_file(filename, "r");
      curl_easy_setopt(curl, CURLOPT_READDATA, state->upload_file);
    } else {
      rb_raise(rb_eArgError, "Must provide either data or a filename when doing a PUT or POST");
    }
  } else if (action == rb_intern("head")) {
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1);
  } else {
    VALUE action_name = rb_funcall(request, rb_intern("action_name"), 0);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, StringValuePtr(action_name));
  }

  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, state->headers);
  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, state->error_buf);

  VALUE url = rb_iv_get(request, "@url");
  if (NIL_P(url)) {
    rb_raise(rb_eArgError, "Must provide a URL");
  }
  curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url));

  VALUE timeout = rb_iv_get(request, "@timeout");
  if (!NIL_P(timeout)) {
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, FIX2INT(timeout));
  }

  timeout = rb_iv_get(request, "@connect_timeout");
  if (!NIL_P(timeout)) {
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, FIX2INT(timeout));
  }

  VALUE redirects = rb_iv_get(request, "@max_redirects");
  if (!NIL_P(redirects)) {
    int r = FIX2INT(redirects);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, r == 0 ? 0 : 1);
    curl_easy_setopt(curl, CURLOPT_MAXREDIRS, r);
  }

  VALUE proxy = rb_iv_get(request, "@proxy");
  if (!NIL_P(proxy)) {
      curl_easy_setopt(curl, CURLOPT_PROXY, StringValuePtr(proxy));
  }

  VALUE credentials = rb_funcall(request, rb_intern("credentials"), 0);
  if (!NIL_P(credentials)) {
    curl_easy_setopt(curl, CURLOPT_HTTPAUTH, FIX2INT(rb_iv_get(request, "@auth_type")));
    curl_easy_setopt(curl, CURLOPT_USERPWD, StringValuePtr(credentials));
  }

  VALUE insecure = rb_iv_get(request, "@insecure");
  if(!NIL_P(insecure)) {
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1);
  }

}

// Use the info in a Curl handle to create a new Response object. static VALUE create_response(CURL* curl) {

  VALUE response = rb_class_new_instance(0, 0,
                      rb_const_get(mPatron, rb_intern("Response")));

  char* url = NULL;
  curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url);
  rb_iv_set(response, "@url", rb_str_new2(url));

  long code = 0;
  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
  rb_iv_set(response, "@status", INT2NUM(code));

  long count = 0;
  curl_easy_getinfo(curl, CURLINFO_REDIRECT_COUNT, &count);
  rb_iv_set(response, "@redirect_count", INT2NUM(count));

  return response;

}

// Raise an exception based on the Curl error code. static VALUE select_error(CURLcode code) {

  VALUE error = Qnil;
  switch (code) {
    case CURLE_UNSUPPORTED_PROTOCOL:  error = eUnsupportedProtocol; break;
    case CURLE_URL_MALFORMAT:         error = eURLFormatError;      break;
    case CURLE_COULDNT_RESOLVE_HOST:  error = eHostResolutionError; break;
    case CURLE_COULDNT_CONNECT:       error = eConnectionFailed;    break;
    case CURLE_PARTIAL_FILE:          error = ePartialFileError;    break;
    case CURLE_OPERATION_TIMEDOUT:    error = eTimeoutError;        break;
    case CURLE_TOO_MANY_REDIRECTS:    error = eTooManyRedirects;    break;

    default: error = ePatronError;
  }

  return error;

}

// Perform the actual HTTP request by calling libcurl. static VALUE perform_request(VALUE self) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  CURL* curl = state->handle;

  // headers
  VALUE header_buffer = rb_str_buf_new(32768);
  curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &session_write_handler);
  curl_easy_setopt(curl, CURLOPT_HEADERDATA, header_buffer);

  // body
  VALUE body_buffer = Qnil;
  if (!state->download_file) {
    body_buffer = rb_str_buf_new(32768);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &session_write_handler);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, body_buffer);
  }

if defined(HAVE_TBR) && defined(USE_TBR)

  CURLcode ret = rb_thread_blocking_region(curl_easy_perform, curl, RUBY_UBF_IO, 0);

else

  CURLcode ret = curl_easy_perform(curl);

endif

  if (CURLE_OK == ret) {
    VALUE response = create_response(curl);
    if (!NIL_P(body_buffer)) {
      rb_iv_set(response, "@body", body_buffer);
    }
    rb_funcall(response, rb_intern("parse_headers"), 1, header_buffer);
    return response;
  } else {
    rb_raise(select_error(ret), "%s", state->error_buf);
  }

}

// Cleanup after each request by resetting the Curl handle and deallocating all // request related objects such as the header slist. static VALUE cleanup(VALUE self) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);

  curl_easy_reset(state->handle);

  if (state->headers) {
    curl_slist_free_all(state->headers);
    state->headers = NULL;
  }

  if (state->download_file) {
    fclose(state->download_file);
    state->download_file = NULL;
  }

  if (state->upload_file) {
    fclose(state->upload_file);
    state->upload_file = NULL;
  }

  state->upload_buf = NULL;

  return Qnil;

}

VALUE session_handle_request(VALUE self, VALUE request) {

  set_options_from_request(self, request);
  return rb_ensure(&perform_request, self, &cleanup, self);

}

VALUE enable_cookie_session(VALUE self, VALUE file) {

  struct curl_state *state;
  Data_Get_Struct(self, struct curl_state, state);
  CURL* curl = state->handle;
  char* file_path = RSTRING_PTR(file);
  if (file_path != NULL && strlen(file_path) != 0) {
    curl_easy_setopt(curl, CURLOPT_COOKIEJAR, file_path);
  }
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, file_path);
  return Qnil;

}

//—————————————————————————— // Extension initialization //

void Init_session_ext() {

  curl_global_init(CURL_GLOBAL_ALL);
  rb_require("patron/error");

  mPatron = rb_define_module("Patron");

  ePatronError = rb_const_get(mPatron, rb_intern("Error"));

  eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
  eURLFormatError = rb_const_get(mPatron, rb_intern("URLFormatError"));
  eHostResolutionError = rb_const_get(mPatron, rb_intern("HostResolutionError"));
  eConnectionFailed = rb_const_get(mPatron, rb_intern("ConnectionFailed"));
  ePartialFileError = rb_const_get(mPatron, rb_intern("PartialFileError"));
  eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
  eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));

  rb_define_module_function(mPatron, "libcurl_version", libcurl_version, 0);

  cSession = rb_define_class_under(mPatron, "Session", rb_cObject);
  cRequest = rb_define_class_under(mPatron, "Request", rb_cObject);
  rb_define_alloc_func(cSession, session_alloc);

  rb_define_method(cSession, "ext_initialize", session_ext_initialize, 0);
  rb_define_method(cSession, "escape",         session_escape,         1);
  rb_define_method(cSession, "unescape",       session_unescape,       1);
  rb_define_method(cSession, "handle_request", session_handle_request, 1);
  rb_define_method(cSession, "enable_cookie_session", enable_cookie_session, 1);

  rb_define_const(cRequest, "AuthBasic",  INT2FIX(CURLAUTH_BASIC));
  rb_define_const(cRequest, "AuthDigest", INT2FIX(CURLAUTH_DIGEST));
  rb_define_const(cRequest, "AuthAny",    INT2FIX(CURLAUTH_ANY));

}

Methods

Constants

VALID_ACTIONS = [:get, :put, :post, :delete, :head, :copy]
AuthBasic = INT2FIX(CURLAUTH_BASIC)
AuthDigest = INT2FIX(CURLAUTH_DIGEST)
AuthAny = INT2FIX(CURLAUTH_ANY)
AuthBasic = INT2FIX(CURLAUTH_BASIC)
AuthDigest = INT2FIX(CURLAUTH_DIGEST)
AuthAny = INT2FIX(CURLAUTH_ANY)

Attributes

action  [R] 
auth_type  [R] 
auth_type  [RW] 
connect_timeout  [R] 
file_name  [RW] 
headers  [R] 
insecure  [RW] 
max_redirects  [R] 
password  [RW] 
proxy  [RW] 
timeout  [R] 
url  [RW] 
username  [RW] 

Public Class methods

[Source]

    # File lib/patron/request.rb, line 35
35:     def initialize
36:       @action = :get
37:       @headers = {}
38:       @timeout = 0
39:       @connect_timeout = 0
40:       @max_redirects = -1
41:     end

Public Instance methods

[Source]

    # File lib/patron/request.rb, line 82
82:     def action=(new_action)
83:       if !VALID_ACTIONS.include?(new_action)
84:         raise ArgumentError, "Action must be one of #{VALID_ACTIONS.join(', ')}"
85:       end
86: 
87:       @action = new_action
88:     end

[Source]

     # File lib/patron/request.rb, line 122
122:     def action_name
123:       @action.to_s.upcase
124:     end

Set the type of authentication to use for this request.

@param [String, Symbol] type - The type of authentication to use for this request, can be one of

  :basic, :digest, or :any

@example

  sess.username = "foo"
  sess.password = "sekrit"
  sess.auth_type = :digest

[Source]

    # File lib/patron/request.rb, line 56
56:     def auth_type=(type=:basic)
57:       @auth_type = case type
58:       when :basic, "basic"
59:         Request::AuthBasic
60:       when :digest, "digest"
61:         Request::AuthDigest
62:       when :any, "any"
63:         Request::AuthAny
64:       else
65:         raise "#{type.inspect} is an unknown authentication type"
66:       end
67:     end

[Source]

     # File lib/patron/request.rb, line 98
 98:     def connect_timeout=(new_timeout)
 99:       if new_timeout && new_timeout.to_i < 1
100:         raise ArgumentError, "Timeout must be a positive integer greater than 0"
101:       end
102: 
103:       @connect_timeout = new_timeout.to_i
104:     end

[Source]

     # File lib/patron/request.rb, line 126
126:     def credentials
127:       return nil if username.nil? || password.nil?
128:       "#{username}:#{password}"
129:     end

[Source]

     # File lib/patron/request.rb, line 114
114:     def headers=(new_headers)
115:       if !new_headers.kind_of?(Hash)
116:         raise ArgumentError, "Headers must be a hash"
117:       end
118: 
119:       @headers = new_headers
120:     end

[Source]

     # File lib/patron/request.rb, line 106
106:     def max_redirects=(new_max_redirects)
107:       if new_max_redirects.to_i < -1
108:         raise ArgumentError, "Max redirects must be a positive integer, 0 or -1"
109:       end
110: 
111:       @max_redirects = new_max_redirects.to_i
112:     end

[Source]

    # File lib/patron/request.rb, line 90
90:     def timeout=(new_timeout)
91:       if new_timeout && new_timeout.to_i < 1
92:         raise ArgumentError, "Timeout must be a positive integer greater than 0"
93:       end
94: 
95:       @timeout = new_timeout.to_i
96:     end

[Source]

    # File lib/patron/request.rb, line 78
78:     def upload_data
79:       @upload_data
80:     end

[Source]

    # File lib/patron/request.rb, line 69
69:     def upload_data=(data)
70:       @upload_data = case data
71:       when Hash
72:         hash_to_string(data)
73:       else
74:         data
75:       end
76:     end

[Validate]