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.
- data/History.txt +2 -2
- data/README.textile +2 -2
- data/Rakefile +11 -5
- data/demo/demo_spec.rb +3 -3
- data/lib/rack/client.rb +29 -25
- data/lib/rack/client/adapter.rb +6 -0
- data/lib/rack/client/adapter/base.rb +57 -0
- data/lib/rack/client/adapter/simple.rb +49 -0
- data/lib/rack/client/auth/abstract/challenge.rb +53 -0
- data/lib/rack/client/auth/basic.rb +57 -0
- data/lib/rack/client/auth/digest/challenge.rb +38 -0
- data/lib/rack/client/auth/digest/md5.rb +78 -0
- data/lib/rack/client/auth/digest/params.rb +10 -0
- data/lib/rack/client/body.rb +12 -0
- data/lib/rack/client/cache.rb +19 -0
- data/lib/rack/client/cache/cachecontrol.rb +195 -0
- data/lib/rack/client/cache/context.rb +95 -0
- data/lib/rack/client/cache/entitystore.rb +77 -0
- data/lib/rack/client/cache/key.rb +51 -0
- data/lib/rack/client/cache/metastore.rb +133 -0
- data/lib/rack/client/cache/options.rb +147 -0
- data/lib/rack/client/cache/request.rb +46 -0
- data/lib/rack/client/cache/response.rb +62 -0
- data/lib/rack/client/cache/storage.rb +43 -0
- data/lib/rack/client/cookie_jar.rb +17 -0
- data/lib/rack/client/cookie_jar/context.rb +59 -0
- data/lib/rack/client/cookie_jar/cookie.rb +59 -0
- data/lib/rack/client/cookie_jar/cookiestore.rb +36 -0
- data/lib/rack/client/cookie_jar/options.rb +43 -0
- data/lib/rack/client/cookie_jar/request.rb +15 -0
- data/lib/rack/client/cookie_jar/response.rb +16 -0
- data/lib/rack/client/cookie_jar/storage.rb +34 -0
- data/lib/rack/client/dual_band.rb +13 -0
- data/lib/rack/client/follow_redirects.rb +47 -20
- data/lib/rack/client/handler.rb +10 -0
- data/lib/rack/client/handler/em-http.rb +66 -0
- data/lib/rack/client/handler/excon.rb +50 -0
- data/lib/rack/client/handler/net_http.rb +85 -0
- data/lib/rack/client/handler/typhoeus.rb +62 -0
- data/lib/rack/client/headers.rb +49 -0
- data/lib/rack/client/parser.rb +18 -0
- data/lib/rack/client/parser/base.rb +25 -0
- data/lib/rack/client/parser/body_collection.rb +50 -0
- data/lib/rack/client/parser/context.rb +15 -0
- data/lib/rack/client/parser/json.rb +54 -0
- data/lib/rack/client/parser/middleware.rb +8 -0
- data/lib/rack/client/parser/request.rb +21 -0
- data/lib/rack/client/parser/response.rb +19 -0
- data/lib/rack/client/parser/yaml.rb +52 -0
- data/lib/rack/client/response.rb +9 -0
- data/lib/rack/client/version.rb +5 -0
- data/spec/apps/example.org.ru +47 -3
- data/spec/auth/basic_spec.rb +69 -0
- data/spec/auth/digest/md5_spec.rb +69 -0
- data/spec/cache_spec.rb +40 -0
- data/spec/cookie_jar_spec.rb +37 -0
- data/spec/endpoint_spec.rb +4 -13
- data/spec/follow_redirect_spec.rb +27 -0
- data/spec/handler/async_api_spec.rb +69 -0
- data/spec/handler/em_http_spec.rb +22 -0
- data/spec/handler/excon_spec.rb +7 -0
- data/spec/handler/net_http_spec.rb +8 -0
- data/spec/handler/sync_api_spec.rb +55 -0
- data/spec/handler/typhoeus_spec.rb +22 -0
- data/spec/middleware_helper.rb +37 -0
- data/spec/middleware_spec.rb +48 -5
- data/spec/parser/json_spec.rb +22 -0
- data/spec/parser/yaml_spec.rb +22 -0
- data/spec/server_helper.rb +72 -0
- data/spec/spec_helper.rb +17 -3
- metadata +86 -31
- data/lib/rack/client/auth.rb +0 -13
- data/lib/rack/client/http.rb +0 -77
- data/spec/auth_spec.rb +0 -22
- data/spec/core_spec.rb +0 -123
- data/spec/redirect_spec.rb +0 -12
@@ -0,0 +1,59 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
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
|
+
request = Request.new(env)
|
16
|
+
cookies = lookup(request)
|
17
|
+
request.inject(cookies)
|
18
|
+
|
19
|
+
response = Response.new(*@app.call(request.env))
|
20
|
+
cookies = Cookie.merge(cookies, response.cookies)
|
21
|
+
store cookies
|
22
|
+
|
23
|
+
response['rack-client-cookiejar.cookies'] = cookies.map {|c| c.to_header } * ', ' unless cookies.empty?
|
24
|
+
response.finish
|
25
|
+
end
|
26
|
+
|
27
|
+
def async_call(env)
|
28
|
+
request = Request.new(env)
|
29
|
+
cookies = lookup(request)
|
30
|
+
request.inject(cookies)
|
31
|
+
|
32
|
+
@app.call(request.env) do |request_parts|
|
33
|
+
response = Response.new(*request_parts)
|
34
|
+
cookies = Cookie.merge(cookies, response.cookies)
|
35
|
+
store cookies
|
36
|
+
|
37
|
+
response['rack-client-cookiejar.cookies'] = cookies.map {|c| c.to_header } * ', ' unless cookies.empty?
|
38
|
+
yield response.finish
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def lookup(request)
|
43
|
+
cookiestore.match(request.host, request.path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def store(cookies)
|
47
|
+
cookies.each do |cookie|
|
48
|
+
cookiestore.store(cookie)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def cookiestore
|
53
|
+
uri = options['rack-client-cookiejar.cookiestore']
|
54
|
+
storage.resolve_cookiestore_uri(uri)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
4
|
+
class Cookie < Struct.new(:key, :value, :domain, :path)
|
5
|
+
|
6
|
+
def self.merge(bottom, top)
|
7
|
+
bottom.reject {|a| top.any? {|b| a == b } } | top
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parse(raw)
|
11
|
+
raw.split(', ').map {|header| from(header) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from(header)
|
15
|
+
data = header.split('; ')
|
16
|
+
tuple = data.shift.split('=')
|
17
|
+
parts = data.map {|s| s.split('=') }
|
18
|
+
|
19
|
+
new parts.inject('key'=> tuple.first, 'value'=> tuple.last) {|h,(k,v)| h.update(k => v)}
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(parts = {})
|
23
|
+
parts.each do |k,v|
|
24
|
+
send(:"#{k}=", v)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_key
|
29
|
+
[ key, domain, path ] * ';'
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_header
|
33
|
+
hash = members.zip(values).inject({}) {|h,(k,v)| h.update(k => v) }.reject {|k,v| v.nil?}
|
34
|
+
"#{hash.delete('key')}=#{hash.delete('value')}" << ('; ' + hash.map {|(k,v)| "#{k}=#{v}" } * '; ' unless hash.empty?)
|
35
|
+
end
|
36
|
+
|
37
|
+
def eql?(other)
|
38
|
+
to_key == other.to_key
|
39
|
+
end
|
40
|
+
|
41
|
+
def match?(domain, path)
|
42
|
+
fuzzy_domain_equal(domain) && fuzzy_path_equal(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fuzzy_domain_equal(other_domain)
|
46
|
+
if domain =~ /^\./
|
47
|
+
other_domain =~ /#{Regexp.escape(domain)}$/
|
48
|
+
else
|
49
|
+
domain == other_domain
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def fuzzy_path_equal(other_path)
|
54
|
+
path == '/' || path == other_path
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
4
|
+
class CookieStore
|
5
|
+
def store(cookie)
|
6
|
+
write cookie.to_key, cookie.to_header
|
7
|
+
end
|
8
|
+
|
9
|
+
def match(domain, path)
|
10
|
+
cookies = map {|header| Cookie.from(header) }
|
11
|
+
cookies.select {|cookie| cookie.match?(domain, path) }
|
12
|
+
end
|
13
|
+
|
14
|
+
class Heap < CookieStore
|
15
|
+
def initialize
|
16
|
+
@heap = Hash.new {|h,k| h[k] = [] }
|
17
|
+
end
|
18
|
+
|
19
|
+
def write(key, value)
|
20
|
+
@heap[key] << value
|
21
|
+
end
|
22
|
+
|
23
|
+
def map
|
24
|
+
@heap.values.flatten.map {|*a| yield *a }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.resolve(uri)
|
28
|
+
new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
HEAP = Heap
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
4
|
+
module Options
|
5
|
+
def self.option_accessor(key)
|
6
|
+
name = option_name(key)
|
7
|
+
define_method(key) { || options[name] }
|
8
|
+
define_method("#{key}=") { |value| options[name] = value }
|
9
|
+
define_method("#{key}?") { || !! options[name] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def options
|
13
|
+
@default_options.merge(@options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def options=(hash = {})
|
17
|
+
@options = hash.each { |key,value| write_option(key, value) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def option_name(key)
|
21
|
+
case key
|
22
|
+
when Symbol ; "rack-client-cookiejar.#{key}"
|
23
|
+
when String ; key
|
24
|
+
else raise ArgumentError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
module_function :option_name
|
28
|
+
|
29
|
+
option_accessor :storage
|
30
|
+
option_accessor :cookiestore
|
31
|
+
option_accessor :cookies
|
32
|
+
|
33
|
+
def initialize_options(options={})
|
34
|
+
@default_options = {
|
35
|
+
'rack-client-cookiejar.storage' => Storage.new,
|
36
|
+
'rack-client-cookiejar.cookiestore' => 'heap:/',
|
37
|
+
}
|
38
|
+
self.options = options
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
4
|
+
class Request < Rack::Request
|
5
|
+
def inject(cookies)
|
6
|
+
if raw_cookies = env['HTTP_COOKIE']
|
7
|
+
cookies = Cookie.merge(cookies, raw_cookies)
|
8
|
+
end
|
9
|
+
|
10
|
+
env['HTTP_COOKIE'] = cookies.map {|c| c.to_header } * ', ' unless cookies.empty?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
4
|
+
class Response < Client::Response
|
5
|
+
def cookies
|
6
|
+
return [] unless set_cookie
|
7
|
+
Cookie.parse(set_cookie.last)
|
8
|
+
end
|
9
|
+
|
10
|
+
def set_cookie
|
11
|
+
@set_cookie ||= headers.detect {|(k,v)| k =~ /Set-Cookie/i }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module CookieJar
|
4
|
+
class Storage
|
5
|
+
def initialize
|
6
|
+
@cookiestores = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolve_cookiestore_uri(uri)
|
10
|
+
@cookiestores[uri.to_s] ||= create_store(uri)
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_store(uri)
|
14
|
+
if uri.respond_to?(:scheme) || uri.respond_to?(:to_str)
|
15
|
+
uri = URI.parse(uri) unless uri.respond_to?(:scheme)
|
16
|
+
if CookieStore.const_defined?(uri.scheme.upcase)
|
17
|
+
klass = CookieStore.const_get(uri.scheme.upcase)
|
18
|
+
klass.resolve(uri)
|
19
|
+
else
|
20
|
+
fail "Unknown storage provider: #{uri.to_s}"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
fail "Unknown storage provider: #{uri.to_s}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@@singleton_instance = new
|
28
|
+
def self.instance
|
29
|
+
@@singleton_instance
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,27 +1,54 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
class FollowRedirects
|
4
|
+
include DualBand
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
new_env[k] = v
|
10
|
+
def async_call(env, &block)
|
11
|
+
@app.call(env) do |tuple|
|
12
|
+
response = Response.new(*tuple)
|
13
|
+
|
14
|
+
if response.redirect?
|
15
|
+
follow_redirect(response, env, &block)
|
16
|
+
else
|
17
|
+
yield response.finish
|
17
18
|
end
|
18
19
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
call(env)
|
23
|
-
|
24
|
-
|
20
|
+
end
|
21
|
+
|
22
|
+
def sync_call(env, &block)
|
23
|
+
response = Response.new(*@app.call(env))
|
24
|
+
response.redirect? ? follow_redirect(response, env, &block) : response
|
25
|
+
end
|
26
|
+
|
27
|
+
def follow_redirect(response, env, &block)
|
28
|
+
call(next_env(response, env), &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def next_env(response, env)
|
32
|
+
env = env.dup
|
33
|
+
|
34
|
+
original = URI.parse(env['REQUEST_URI'])
|
35
|
+
redirection = URI.parse(response['Location'])
|
36
|
+
|
37
|
+
uri = original.merge(redirection)
|
38
|
+
|
39
|
+
env.update 'REQUEST_METHOD' => 'GET'
|
40
|
+
env.update 'PATH_INFO' => uri.path.empty? ? '/' : uri.path
|
41
|
+
env.update 'REQUEST_URI' => uri.to_s
|
42
|
+
env.update 'SERVER_NAME' => uri.host
|
43
|
+
env.update 'SERVER_PORT' => uri.port
|
44
|
+
env.update 'SCRIPT_NAME' => ''
|
45
|
+
|
46
|
+
env.update 'rack.url_scheme' => uri.scheme
|
47
|
+
|
48
|
+
env.update 'HTTPS' => env["rack.url_scheme"] == "https" ? "on" : "off"
|
49
|
+
|
50
|
+
env
|
25
51
|
end
|
26
52
|
end
|
27
53
|
end
|
54
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Rack
|
2
|
+
module Client
|
3
|
+
module Handler
|
4
|
+
autoload :NetHTTP, 'rack/client/handler/net_http'
|
5
|
+
autoload :Excon, 'rack/client/handler/excon'
|
6
|
+
autoload :EmHttp, 'rack/client/handler/em-http'
|
7
|
+
autoload :Typhoeus, 'rack/client/handler/typhoeus'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'em-http'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Client
|
5
|
+
module Handler
|
6
|
+
class EmHttp
|
7
|
+
include Rack::Client::DualBand
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
@uri = URI.parse(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def sync_call(env)
|
14
|
+
raise("Synchronous API is not supported for EmHttp Handler") unless block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def async_call(env)
|
18
|
+
request = Rack::Request.new(env)
|
19
|
+
|
20
|
+
EM.schedule do
|
21
|
+
em_http = connection(request.path).send(request.request_method.downcase, request_options(request))
|
22
|
+
em_http.callback do
|
23
|
+
yield parse(em_http).finish
|
24
|
+
end
|
25
|
+
|
26
|
+
em_http.errback do
|
27
|
+
yield parse(em_http).finish
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def connection(path)
|
33
|
+
@connection ||= EventMachine::HttpRequest.new((@uri + path).to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def request_options(request)
|
37
|
+
options = {}
|
38
|
+
|
39
|
+
if request.body
|
40
|
+
options[:body] = case request.body
|
41
|
+
when Array then request.body.to_s
|
42
|
+
when StringIO then request.body.string
|
43
|
+
when IO then request.body.read
|
44
|
+
when String then request.body
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
options
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse(em_http)
|
52
|
+
body = em_http.response.empty? ? [] : StringIO.new(em_http.response)
|
53
|
+
Response.new(em_http.response_header.status, Headers.new(em_http.response_header).to_http, body)
|
54
|
+
end
|
55
|
+
|
56
|
+
def normalize_headers(em_http)
|
57
|
+
headers = em_http.response_header
|
58
|
+
|
59
|
+
headers['LOCATION'] = URI.parse(headers['LOCATION']).path if headers.include?('LOCATION')
|
60
|
+
|
61
|
+
headers
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'excon'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Client
|
5
|
+
module Handler
|
6
|
+
class Excon
|
7
|
+
include DualBand
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
@uri = URI.parse(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def async_call(env)
|
14
|
+
raise("Asynchronous API is not supported for EmHttp Handler") unless block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def sync_call(env)
|
18
|
+
request = Rack::Request.new(env)
|
19
|
+
|
20
|
+
body = case request.body
|
21
|
+
when StringIO then request.body.string
|
22
|
+
when IO then request.body.read
|
23
|
+
when Array then request.body.to_s
|
24
|
+
when String then request.body
|
25
|
+
end
|
26
|
+
|
27
|
+
response = parse connection.request(:method => request.request_method,
|
28
|
+
:path => request.path,
|
29
|
+
:headers => Headers.from(env).to_http,
|
30
|
+
:body => body)
|
31
|
+
|
32
|
+
response.finish
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse(excon_response)
|
36
|
+
body = excon_response.body.empty? ? [] : StringIO.new(excon_response.body)
|
37
|
+
Response.new(excon_response.status, Headers.new(excon_response.headers).to_http, body)
|
38
|
+
end
|
39
|
+
|
40
|
+
def connection
|
41
|
+
connection_table[self] ||= ::Excon.new(@uri.to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
def connection_table
|
45
|
+
Thread.current[:_rack_client_excon_connections] ||= {}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|