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



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

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.



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

def app
  @app
end

#optionsObject (readonly)

Returns the value of attribute options.



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

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:



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

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)


369
370
371
# File 'lib/rack/request_replication/forwarder.rb', line 369

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)


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

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)


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

def cookies_id( request )
  cs = request.cookies
  sess = cs && cs[options[:session_key]]
  sess_id = sess && sess.split("\n--").last
  sess_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: {})

    ({})



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

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: {})

    ({})



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

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: {})

    ({})



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

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: {})

    ({})



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

def create_patch_request( uri, opts = {} )
  forward_request = Net::HTTP::Patch.new uri.request_uri
  forward_request.set_form_data opts[:params]
  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: {})

    ({})



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

def create_post_request( uri, opts = {} )
  forward_request = Net::HTTP::Post.new uri.request_uri
  forward_request.set_form_data opts[:params]
  forward_request
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: {})

    ({})



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

def create_put_request( uri, opts = {} )
  forward_request = Net::HTTP::Put.new uri.request_uri
  forward_request.set_form_data opts[:params]
  forward_request
end

#csrf_token(request) ⇒ Object

The CSRF-token to use.

Parameters:

  • request (Rack::Request)


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

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)


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

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)


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

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)


322
323
324
325
326
# File 'lib/rack/request_replication/forwarder.rb', line 322

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

#loggerObject

Logger that logs to STDOUT



378
379
380
# File 'lib/rack/request_replication/forwarder.rb', line 378

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)


359
360
361
# File 'lib/rack/request_replication/forwarder.rb', line 359

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.



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

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)


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

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)


289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/rack/request_replication/forwarder.rb', line 289

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)


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

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)


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

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)


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

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