rack-client 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|