-
Notifications
You must be signed in to change notification settings - Fork 323
/
redirector.rb
101 lines (75 loc) · 2.82 KB
/
redirector.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# frozen_string_literal: true
require "set"
require "http/headers"
module HTTP
class Redirector
# Notifies that we reached max allowed redirect hops
class TooManyRedirectsError < ResponseError; end
# Notifies that following redirects got into an endless loop
class EndlessRedirectError < TooManyRedirectsError; end
# HTTP status codes which indicate redirects
REDIRECT_CODES = [300, 301, 302, 303, 307, 308].to_set.freeze
# Codes which which should raise StateError in strict mode if original
# request was any of {UNSAFE_VERBS}
STRICT_SENSITIVE_CODES = [300, 301, 302].to_set.freeze
# Insecure http verbs, which should trigger StateError in strict mode
# upon {STRICT_SENSITIVE_CODES}
UNSAFE_VERBS = %i[put delete post].to_set.freeze
# Verbs which will remain unchanged upon See Other response.
SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
# @!attribute [r] strict
# Returns redirector policy.
# @return [Boolean]
attr_reader :strict
# @!attribute [r] max_hops
# Returns maximum allowed hops.
# @return [Fixnum]
attr_reader :max_hops
# @param [Hash] opts
# @option opts [Boolean] :strict (true) redirector hops policy
# @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
def initialize(opts = {}) # rubocop:disable Style/OptionHash
@strict = opts.fetch(:strict, true)
@max_hops = opts.fetch(:max_hops, 5).to_i
end
# Follows redirects until non-redirect response found
def perform(request, response)
@request = request
@response = response
@visited = []
while REDIRECT_CODES.include? @response.status.code
@visited << "#{@request.verb} #{@request.uri}"
raise TooManyRedirectsError if too_many_hops?
raise EndlessRedirectError if endless_loop?
@response.flush
@request = redirect_to @response.headers[Headers::LOCATION]
@response = yield @request
end
@response
end
private
# Check if we reached max amount of redirect hops
# @return [Boolean]
def too_many_hops?
1 <= @max_hops && @max_hops < @visited.count
end
# Check if we got into an endless loop
# @return [Boolean]
def endless_loop?
2 <= @visited.count(@visited.last)
end
# Redirect policy for follow
# @return [Request]
def redirect_to(uri)
raise StateError, "no Location header in redirect" unless uri
verb = @request.verb
code = @response.status.code
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
raise StateError, "can't follow #{@response.status} redirect" if @strict
verb = :get
end
verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303 == code
@request.redirect(uri, verb)
end
end
end