hhry-typhoeus 0.4.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.
- data/CHANGELOG.md +93 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +455 -0
- data/Rakefile +23 -0
- data/lib/typhoeus.rb +57 -0
- data/lib/typhoeus/curl.rb +453 -0
- data/lib/typhoeus/easy.rb +115 -0
- data/lib/typhoeus/easy/auth.rb +14 -0
- data/lib/typhoeus/easy/callbacks.rb +33 -0
- data/lib/typhoeus/easy/ffi_helper.rb +61 -0
- data/lib/typhoeus/easy/infos.rb +86 -0
- data/lib/typhoeus/easy/options.rb +115 -0
- data/lib/typhoeus/easy/proxy.rb +20 -0
- data/lib/typhoeus/easy/ssl.rb +82 -0
- data/lib/typhoeus/filter.rb +28 -0
- data/lib/typhoeus/form.rb +61 -0
- data/lib/typhoeus/header.rb +54 -0
- data/lib/typhoeus/hydra.rb +246 -0
- data/lib/typhoeus/hydra/callbacks.rb +24 -0
- data/lib/typhoeus/hydra/connect_options.rb +61 -0
- data/lib/typhoeus/hydra/stubbing.rb +68 -0
- data/lib/typhoeus/hydra_mock.rb +131 -0
- data/lib/typhoeus/multi.rb +146 -0
- data/lib/typhoeus/param_processor.rb +43 -0
- data/lib/typhoeus/remote.rb +310 -0
- data/lib/typhoeus/remote_method.rb +108 -0
- data/lib/typhoeus/remote_proxy_object.rb +50 -0
- data/lib/typhoeus/request.rb +278 -0
- data/lib/typhoeus/response.rb +122 -0
- data/lib/typhoeus/utils.rb +58 -0
- data/lib/typhoeus/version.rb +3 -0
- metadata +178 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Hydra
|
3
|
+
module Callbacks
|
4
|
+
def self.extended(base)
|
5
|
+
class << base
|
6
|
+
attr_accessor :global_hooks
|
7
|
+
end
|
8
|
+
base.global_hooks = Hash.new { |h, k| h[k] = [] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def after_request_before_on_complete(&block)
|
12
|
+
global_hooks[:after_request_before_on_complete] << block
|
13
|
+
end
|
14
|
+
|
15
|
+
def run_global_hooks_for(name, request)
|
16
|
+
global_hooks[name].each { |hook| hook.call(request) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear_global_hooks
|
20
|
+
global_hooks.clear
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Hydra
|
3
|
+
class NetConnectNotAllowedError < StandardError; end
|
4
|
+
|
5
|
+
module ConnectOptions
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
# This method checks to see if we should raise an error on
|
11
|
+
# a request.
|
12
|
+
#
|
13
|
+
# @raises NetConnectNotAllowedError
|
14
|
+
def check_allow_net_connect!(request)
|
15
|
+
return if Typhoeus::Hydra.allow_net_connect?
|
16
|
+
return if Typhoeus::Hydra.ignore_hosts.include?(request.host_domain)
|
17
|
+
|
18
|
+
raise NetConnectNotAllowedError, "Real HTTP requests are not allowed. Unregistered request: #{request.inspect}"
|
19
|
+
end
|
20
|
+
private :check_allow_net_connect!
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def self.extended(base)
|
24
|
+
class << base
|
25
|
+
attr_accessor :allow_net_connect
|
26
|
+
attr_accessor :ignore_localhost
|
27
|
+
end
|
28
|
+
base.allow_net_connect = true
|
29
|
+
base.ignore_localhost = false
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns whether we allow external HTTP connections.
|
33
|
+
# Useful for mocking/tests.
|
34
|
+
#
|
35
|
+
# @return [boolean] true/false
|
36
|
+
def allow_net_connect?
|
37
|
+
allow_net_connect
|
38
|
+
end
|
39
|
+
|
40
|
+
def ignore_localhost?
|
41
|
+
ignore_localhost
|
42
|
+
end
|
43
|
+
|
44
|
+
def ignore_hosts
|
45
|
+
@ignore_hosts ||= []
|
46
|
+
|
47
|
+
if ignore_localhost?
|
48
|
+
@ignore_hosts + Typhoeus::Request::LOCALHOST_ALIASES
|
49
|
+
else
|
50
|
+
@ignore_hosts
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def ignore_hosts=(hosts)
|
55
|
+
@ignore_hosts = hosts
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Hydra
|
3
|
+
module Stubbing
|
4
|
+
module SharedMethods
|
5
|
+
def stub(method, url, options = {})
|
6
|
+
stubs << HydraMock.new(url, method, options)
|
7
|
+
stubs.last
|
8
|
+
end
|
9
|
+
|
10
|
+
def clear_stubs
|
11
|
+
self.stubs = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def register_stub_finder(&block)
|
15
|
+
stub_finders << block
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_stub_from_request(request)
|
19
|
+
stub_finders.each do |finder|
|
20
|
+
if response = finder.call(request)
|
21
|
+
mock = HydraMock.new(/.*/, :any)
|
22
|
+
mock.and_return(response)
|
23
|
+
return mock
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
stubs.detect { |stub| stub.matches?(request) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def stub_finders
|
31
|
+
@stub_finders ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.extended(base)
|
35
|
+
class << base
|
36
|
+
attr_accessor :stubs
|
37
|
+
end
|
38
|
+
base.stubs = []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.included(base)
|
43
|
+
base.extend(SharedMethods)
|
44
|
+
base.class_eval do
|
45
|
+
attr_accessor :stubs
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def assign_to_stub(request)
|
50
|
+
m = find_stub_from_request(request)
|
51
|
+
|
52
|
+
# Fallback to global stubs.
|
53
|
+
m ||= self.class.find_stub_from_request(request)
|
54
|
+
|
55
|
+
if m
|
56
|
+
m.add_request(request)
|
57
|
+
@active_stubs << m
|
58
|
+
m
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
private :assign_to_stub
|
64
|
+
|
65
|
+
include SharedMethods
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class HydraMock
|
3
|
+
attr_reader :url, :method, :requests, :uri
|
4
|
+
|
5
|
+
def initialize(url, method, options = {})
|
6
|
+
@url = url
|
7
|
+
@uri = URI.parse(url) if url.kind_of?(String)
|
8
|
+
@method = method
|
9
|
+
@requests = []
|
10
|
+
@options = options
|
11
|
+
if @options[:headers]
|
12
|
+
@options[:headers] = Typhoeus::Header.new(@options[:headers])
|
13
|
+
end
|
14
|
+
|
15
|
+
@current_response_index = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def body
|
19
|
+
@options[:body]
|
20
|
+
end
|
21
|
+
|
22
|
+
def body?
|
23
|
+
@options.has_key?(:body)
|
24
|
+
end
|
25
|
+
|
26
|
+
def headers
|
27
|
+
@options[:headers]
|
28
|
+
end
|
29
|
+
|
30
|
+
def headers?
|
31
|
+
@options.has_key?(:headers)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_request(request)
|
35
|
+
@requests << request
|
36
|
+
end
|
37
|
+
|
38
|
+
def and_return(val)
|
39
|
+
if val.respond_to?(:each)
|
40
|
+
@responses = val
|
41
|
+
else
|
42
|
+
@responses = [val]
|
43
|
+
end
|
44
|
+
|
45
|
+
# make sure to mark them as a mock.
|
46
|
+
@responses.each { |r| r.mock = true }
|
47
|
+
|
48
|
+
val
|
49
|
+
end
|
50
|
+
|
51
|
+
def response
|
52
|
+
if @current_response_index == (@responses.length - 1)
|
53
|
+
@responses.last
|
54
|
+
else
|
55
|
+
value = @responses[@current_response_index]
|
56
|
+
@current_response_index += 1
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def matches?(request)
|
62
|
+
if !method_matches?(request) or !url_matches?(request)
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
|
66
|
+
if body?
|
67
|
+
return false unless body_matches?(request)
|
68
|
+
end
|
69
|
+
|
70
|
+
if headers?
|
71
|
+
return false unless headers_match?(request)
|
72
|
+
end
|
73
|
+
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def method_matches?(request)
|
79
|
+
self.method == :any or self.method == request.method
|
80
|
+
end
|
81
|
+
|
82
|
+
def url_matches?(request)
|
83
|
+
if url.kind_of?(String)
|
84
|
+
request_uri = URI.parse(request.url)
|
85
|
+
request_uri == self.uri
|
86
|
+
else
|
87
|
+
self.url =~ request.url
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def body_matches?(request)
|
92
|
+
!request.body.nil? && !request.body.empty? && request.body == self.body
|
93
|
+
end
|
94
|
+
|
95
|
+
def headers_match?(request)
|
96
|
+
request_headers = Header.new(request.headers)
|
97
|
+
|
98
|
+
if empty_headers?(self.headers)
|
99
|
+
empty_headers?(request_headers)
|
100
|
+
else
|
101
|
+
return false if empty_headers?(request_headers)
|
102
|
+
|
103
|
+
headers.each do |key, value|
|
104
|
+
return false unless header_value_matches?(value, request_headers[key])
|
105
|
+
end
|
106
|
+
|
107
|
+
true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def header_value_matches?(mock_value, request_value)
|
112
|
+
mock_arr = mock_value.is_a?(Array) ? mock_value : [mock_value]
|
113
|
+
request_arr = request_value.is_a?(Array) ? request_value : [request_value]
|
114
|
+
|
115
|
+
return false unless mock_arr.size == request_arr.size
|
116
|
+
mock_arr.all? do |value|
|
117
|
+
request_arr.any? { |a| value === a }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def empty_headers?(headers)
|
122
|
+
# We consider the default User-Agent header to be empty since
|
123
|
+
# Typhoeus always adds that.
|
124
|
+
headers.nil? || headers.empty? || default_typhoeus_headers?(headers)
|
125
|
+
end
|
126
|
+
|
127
|
+
def default_typhoeus_headers?(headers)
|
128
|
+
headers.size == 1 && headers['User-Agent'] == Typhoeus::USER_AGENT
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Multi
|
3
|
+
attr_reader :easy_handles
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
Curl.init
|
7
|
+
|
8
|
+
@handle = Curl.multi_init
|
9
|
+
@active = 0
|
10
|
+
@running = 0
|
11
|
+
@easy_handles = []
|
12
|
+
|
13
|
+
@timeout = ::FFI::MemoryPointer.new(:long)
|
14
|
+
@timeval = Curl::Timeval.new
|
15
|
+
@fd_read = Curl::FDSet.new
|
16
|
+
@fd_write = Curl::FDSet.new
|
17
|
+
@fd_excep = Curl::FDSet.new
|
18
|
+
@max_fd = ::FFI::MemoryPointer.new(:int)
|
19
|
+
|
20
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(self))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.finalizer(multi)
|
24
|
+
proc { Curl.multi_cleanup(multi.handle) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(easy)
|
28
|
+
raise "trying to add easy handle twice" if @easy_handles.include?(easy)
|
29
|
+
easy.set_headers() if easy.headers.empty?
|
30
|
+
|
31
|
+
code = Curl.multi_add_handle(@handle, easy.handle)
|
32
|
+
raise RuntimeError.new("An error occured adding the handle: #{code}: #{Curl.multi_strerror(code)}") if code != :call_multi_perform and code != :ok
|
33
|
+
|
34
|
+
do_perform if code == :call_multi_perform
|
35
|
+
|
36
|
+
@active += 1
|
37
|
+
@easy_handles << easy
|
38
|
+
easy
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove(easy)
|
42
|
+
if @easy_handles.include?(easy)
|
43
|
+
@active -= 1
|
44
|
+
Curl.multi_remove_handle(@handle, easy.handle)
|
45
|
+
@easy_handles.delete(easy)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def perform
|
50
|
+
while @active > 0
|
51
|
+
run
|
52
|
+
while @running > 0
|
53
|
+
# get the curl-suggested timeout
|
54
|
+
code = Curl.multi_timeout(@handle, @timeout)
|
55
|
+
raise RuntimeError.new("an error occured getting the timeout: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
|
56
|
+
timeout = @timeout.read_long
|
57
|
+
if timeout == 0 # no delay
|
58
|
+
run
|
59
|
+
next
|
60
|
+
elsif timeout < 0
|
61
|
+
timeout = 1
|
62
|
+
end
|
63
|
+
|
64
|
+
# load the fd sets from the multi handle
|
65
|
+
@fd_read.clear
|
66
|
+
@fd_write.clear
|
67
|
+
@fd_excep.clear
|
68
|
+
code = Curl.multi_fdset(@handle, @fd_read, @fd_write, @fd_excep, @max_fd)
|
69
|
+
raise RuntimeError.new("an error occured getting the fdset: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
|
70
|
+
|
71
|
+
max_fd = @max_fd.read_int
|
72
|
+
if max_fd == -1
|
73
|
+
# curl is doing something special so let it run for a moment
|
74
|
+
sleep(0.001)
|
75
|
+
else
|
76
|
+
@timeval[:sec] = timeout / 1000
|
77
|
+
@timeval[:usec] = (timeout * 1000) % 1000000
|
78
|
+
|
79
|
+
code = Curl.select(max_fd + 1, @fd_read, @fd_write, @fd_excep, @timeval)
|
80
|
+
raise RuntimeError.new("error on thread select: #{::FFI.errno}") if code < 0
|
81
|
+
end
|
82
|
+
|
83
|
+
run
|
84
|
+
end
|
85
|
+
end
|
86
|
+
reset_easy_handles
|
87
|
+
end
|
88
|
+
|
89
|
+
def fire_and_forget
|
90
|
+
run
|
91
|
+
end
|
92
|
+
|
93
|
+
# check for finished easy handles and remove from the multi handle
|
94
|
+
def read_info
|
95
|
+
msgs_left = ::FFI::MemoryPointer.new(:int)
|
96
|
+
while not (msg = Curl.multi_info_read(@handle, msgs_left)).null?
|
97
|
+
next if msg[:code] != :done
|
98
|
+
|
99
|
+
easy = @easy_handles.find {|easy| easy.handle == msg[:easy_handle] }
|
100
|
+
next if not easy
|
101
|
+
|
102
|
+
response_code = ::FFI::MemoryPointer.new(:long)
|
103
|
+
response_code.write_long(-1)
|
104
|
+
Curl.easy_getinfo(easy.handle, :response_code, response_code)
|
105
|
+
response_code = response_code.read_long
|
106
|
+
remove(easy)
|
107
|
+
|
108
|
+
easy.curl_return_code = msg[:data][:code]
|
109
|
+
if easy.curl_return_code != 0 then easy.failure
|
110
|
+
elsif (200..299).member?(response_code) or response_code == 0 then easy.success
|
111
|
+
else easy.failure
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def cleanup
|
117
|
+
Curl.multi_cleanup(@handle)
|
118
|
+
@active = 0
|
119
|
+
@running = 0
|
120
|
+
@easy_handles = []
|
121
|
+
end
|
122
|
+
|
123
|
+
def reset_easy_handles
|
124
|
+
@easy_handles.dup.each do |easy|
|
125
|
+
remove(easy)
|
126
|
+
yield easy if block_given?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# called by perform and fire_and_forget
|
133
|
+
def run
|
134
|
+
begin code = do_perform end while code == :call_multi_perform
|
135
|
+
raise RuntimeError.new("an error occured while running perform: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
|
136
|
+
read_info
|
137
|
+
end
|
138
|
+
|
139
|
+
def do_perform
|
140
|
+
running = ::FFI::MemoryPointer.new(:int)
|
141
|
+
code = Curl.multi_perform(@handle, running)
|
142
|
+
@running = running.read_int
|
143
|
+
code
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Typhoeus
|
4
|
+
class ParamProcessor
|
5
|
+
class << self
|
6
|
+
def traverse_params_hash(hash, result = nil, current_key = nil)
|
7
|
+
result ||= { :files => [], :params => [] }
|
8
|
+
|
9
|
+
hash.keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
|
10
|
+
new_key = (current_key ? "#{current_key}[#{key}]" : key).to_s
|
11
|
+
current_value = hash[key]
|
12
|
+
process_value current_value, :result => result, :new_key => new_key
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
def process_value(current_value, options)
|
18
|
+
result = options[:result]
|
19
|
+
new_key = options[:new_key]
|
20
|
+
|
21
|
+
case current_value
|
22
|
+
when Hash
|
23
|
+
traverse_params_hash(current_value, result, new_key)
|
24
|
+
when Array
|
25
|
+
current_value.each do |v|
|
26
|
+
result[:params] << [new_key, v.to_s]
|
27
|
+
end
|
28
|
+
when File, Tempfile
|
29
|
+
filename = File.basename(current_value.path)
|
30
|
+
types = MIME::Types.type_for(filename)
|
31
|
+
result[:files] << [
|
32
|
+
new_key,
|
33
|
+
filename,
|
34
|
+
types.empty? ? 'application/octet-stream' : types[0].to_s,
|
35
|
+
File.expand_path(current_value.path)
|
36
|
+
]
|
37
|
+
else
|
38
|
+
result[:params] << [new_key, current_value.to_s]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|