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’)



37
38
39
40
41
42
43
44
45
46
47
48
# 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

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



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

def app
  @app
end

#optionsObject (readonly)

Returns the value of attribute options.



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

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:



55
56
57
58
59
# 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) ⇒ Object

Request scheme without the ://

Parameters:

  • request (Rack::Request)


410
411
412
# File 'lib/rack/request_replication/forwarder.rb', line 410

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)


176
177
178
179
# 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) ⇒ 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)


190
191
192
193
194
195
# 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 = {}) ⇒ 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: {})

    ({})



267
268
269
# 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 = {}) ⇒ 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: {})

    ({})



206
207
208
# 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 = {}) ⇒ Object

Prepare a HEAD request to the forward app.

The passed in options hash is ignored.

Parameters:

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

    ({})



319
320
321
# 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 = {}) ⇒ 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: {})

    ({})



280
281
282
# 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 = {}) ⇒ 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: {})

    ({})



252
253
254
255
256
# 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 = {}) ⇒ 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: {})

    ({})



220
221
222
223
224
# 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 = {}) ⇒ 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: {})

    ({})



293
294
295
# 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 = {}) ⇒ 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: {})

    ({})



236
237
238
239
240
# 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 = {}) ⇒ Object

Prepare a TRACE request to the forward app.

The passed in options hash is ignored.

Parameters:

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

    ({})



306
307
308
# 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) ⇒ Object

The CSRF-token to use.

Parameters:

  • request (Rack::Request)


113
114
115
116
117
118
# 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) ⇒ Object

Pull CSRF token from the HTML document’s header.

Parameters:

  • response (Net::HTTP::Response)


141
142
143
144
145
146
147
148
149
# 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) ⇒ Object

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

Parameters:

  • request (Rack::Request)


376
377
378
379
380
# 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) ⇒ Object

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

Parameters:

  • request (Rack::Request)


363
364
365
366
367
# 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

#loggerObject

Logger that logs to STDOUT



419
420
421
# File 'lib/rack/request_replication/forwarder.rb', line 419

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)


400
401
402
# File 'lib/rack/request_replication/forwarder.rb', line 400

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.



386
387
388
389
390
391
392
# 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) ⇒ Object

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

Parameters:

  • request (Rack::Request)


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

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

Parameters:

  • request (Rack::Request)


330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# 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) ⇒ 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)


159
160
161
162
163
164
# 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) ⇒ Object

Update CSRF token to bypass XSS errors in Rails.

Parameters:

  • request (Rack::Request)


125
126
127
128
129
130
131
132
133
# 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) ⇒ Object

Update CSRF token and cookies.

Parameters:

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


102
103
104
105
# 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