class SAPOCI::Connect::Middleware::FollowRedirects

Public: Follow HTTP 301, 302, 303, 307, and 308 redirects.

For HTTP 301, 302, and 303, the original GET, POST, PUT, DELETE, or PATCH request gets converted into a GET. With `standards_compliant: true`, however, the HTTP method after 301/302 remains unchanged. This allows you to opt into HTTP/1.1 compliance and act unlike the major web browsers.

This middleware currently only works with synchronous requests; i.e. it doesn't support parallelism.

Example:

Faraday.new(url: url) do |faraday|
  faraday.use SAPOCI::Connect::Middleware::FollowRedirects
  faraday.adapter Faraday.default_adapter
end

Constants

ALLOWED_METHODS

HTTP methods for which 30x redirects can be followed

AUTH_HEADER
ENV_TO_CLEAR

Keys in env hash which will get cleared between requests

FOLLOW_LIMIT

Default value for max redirects followed

REDIRECT_CODES

HTTP redirect status codes that this middleware implements

URI_UNSAFE

Regex that matches characters that need to be escaped in URLs, sans the “%” character which we assume already represents an escaped sequence.

Public Class Methods

new(app, options = {}) click to toggle source

Public: Initialize the middleware.

options - An options Hash (default: {}):

:limit                      - A Numeric redirect limit (default: 3)
:standards_compliant        - A Boolean indicating whether to respect
                             the HTTP spec when following 301/302
                             (default: false)
:callback                   - A callable used on redirects
                             with the old and new envs
:cookies                    - An Array of Strings (e.g.
                             ['cookie1', 'cookie2']) to choose
                             cookies to be kept, or :all to keep
                             all cookies (default: []).
:clear_authorization_header - A Boolean indicating whether the request
                             Authorization header should be cleared on
                             redirects (default: true)
Calls superclass method
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 79
def initialize(app, options = {})
  super(app)
  @options = options

  @convert_to_get = Set.new [303]
  @convert_to_get << 301 << 302 unless standards_compliant?
end

Public Instance Methods

call(env) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 87
def call(env)
  perform_with_redirection(env, follow_limit)
end

Private Instance Methods

callback() click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 151
def callback
  @options[:callback]
end
clear_authorization_header(env, from_url, to_url) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 166
def clear_authorization_header(env, from_url, to_url)
  return env if redirect_to_same_host?(from_url, to_url)
  return env unless @options.fetch(:clear_authorization_header, true)

  env[:request_headers].delete(AUTH_HEADER)
end
collect_cookies(env) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 183
def collect_cookies(env)
  if response_cookies = env[:response_headers]['Set-Cookie']
    @cookies = WEBrick::Cookie.parse_set_cookies(response_cookies)
    @cookies.inject([]) do |result, cookie|
      # only send back name and value
      if @options[:cookies] == :all || (@options[:cookies].include?(cookie.name.to_sym) || @options[:cookies].include?(cookie.name.to_s))
        result << cookie.name + "=" + cookie.value
      end
    end.to_a.uniq.join(";")
  else
    nil
  end
end
convert_to_get?(response) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 93
def convert_to_get?(response)
  !%i[head options].include?(response.env[:method]) &&
    @convert_to_get.include?(response.status)
end
follow_limit() click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 143
def follow_limit
  @options.fetch(:limit, FOLLOW_LIMIT)
end
follow_redirect?(env, response) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 138
def follow_redirect?(env, response)
  ALLOWED_METHODS.include?(env[:method]) &&
    REDIRECT_CODES.include?(response.status)
end
perform_with_redirection(env, follows) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 98
def perform_with_redirection(env, follows)
  request_body = env[:body]
  response = @app.call(env)

  response.on_complete do |response_env|
    if follow_redirect?(response_env, response)
      raise SAPOCI::Connect::Middleware::RedirectLimitReached, response if follows.zero?

      new_request_env = update_env(response_env.dup, request_body, response)
      callback&.call(response_env, new_request_env)
      response = perform_with_redirection(new_request_env, follows - 1)
    end
  end
  response
end
redirect_to_same_host?(from_url, to_url) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 173
def redirect_to_same_host?(from_url, to_url)
  return true if to_url.start_with?('/')

  from_uri = URI.parse(from_url)
  to_uri = URI.parse(to_url)

  [from_uri.scheme, from_uri.host, from_uri.port] ==
    [to_uri.scheme, to_uri.host, to_uri.port]
end
safe_escape(uri) click to toggle source

Internal: escapes unsafe characters from an URL which might be a path component only or a fully qualified URI so that it can be joined onto an URI:HTTP using the `+` operator. Doesn't escape “%” characters so to not risk double-escaping.

# File lib/sapoci/connect/middleware/follow_redirects.rb, line 159
def safe_escape(uri)
  uri = uri.split('#')[0] # we want to remove the fragment if present
  uri.to_s.gsub(URI_UNSAFE) do |match|
    '%' + match.unpack('H2' * match.bytesize).join('%').upcase
  end
end
standards_compliant?() click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 147
def standards_compliant?
  @options.fetch(:standards_compliant, false)
end
update_env(env, request_body, response) click to toggle source
# File lib/sapoci/connect/middleware/follow_redirects.rb, line 114
def update_env(env, request_body, response)
  redirect_from_url = env[:url].to_s
  redirect_to_url = safe_escape(response['location'] || '')
  raise RedirectWithoutLocation, response if redirect_to_url.blank?
  env[:url] += redirect_to_url

  if @options[:cookies] && cookie_string = collect_cookies(env)
    env[:request_headers]['Cookie'] = cookie_string
  end

  ENV_TO_CLEAR.each { |key| env.delete key }

  if convert_to_get?(response)
    env[:method] = :get
    env[:body] = nil
  else
    env[:body] = request_body
  end

  clear_authorization_header(env, redirect_from_url, redirect_to_url)

  env
end