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.

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
  • :redis (Hash{Symbol => Object})

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



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/rack/request_replication/forwarder.rb', line 32

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)



50
51
52
53
54
# File 'lib/rack/request_replication/forwarder.rb', line 50

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

#clean_scheme(request) ⇒ Object

Request scheme without the ://



362
363
364
# File 'lib/rack/request_replication/forwarder.rb', line 362

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.



165
166
167
168
169
170
# File 'lib/rack/request_replication/forwarder.rb', line 165

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.



181
182
183
184
185
186
# File 'lib/rack/request_replication/forwarder.rb', line 181

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.



258
259
260
# File 'lib/rack/request_replication/forwarder.rb', line 258

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.



197
198
199
# File 'lib/rack/request_replication/forwarder.rb', line 197

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.



271
272
273
# File 'lib/rack/request_replication/forwarder.rb', line 271

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.



243
244
245
246
247
# File 'lib/rack/request_replication/forwarder.rb', line 243

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.



211
212
213
214
215
# File 'lib/rack/request_replication/forwarder.rb', line 211

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.



227
228
229
230
231
# File 'lib/rack/request_replication/forwarder.rb', line 227

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.



102
103
104
105
106
107
# File 'lib/rack/request_replication/forwarder.rb', line 102

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.



130
131
132
133
134
135
136
137
138
# File 'lib/rack/request_replication/forwarder.rb', line 130

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.



328
329
330
331
332
# File 'lib/rack/request_replication/forwarder.rb', line 328

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.



315
316
317
318
319
# File 'lib/rack/request_replication/forwarder.rb', line 315

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

#loggerObject

Logger that logs to STDOUT



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

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

#port_matches_scheme?(request) ⇒ Boolean

Checks if the request scheme matches the destination port.



352
353
354
# File 'lib/rack/request_replication/forwarder.rb', line 352

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.



338
339
340
341
342
343
344
# File 'lib/rack/request_replication/forwarder.rb', line 338

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.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/rack/request_replication/forwarder.rb', line 62

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)

  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.



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/rack/request_replication/forwarder.rb', line 282

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.



148
149
150
151
152
153
# File 'lib/rack/request_replication/forwarder.rb', line 148

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.



114
115
116
117
118
119
120
121
122
# File 'lib/rack/request_replication/forwarder.rb', line 114

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.



91
92
93
94
# File 'lib/rack/request_replication/forwarder.rb', line 91

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