How to add/remove rack middleware while server is running? - ruby-on-rails

In capybara specs I want to test absence of XSS vulnerability. We use selenium-webdriver with chromium to run browser specs, but chrome by default has XSS protection, which may be disabled by setting X-XSS-Protection header to 0. I wrote a middleware to set this header, and it works if enabled in config/environments/test.rb. As this header is required only in this spec, I don't want to have it enabled for all specs.
I tried following:
describe 'without xss protection' do
before :all do
Rails.configuration.middleware.use Rack::DisableXssProtection
end
after :all do
Rails.configuration.middleware.delete Rack::DisableXssProtection
end
it 'should not have xss', :needs_browser do
visit new_order_path
page.driver.execute_script <<-EOF
$("<input/>", {
id: "new_input",
name: "bad_field",
type: "radio",
value: "<script>alert('fail');</script>"
}).appendTo("#some_form");
EOF
find('#new_input').click
click_on 'submit'
end
end
If I stop anywhere inside this spec, I can see it in Rails.configuration.middleware, but it is not called (header is not set and if I put raise in this middleware it is ignored).
So, how can I add/remove middleware while server is running?
EDIT: middleware is just the following:
module Rack
class DisableXssProtection
def initialize(app)
#app = app
end
def call(env)
status, headers, body = #app.call(env)
headers['X-XSS-Protection'] = '0'
[status, headers, body]
end
end
end

As you're testing Rack::DisableXssProtection itself, it would make sense to extract it as a gem, and test it in isolation with a dummy Rails application.

Related

no method error "merge" in Rails middleware

Hi I am building a Rails/Rack middleware for the Mailgun api, and doing a simple test for connection, which is fine in Postman with exact same params.
It is showing an error in test, Failure/Error: #app.call(env)
NoMethodError:
undefined method `merge!' for #<Mailgun::Tracking::MailLogger:0x0000557e6bc8bb50>
on running RSpec, and no merge method is attempted at all, so dont know where its come from. The api_key and endpoint are initialized in config/initialisers folder as per the Mailgun Tracking gem (which is being used) guidelines.
Only two other files used and code below.
MailLogger class under Mailgun::Tracking module, registered as middleware in app.
require 'logger'
require 'mailgun/tracking'
require 'rack/contrib'
require 'rack'
require 'byebug'
module Mailgun::Tracking
class MailLogger
def initialize(app, options = {
"HTTP_X_MAILGUN_API_KEY" => ENV['MAILGUN_API_KEY'],
"HTTP_X_MAILGUN_ENDPOINT" => ENV['MAILGUN_ENDPOINT']
})
#app = app
#options = options
end
def call(env)
#app.call(env)
end
end
end
The RSpec test
require 'logger'
require "json"
require "rails_helper"
Bundler.require(*Rails.groups)
require_relative "../../lib/middleware/mailgun/mailgun_tracking.rb"
RSpec.describe Mailgun::Tracking::MailLogger, type: 'Mailgun Webhook' do
subject(:response) { app.call(env) }
# env to pass in middleware, url endpoint & key
let(:app) { described_class.new(Rails.application) }
let(:env) { described_class.new(app, options = {
"HTTP_X_MAILGUN_API_KEY" => ENV['MAILGUN_API_KEY'],
"HTTP_X_MAILGUN_ENDPOINT" => ENV['MAILGUN_ENDPOINT']
}) }
it "returns a 200 status code" do
expect(response).to eq([200, {}, ["OK"]])
end
end
Am just looking for a 200, OK on connection, as I already get in Postman with the same header (api_key) and endpoint (events)
but throws this mysterious missing "merge" method error
Not encountered before.
Anyone know why?
Thanks
The actual code calling merge! is hidden in rack implementation and as such is filtered out from the error message. Your problem is that env you define in your test is not an instance of Rack::Environment but rather instance of your middleware. You can generate mocked environment instance using, for example, Rack::MockRequest.env_for("/", method: :get)
That being said, your unit test is currently testing the whole of the rack stack, including the routes and other middlewares. You can isolate your test better by using a mock application:
let(:app) { described_class.new(->(env) [200, {}, 'success']) }

Feature tests not passing REQUEST_URI to a middleware with RSpec and Capybara

I am trying to setup feature testing on an application.
I decided to install Capybara, and thus added it to my project's Gemfile:
group :test do
gem "capybara"
end
I declare my tests on the spec/feature folder, and the test manages to execute:
require "rails_helper"
feature 'My Feature' do
scenario 'User visits feature page' do
visit '/my-feature'
expect(page).to have_text('Stuff')
end
end
Issue: I have an URL middleware that does not detect the env['REQUEST_URI] flag and thus my test fails:
class UrlNormalizationMiddleware
def initialize(app)
#app = app
end
def call(env)
uri_items = env['REQUEST_URI'].split('?')
...
#app.call(env)
end
end
The actual application loads, and passes values on env['REQUEST_URI'], but doesn't on the test environment.
Anything else that I need to setup?
Thanks!
REQUEST_URI is not part of the rack spec, which means it's not guaranteed to be set, and you shouldn't be using it in your middleware. Instead you should be using things like PATH_INFO, QUERY_STRING, etc. which are specified in the rack spec and should therefore be available - https://github.com/rack/rack/blob/master/SPEC

Adding authentication info in Rails proxy middleware

I am using the rack-proxy gem in Rails to proxy requests to an external server. Thing is, the external endpoint requires authentication. How do I provide that information from the middleware?
Here's what I have so far:
require 'rack/proxy'
class MyProxy < Rack::Proxy
MY_REQUEST = %r{^/path/(.*)}
def initialize(app)
#app = app
end
def call(env)
if m = MY_REQUEST.match(env['PATH_INFO'])
env['PATH_INFO'] = "https://otherserver.org/#{m[1]}"
env['HTTP_HOST'] = "otherserver.org"
#the otherserver.org endpoint requires authentication
super env
else
#app.call(env)
end
end
end
Depends on what kind of authentication the other server is using. If its just plain HTTP Authentication you can do something like:
env['Authentication'] = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
Where the value part follows the spec at: http://en.wikipedia.org/wiki/Basic_access_authentication#cite_ref-8
Following the Rack spec here gave me good pointers - http://rubydoc.info/github/rack/rack/master/file/SPEC
Had to do:
env['HTTP_AUTHORIZATION'] = 'Basic <base64 username:password>'

RSpec and Rack middleware: how to make sure that it is active in a spec/dummy request spec

I have a Rack middleware that inserts itself automatically into the app when the gem is specified within the Gemfile:
gem 'headhunter'
Then the middleware registers itself like this:
module Headhunter
module Rails
class Railtie < ::Rails::Railtie
initializer "headhunter.hijack" do |app|
head_hunter = Runner.new(::Rails.root)
at_exit do
head_hunter.report
head_hunter.clean_up!
end
app.middleware.insert(0, Headhunter::Rack::CapturingMiddleware, head_hunter)
end
end
end
end
Now I want to make sure that the middleware is actually inserted in an RSpec feature/request spec.
I have a dummy Rails app in spec/dummy which does the following:
require 'spec_helper'
feature 'Middleware integration' do
scenario "Integrating the middleware into the Rack stack" do
expect(Headhunter::Rack::CapturingMiddleware.any_instance).to receive(:call)
visit posts_path
end
end
Sadly this fails and I don't know why. By placing a binding.pry within call I can tell for sure that it is called.
Is there maybe even a better way to test this? Thanks a lot for help.

how to setup rails Authenticity Token to work with multiple domains?

I'm building an app that uses subdomains as account handles (myaccount.domain.com) and I have my sessions configured to work across the sub-domains like so:
config.action_controller.session = {:domain => '.domain.com'}
In addition to the subdomain a user can input a real domain name when they are creating their account. My Nginx config is setup to watch for *.com *.net etc, and this is working to serve out the pages.
The problem comes when a site visitor submits a comment form on a custom domain that was input by the user. The code is throwing an "Invalid AuthenticityToken" exception. I'm 99% sure this is because the domain the user is on isn't specified as the domain in the config.action_controller.session. Thus the authenticity token isn't getting matched up because Rails can't find their session.
So, the question is: Can you set config.action_controller.session to more than 1 domain, and if so can you add / remove from that value at runtime without restarting the app?
I found the answer to this question here: http://codetunes.com/2009/04/17/dynamic-cookie-domains-with-racks-middleware/
This solution worked for me because my app was running on Rails 2.3.5, which uses Rack. The request comes from web server, goes through middleware layers and enters the application. So this middleware layer detects the host with which the application is accessed and sets cookie domain for the request. Here it is:
# app/middlewares/set_cookie_domain.rb
class SetCookieDomain
def initialize(app, default_domain)
#app = app
#default_domain = default_domain
end
def call(env)
host = env["HTTP_HOST"].split(':').first
env["rack.session.options"][:domain] = custom_domain?(host) ? ".#{host}" : "#{#default_domain}"
#app.call(env)
end
def custom_domain?(host)
domain = #default_domain.sub(/^\./, '')
host !~ Regexp.new("#{domain}$", Regexp::IGNORECASE)
end
end
# turn it on in environment.rb
config.load_paths += %W( #{RAILS_ROOT}/app/middlewares )
# production.rb
config.middleware.use "SetCookieDomain", ".example.org"
.example.org is the default domain that will be used unless the application is accessed via custom domain (like site.com), we give it different values depending on environment (production/staging/development etc).
# tests/integration/set_cookie_domain_test.rb (using Shoulda and Webrat)
require 'test_helper'
class SetCookieDomainTest < ActionController::IntegrationTest
context "when accessing site at example.org" do
setup do
host! 'example.org'
visit '/'
end
should "set cookie_domain to .example.org" do
assert_equal '.example.org', #integration_session.controller.request.session_options[:domain]
end
end
context "when accessing site at site.com" do
setup do
host! 'site.com'
visit '/'
end
should "set cookie_domain to .site.com" do
assert_equal '.site.com', #integration_session.controller.request.session_options[:domain]
end
end
context "when accessing site at site.example.org" do
setup do
host! 'site.example.org'
visit '/'
end
should "set cookie_domain to .example.org" do
assert_equal '.example.org', #integration_session.controller.request.session_options[:domain]
end
end
end

Resources