ruby-http-session 1.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ class HTTP::Session
2
+ class ConnectionPool
3
+ include MonitorMixin
4
+
5
+ DEFAULT_POOL_TIMEOUT = 5
6
+ DEFAULT_POOL_MAXSIZE = 5
7
+ DEFAULT_CONN_KEEP_ALIVE_TIMEOUT = 5
8
+
9
+ # The number of connections can be reused.
10
+ attr_reader :maxsize
11
+
12
+ # Returns a new instance of ConnectionPool.
13
+ #
14
+ # @param [Hash] opts
15
+ def initialize(opts, &blk)
16
+ super()
17
+
18
+ @host = opts.fetch(:host)
19
+ @timeout = opts.fetch(:timeout, DEFAULT_CONN_KEEP_ALIVE_TIMEOUT)
20
+ @maxsize = opts.fetch(:maxsize, DEFAULT_POOL_MAXSIZE)
21
+
22
+ @create_blk = blk
23
+ @created = 0
24
+ @que = []
25
+ @resource = new_cond
26
+ end
27
+
28
+ # Obtain a connection.
29
+ def with(timeout: DEFAULT_POOL_TIMEOUT, &blk)
30
+ conn = checkout(timeout: timeout)
31
+ blk.call(conn)
32
+ ensure
33
+ checkin(conn) if conn
34
+ end
35
+
36
+ # The number of connections available.
37
+ def size
38
+ @maxsize - @created + @que.size
39
+ end
40
+
41
+ private
42
+
43
+ def checkout(timeout:)
44
+ deadline = current_time + timeout
45
+
46
+ synchronize do
47
+ loop do
48
+ # Reuse a free connection.
49
+ if @que.size > 0
50
+ break @que.pop
51
+ end
52
+
53
+ # Create a new connection if the pool does not reach @maxsize.
54
+ if @created < @maxsize
55
+ @created += 1
56
+ break make_conn
57
+ end
58
+
59
+ # .
60
+ to_wait = deadline - current_time
61
+ raise HTTP::Session::Exceptions::PoolTimeoutError, "Waited #{timeout} sec, #{size}/#{maxsize} available" if to_wait <= 0
62
+
63
+ # Block until a connection is put back to @que.
64
+ @resource.wait(to_wait)
65
+ end
66
+ end
67
+ end
68
+
69
+ def checkin(conn)
70
+ synchronize do
71
+ # Put back to @que.
72
+ @que.push(conn)
73
+
74
+ # Wakes up all threads waiting for this resource.
75
+ @resource.broadcast
76
+ end
77
+ end
78
+
79
+ def make_conn
80
+ opts = {persistent: @host, keep_alive_timeout: @timeout}
81
+ @create_blk.call(opts)
82
+ end
83
+
84
+ def current_time
85
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,24 @@
1
+ class HTTP::Session
2
+ class Context
3
+ class FollowContext
4
+ def initialize(opts)
5
+ @request = opts[:request]
6
+ @verb = opts[:verb]
7
+ @uri = opts[:uri]
8
+ end
9
+
10
+ def same_origin?
11
+ @request.uri.origin == @uri.origin
12
+ end
13
+
14
+ def cross_origin?
15
+ !same_origin?
16
+ end
17
+
18
+ def should_drop_body?
19
+ @verb == :get &&
20
+ HTTP::Session::Redirector::UNSAFE_VERBS.include?(@request.verb)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ class HTTP::Session
2
+ class Context
3
+ # @!attribute [r] follow
4
+ # @return [nil, FollowContext]
5
+ attr_reader :follow
6
+
7
+ # @param [Hash] options
8
+ # @option options [nil, FollowContext] :follow
9
+ def initialize(options)
10
+ @follow = options[:follow]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ class HTTP::Session
2
+ class Cookies
3
+ extend Forwardable
4
+ include MonitorMixin
5
+
6
+ # @!method enabled?
7
+ # True when it is enabled.
8
+ # @return [Boolean]
9
+ def_delegator :@options, :enabled?
10
+
11
+ # @param [Options::CookiesOption] options
12
+ def initialize(options)
13
+ super()
14
+ @options = options
15
+ end
16
+
17
+ # Read cookies.
18
+ #
19
+ # @return [nil, Hash]
20
+ def read(uri)
21
+ synchronize do
22
+ read_cookies(uri)
23
+ end
24
+ end
25
+
26
+ # Write cookies.
27
+ #
28
+ # @param [Response] res
29
+ # @return [void]
30
+ def write(res)
31
+ synchronize do
32
+ write_cookies(res)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def read_cookies(uri)
39
+ return if jar.empty?
40
+ jar.cookies(uri).each_with_object({}) { |c, h| h[c.name] = c.value }
41
+ end
42
+
43
+ def write_cookies(res)
44
+ req = res.request
45
+ res.headers.get(HTTP::Headers::SET_COOKIE).each do |header|
46
+ jar.parse(header, req.uri)
47
+ end
48
+ end
49
+
50
+ # Only available when #enbled? has the value true.
51
+ def jar
52
+ @options.jar
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ class HTTP::Session
2
+ module Exceptions
3
+ Error = Class.new(HTTP::Error)
4
+ PoolTimeoutError = Class.new(Error)
5
+ RedirectError = Class.new(Error)
6
+ end
7
+ end
@@ -1,5 +1,3 @@
1
- require "set"
2
-
3
1
  class HTTP::Session
4
2
  module Features
5
3
  class AutoInflate < HTTP::Feature
@@ -53,8 +51,6 @@ class HTTP::Session
53
51
  ensure
54
52
  zstream.close
55
53
  end
56
-
57
- HTTP::Options.register_feature(:hsf_auto_inflate, self)
58
54
  end
59
55
  end
60
56
  end
@@ -0,0 +1,5 @@
1
+ class HTTP::Session
2
+ module Features
3
+ HTTP::Options.register_feature(:hsf_auto_inflate, HTTP::Session::Features::AutoInflate)
4
+ end
5
+ end
@@ -1,40 +1,34 @@
1
1
  class HTTP::Session
2
2
  class Options
3
3
  class CacheOption
4
+ include Optionable
5
+
4
6
  # @!attribute [r] store
5
7
  # @return [ActiveSupport::Cache::Store]
6
8
  attr_reader :store
7
9
 
8
- # @param [Hash] options
9
- # @option options [Boolean] :private set true if it is a private cache
10
- # @option options [Boolean] :shared set true if it is a shared cache
11
- # @option options [ActiveSupport::Cache::Store] :store
12
- def initialize(options)
13
- options =
14
- case options
15
- when nil, false then {enabled: false}
16
- when true then {enabled: true}
17
- else options
18
- end
19
-
20
- # Enabled / Disabled
21
- @enabled = options.fetch(:enabled, true)
10
+ # @param [Hash] opts
11
+ # @option opts [Boolean] :private set true if it is a private cache
12
+ # @option opts [Boolean] :shared set true if it is a shared cache
13
+ # @option opts [ActiveSupport::Cache::Store] :store
14
+ def initialize(opts)
15
+ initialize_options(opts)
22
16
 
23
17
  # Shared Cache / Private Cache
24
18
  @shared =
25
- if options.key?(:shared)
26
- raise ArgumentError, ":shared and :private cannot be used at the same time" if options.key?(:private)
27
- !!options[:shared]
28
- elsif options.key?(:private)
29
- !options[:private]
19
+ if @options.key?(:shared)
20
+ raise ArgumentError, ":shared and :private cannot be used at the same time" if @options.key?(:private)
21
+ !!@options[:shared]
22
+ elsif @options.key?(:private)
23
+ !@options[:private]
30
24
  else
31
25
  true
32
26
  end
33
27
 
34
28
  # Cache Store
35
29
  @store =
36
- if @enabled
37
- store = options[:store]
30
+ if enabled?
31
+ store = @options[:store]
38
32
  if store.respond_to?(:read) && store.respond_to?(:write)
39
33
  store
40
34
  else
@@ -43,11 +37,6 @@ class HTTP::Session
43
37
  end
44
38
  end
45
39
 
46
- # Indicates whether or not the session cache feature is enabled.
47
- def enabled?
48
- @enabled
49
- end
50
-
51
40
  # True when it is a shared cache.
52
41
  #
53
42
  # Shared Cache that exists between the origin server and clients (e.g. Proxy, CDN).
@@ -64,6 +53,11 @@ class HTTP::Session
64
53
  !shared_cache?
65
54
  end
66
55
 
56
+ # @!visibility private
57
+ def freeze
58
+ super
59
+ end
60
+
67
61
  private
68
62
 
69
63
  def lookup_store(store)
@@ -1,21 +1,41 @@
1
1
  class HTTP::Session
2
2
  class Options
3
3
  class CookiesOption
4
- # @param [Hash] options
5
- def initialize(options)
6
- options =
7
- case options
8
- when nil, false then {enabled: false}
9
- when true then {enabled: true}
10
- else options
4
+ include Optionable
5
+
6
+ # @!attribute [r] jar
7
+ # @return [HTTP::CookieJar]
8
+ attr_reader :jar
9
+
10
+ # @param [Hash] opts
11
+ # @option opts [HTTP::CookieJar] :jar
12
+ def initialize(opts)
13
+ initialize_options(opts)
14
+
15
+ # CookieJar
16
+ @jar =
17
+ if enabled?
18
+ jar = @options[:jar]
19
+ lookup_jar(jar)
11
20
  end
21
+ end
12
22
 
13
- @enabled = options.fetch(:enabled, true)
23
+ # @!visibility private
24
+ def freeze
25
+ super
14
26
  end
15
27
 
16
- # Indicates whether or not the session cookie feature is enabled.
17
- def enabled?
18
- @enabled
28
+ private
29
+
30
+ def lookup_jar(jar)
31
+ case jar
32
+ when Hash
33
+ HTTP::CookieJar.new(**jar)
34
+ when nil
35
+ HTTP::CookieJar.new
36
+ else
37
+ jar
38
+ end
19
39
  end
20
40
  end
21
41
  end
@@ -0,0 +1,21 @@
1
+ class HTTP::Session
2
+ class Options
3
+ module Optionable
4
+ def initialize_options(opts)
5
+ @options =
6
+ case opts
7
+ when nil, false then {enabled: false}
8
+ when true then {enabled: true}
9
+ else opts
10
+ end
11
+
12
+ @enabled =
13
+ @options.fetch(:enabled, true)
14
+ end
15
+
16
+ def enabled?
17
+ @enabled
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ class HTTP::Session
2
+ class Options
3
+ class PersistentOption
4
+ include Optionable
5
+
6
+ # @!attribute [r] pools
7
+ # @return [Hash]
8
+ attr_reader :pools
9
+
10
+ # @param [Hash] opts
11
+ # @option opts [Hash] :pools parameters used to create ConnectionPool
12
+ def initialize(opts)
13
+ initialize_options(opts)
14
+
15
+ @pools =
16
+ normalize_pools(@options.fetch(:pools, {"*" => true}))
17
+ end
18
+
19
+ # @!visibility private
20
+ def freeze
21
+ super.tap do
22
+ pools.freeze
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def normalize_pools(pools)
29
+ pools.transform_keys do |k|
30
+ (k == "*") ? k : HTTP::URI.parse(k).origin
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -8,6 +8,10 @@ class HTTP::Session
8
8
  # @return [CacheOption]
9
9
  attr_reader :cache
10
10
 
11
+ # @!attribute [r] persistent
12
+ # @return [PersistentOption]
13
+ attr_reader :persistent
14
+
11
15
  # @!attribute [r] http
12
16
  # @return [HTTP::Options]
13
17
  attr_reader :http
@@ -16,6 +20,7 @@ class HTTP::Session
16
20
  def initialize(options)
17
21
  @cookies = HTTP::Session::Options::CookiesOption.new(options.fetch(:cookies, false))
18
22
  @cache = HTTP::Session::Options::CacheOption.new(options.fetch(:cache, false))
23
+ @persistent = HTTP::Session::Options::PersistentOption.new(options.fetch(:persistent, false))
19
24
  @http = HTTP::Options.new(
20
25
  options.fetch(:http, {}).slice(
21
26
  :cookies,
@@ -76,5 +81,15 @@ class HTTP::Session
76
81
  def with_proxy(proxy)
77
82
  tap { @http = @http.with_proxy(proxy) }
78
83
  end
84
+
85
+ # @!visibility private
86
+ def freeze
87
+ super.tap do
88
+ cookies.freeze
89
+ cache.freeze
90
+ persistent.freeze
91
+ http.freeze
92
+ end
93
+ end
79
94
  end
80
95
  end
@@ -0,0 +1,67 @@
1
+ class HTTP::Session
2
+ class PoolManager
3
+ include MonitorMixin
4
+
5
+ # Returns a new instance of PoolManager.
6
+ #
7
+ # @param [Options::PersistentOption] options
8
+ # @param [HTTP::Session] session
9
+ def initialize(options, session)
10
+ super()
11
+ @options = options
12
+ @session = session
13
+ @pools = {}
14
+ end
15
+
16
+ # Obtain a connection.
17
+ def with(uri, &blk)
18
+ return with_new_conn(&blk) unless @options.enabled?
19
+
20
+ origin = HTTP::URI.parse(uri).origin
21
+ return with_new_conn(&blk) unless connection_pool_usable?(origin)
22
+
23
+ with_reusable_conn(origin, &blk)
24
+ end
25
+
26
+ private
27
+
28
+ def with_new_conn(&blk)
29
+ blk.call(make_conn)
30
+ end
31
+
32
+ def with_reusable_conn(origin, &blk)
33
+ pool = connection_pool_from_origin(origin)
34
+ pool.with { |conn| blk.call(conn) }
35
+ end
36
+
37
+ def make_conn(opts = {})
38
+ opts = @session.default_options.http.merge(opts)
39
+ HTTP::Session::Client.new(opts, @session)
40
+ end
41
+
42
+ def connection_pool_usable?(origin)
43
+ opts = connection_pool_options_from_origin(origin)
44
+ return false if opts.nil?
45
+ return false if opts == false
46
+ true
47
+ end
48
+
49
+ def connection_pool_options_from_origin(origin)
50
+ return @options.pools[origin] if @options.pools.key?(origin)
51
+ @options.pools["*"]
52
+ end
53
+
54
+ def connection_pool_from_origin(origin)
55
+ synchronize do
56
+ return @pools[origin] if @pools.key?(origin)
57
+
58
+ opts = connection_pool_options_from_origin(origin).dup
59
+ opts = {} if opts == true
60
+ opts[:host] = origin
61
+ @pools[origin] = ConnectionPool.new(opts, &->(conn_opts) {
62
+ make_conn(conn_opts)
63
+ })
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,89 @@
1
+ class HTTP::Session
2
+ # Mostly borrowed from [http/lib/http/redirector.rb](https://github.com/httprb/http/blob/main/lib/http/redirector.rb)
3
+ class Redirector
4
+ # HTTP status codes which indicate redirects
5
+ REDIRECT_CODES = HTTP::Redirector::REDIRECT_CODES
6
+
7
+ # Codes which which should raise StateError in strict mode if original
8
+ # request was any of {UNSAFE_VERBS}
9
+ STRICT_SENSITIVE_CODES = HTTP::Redirector::STRICT_SENSITIVE_CODES
10
+
11
+ # Insecure http verbs, which should trigger StateError in strict mode
12
+ # upon {STRICT_SENSITIVE_CODES}
13
+ UNSAFE_VERBS = HTTP::Redirector::UNSAFE_VERBS
14
+
15
+ # Verbs which will remain unchanged upon See Other response.
16
+ SEE_OTHER_ALLOWED_VERBS = HTTP::Redirector::SEE_OTHER_ALLOWED_VERBS
17
+
18
+ # @param [Hash] opts
19
+ # @option opts [Boolean] :strict (true) redirector hops policy
20
+ # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
21
+ def initialize(opts = {})
22
+ @strict = opts.fetch(:strict, true)
23
+ @max_hops = opts.fetch(:max_hops, 5).to_i
24
+ @on_redirect = opts.fetch(:on_redirect, nil)
25
+ end
26
+
27
+ def perform(response, &blk)
28
+ request = response.request
29
+ visited = []
30
+ history = []
31
+
32
+ while REDIRECT_CODES.include?(response.status.code)
33
+ history << response
34
+ visited << "#{request.verb} #{request.uri}"
35
+ raise HTTP::Session::Exceptions::RedirectError, "too many hops" if too_many_hops?(visited)
36
+ raise HTTP::Session::Exceptions::RedirectError, "endless loop" if endless_loop?(visited)
37
+
38
+ location = response.headers.get(HTTP::Headers::LOCATION).inject(:+)
39
+ raise HTTP::Session::Exceptions::RedirectError, "no Location header in redirect" unless location
40
+
41
+ verb = make_redirect_to_verb(response, request)
42
+ uri = make_redirect_to_uri(response, request, location)
43
+ ctx = make_redirect_to_ctx(response, request, verb, uri)
44
+
45
+ @on_redirect.call(response, request) if @on_redirect.respond_to?(:call)
46
+ response = blk.call(verb, uri, ctx)
47
+ request = response.request
48
+ end
49
+
50
+ response.history = history
51
+ response
52
+ end
53
+
54
+ private
55
+
56
+ def too_many_hops?(visited)
57
+ @max_hops >= 1 && @max_hops < visited.count
58
+ end
59
+
60
+ def endless_loop?(visited)
61
+ visited.count(visited.last) >= 2
62
+ end
63
+
64
+ def make_redirect_to_verb(response, request)
65
+ verb = request.verb
66
+ code = response.status.code
67
+
68
+ if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
69
+ raise HTTP::Session::Exceptions::RedirectError, "can't follow #{response.status} redirect" if @strict
70
+ verb = :get
71
+ end
72
+ if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && code == 303
73
+ verb = :get
74
+ end
75
+
76
+ verb
77
+ end
78
+
79
+ def make_redirect_to_uri(response, request, location)
80
+ request.uri.join(location)
81
+ end
82
+
83
+ def make_redirect_to_ctx(response, request, verb, uri)
84
+ HTTP::Session::Context::FollowContext.new(
85
+ request: request, verb: verb, uri: uri
86
+ )
87
+ end
88
+ end
89
+ end
@@ -1,5 +1,3 @@
1
- require "forwardable"
2
-
3
1
  class HTTP::Session
4
2
  class Response < SimpleDelegator
5
3
  class StringBody
@@ -1,5 +1,5 @@
1
1
  module HTTP
2
2
  class Session
3
- VERSION = "1.0.1"
3
+ VERSION = "2.1.0"
4
4
  end
5
5
  end