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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
|
# frozen_string_literal: true
module HTTPX
InsecureRedirectError = Class.new(Error)
module Plugins
#
# This plugin adds support for automatically following redirect (status 30X) responses.
#
# It has a default upper bound of followed redirects (see *MAX_REDIRECTS* and the *max_redirects* option),
# after which it will return the last redirect response. It will **not** raise an exception.
#
# It doesn't follow insecure redirects (https -> http) by default (see *follow_insecure_redirects*).
#
# It doesn't propagate authorization related headers to requests redirecting to different origins
# (see *allow_auth_to_other_origins*) to override.
#
# It allows customization of when to redirect via the *redirect_on* callback option).
#
# https://gitlab.com/os85/httpx/wikis/Follow-Redirects
#
module FollowRedirects
MAX_REDIRECTS = 3
REDIRECT_STATUS = (300..399).freeze
REQUEST_BODY_HEADERS = %w[transfer-encoding content-encoding content-type content-length content-language content-md5 trailer].freeze
using URIExtensions
# adds support for the following options:
#
# :max_redirects :: max number of times a request will be redirected (defaults to <tt>3</tt>).
# :follow_insecure_redirects :: whether redirects to an "http://" URI, when coming from an "https//", are allowed
# (defaults to <tt>false</tt>).
# :allow_auth_to_other_origins :: whether auth-related headers, such as "Authorization", are propagated on redirection
# (defaults to <tt>false</tt>).
# :redirect_on :: optional callback which receives the redirect location and can halt the redirect chain if it returns <tt>false</tt>.
module OptionsMethods
private
def option_max_redirects(value)
num = Integer(value)
raise TypeError, ":max_redirects must be positive" if num.negative?
num
end
def option_follow_insecure_redirects(value)
value
end
def option_allow_auth_to_other_origins(value)
value
end
def option_redirect_on(value)
raise TypeError, ":redirect_on must be callable" unless value.respond_to?(:call)
value
end
end
module InstanceMethods
# returns a session with the *max_redirects* option set to +n+
def max_redirects(n)
with(max_redirects: n.to_i)
end
private
def fetch_response(request, selector, options)
redirect_request = request.redirect_request
response = super(redirect_request, selector, options)
return unless response
max_redirects = redirect_request.max_redirects
return response unless response.is_a?(Response)
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
return response unless max_redirects.positive?
redirect_uri = __get_location_from_response(response)
if options.redirect_on
redirect_allowed = options.redirect_on.call(redirect_uri)
return response unless redirect_allowed
end
# build redirect request
request_body = redirect_request.body
redirect_method = "GET"
redirect_params = {}
if response.status == 305 && options.respond_to?(:proxy)
request_body.rewind
# The requested resource MUST be accessed through the proxy given by
# the Location field. The Location field gives the URI of the proxy.
redirect_options = options.merge(headers: redirect_request.headers,
proxy: { uri: redirect_uri },
max_redirects: max_redirects - 1)
redirect_params[:body] = request_body
redirect_uri = redirect_request.uri
options = redirect_options
else
redirect_headers = redirect_request_headers(redirect_request.uri, redirect_uri, request.headers, options)
redirect_opts = Hash[options]
redirect_params[:max_redirects] = max_redirects - 1
unless request_body.empty?
if response.status == 307
# The method and the body of the original request are reused to perform the redirected request.
redirect_method = redirect_request.verb
request_body.rewind
redirect_params[:body] = request_body
else
# redirects are **ALWAYS** GET, so remove body-related headers
REQUEST_BODY_HEADERS.each do |h|
redirect_headers.delete(h)
end
redirect_params[:body] = nil
end
end
options = options.class.new(redirect_opts.merge(headers: redirect_headers.to_h))
end
redirect_uri = Utils.to_uri(redirect_uri)
if !options.follow_insecure_redirects &&
response.uri.scheme == "https" &&
redirect_uri.scheme == "http"
error = InsecureRedirectError.new(redirect_uri.to_s)
error.set_backtrace(caller)
return ErrorResponse.new(request, error)
end
retry_request = build_request(redirect_method, redirect_uri, redirect_params, options)
request.redirect_request = retry_request
redirect_after = response.headers["retry-after"]
if redirect_after
# Servers send the "Retry-After" header field to indicate how long the
# user agent ought to wait before making a follow-up request.
# When sent with any 3xx (Redirection) response, Retry-After indicates
# the minimum time that the user agent is asked to wait before issuing
# the redirected request.
#
redirect_after = Utils.parse_retry_after(redirect_after)
retry_start = Utils.now
log { "redirecting after #{redirect_after} secs..." }
selector.after(redirect_after) do
if (response = request.response)
response.finish!
retry_request.response = response
# request has terminated abruptly meanwhile
retry_request.emit(:response, response)
else
log { "redirecting (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
send_request(retry_request, selector, options)
end
end
else
send_request(retry_request, selector, options)
end
nil
end
# :nodoc:
def redirect_request_headers(original_uri, redirect_uri, headers, options)
headers = headers.dup
return headers if options.allow_auth_to_other_origins
return headers unless headers.key?("authorization")
return headers if original_uri.origin == redirect_uri.origin
headers.delete("authorization")
headers
end
# :nodoc:
def __get_location_from_response(response)
# @type var location_uri: http_uri
location_uri = URI(response.headers["location"])
location_uri = response.uri.merge(location_uri) if location_uri.relative?
location_uri
end
end
module RequestMethods
# returns the top-most original HTTPX::Request from the redirect chain
attr_accessor :root_request
# returns the follow-up redirect request, or itself
def redirect_request
@redirect_request || self
end
# sets the follow-up redirect request
def redirect_request=(req)
@redirect_request = req
req.root_request = @root_request || self
@response = nil
end
def response
return super unless @redirect_request && @response.nil?
@redirect_request.response
end
def max_redirects
@options.max_redirects || MAX_REDIRECTS
end
end
module ConnectionMethods
private
def set_request_request_timeout(request)
return unless request.root_request.nil?
super
end
end
end
register_plugin :follow_redirects, FollowRedirects
end
end
|