Class: Rack::RequestReplication::Forwarder

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/request_replication/forwarder.rb

Overview

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Forwarder

Returns a new instance of Forwarder.

Parameters:

  • app (#call)
  • options (Hash{Symbol => Object}) (defaults to: {})

Options Hash (options):

  • :host (String) — default: 'localhost'
  • :port (Integer) — default: 8080
  • :session_key (String) — default: 'rack.session'
  • :use_ssl (Bool) — default: false
  • :verify_ssl (Bool) — default: true
  • :basic_auth (Hash{Symbol => Object})

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

  • :redis (Hash{Symbol => Object})

    @option redis [String] :host (‘localhost’) @option redis [Integer] :port (6379) @option redis [String] :db (‘rack-request-replication’)



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/rack/request_replication/forwarder.rb', line 36

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

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



17
18
19
# File 'lib/rack/request_replication/forwarder.rb', line 17

def app
  @app
end

#optionsObject (readonly)

Returns the value of attribute options.



18
19
20
# File 'lib/rack/request_replication/forwarder.rb', line 18

def options
  @options
end

Instance Method Details

#call(env) ⇒ Array(Integer, Hash, #each)

Parameters:

  • env (Hash{String => String})

Returns:

  • (Array(Integer, Hash, #each))

See Also:



54
55
56
57
58
# File 'lib/rack/request_replication/forwarder.rb', line 54

def call(env)
  request = Rack::Request.new(env)
  replicate(request)
  app.call(env)
end

#clean_scheme(request) ⇒ Object

Request scheme without the ://

Parameters:

  • request (Rack::Request)


381
382
383
# File 'lib/rack/request_replication/forwarder.rb', line 381

def clean_scheme(request)
  request.scheme.match(/^\w+/)[0]
end

#cookies(request) ⇒ Object

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.

Parameters:

  • request (Rack::Request)


173
174
175
176
# File 'lib/rack/request_replication/forwarder.rb', line 173

def cookies(request)
  return (request.cookies || "") unless cookies_id(request)
  redis.get(cookies_id(request)) || request.cookies || {}
end

#cookies_id(request) ⇒ Object

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.

Parameters:

  • request (Rack::Request)


187
188
189
190
191
192
# File 'lib/rack/request_replication/forwarder.rb', line 187

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 = {}) ⇒ Object

Prepare a DELETE request to the forward app.

The passed in options hash is ignored.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



264
265
266
# File 'lib/rack/request_replication/forwarder.rb', line 264

def create_delete_request(uri, opts = {})
  Net::HTTP::Delete.new(uri.request_uri)
end

#create_get_request(uri, opts = {}) ⇒ Object

Prepare a GET request to the forward app.

The passed in options hash is ignored.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



203
204
205
# File 'lib/rack/request_replication/forwarder.rb', line 203

def create_get_request(uri, opts = {})
  Net::HTTP::Get.new(uri.request_uri)
end

#create_options_request(uri, opts = {}) ⇒ Object

Prepare a OPTIONS request to the forward app.

The passed in options hash is ignored.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



277
278
279
# File 'lib/rack/request_replication/forwarder.rb', line 277

def create_options_request(uri, opts = {})
  Net::HTTP::Options.new(uri.request_uri)
end

#create_patch_request(uri, opts = {}) ⇒ Object

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.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



249
250
251
252
253
# File 'lib/rack/request_replication/forwarder.rb', line 249

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 = {}) ⇒ Object

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.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



217
218
219
220
221
# File 'lib/rack/request_replication/forwarder.rb', line 217

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 = {}) ⇒ Object

Prepare a PROPFIND request to the forward app.

The passed in options hash is ignored.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



290
291
292
# File 'lib/rack/request_replication/forwarder.rb', line 290

def create_propfind_request(uri, opts = {})
  Net::HTTP::Propfind.new(uri.request_uri)
end

#create_put_request(uri, opts = {}) ⇒ Object

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.

Parameters:

  • uri (URI)
  • opts (Hash{Symbol => Object}) (defaults to: {})

    ({})



233
234
235
236
237
# File 'lib/rack/request_replication/forwarder.rb', line 233

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

#csrf_token(request) ⇒ Object

The CSRF-token to use.

Parameters:

  • request (Rack::Request)


110
111
112
113
114
115
# File 'lib/rack/request_replication/forwarder.rb', line 110

def csrf_token(request)
  token = request.params["authenticity_token"]
  return if token.nil?

  redis.get("csrf-#{token}") || token
end

#csrf_token_from(response) ⇒ Object

Pull CSRF token from the HTML document’s header.

Parameters:

  • response (Net::HTTP::Response)


138
139
140
141
142
143
144
145
146
# File 'lib/rack/request_replication/forwarder.rb', line 138

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) ⇒ Object

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

Parameters:

  • request (Rack::Request)


347
348
349
350
351
# File 'lib/rack/request_replication/forwarder.rb', line 347

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) ⇒ Object

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

Parameters:

  • request (Rack::Request)


334
335
336
337
338
# File 'lib/rack/request_replication/forwarder.rb', line 334

def forward_uri(request)
  url = "#{request.scheme}://#{forward_host_with_port(request)}"
  url << request.fullpath
  URI(url)
end

#loggerObject

Logger that logs to STDOUT



390
391
392
# File 'lib/rack/request_replication/forwarder.rb', line 390

def logger
  @logger ||= ::Logger.new(STDOUT)
end

#port_matches_scheme?(request) ⇒ Boolean

Checks if the request scheme matches the destination port.

Parameters:

  • request (Rack::Request)

Returns:

  • (Boolean)


371
372
373
# File 'lib/rack/request_replication/forwarder.rb', line 371

def port_matches_scheme?(request)
  options[:port].to_i == DEFAULT_PORTS[clean_scheme(request)]
end

#redisObject

Persistent Redis connection that is used to store cookies.



357
358
359
360
361
362
363
# File 'lib/rack/request_replication/forwarder.rb', line 357

def redis
  @redis ||= Redis.new({
    host: 'localhost',
    port: 6379,
    db: 'rack-request-replication'
  }.merge(options[:redis]))
end

#replicate(request) ⇒ Object

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

Parameters:

  • request (Rack::Request)


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
# File 'lib/rack/request_replication/forwarder.rb', line 66

def replicate(request)
  opts = replicate_options_and_data(request)
  uri = forward_uri(request)

  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) ⇒ Object

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

Parameters:

  • request (Rack::Request)


301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/rack/request_replication/forwarder.rb', line 301

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) ⇒ Object

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.

Parameters:

  • request (Rack::Request)
  • response (Net::HTTP::Response)


156
157
158
159
160
161
# File 'lib/rack/request_replication/forwarder.rb', line 156

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) ⇒ Object

Update CSRF token to bypass XSS errors in Rails.

Parameters:

  • request (Rack::Request)


122
123
124
125
126
127
128
129
130
# File 'lib/rack/request_replication/forwarder.rb', line 122

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) ⇒ Object

Update CSRF token and cookies.

Parameters:

  • request (Rack::Request)
  • response (Net::HTTP::Response)


99
100
101
102
# File 'lib/rack/request_replication/forwarder.rb', line 99

def update_csrf_token_and_cookies(request, response)
  update_csrf_token(request, response)
  update_cookies(request, response)
end