rack-client 0.1.1 → 0.3.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 (76) hide show
  1. data/History.txt +2 -2
  2. data/README.textile +2 -2
  3. data/Rakefile +11 -5
  4. data/demo/demo_spec.rb +3 -3
  5. data/lib/rack/client.rb +29 -25
  6. data/lib/rack/client/adapter.rb +6 -0
  7. data/lib/rack/client/adapter/base.rb +57 -0
  8. data/lib/rack/client/adapter/simple.rb +49 -0
  9. data/lib/rack/client/auth/abstract/challenge.rb +53 -0
  10. data/lib/rack/client/auth/basic.rb +57 -0
  11. data/lib/rack/client/auth/digest/challenge.rb +38 -0
  12. data/lib/rack/client/auth/digest/md5.rb +78 -0
  13. data/lib/rack/client/auth/digest/params.rb +10 -0
  14. data/lib/rack/client/body.rb +12 -0
  15. data/lib/rack/client/cache.rb +19 -0
  16. data/lib/rack/client/cache/cachecontrol.rb +195 -0
  17. data/lib/rack/client/cache/context.rb +95 -0
  18. data/lib/rack/client/cache/entitystore.rb +77 -0
  19. data/lib/rack/client/cache/key.rb +51 -0
  20. data/lib/rack/client/cache/metastore.rb +133 -0
  21. data/lib/rack/client/cache/options.rb +147 -0
  22. data/lib/rack/client/cache/request.rb +46 -0
  23. data/lib/rack/client/cache/response.rb +62 -0
  24. data/lib/rack/client/cache/storage.rb +43 -0
  25. data/lib/rack/client/cookie_jar.rb +17 -0
  26. data/lib/rack/client/cookie_jar/context.rb +59 -0
  27. data/lib/rack/client/cookie_jar/cookie.rb +59 -0
  28. data/lib/rack/client/cookie_jar/cookiestore.rb +36 -0
  29. data/lib/rack/client/cookie_jar/options.rb +43 -0
  30. data/lib/rack/client/cookie_jar/request.rb +15 -0
  31. data/lib/rack/client/cookie_jar/response.rb +16 -0
  32. data/lib/rack/client/cookie_jar/storage.rb +34 -0
  33. data/lib/rack/client/dual_band.rb +13 -0
  34. data/lib/rack/client/follow_redirects.rb +47 -20
  35. data/lib/rack/client/handler.rb +10 -0
  36. data/lib/rack/client/handler/em-http.rb +66 -0
  37. data/lib/rack/client/handler/excon.rb +50 -0
  38. data/lib/rack/client/handler/net_http.rb +85 -0
  39. data/lib/rack/client/handler/typhoeus.rb +62 -0
  40. data/lib/rack/client/headers.rb +49 -0
  41. data/lib/rack/client/parser.rb +18 -0
  42. data/lib/rack/client/parser/base.rb +25 -0
  43. data/lib/rack/client/parser/body_collection.rb +50 -0
  44. data/lib/rack/client/parser/context.rb +15 -0
  45. data/lib/rack/client/parser/json.rb +54 -0
  46. data/lib/rack/client/parser/middleware.rb +8 -0
  47. data/lib/rack/client/parser/request.rb +21 -0
  48. data/lib/rack/client/parser/response.rb +19 -0
  49. data/lib/rack/client/parser/yaml.rb +52 -0
  50. data/lib/rack/client/response.rb +9 -0
  51. data/lib/rack/client/version.rb +5 -0
  52. data/spec/apps/example.org.ru +47 -3
  53. data/spec/auth/basic_spec.rb +69 -0
  54. data/spec/auth/digest/md5_spec.rb +69 -0
  55. data/spec/cache_spec.rb +40 -0
  56. data/spec/cookie_jar_spec.rb +37 -0
  57. data/spec/endpoint_spec.rb +4 -13
  58. data/spec/follow_redirect_spec.rb +27 -0
  59. data/spec/handler/async_api_spec.rb +69 -0
  60. data/spec/handler/em_http_spec.rb +22 -0
  61. data/spec/handler/excon_spec.rb +7 -0
  62. data/spec/handler/net_http_spec.rb +8 -0
  63. data/spec/handler/sync_api_spec.rb +55 -0
  64. data/spec/handler/typhoeus_spec.rb +22 -0
  65. data/spec/middleware_helper.rb +37 -0
  66. data/spec/middleware_spec.rb +48 -5
  67. data/spec/parser/json_spec.rb +22 -0
  68. data/spec/parser/yaml_spec.rb +22 -0
  69. data/spec/server_helper.rb +72 -0
  70. data/spec/spec_helper.rb +17 -3
  71. metadata +86 -31
  72. data/lib/rack/client/auth.rb +0 -13
  73. data/lib/rack/client/http.rb +0 -77
  74. data/spec/auth_spec.rb +0 -22
  75. data/spec/core_spec.rb +0 -123
  76. data/spec/redirect_spec.rb +0 -12
@@ -0,0 +1,10 @@
1
+ module Rack
2
+ module Client
3
+ module Auth
4
+ module Digest
5
+ class Params < Rack::Auth::Digest::Params
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Rack
2
+ module Client
3
+ module Parser
4
+ class Body < Array
5
+
6
+ def initialize(&proc)
7
+ @proc = proc
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ def self.new(backend, options={}, &b)
5
+ Context.new(backend, options, &b)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'rack/client/cache/options'
12
+ require 'rack/client/cache/cachecontrol'
13
+ require 'rack/client/cache/context'
14
+ require 'rack/client/cache/entitystore'
15
+ require 'rack/client/cache/key'
16
+ require 'rack/client/cache/metastore'
17
+ require 'rack/client/cache/request'
18
+ require 'rack/client/cache/response'
19
+ require 'rack/client/cache/storage'
@@ -0,0 +1,195 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+
5
+ # Parses a Cache-Control header and exposes the directives as a Hash.
6
+ # Directives that do not have values are set to +true+.
7
+ class CacheControl < Hash
8
+ def initialize(value=nil)
9
+ parse(value)
10
+ end
11
+
12
+ # Indicates that the response MAY be cached by any cache, even if it
13
+ # would normally be non-cacheable or cacheable only within a non-
14
+ # shared cache.
15
+ #
16
+ # A response may be considered public without this directive if the
17
+ # private directive is not set and the request does not include an
18
+ # Authorization header.
19
+ def public?
20
+ self['public']
21
+ end
22
+
23
+ # Indicates that all or part of the response message is intended for
24
+ # a single user and MUST NOT be cached by a shared cache. This
25
+ # allows an origin server to state that the specified parts of the
26
+ # response are intended for only one user and are not a valid
27
+ # response for requests by other users. A private (non-shared) cache
28
+ # MAY cache the response.
29
+ #
30
+ # Note: This usage of the word private only controls where the
31
+ # response may be cached, and cannot ensure the privacy of the
32
+ # message content.
33
+ def private?
34
+ self['private']
35
+ end
36
+
37
+ # When set in a response, a cache MUST NOT use the response to satisfy a
38
+ # subsequent request without successful revalidation with the origin
39
+ # server. This allows an origin server to prevent caching even by caches
40
+ # that have been configured to return stale responses to client requests.
41
+ #
42
+ # Note that this does not necessary imply that the response may not be
43
+ # stored by the cache, only that the cache cannot serve it without first
44
+ # making a conditional GET request with the origin server.
45
+ #
46
+ # When set in a request, the server MUST NOT use a cached copy for its
47
+ # response. This has quite different semantics compared to the no-cache
48
+ # directive on responses. When the client specifies no-cache, it causes
49
+ # an end-to-end reload, forcing each cache to update their cached copies.
50
+ def no_cache?
51
+ self['no-cache']
52
+ end
53
+
54
+ # Indicates that the response MUST NOT be stored under any circumstances.
55
+ #
56
+ # The purpose of the no-store directive is to prevent the
57
+ # inadvertent release or retention of sensitive information (for
58
+ # example, on backup tapes). The no-store directive applies to the
59
+ # entire message, and MAY be sent either in a response or in a
60
+ # request. If sent in a request, a cache MUST NOT store any part of
61
+ # either this request or any response to it. If sent in a response,
62
+ # a cache MUST NOT store any part of either this response or the
63
+ # request that elicited it. This directive applies to both non-
64
+ # shared and shared caches. "MUST NOT store" in this context means
65
+ # that the cache MUST NOT intentionally store the information in
66
+ # non-volatile storage, and MUST make a best-effort attempt to
67
+ # remove the information from volatile storage as promptly as
68
+ # possible after forwarding it.
69
+ #
70
+ # The purpose of this directive is to meet the stated requirements
71
+ # of certain users and service authors who are concerned about
72
+ # accidental releases of information via unanticipated accesses to
73
+ # cache data structures. While the use of this directive might
74
+ # improve privacy in some cases, we caution that it is NOT in any
75
+ # way a reliable or sufficient mechanism for ensuring privacy. In
76
+ # particular, malicious or compromised caches might not recognize or
77
+ # obey this directive, and communications networks might be
78
+ # vulnerable to eavesdropping.
79
+ def no_store?
80
+ self['no-store']
81
+ end
82
+
83
+ # The expiration time of an entity MAY be specified by the origin
84
+ # server using the Expires header (see section 14.21). Alternatively,
85
+ # it MAY be specified using the max-age directive in a response. When
86
+ # the max-age cache-control directive is present in a cached response,
87
+ # the response is stale if its current age is greater than the age
88
+ # value given (in seconds) at the time of a new request for that
89
+ # resource. The max-age directive on a response implies that the
90
+ # response is cacheable (i.e., "public") unless some other, more
91
+ # restrictive cache directive is also present.
92
+ #
93
+ # If a response includes both an Expires header and a max-age
94
+ # directive, the max-age directive overrides the Expires header, even
95
+ # if the Expires header is more restrictive. This rule allows an origin
96
+ # server to provide, for a given response, a longer expiration time to
97
+ # an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be
98
+ # useful if certain HTTP/1.0 caches improperly calculate ages or
99
+ # expiration times, perhaps due to desynchronized clocks.
100
+ #
101
+ # Many HTTP/1.0 cache implementations will treat an Expires value that
102
+ # is less than or equal to the response Date value as being equivalent
103
+ # to the Cache-Control response directive "no-cache". If an HTTP/1.1
104
+ # cache receives such a response, and the response does not include a
105
+ # Cache-Control header field, it SHOULD consider the response to be
106
+ # non-cacheable in order to retain compatibility with HTTP/1.0 servers.
107
+ #
108
+ # When the max-age directive is included in the request, it indicates
109
+ # that the client is willing to accept a response whose age is no
110
+ # greater than the specified time in seconds.
111
+ def max_age
112
+ self['max-age'].to_i if key?('max-age')
113
+ end
114
+
115
+ # If a response includes an s-maxage directive, then for a shared
116
+ # cache (but not for a private cache), the maximum age specified by
117
+ # this directive overrides the maximum age specified by either the
118
+ # max-age directive or the Expires header. The s-maxage directive
119
+ # also implies the semantics of the proxy-revalidate directive. i.e.,
120
+ # that the shared cache must not use the entry after it becomes stale
121
+ # to respond to a subsequent request without first revalidating it with
122
+ # the origin server. The s-maxage directive is always ignored by a
123
+ # private cache.
124
+ def shared_max_age
125
+ self['s-maxage'].to_i if key?('s-maxage')
126
+ end
127
+ alias_method :s_maxage, :shared_max_age
128
+
129
+ # Because a cache MAY be configured to ignore a server's specified
130
+ # expiration time, and because a client request MAY include a max-
131
+ # stale directive (which has a similar effect), the protocol also
132
+ # includes a mechanism for the origin server to require revalidation
133
+ # of a cache entry on any subsequent use. When the must-revalidate
134
+ # directive is present in a response received by a cache, that cache
135
+ # MUST NOT use the entry after it becomes stale to respond to a
136
+ # subsequent request without first revalidating it with the origin
137
+ # server. (I.e., the cache MUST do an end-to-end revalidation every
138
+ # time, if, based solely on the origin server's Expires or max-age
139
+ # value, the cached response is stale.)
140
+ #
141
+ # The must-revalidate directive is necessary to support reliable
142
+ # operation for certain protocol features. In all circumstances an
143
+ # HTTP/1.1 cache MUST obey the must-revalidate directive; in
144
+ # particular, if the cache cannot reach the origin server for any
145
+ # reason, it MUST generate a 504 (Gateway Timeout) response.
146
+ #
147
+ # Servers SHOULD send the must-revalidate directive if and only if
148
+ # failure to revalidate a request on the entity could result in
149
+ # incorrect operation, such as a silently unexecuted financial
150
+ # transaction. Recipients MUST NOT take any automated action that
151
+ # violates this directive, and MUST NOT automatically provide an
152
+ # unvalidated copy of the entity if revalidation fails.
153
+ def must_revalidate?
154
+ self['must-revalidate']
155
+ end
156
+
157
+ # The proxy-revalidate directive has the same meaning as the must-
158
+ # revalidate directive, except that it does not apply to non-shared
159
+ # user agent caches. It can be used on a response to an
160
+ # authenticated request to permit the user's cache to store and
161
+ # later return the response without needing to revalidate it (since
162
+ # it has already been authenticated once by that user), while still
163
+ # requiring proxies that service many users to revalidate each time
164
+ # (in order to make sure that each user has been authenticated).
165
+ # Note that such authenticated responses also need the public cache
166
+ # control directive in order to allow them to be cached at all.
167
+ def proxy_revalidate?
168
+ self['proxy-revalidate']
169
+ end
170
+
171
+ def to_s
172
+ bools, vals = [], []
173
+ each do |key,value|
174
+ if value == true
175
+ bools << key
176
+ elsif value
177
+ vals << "#{key}=#{value}"
178
+ end
179
+ end
180
+ (bools.sort + vals.sort).join(', ')
181
+ end
182
+
183
+ private
184
+ def parse(value)
185
+ return if value.nil? || value.empty?
186
+ value.delete(' ').split(',').inject(self) do |hash,part|
187
+ name, value = part.split('=', 2)
188
+ hash[name.downcase] = (value || true) unless name.empty?
189
+ hash
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,95 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class Context
5
+ include Options
6
+ include DualBand
7
+
8
+ def initialize(app, options = {})
9
+ @app = app
10
+
11
+ initialize_options options
12
+ end
13
+
14
+ def sync_call(env)
15
+ @trace = []
16
+ request = Request.new(options.merge(env))
17
+
18
+ return @app.call(env) unless request.cacheable?
19
+
20
+ response = Response.new(*@app.call(env = request.env))
21
+
22
+ if response.not_modified?
23
+ response = lookup(request)
24
+ elsif response.cacheable?
25
+ store(request, response)
26
+ else
27
+ pass
28
+ end
29
+
30
+ trace = @trace.join(', ')
31
+ response.headers['X-Rack-Client-Cache'] = trace
32
+
33
+ response.to_a
34
+ end
35
+
36
+ def async_call(env)
37
+ @trace = []
38
+ request = Request.new(options.merge(env))
39
+
40
+ if request.cacheable?
41
+ @app.call(env = request.env) do |response_parts|
42
+ response = Response.new(*response_parts)
43
+
44
+ if response.not_modified?
45
+ response = lookup(request)
46
+ elsif response.cacheable?
47
+ store(request, response)
48
+ else
49
+ pass
50
+ end
51
+
52
+ trace = @trace.join(', ')
53
+ response.headers['X-Rack-Client-Cache'] = trace
54
+
55
+ yield response.to_a
56
+ end
57
+ else
58
+ @app.call(env) {|*response| yield *response }
59
+ end
60
+ end
61
+
62
+ def lookup(request)
63
+ begin
64
+ entry = metastore.lookup(request, entitystore)
65
+ record :fresh
66
+ entry
67
+ rescue Exception => e
68
+ log_error(e)
69
+ return pass
70
+ end
71
+ end
72
+
73
+ def store(request, response)
74
+ metastore.store(request, response, entitystore)
75
+ record :store
76
+ end
77
+
78
+ def metastore
79
+ uri = options['rack-client-cache.metastore']
80
+ storage.resolve_metastore_uri(uri)
81
+ end
82
+
83
+ def entitystore
84
+ uri = options['rack-client-cache.entitystore']
85
+ storage.resolve_entitystore_uri(uri)
86
+ end
87
+
88
+ # Record that an event took place.
89
+ def record(event)
90
+ @trace << event
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,77 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class EntityStore
5
+ # Read body calculating the SHA1 checksum and size while
6
+ # yielding each chunk to the block. If the body responds to close,
7
+ # call it after iteration is complete. Return a two-tuple of the form:
8
+ # [ hexdigest, size ].
9
+ def slurp(body)
10
+ digest, size = Digest::SHA1.new, 0
11
+ body.each do |part|
12
+ size += bytesize(part)
13
+ digest << part
14
+ yield part
15
+ end
16
+ body.close if body.respond_to? :close
17
+ [digest.hexdigest, size]
18
+ end
19
+
20
+ if ''.respond_to?(:bytesize)
21
+ def bytesize(string); string.bytesize; end
22
+ else
23
+ def bytesize(string); string.size; end
24
+ end
25
+
26
+ private :slurp, :bytesize
27
+
28
+ class Heap < EntityStore
29
+
30
+ # Create the store with the specified backing Hash.
31
+ def initialize(hash={})
32
+ @hash = hash
33
+ end
34
+
35
+ # Determine whether the response body with the specified key (SHA1)
36
+ # exists in the store.
37
+ def exist?(key)
38
+ @hash.include?(key)
39
+ end
40
+
41
+ # Return an object suitable for use as a Rack response body for the
42
+ # specified key.
43
+ def open(key)
44
+ (body = @hash[key]) && body.dup
45
+ end
46
+
47
+ # Read all data associated with the given key and return as a single
48
+ # String.
49
+ def read(key)
50
+ (body = @hash[key]) && body.join
51
+ end
52
+
53
+ # Write the Rack response body immediately and return the SHA1 key.
54
+ def write(body)
55
+ buf = []
56
+ key, size = slurp(body) { |part| buf << part }
57
+ @hash[key] = buf
58
+ [key, size]
59
+ end
60
+
61
+ # Remove the body corresponding to key; return nil.
62
+ def purge(key)
63
+ @hash.delete(key)
64
+ nil
65
+ end
66
+
67
+ def self.resolve(uri)
68
+ new
69
+ end
70
+ end
71
+
72
+ HEAP = Heap
73
+ MEM = Heap
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,51 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class Key
5
+ include Rack::Utils
6
+ def self.call(request)
7
+ new(request).generate
8
+ end
9
+
10
+ def initialize(request)
11
+ @request = request
12
+ end
13
+
14
+ # Generate a normalized cache key for the request.
15
+ def generate
16
+ parts = []
17
+ parts << @request.scheme << "://"
18
+ parts << @request.host
19
+
20
+ if @request.scheme == "https" && @request.port != 443 ||
21
+ @request.scheme == "http" && @request.port != 80
22
+ parts << ":" << @request.port.to_s
23
+ end
24
+
25
+ parts << @request.script_name
26
+ parts << @request.path_info
27
+
28
+ if qs = query_string
29
+ parts << "?"
30
+ parts << qs
31
+ end
32
+
33
+ parts.join
34
+ end
35
+
36
+ private
37
+ # Build a normalized query string by alphabetizing all keys/values
38
+ # and applying consistent escaping.
39
+ def query_string
40
+ return nil if @request.query_string.nil?
41
+
42
+ @request.query_string.split(/[&;] */n).
43
+ map { |p| unescape(p).split('=', 2) }.
44
+ sort.
45
+ map { |k,v| "#{escape(k)}=#{escape(v)}" }.
46
+ join('&')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end