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
|
# 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
|