pezra-resourceful 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/Manifest +34 -0
- data/README.markdown +84 -0
- data/Rakefile +91 -0
- data/lib/resourceful.rb +18 -0
- data/lib/resourceful/authentication_manager.rb +108 -0
- data/lib/resourceful/cache_manager.rb +240 -0
- data/lib/resourceful/exceptions.rb +34 -0
- data/lib/resourceful/header.rb +126 -0
- data/lib/resourceful/http_accessor.rb +104 -0
- data/lib/resourceful/memcache_cache_manager.rb +75 -0
- data/lib/resourceful/multipart_form_data.rb +51 -0
- data/lib/resourceful/net_http_adapter.rb +78 -0
- data/lib/resourceful/options_interpretation.rb +72 -0
- data/lib/resourceful/request.rb +234 -0
- data/lib/resourceful/resource.rb +178 -0
- data/lib/resourceful/response.rb +222 -0
- data/lib/resourceful/stubbed_resource_proxy.rb +47 -0
- data/lib/resourceful/util.rb +6 -0
- data/resourceful.gemspec +49 -0
- data/spec/acceptance/authorization_spec.rb +16 -0
- data/spec/acceptance/caching_spec.rb +190 -0
- data/spec/acceptance/header_spec.rb +24 -0
- data/spec/acceptance/redirecting_spec.rb +12 -0
- data/spec/acceptance/resource_spec.rb +84 -0
- data/spec/acceptance_shared_specs.rb +44 -0
- data/spec/caching_spec.rb +89 -0
- data/spec/old_acceptance_specs.rb +378 -0
- data/spec/resourceful/multipart_form_data_spec.rb +79 -0
- data/spec/resourceful/resource_spec.rb +20 -0
- data/spec/simple_sinatra_server.rb +74 -0
- data/spec/simple_sinatra_server_spec.rb +98 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +29 -0
- metadata +167 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
module Resourceful
|
3
|
+
|
4
|
+
# This exception used to indicate that the request did not succeed.
|
5
|
+
# The HTTP response is included so that the appropriate actions can
|
6
|
+
# be taken based on the details of that response
|
7
|
+
class UnsuccessfulHttpRequestError < Exception
|
8
|
+
attr_reader :http_response, :http_request
|
9
|
+
|
10
|
+
# Initialize new error from the HTTP request and response attributes.
|
11
|
+
def initialize(http_request, http_response)
|
12
|
+
super("#{http_request.method} request to <#{http_request.uri}> failed with code #{http_response.code}")
|
13
|
+
@http_request = http_request
|
14
|
+
@http_response = http_response
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MalformedServerResponse < UnsuccessfulHttpRequestError
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# Exception indicating that the server used a content coding scheme
|
23
|
+
# that Resourceful is unable to handle.
|
24
|
+
class UnsupportedContentCoding < Exception
|
25
|
+
end
|
26
|
+
|
27
|
+
# Raised when a body is supplied, but not a content-type header
|
28
|
+
class MissingContentType < ArgumentError
|
29
|
+
def initialize
|
30
|
+
super("A Content-Type must be specified when an entity-body is supplied.")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# A case-normalizing Hash, adjusting on [] and []=.
|
2
|
+
# Shamelessly swiped from Rack
|
3
|
+
module Resourceful
|
4
|
+
class Header < Hash
|
5
|
+
def initialize(hash={})
|
6
|
+
hash.each { |k, v| self[k] = v }
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_hash
|
10
|
+
{}.replace(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](k)
|
14
|
+
super capitalize(k)
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(k, v)
|
18
|
+
super capitalize(k), v
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_key?(k)
|
22
|
+
super capitalize(k)
|
23
|
+
end
|
24
|
+
|
25
|
+
def capitalize(k)
|
26
|
+
k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }.gsub('_', '-')
|
27
|
+
end
|
28
|
+
|
29
|
+
def each_field(&blk)
|
30
|
+
to_hash.each { |k,v|
|
31
|
+
blk.call capitalize(k), v
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
HEADERS = %w[
|
36
|
+
Accept
|
37
|
+
Accept-Charset
|
38
|
+
Accept-Encoding
|
39
|
+
Accept-Language
|
40
|
+
Accept-Ranges
|
41
|
+
Age
|
42
|
+
Allow
|
43
|
+
Authorization
|
44
|
+
Cache-Control
|
45
|
+
Connection
|
46
|
+
Content-Encoding
|
47
|
+
Content-Language
|
48
|
+
Content-Length
|
49
|
+
Content-Location
|
50
|
+
Content-MD5
|
51
|
+
Content-Range
|
52
|
+
Content-Type
|
53
|
+
Date
|
54
|
+
ETag
|
55
|
+
Expect
|
56
|
+
Expires
|
57
|
+
From
|
58
|
+
Host
|
59
|
+
If-Match
|
60
|
+
If-Modified-Since
|
61
|
+
If-None-Match
|
62
|
+
If-Range
|
63
|
+
If-Unmodified-Since
|
64
|
+
Keep-Alive
|
65
|
+
Last-Modified
|
66
|
+
Location
|
67
|
+
Max-Forwards
|
68
|
+
Pragma
|
69
|
+
Proxy-Authenticate
|
70
|
+
Proxy-Authorization
|
71
|
+
Range
|
72
|
+
Referer
|
73
|
+
Retry-After
|
74
|
+
Server
|
75
|
+
TE
|
76
|
+
Trailer
|
77
|
+
Transfer-Encoding
|
78
|
+
Upgrade
|
79
|
+
User-Agent
|
80
|
+
Vary
|
81
|
+
Via
|
82
|
+
Warning
|
83
|
+
WWW-Authenticate
|
84
|
+
]
|
85
|
+
|
86
|
+
HEADERS.each do |header|
|
87
|
+
const = header.upcase.gsub('-', '_')
|
88
|
+
meth = header.downcase.gsub('-', '_')
|
89
|
+
|
90
|
+
class_eval <<-RUBY, __FILE__, __LINE__
|
91
|
+
#{const} = "#{header}".freeze # ACCEPT = "accept".freeze
|
92
|
+
|
93
|
+
def #{meth} # def accept
|
94
|
+
self[#{const}] # self[ACCEPT]
|
95
|
+
end # end
|
96
|
+
|
97
|
+
def #{meth}=(str) # def accept=(str)
|
98
|
+
self[#{const}] = str # self[ACCEPT] = str
|
99
|
+
end # end
|
100
|
+
RUBY
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
HOP_BY_HOP_HEADERS = [
|
105
|
+
CONNECTION,
|
106
|
+
KEEP_ALIVE,
|
107
|
+
PROXY_AUTHENTICATE,
|
108
|
+
PROXY_AUTHORIZATION,
|
109
|
+
TE,
|
110
|
+
TRAILER,
|
111
|
+
TRANSFER_ENCODING,
|
112
|
+
UPGRADE
|
113
|
+
].freeze
|
114
|
+
|
115
|
+
NON_MODIFIABLE_HEADERS = [
|
116
|
+
CONTENT_LOCATION,
|
117
|
+
CONTENT_MD5,
|
118
|
+
ETAG,
|
119
|
+
LAST_MODIFIED,
|
120
|
+
EXPIRES
|
121
|
+
].freeze
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
require 'resourceful/options_interpretation'
|
4
|
+
require 'resourceful/authentication_manager'
|
5
|
+
require 'resourceful/cache_manager'
|
6
|
+
require 'resourceful/resource'
|
7
|
+
require 'resourceful/stubbed_resource_proxy'
|
8
|
+
|
9
|
+
module Resourceful
|
10
|
+
# This is an imitation Logger used when no real logger is
|
11
|
+
# registered. This allows most of the code to assume that there
|
12
|
+
# is always a logger available, which significantly improved the
|
13
|
+
# readability of the logging related code.
|
14
|
+
class BitBucketLogger
|
15
|
+
def warn(*args); end
|
16
|
+
def info(*args); end
|
17
|
+
def debug(*args); end
|
18
|
+
end
|
19
|
+
|
20
|
+
# This is the simplest logger. It just writes everything to STDOUT.
|
21
|
+
class StdOutLogger
|
22
|
+
def warn(*args); puts args; end
|
23
|
+
def info(*args); puts args; end
|
24
|
+
def debug(*args); puts args; end
|
25
|
+
end
|
26
|
+
|
27
|
+
# This class provides a simple interface to the functionality
|
28
|
+
# provided by the Resourceful library. Conceptually this object
|
29
|
+
# acts a collection of all the resources available via HTTP.
|
30
|
+
class HttpAccessor
|
31
|
+
include OptionsInterpretation
|
32
|
+
|
33
|
+
# A logger object to which messages about the activities of this
|
34
|
+
# object will be written. This should be an object that responds
|
35
|
+
# to +#info(message)+ and +#debug(message)+.
|
36
|
+
#
|
37
|
+
# Errors will not be logged. Instead an exception will be raised
|
38
|
+
# and the application code should log it if appropriate.
|
39
|
+
attr_accessor :logger, :cache_manager
|
40
|
+
|
41
|
+
attr_reader :auth_manager
|
42
|
+
attr_reader :user_agent_tokens
|
43
|
+
|
44
|
+
##
|
45
|
+
# The adapter this accessor will use to make the actual HTTP requests.
|
46
|
+
attr_reader :http_adapter
|
47
|
+
|
48
|
+
# Initializes a new HttpAccessor. Valid options:
|
49
|
+
#
|
50
|
+
# `:logger`
|
51
|
+
# : A Logger object that the new HTTP accessor should send log messages
|
52
|
+
#
|
53
|
+
# `:user_agent`
|
54
|
+
# : One or more additional user agent tokens to added to the user agent string.
|
55
|
+
#
|
56
|
+
# `:cache_manager`
|
57
|
+
# : The cache manager this accessor should use.
|
58
|
+
#
|
59
|
+
# `:authenticator`
|
60
|
+
# : Add a single authenticator for this accessor.
|
61
|
+
#
|
62
|
+
# `:authenticators`
|
63
|
+
# : Enumerable of the authenticators for this accessor.
|
64
|
+
#
|
65
|
+
# `http_adapter`
|
66
|
+
# : The HttpAdapter to be used by this accessor
|
67
|
+
#
|
68
|
+
#
|
69
|
+
def initialize(options = {})
|
70
|
+
@user_agent_tokens = [RESOURCEFUL_USER_AGENT_TOKEN]
|
71
|
+
@auth_manager = AuthenticationManager.new()
|
72
|
+
|
73
|
+
extract_opts(options) do |opts|
|
74
|
+
@user_agent_tokens.push(*opts.extract(:user_agent, :default => []) {|ua| [ua].flatten})
|
75
|
+
|
76
|
+
self.logger = opts.extract(:logger, :default => BitBucketLogger.new)
|
77
|
+
@cache_manager = opts.extract(:cache_manager, :default => NullCacheManager.new)
|
78
|
+
@http_adapter = opts.extract(:http_adapter, :default => lambda{NetHttpAdapter.new})
|
79
|
+
|
80
|
+
opts.extract(:authenticator, :required => false).tap{|a| add_authenticator(a) if a}
|
81
|
+
opts.extract(:authenticators, :default => []).each { |a| add_authenticator(a) }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns the string that identifies this HTTP accessor. If you
|
86
|
+
# want to add a token to the user agent string simply add the new
|
87
|
+
# token to the end of +#user_agent_tokens+.
|
88
|
+
def user_agent_string
|
89
|
+
user_agent_tokens.reverse.join(' ')
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a resource object representing the resource indicated
|
93
|
+
# by the specified URI. A resource object will be created if necessary.
|
94
|
+
def resource(uri, opts = {})
|
95
|
+
resource = Resource.new(self, uri, opts)
|
96
|
+
end
|
97
|
+
alias [] resource
|
98
|
+
|
99
|
+
# Adds an Authenticator to the set used by the accessor.
|
100
|
+
def add_authenticator(an_authenticator)
|
101
|
+
auth_manager.add_auth_handler(an_authenticator)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "resourceful/cache_manager"
|
2
|
+
|
3
|
+
require 'memcache'
|
4
|
+
|
5
|
+
module Resourceful
|
6
|
+
class MemcacheCacheManager < AbstractCacheManager
|
7
|
+
|
8
|
+
# Create a new Memcached backed cache manager
|
9
|
+
#
|
10
|
+
# @param [*String] memcache_servers
|
11
|
+
# list of all Memcached servers this cache manager should use.
|
12
|
+
def initialize(*memcache_servers)
|
13
|
+
@memcache = MemCache.new(memcache_servers, :multithread => true)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Finds a previously cached response to the provided request. The
|
17
|
+
# response returned may be stale.
|
18
|
+
#
|
19
|
+
# @param [Resourceful::Request] request
|
20
|
+
# The request for which we are looking for a response.
|
21
|
+
#
|
22
|
+
# @return [Resourceful::Response, nil]
|
23
|
+
# A (possibly stale) response for the request provided or nil if
|
24
|
+
# no matching response is found.
|
25
|
+
def lookup(request)
|
26
|
+
resp = cache_entries_for(request)[request]
|
27
|
+
return if resp.nil?
|
28
|
+
|
29
|
+
resp.authoritative = false
|
30
|
+
|
31
|
+
resp
|
32
|
+
end
|
33
|
+
|
34
|
+
# Store a response in the cache.
|
35
|
+
#
|
36
|
+
# This method is smart enough to not store responses that cannot be
|
37
|
+
# cached (Vary: * or Cache-Control: no-cache, private, ...)
|
38
|
+
#
|
39
|
+
# @param [Resourceful::Request] request
|
40
|
+
# The request used to obtain the response. This is needed so the
|
41
|
+
# values from the response's Vary header can be stored.
|
42
|
+
# @param [Resourceful::Response] response
|
43
|
+
# The response to be stored.
|
44
|
+
def store(request, response)
|
45
|
+
return unless response.cachable?
|
46
|
+
|
47
|
+
entries = cache_entries_for(request)
|
48
|
+
entries[request] = response
|
49
|
+
|
50
|
+
@memcache[request.to_mc_key] = entries
|
51
|
+
end
|
52
|
+
|
53
|
+
# Invalidates a all cached entries for a uri.
|
54
|
+
#
|
55
|
+
# This is used, for example, to invalidate the cache for a resource
|
56
|
+
# that gets POSTed to.
|
57
|
+
#
|
58
|
+
# @param [String] uri
|
59
|
+
# The uri of the resource to be invalidated
|
60
|
+
def invalidate(uri)
|
61
|
+
@memcache.delete(uri_hash(uri))
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
##
|
68
|
+
# The memcache proxy.
|
69
|
+
attr_reader :memcache
|
70
|
+
|
71
|
+
def cache_entries_for(a_request)
|
72
|
+
@memcache.get(uri_hash(a_request.uri)) || Resourceful::CacheEntryCollection.new
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
module Resourceful
|
3
|
+
class MultipartFormData
|
4
|
+
FileParamValue = Struct.new(:content, :file_name, :content_type)
|
5
|
+
|
6
|
+
def initialize()
|
7
|
+
@form_data = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(name, value)
|
11
|
+
form_data << [name, value]
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_file(name, file_name, content_type="application/octet-stream")
|
15
|
+
add(name, FileParamValue.new(File.new(file_name, 'r'), File.basename(file_name), content_type))
|
16
|
+
end
|
17
|
+
|
18
|
+
def content_type
|
19
|
+
"multipart/form-data; boundary=#{boundary}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def read
|
23
|
+
StringIO.new.tap do |out|
|
24
|
+
form_data.each do |key, val|
|
25
|
+
out << "\r\n--" << boundary
|
26
|
+
out << "\r\nContent-Disposition: form-data; name=\"#{key}\""
|
27
|
+
if val.kind_of?(FileParamValue)
|
28
|
+
out << "; filename=\"#{val.file_name}\""
|
29
|
+
out << "\r\nContent-Type: #{val.content_type}"
|
30
|
+
end
|
31
|
+
out << "\r\n\r\n"
|
32
|
+
if val.kind_of?(FileParamValue)
|
33
|
+
out << val.content.read
|
34
|
+
else
|
35
|
+
out << val.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
out << "\r\n--#{boundary}--"
|
39
|
+
end.string
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
attr_reader :form_data
|
44
|
+
|
45
|
+
def boundary
|
46
|
+
@boundary ||= (0..30).map{BOUNDARY_CHARS[rand(BOUNDARY_CHARS.length)]}.join
|
47
|
+
end
|
48
|
+
|
49
|
+
BOUNDARY_CHARS = [('a'..'z').to_a,('A'..'Z').to_a,(0..9).to_a].flatten
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
require 'pathname'
|
6
|
+
require 'resourceful/header'
|
7
|
+
|
8
|
+
module Addressable
|
9
|
+
class URI
|
10
|
+
def absolute_path
|
11
|
+
absolute_path = ""
|
12
|
+
absolute_path << self.path.to_s
|
13
|
+
absolute_path << "?#{self.query}" if self.query != nil
|
14
|
+
absolute_path << "##{self.fragment}" if self.fragment != nil
|
15
|
+
return absolute_path
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Resourceful
|
21
|
+
|
22
|
+
class NetHttpAdapter
|
23
|
+
# Make an HTTP request using the standard library net/http.
|
24
|
+
#
|
25
|
+
# Will use a proxy defined in the http_proxy environment variable, if set.
|
26
|
+
#
|
27
|
+
# @param [#read] body
|
28
|
+
# An IO-ish thing containing the body of the request
|
29
|
+
#
|
30
|
+
def make_request(method, uri, body = nil, header = nil)
|
31
|
+
uri = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri)
|
32
|
+
|
33
|
+
req = net_http_request_class(method).new(uri.absolute_path)
|
34
|
+
header.each_field { |k,v| req[k] = v } if header
|
35
|
+
https = ("https" == uri.scheme)
|
36
|
+
conn = Net::HTTP.Proxy(*proxy_details).new(uri.host, uri.port || (https ? 443 : 80))
|
37
|
+
conn.use_ssl = https
|
38
|
+
begin
|
39
|
+
conn.start
|
40
|
+
res = if body
|
41
|
+
conn.request(req, body.read)
|
42
|
+
else
|
43
|
+
conn.request(req)
|
44
|
+
end
|
45
|
+
ensure
|
46
|
+
conn.finish if conn.started?
|
47
|
+
end
|
48
|
+
|
49
|
+
[ Integer(res.code),
|
50
|
+
Resourceful::Header.new(res.header.to_hash),
|
51
|
+
res.body
|
52
|
+
]
|
53
|
+
ensure
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Parse proxy details from http_proxy environment variable
|
60
|
+
def proxy_details
|
61
|
+
proxy = Addressable::URI.parse(ENV["http_proxy"])
|
62
|
+
[proxy.host, proxy.port, proxy.user, proxy.password] if proxy
|
63
|
+
end
|
64
|
+
|
65
|
+
def net_http_request_class(method)
|
66
|
+
case method
|
67
|
+
when :get then Net::HTTP::Get
|
68
|
+
when :head then Net::HTTP::Head
|
69
|
+
when :post then Net::HTTP::Post
|
70
|
+
when :put then Net::HTTP::Put
|
71
|
+
when :delete then Net::HTTP::Delete
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|