File: follow_redirects.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (233 lines) | stat: -rw-r--r-- 8,497 bytes parent folder | download
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