class Rack::RequestReplication::Forwarder

This class implements forwarding of requests to another host and/or port.

Attributes

app[R]
options[R]

Public Class Methods

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

@param [#call] app @param [Hash{Symbol => Object}] options @option options [String] :host (‘localhost’) @option options [Integer] :port (8080) @option options [String] :session_key (‘rack.session’) @option options [Bool] :use_ssl (false) @option options [Bool] :verify_ssl (true) @option options [Hash{Symbol => Object}] :basic_auth

@option basic_auth [String] :user
@option basic_auth [String] :password

@option options [Hash{Symbol => Object}] :redis

@option redis [String]  :host ('localhost')
@option redis [Integer] :port (6379)
@option redis [String]  :db   ('rack-request-replication')
# File lib/rack/request_replication/forwarder.rb, line 37
def initialize(app, options = {})
  @app = app
  @options = {
    host: 'localhost',
    port: 8080,
    use_ssl: false,
    verify_ssl: true,
    session_key: 'rack.session',
    root_url: '/',
    redis: {}
  }.merge(options)
end

Public Instance Methods

call(env) click to toggle source

@param [Hash{String => String}] env @return [Array(Integer, Hash, each)] @see rack.rubyforge.org/doc/SPEC.html

# File lib/rack/request_replication/forwarder.rb, line 55
def call(env)
  request = Rack::Request.new(env)
  replicate(request)
  app.call(env)
end
clean_scheme(request) click to toggle source

Request scheme without the ://

@param [Rack::Request] request @returns [String]

# File lib/rack/request_replication/forwarder.rb, line 410
def clean_scheme(request)
  request.scheme.match(/^\w+/)[0]
end
cookies(request) click to toggle source

Cookies Hash to use for the forwarded request.

Tries to find the cookies from earlier forwarded requests in the Redis store, otherwise falls back to the cookies from the source app.

@param [Rack::Request] request @returns [Hash]

# File lib/rack/request_replication/forwarder.rb, line 176
def cookies(request)
  return (request.cookies || "") unless cookies_id(request)
  redis.get(cookies_id(request)) || request.cookies || {}
end
cookies_id(request) click to toggle source

The key to use when looking up cookie stores in Redis for forwarding requests. Needed for session persistence over forwarded requests for the same user in the source app.

@param [Rack::Request] request @returns [String]

# File lib/rack/request_replication/forwarder.rb, line 190
def cookies_id(request)
  cs = request.cookies
  session = cs && cs[options[:session_key]]
  session_id = session && session.split("\n--").last
  session_id
end
create_delete_request(uri, opts = {}) click to toggle source

Prepare a DELETE request to the forward app.

The passed in options hash is ignored.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Delete]

# File lib/rack/request_replication/forwarder.rb, line 267
def create_delete_request(uri, opts = {})
  Net::HTTP::Delete.new(uri.request_uri)
end
create_get_request(uri, opts = {}) click to toggle source

Prepare a GET request to the forward app.

The passed in options hash is ignored.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Get]

# File lib/rack/request_replication/forwarder.rb, line 206
def create_get_request(uri, opts = {})
  Net::HTTP::Get.new(uri.request_uri)
end
create_head_request(uri, opts = {}) click to toggle source

Prepare a HEAD request to the forward app.

The passed in options hash is ignored.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Head]

# File lib/rack/request_replication/forwarder.rb, line 319
def create_head_request(uri, opts = {})
  Net::HTTP::Head.new(uri.request_uri)
end
create_options_request(uri, opts = {}) click to toggle source

Prepare a OPTIONS request to the forward app.

The passed in options hash is ignored.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Options]

# File lib/rack/request_replication/forwarder.rb, line 280
def create_options_request(uri, opts = {})
  Net::HTTP::Options.new(uri.request_uri)
end
create_patch_request(uri, opts = {}) click to toggle source

Prepare a PATCH request to the forward app.

The passed in options hash contains all the data from the request that needs to be forwarded.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Patch]

# File lib/rack/request_replication/forwarder.rb, line 252
def create_patch_request(uri, opts = {})
  forward_request = Net::HTTP::Patch.new(uri.request_uri)
  forward_request.body = opts[:params].to_query
  forward_request
end
create_post_request(uri, opts = {}) click to toggle source

Prepare a POST request to the forward app.

The passed in options hash contains all the data from the request that needs to be forwarded.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Post]

# File lib/rack/request_replication/forwarder.rb, line 220
def create_post_request(uri, opts = {})
  forward_request = Net::HTTP::Post.new(uri.request_uri)
  forward_request.body = opts[:params].to_query
  forward_request
end
create_propfind_request(uri, opts = {}) click to toggle source

Prepare a PROPFIND request to the forward app.

The passed in options hash is ignored.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Propfind]

# File lib/rack/request_replication/forwarder.rb, line 293
def create_propfind_request(uri, opts = {})
  Net::HTTP::Propfind.new(uri.request_uri)
end
create_put_request(uri, opts = {}) click to toggle source

Prepare a PUT request to the forward app.

The passed in options hash contains all the data from the request that needs to be forwarded.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Put]

# File lib/rack/request_replication/forwarder.rb, line 236
def create_put_request(uri, opts = {})
  forward_request = Net::HTTP::Put.new(uri.request_uri)
  forward_request.body = opts[:params].to_query
  forward_request
end
create_trace_request(uri, opts = {}) click to toggle source

Prepare a TRACE request to the forward app.

The passed in options hash is ignored.

@param [URI] uri @param [Hash{Symbol => Object}] opts ({}) @returns [Net:HTTP::Trace]

# File lib/rack/request_replication/forwarder.rb, line 306
def create_trace_request(uri, opts = {})
  Net::HTTP::Trace.new(uri.request_uri)
end
csrf_token(request) click to toggle source

The CSRF-token to use.

@param [Rack::Request] request @returns [String]

# File lib/rack/request_replication/forwarder.rb, line 113
def csrf_token(request)
  token = request.params["authenticity_token"]
  return if token.nil?

  redis.get("csrf-#{token}") || token
end
csrf_token_from(response) click to toggle source

Pull CSRF token from the HTML document’s header.

@param [Net::HTTP::Response] response @returns [String]

# File lib/rack/request_replication/forwarder.rb, line 141
def csrf_token_from(response)
  response.split("\n").
    select{|l| l.match(/csrf-token/) }.
    first.split(" ").
    select{|t| t.match(/^content=/)}.first.
    match(/content="(.*)"/)[1]
rescue
  nil
end
forward_host_with_port(request) click to toggle source

The host to forward to including the port if the port does not match the current scheme.

@param [Rack::Request] request @returns [String]

# File lib/rack/request_replication/forwarder.rb, line 376
def forward_host_with_port(request)
  host = options[:host].to_s
  host = "#{host}:#{options[:port]}" unless port_matches_scheme?(request)
  host
end
forward_uri(request) click to toggle source

Creates a URI based on the request info and the options set.

@param [Rack::Request] request @returns [URI]

# File lib/rack/request_replication/forwarder.rb, line 363
def forward_uri(request)
  url = "#{request.scheme}://#{forward_host_with_port(request)}"
  url << request.fullpath
  URI(url)
end
logger() click to toggle source

Logger that logs to STDOUT

@returns [Logger]

# File lib/rack/request_replication/forwarder.rb, line 419
def logger
  @logger ||= ::Logger.new(STDOUT)
end
port_matches_scheme?(request) click to toggle source

Checks if the request scheme matches the destination port.

@param [Rack::Request] request @returns [boolean]

# File lib/rack/request_replication/forwarder.rb, line 400
def port_matches_scheme?(request)
  options[:port].to_i == DEFAULT_PORTS[clean_scheme(request)]
end
redis() click to toggle source

Persistent Redis connection that is used to store cookies.

# File lib/rack/request_replication/forwarder.rb, line 386
def redis
  @redis ||= Redis.new({
    host: 'localhost',
    port: 6379,
    db: 'rack-request-replication'
  }.merge(options[:redis]))
end
replicate(request) click to toggle source

Replicates the request and passes it on to the request forwarder.

@param [Rack::Request] request

# File lib/rack/request_replication/forwarder.rb, line 67
def replicate(request)
  opts = replicate_options_and_data(request)
  uri = forward_uri(request)

  return unless VALID_REQUEST_METHODS.include?(opts[:request_method].downcase)

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = options[:use_ssl]
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless options[:verify_ssl]

  forward_request = send("create_#{opts[:request_method].downcase}_request", uri, opts)
  forward_request.add_field("Accept", opts[:accept])
  forward_request.add_field("Accept-Encoding", opts[:accept_encoding])
  forward_request.add_field("Host", request.host)

  if options[:basic_auth]
    forward_request.basic_auth options[:basic_auth][:user], options[:basic_auth][:password]
  end

  Thread.new do
    begin
      forward_request.add_field("Cookie", cookies(request))
      update_csrf_token_and_cookies(request, http.request(forward_request))
    rescue => e
      logger.debug "Replicating request failed with: #{e.message}"
    end
  end
end
replicate_options_and_data(request) click to toggle source

Replicates all the options and data that was in the original request and puts them in a Hash.

@param [Rack::Request] request @returns [Hash]

# File lib/rack/request_replication/forwarder.rb, line 330
def replicate_options_and_data(request)
  replicated_options ||= {}
  %w(
    accept_encoding
    body
    request_method
    content_charset
    media_type
    media_type_params
    params
    referer
    request_method
    user_agent
    url
  ).map(&:to_sym).each do |m|
    value = request.send(m)
    replicated_options[m] = value unless value.nil?
  end

  if replicated_options[:params]["authenticity_token"]
    replicated_options[:params]["authenticity_token"] = csrf_token(request)
  end

  replicated_options
end
update_cookies(request, response) click to toggle source

Update cookies from the forwarded request using the session id from the cookie of the source app as a key. The cookie is stored in Redis.

@param [Rack::Request] request @param [Net::HTTP::Response] response

# File lib/rack/request_replication/forwarder.rb, line 159
def update_cookies(request, response)
  return unless cookies_id(request)
  cookie = response.to_hash['set-cookie'].collect{ |ea|ea[/^.*?;/] }.join rescue {}
  cookie = Hash[cookie.split(";").map{ |d|d.split('=') }] rescue {}
  redis.set(cookies_id(request), cookie)
end
update_csrf_token(request, response) click to toggle source

Update CSRF token to bypass XSS errors in Rails.

@param [Rack::Request] request

# File lib/rack/request_replication/forwarder.rb, line 125
def update_csrf_token(request, response)
  token = request.params["authenticity_token"]
  return if token.nil?

  response_token = csrf_token_from response
  return token if response_token.nil?

  redis.set "csrf-#{token}", response_token
end
update_csrf_token_and_cookies(request, response) click to toggle source

Update CSRF token and cookies.

@param [Rack::Request] request @param [Net::HTTP::Response] response

# File lib/rack/request_replication/forwarder.rb, line 102
def update_csrf_token_and_cookies(request, response)
  update_csrf_token(request, response)
  update_cookies(request, response)
end