typhoeus 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG.markdown +31 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +30 -0
  5. data/README.textile +333 -0
  6. data/Rakefile +38 -0
  7. data/VERSION +1 -0
  8. data/benchmarks/profile.rb +25 -0
  9. data/benchmarks/vs_nethttp.rb +35 -0
  10. data/examples/twitter.rb +21 -0
  11. data/ext/typhoeus/.gitignore +6 -0
  12. data/ext/typhoeus/extconf.rb +3 -2
  13. data/ext/typhoeus/native.h +7 -0
  14. data/ext/typhoeus/typhoeus_multi.c +1 -2
  15. data/lib/typhoeus/.gitignore +1 -0
  16. data/lib/typhoeus/easy.rb +172 -42
  17. data/lib/typhoeus/hydra/callbacks.rb +24 -0
  18. data/lib/typhoeus/hydra/connect_options.rb +45 -0
  19. data/lib/typhoeus/hydra/stubbing.rb +52 -0
  20. data/lib/typhoeus/hydra.rb +85 -59
  21. data/lib/typhoeus/hydra_mock.rb +131 -0
  22. data/lib/typhoeus/multi.rb +4 -3
  23. data/lib/typhoeus/normalized_header_hash.rb +58 -0
  24. data/lib/typhoeus/remote_proxy_object.rb +6 -6
  25. data/lib/typhoeus/request.rb +118 -31
  26. data/lib/typhoeus/response.rb +61 -4
  27. data/lib/typhoeus/service.rb +20 -0
  28. data/lib/typhoeus/utils.rb +24 -0
  29. data/lib/typhoeus.rb +11 -8
  30. data/profilers/valgrind.rb +24 -0
  31. data/spec/fixtures/result_set.xml +60 -0
  32. data/spec/servers/app.rb +48 -1
  33. data/spec/spec_helper.rb +1 -1
  34. data/spec/typhoeus/easy_spec.rb +126 -4
  35. data/spec/typhoeus/hydra_mock_spec.rb +300 -0
  36. data/spec/typhoeus/hydra_spec.rb +456 -0
  37. data/spec/typhoeus/normalized_header_hash_spec.rb +41 -0
  38. data/spec/typhoeus/remote_spec.rb +2 -2
  39. data/spec/typhoeus/request_spec.rb +247 -0
  40. data/spec/typhoeus/response_spec.rb +104 -1
  41. data/spec/typhoeus/utils_spec.rb +22 -0
  42. data/typhoeus.gemspec +123 -0
  43. metadata +145 -34
  44. data/ext/typhoeus/Makefile +0 -157
@@ -1,22 +1,87 @@
1
+ require 'uri'
2
+
1
3
  module Typhoeus
2
4
  class Request
3
- attr_accessor :method, :params, :body, :headers, :timeout, :user_agent, :response, :cache_timeout
4
5
  attr_reader :url
5
-
6
+ attr_writer :headers
7
+ attr_accessor :method, :params, :body, :connect_timeout, :timeout,
8
+ :user_agent, :response, :cache_timeout, :follow_location,
9
+ :max_redirects, :proxy, :disable_ssl_peer_verification,
10
+ :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_type,
11
+ :ssl_key_password, :ssl_cacert, :ssl_capath, :verbose,
12
+ :username, :password, :auth_method, :user_agent
13
+
14
+ # Initialize a new Request
15
+ #
16
+ # Options:
17
+ # * +url+ : Endpoint (URL) of the request
18
+ # * +options+ : A hash containing options among :
19
+ # ** +:method+ : :get (default) / :post / :put
20
+ # ** +:params+ : params as a Hash
21
+ # ** +:body+
22
+ # ** +:timeout+ : timeout (ms)
23
+ # ** +:connect_timeout+ : connect timeout (ms)
24
+ # ** +:headers+ : headers as Hash
25
+ # ** +:user_agent+ : user agent (string)
26
+ # ** +:cache_timeout+ : cache timeout (ms)
27
+ # ** +:follow_location
28
+ # ** +:max_redirects
29
+ # ** +:proxy
30
+ # ** +:disable_ssl_peer_verification
31
+ # ** +:ssl_cert
32
+ # ** +:ssl_cert_type
33
+ # ** +:ssl_key
34
+ # ** +:ssl_key_type
35
+ # ** +:ssl_key_password
36
+ # ** +:ssl_cacert
37
+ # ** +:ssl_capath
38
+ # ** +:verbose
39
+ # ** +:username
40
+ # ** +:password
41
+ # ** +:auth_method
42
+ #
6
43
  def initialize(url, options = {})
7
44
  @method = options[:method] || :get
8
45
  @params = options[:params]
9
46
  @body = options[:body]
10
47
  @timeout = options[:timeout]
48
+ @connect_timeout = options[:connect_timeout]
11
49
  @headers = options[:headers] || {}
12
50
  @user_agent = options[:user_agent] || Typhoeus::USER_AGENT
13
51
  @cache_timeout = options[:cache_timeout]
14
- @url = @params ? "#{url}?#{params_string}" : url
52
+ @follow_location = options[:follow_location]
53
+ @max_redirects = options[:max_redirects]
54
+ @proxy = options[:proxy]
55
+ @disable_ssl_peer_verification = options[:disable_ssl_peer_verification]
56
+ @ssl_cert = options[:ssl_cert]
57
+ @ssl_cert_type = options[:ssl_cert_type]
58
+ @ssl_key = options[:ssl_key]
59
+ @ssl_key_type = options[:ssl_key_type]
60
+ @ssl_key_password = options[:ssl_key_password]
61
+ @ssl_cacert = options[:ssl_cacert]
62
+ @ssl_capath = options[:ssl_capath]
63
+ @verbose = options[:verbose]
64
+ @username = options[:username]
65
+ @password = options[:password]
66
+ @auth_method = options[:auth_method]
67
+
68
+ if @method == :post
69
+ @url = url
70
+ else
71
+ @url = @params ? "#{url}?#{params_string}" : url
72
+ end
73
+
74
+ @parsed_uri = URI.parse(@url)
75
+
15
76
  @on_complete = nil
16
77
  @after_complete = nil
17
78
  @handled_response = nil
18
79
  end
19
-
80
+
81
+ def localhost?
82
+ %(localhost 127.0.0.1 0.0.0.0).include?(@parsed_uri.host)
83
+ end
84
+
20
85
  def host
21
86
  slash_location = @url.index('/', 8)
22
87
  if slash_location
@@ -26,86 +91,108 @@ module Typhoeus
26
91
  return query_string_location ? @url.slice(0, query_string_location) : @url
27
92
  end
28
93
  end
29
-
94
+
30
95
  def headers
31
96
  @headers["User-Agent"] = @user_agent
32
97
  @headers
33
98
  end
34
-
99
+
35
100
  def params_string
36
- params.keys.sort.collect do |k|
101
+ params.keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |k|
37
102
  value = params[k]
38
103
  if value.is_a? Hash
39
- value.keys.collect {|sk| Rack::Utils.escape("#{k}[#{sk}]") + "=" + Rack::Utils.escape(value[sk].to_s)}
104
+ value.keys.collect {|sk| Typhoeus::Utils.escape("#{k}[#{sk}]") + "=" + Typhoeus::Utils.escape(value[sk].to_s)}
40
105
  elsif value.is_a? Array
41
- key = Rack::Utils.escape(k.to_s)
42
- value.collect { |v| "#{key}=#{Rack::Utils.escape(v.to_s)}" }.join('&')
106
+ key = Typhoeus::Utils.escape(k.to_s)
107
+ value.collect { |v| "#{key}[]=#{Typhoeus::Utils.escape(v.to_s)}" }.join('&')
43
108
  else
44
- "#{Rack::Utils.escape(k.to_s)}=#{Rack::Utils.escape(params[k].to_s)}"
109
+ "#{Typhoeus::Utils.escape(k.to_s)}=#{Typhoeus::Utils.escape(params[k].to_s)}"
45
110
  end
46
111
  end.flatten.join("&")
47
112
  end
48
-
113
+
49
114
  def on_complete(&block)
50
115
  @on_complete = block
51
116
  end
52
-
117
+
53
118
  def on_complete=(proc)
54
119
  @on_complete = proc
55
120
  end
56
-
121
+
57
122
  def after_complete(&block)
58
123
  @after_complete = block
59
124
  end
60
-
125
+
61
126
  def after_complete=(proc)
62
127
  @after_complete = proc
63
128
  end
64
-
129
+
65
130
  def call_handlers
66
131
  if @on_complete
67
132
  @handled_response = @on_complete.call(response)
68
133
  call_after_complete
69
134
  end
70
135
  end
71
-
136
+
72
137
  def call_after_complete
73
- @after_complete.call(@handled_response) if @after_complete
138
+ @after_complete.call(@handled_response) if @after_complete
74
139
  end
75
-
140
+
76
141
  def handled_response=(val)
77
142
  @handled_response = val
78
143
  end
79
-
144
+
80
145
  def handled_response
81
146
  @handled_response || response
82
147
  end
83
-
148
+
149
+ def inspect
150
+ result = ":method => #{self.method.inspect},\n" <<
151
+ "\t:url => #{URI.parse(self.url).to_s}"
152
+ if self.body and !self.body.empty?
153
+ result << ",\n\t:body => #{self.body.inspect}"
154
+ end
155
+
156
+ if self.params and !self.params.empty?
157
+ result << ",\n\t:params => #{self.params.inspect}"
158
+ end
159
+
160
+ if self.headers and !self.headers.empty?
161
+ result << ",\n\t:headers => #{self.headers.inspect}"
162
+ end
163
+
164
+ result
165
+ end
166
+
84
167
  def cache_key
85
168
  Digest::SHA1.hexdigest(url)
86
169
  end
87
-
170
+
88
171
  def self.run(url, params)
89
172
  r = new(url, params)
90
173
  Typhoeus::Hydra.hydra.queue r
91
174
  Typhoeus::Hydra.hydra.run
92
175
  r.response
93
176
  end
94
-
95
- def self.get(url, params)
177
+
178
+ def self.get(url, params = {})
96
179
  run(url, params.merge(:method => :get))
97
180
  end
98
-
99
- def self.post(url, params)
181
+
182
+ def self.post(url, params = {})
100
183
  run(url, params.merge(:method => :post))
101
184
  end
102
-
103
- def self.put(url, params)
185
+
186
+ def self.put(url, params = {})
104
187
  run(url, params.merge(:method => :put))
105
188
  end
106
-
107
- def self.delete(url, params)
189
+
190
+ def self.delete(url, params = {})
108
191
  run(url, params.merge(:method => :delete))
109
192
  end
193
+
194
+ def self.head(url, params = {})
195
+ run(url, params.merge(:method => :head))
196
+ end
110
197
  end
111
- end
198
+ end
@@ -1,19 +1,76 @@
1
1
  module Typhoeus
2
2
  class Response
3
- attr_accessor :request
3
+ attr_accessor :request, :mock
4
4
  attr_reader :code, :headers, :body, :time,
5
5
  :requested_url, :requested_remote_method,
6
- :requested_http_method, :start_time
7
-
6
+ :requested_http_method, :start_time,
7
+ :effective_url
8
+ attr_writer :headers_hash
9
+
8
10
  def initialize(params = {})
9
11
  @code = params[:code]
10
- @headers = params[:headers]
12
+ @status_message = params[:status_message]
13
+ @http_version = params[:http_version]
14
+ @headers = params[:headers] || ''
11
15
  @body = params[:body]
12
16
  @time = params[:time]
13
17
  @requested_url = params[:requested_url]
14
18
  @requested_http_method = params[:requested_http_method]
15
19
  @start_time = params[:start_time]
16
20
  @request = params[:request]
21
+ @effective_url = params[:effective_url]
22
+ @mock = params[:mock] || false # default
23
+ @headers_hash = NormalizedHeaderHash.new(params[:headers_hash]) if params[:headers_hash]
17
24
  end
25
+
26
+ # Returns true if this is a mock response.
27
+ def mock?
28
+ @mock
29
+ end
30
+
31
+ def headers_hash
32
+ @headers_hash ||= begin
33
+ headers.split("\n").map {|o| o.strip}.inject(Typhoeus::NormalizedHeaderHash.new) do |hash, o|
34
+ if o.empty? || o =~ /^HTTP\/[\d\.]+/
35
+ hash
36
+ else
37
+ i = o.index(":") || o.size
38
+ key = o.slice(0, i)
39
+ value = o.slice(i + 1, o.size)
40
+ value = value.strip unless value.nil?
41
+ if hash.has_key? key
42
+ hash[key] = [hash[key], value].flatten
43
+ else
44
+ hash[key] = value
45
+ end
46
+
47
+ hash
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def status_message
54
+ # http://rubular.com/r/eAr1oVYsVa
55
+ @status_message ||= first_header_line ? first_header_line[/\d{3} (.*)$/, 1].chomp : nil
56
+ end
57
+
58
+ def http_version
59
+ @http_version ||= first_header_line ? first_header_line[/HTTP\/(\S+)/, 1] : nil
60
+ end
61
+
62
+ def success?
63
+ @code >= 200 && @code < 300
64
+ end
65
+
66
+ def modified?
67
+ @code != 304
68
+ end
69
+
70
+ private
71
+
72
+ def first_header_line
73
+ @first_header_line ||= headers.split("\n").first
74
+ end
18
75
  end
19
76
  end
@@ -0,0 +1,20 @@
1
+ module Typhoeus
2
+ class Service
3
+ def initialize(host, port)
4
+ @host = host
5
+ @port = port
6
+ end
7
+
8
+ def get(resource, params)
9
+ end
10
+
11
+ def put(resource, params)
12
+ end
13
+
14
+ def post(resource, params)
15
+ end
16
+
17
+ def delete(resource, params)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module Typhoeus
2
+ module Utils
3
+ # Taken from Rack::Utils, 1.2.1 to remove Rack dependency.
4
+ def escape(s)
5
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/u) {
6
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
7
+ }.tr(' ', '+')
8
+ end
9
+ module_function :escape
10
+
11
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
12
+ # String#bytesize under 1.9.
13
+ if ''.respond_to?(:bytesize)
14
+ def bytesize(string)
15
+ string.bytesize
16
+ end
17
+ else
18
+ def bytesize(string)
19
+ string.size
20
+ end
21
+ end
22
+ module_function :bytesize
23
+ end
24
+ end
data/lib/typhoeus.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  $LOAD_PATH.unshift(File.dirname(__FILE__)) unless $LOAD_PATH.include?(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + "/../ext")
2
3
 
3
- require 'rack/utils'
4
4
  require 'digest/sha2'
5
+ require 'typhoeus/utils'
6
+ require 'typhoeus/normalized_header_hash'
5
7
  require 'typhoeus/easy'
6
8
  require 'typhoeus/multi'
7
9
  require 'typhoeus/native'
@@ -12,25 +14,26 @@ require 'typhoeus/remote_proxy_object'
12
14
  require 'typhoeus/response'
13
15
  require 'typhoeus/request'
14
16
  require 'typhoeus/hydra'
17
+ require 'typhoeus/hydra_mock'
15
18
 
16
19
  module Typhoeus
17
- VERSION = "0.1.6"
18
-
20
+ VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").chomp
21
+
19
22
  def self.easy_object_pool
20
23
  @easy_objects ||= []
21
24
  end
22
-
25
+
23
26
  def self.init_easy_object_pool
24
27
  20.times do
25
28
  easy_object_pool << Typhoeus::Easy.new
26
29
  end
27
30
  end
28
-
31
+
29
32
  def self.release_easy_object(easy)
30
33
  easy.reset
31
34
  easy_object_pool << easy
32
35
  end
33
-
36
+
34
37
  def self.get_easy_object
35
38
  if easy_object_pool.empty?
36
39
  Typhoeus::Easy.new
@@ -38,12 +41,12 @@ module Typhoeus
38
41
  easy_object_pool.pop
39
42
  end
40
43
  end
41
-
44
+
42
45
  def self.add_easy_request(easy_object)
43
46
  Thread.current[:curl_multi] ||= Typhoeus::Multi.new
44
47
  Thread.current[:curl_multi].add(easy_object)
45
48
  end
46
-
49
+
47
50
  def self.perform_easy_requests
48
51
  multi = Thread.current[:curl_multi]
49
52
  start_time = Time.now
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # go to ext/typhoeus and run ruby extconf.rb && make before running
3
+ # this.
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + "/../ext")
6
+ require File.dirname(__FILE__) + "/../lib/typhoeus"
7
+
8
+ klass = Class.new { include Typhoeus }
9
+
10
+ loops = ENV['LOOPS'].to_i
11
+ url = ARGV.first || (raise "requires URL!")
12
+
13
+ loops.times do |i|
14
+ puts "On loop #{i}" if i % 10 == 0
15
+ results = []
16
+ 5.times do
17
+ results << klass.get(url)
18
+ end
19
+
20
+ # fire requests
21
+ results[0].code
22
+ end
23
+
24
+ puts "Ran #{loops} loops on #{url}!"
@@ -0,0 +1,60 @@
1
+ <result_set>
2
+ <ttl>20</ttl>
3
+ <result>
4
+ <id>1</id>
5
+ <name>hello</name>
6
+ <description>
7
+ this is a long description for a text field of some kind.
8
+ this is a long description for a text field of some kind.
9
+ this is a long description for a text field of some kind.
10
+ this is a long description for a text field of some kind.
11
+ this is a long description for a text field of some kind.
12
+ this is a long description for a text field of some kind.
13
+ this is a long description for a text field of some kind.
14
+ this is a long description for a text field of some kind.
15
+ this is a long description for a text field of some kind.
16
+ this is a long description for a text field of some kind.
17
+ this is a long description for a text field of some kind.
18
+ this is a long description for a text field of some kind.
19
+ this is a long description for a text field of some kind.
20
+ </description>
21
+ </result>
22
+ <result>
23
+ <id>2</id>
24
+ <name>hello</name>
25
+ <description>
26
+ this is a long description for a text field of some kind.
27
+ this is a long description for a text field of some kind.
28
+ this is a long description for a text field of some kind.
29
+ this is a long description for a text field of some kind.
30
+ this is a long description for a text field of some kind.
31
+ this is a long description for a text field of some kind.
32
+ this is a long description for a text field of some kind.
33
+ this is a long description for a text field of some kind.
34
+ this is a long description for a text field of some kind.
35
+ this is a long description for a text field of some kind.
36
+ this is a long description for a text field of some kind.
37
+ this is a long description for a text field of some kind.
38
+ this is a long description for a text field of some kind.
39
+ </description>
40
+ </result>
41
+ <result>
42
+ <id>3</id>
43
+ <name>hello</name>
44
+ <description>
45
+ this is a long description for a text field of some kind.
46
+ this is a long description for a text field of some kind.
47
+ this is a long description for a text field of some kind.
48
+ this is a long description for a text field of some kind.
49
+ this is a long description for a text field of some kind.
50
+ this is a long description for a text field of some kind.
51
+ this is a long description for a text field of some kind.
52
+ this is a long description for a text field of some kind.
53
+ this is a long description for a text field of some kind.
54
+ this is a long description for a text field of some kind.
55
+ this is a long description for a text field of some kind.
56
+ this is a long description for a text field of some kind.
57
+ this is a long description for a text field of some kind.
58
+ </description>
59
+ </result>
60
+ </result_set>
data/spec/servers/app.rb CHANGED
@@ -1,6 +1,8 @@
1
+ #!/usr/bin/env ruby
1
2
  require 'rubygems'
2
3
  require 'sinatra'
3
4
  require 'json'
5
+ require 'zlib'
4
6
 
5
7
  @@fail_count = 0
6
8
  get '/fail/:number' do
@@ -16,11 +18,56 @@ get '/fail_forever' do
16
18
  error 500, "oh noes!"
17
19
  end
18
20
 
21
+ get '/redirect' do
22
+ redirect '/'
23
+ end
24
+
25
+ get '/bad_redirect' do
26
+ redirect '/bad_redirect'
27
+ end
28
+
29
+ get '/auth_basic/:username/:password' do
30
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
31
+ # Check that we've got a basic auth, and that it's credentials match the ones
32
+ # provided in the request
33
+ if @auth.provided? && @auth.basic? && @auth.credentials == [ params[:username], params[:password] ]
34
+ # auth is valid - confirm it
35
+ true
36
+ else
37
+ # invalid auth - request the authentication
38
+ response['WWW-Authenticate'] = %(Basic realm="Testing HTTP Auth")
39
+ throw(:halt, [401, "Not authorized\n"])
40
+ end
41
+ end
42
+
43
+ get '/auth_ntlm' do
44
+ # we're just checking for the existence if NTLM auth header here. It's validation
45
+ # is too troublesome and really doesn't bother is much, it's up to libcurl to make
46
+ # it valid
47
+ response['WWW-Authenticate'] = 'NTLM'
48
+ is_ntlm_auth = /^NTLM/ =~ request.env['HTTP_AUTHORIZATION']
49
+ true if is_ntlm_auth
50
+ throw(:halt, [401, "Not authorized\n"]) if !is_ntlm_auth
51
+ end
52
+
53
+ get '/gzipped' do
54
+ req_env = request.env.to_json
55
+ z = Zlib::Deflate.new
56
+ gzipped_env = z.deflate(req_env, Zlib::FINISH)
57
+ z.close
58
+ response['Content-Encoding'] = 'gzip'
59
+ gzipped_env
60
+ end
61
+
19
62
  get '/**' do
20
63
  sleep params["delay"].to_i if params.has_key?("delay")
21
64
  request.env.merge!(:body => request.body.read).to_json
22
65
  end
23
66
 
67
+ head '/**' do
68
+ sleep params["delay"].to_i if params.has_key?("delay")
69
+ end
70
+
24
71
  put '/**' do
25
72
  puts request.inspect
26
73
  request.env.merge!(:body => request.body.read).to_json
@@ -34,4 +81,4 @@ end
34
81
  delete '/**' do
35
82
  puts request.inspect
36
83
  request.env.merge!(:body => request.body.read).to_json
37
- end
84
+ end
data/spec/spec_helper.rb CHANGED
@@ -8,4 +8,4 @@ begin require "redgreen" unless ENV['TM_CURRENT_LINE']; rescue LoadError; end
8
8
  path = File.expand_path(File.dirname(__FILE__) + "/../lib/")
9
9
  $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
10
10
 
11
- require "lib/typhoeus"
11
+ require path + '/typhoeus'