Rails https and non-standard ports in vagrant
Our way of dealing with http and https requests from a vagrant machine was to forward from the guest machine to some higher ports such as 8080 and 8433 and then add some rules to the Mac OSX built-in firewall to bind 80 to 8080 and 443 to 8443 on the host machine. Thus, the links and redirects generated by our rails app didn’t need to know anything about the ports that we were using to connect from our outside world.
We decided that we wanted to have as many machines and services running simultaneously as we wanted without having to worry about conflicting ports, so we adopted another strategy in which we specify the forwarded ports in a configuration file in our rails app, and we take care of them as transparently as possible.
We also wanted rails to generate the proper urls for multiple domains, so we set a host header in our nginx directives along with the X-Forwarded-Proto $scheme that lets rails know that it’s dealing with a http or https request.
in our nginx server:
location / {
proxy_pass http://local_3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
To let our app know which are the ports we are using, we set them in a application.yml file. This file is ignored in our repo so each developer can set their own ports.
config/application.yml
development:
http_port: 8080 #leave null for 80
https_port: 8443 #leave null for 443
test:
http_port: null #leave null for 80
https_port: null #leave null for 443
production:
http_port: 9080 #leave null for 80
https_port: 9443 #leave null for 443
We load this settings in our application.rb they way that Ryan proposes in this railscast
config/application.rb
CONFIG = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
CONFIG.merge! CONFIG.fetch(Rails.env, {})
CONFIG.symbolize_keys!
Instead of using Rails.application.routes.default_url_options, we define a method in our ApplicationController in order to have different defaults depending on the current request scheme.
app/controllers/application_controller.rb
def default_url_options(options={})
port = request.ssl? ? CONFIG[:https_port] : CONFIG[:http_port]
options = { :port=> port }
options
end
def default_host_with_port
"#{url_options[:host]}#{":#{url_options[:port]}" if url_options[:port]}"
end
helper_method :default_url_options, :default_host_with_port
the first method gives proper defaults for url routes when the kind of request is preserved, when we want to change for example from a https request to http we need to be specific:
my_route_url(:protocol => 'http', :port=>CONFIG[:http_port])
the helper default_host_with_port
replaces the request.host_with_port
that we were using to specify the path to some assets:
image_tag("//#{default_host_with_port}/path_to_image")
The trickiest part was to take care of the redirects. It was unclear for us how rails was filling in the remaining part of the url every time we used a redirect_to to a path instead of a url, and worst of all, we couldn’t easily change the way other gems handled their redirects, specially when devise relied on warden to handle the response.
Fortunately, we could take advantage of the middleware to detect if we were performing a redirect to another location in our server and set the proper port before letting the response leave our app.
app/middlewate/set_special_ports.rb
class SetSpecialPorts
def initialize(app)
@app = app
end
def call(env)
server_name = env["SERVER_NAME"]
# execute the request using our Rails app
status, headers, body = @app.call(env)
if internal_redirect?(status, headers, server_name)
[status, {"Location" => url_with_port(headers["Location"])}, ['Redirecting you to the new location...']]
else
# just send the response as it is
[status, headers, body]
end
end
def internal_redirect?(status, headers, server_name)
uri = URI.parse(headers["Location"]) if headers["Location"]
[300, 301, 302, 307].include?(status) && uri.try(:host) == server_name
end
# add the ports for http and https defined in our CONFIG settings through application.yml
def url_with_port(url)
uri = URI.parse(url)
uri.port = CONFIG["#{uri.scheme}_port".to_sym]
uri.to_s
end
end
config/application.rb
config.middleware.insert_before "Warden::Manager", "SetSpecialPorts"