Subdomain routing with rails
s2.diffuse.it, gpichler.diffuse.it, adanti.diffuse.it and so on. There are a few problems you have to overcome if you want your users to have their own subdomain:
1. first of all you need a dns wildcard
2. you need to configure apache
3. you need to differentiate between assets (javascripts, images, stylesheets) that are on the main site, and those from the subdomains
4. cookies
5. links to subdomains
The first problem is easy to solve: just tell your dns provider to route all traffic from *.yourdomain.tld to the same ip.
The second problem is easily solved too, with a catch all vhost in apache:
<VirtualHost *:80> ServerName yourdomain.tld DocumentRoot /opt/myapp/public/ RequestHeader set SERVER_NAME yourdomain.tld ProxyRequests Off ProxyPass / http://127.0.0.1:8000/ ProxyPassReverse / http://127.0.0.1:8000/ </VirtualHost>
If this is your first vhost in apache, then all requests going to the ip of your web server will end up in that vhost definition, this means that whatever.yourdomain.tld will end up using that vhost definition (I am assuming we have apache in front of a mongrel listening on 8000 here, but of course this will work with fcgi or mod_rails too).
Ok, now we have all requests to whatever subdomain proxyed to our mongrels on 8000. The RequestHeader set SERVER_NAME part is just a variable I added to the request. I will use this later in rails to get the current host name without any subdomain.
Now let’s move to the rails part. Groovie has very good support for subdomains with it’s routes, but rails (to my knowledge) does not, so I had to monkeypatch it a bit.
The first thing I did was to add a before_filter to get the current user by vhost:
class ApplicationController < ActionController::Base
before_filter :get_appuser
before_filter :get_vhostuser
def get_appuser
@appuser = User.find(session[:user_id])
end
def get_vhostuser
@vhostuser = User.find_by_vhost(request.subdomains.first)
end
end
This way we know what subdomain the current request is on, and who the logged in user is (if he is).
Next we have to make sure that static assets are served always from the root domain (yourdomain.tld). If we use
stylesheet_link_tag “style”
the generated url will be relative to the current host:
<link href=”/stylesheets/style.css” media=”screen” rel=”stylesheet” type=”text/css” />
but we want it to be always our root domain, so the browser can cache them and does not download the same stylesheets and javascripts each time for each subdomain. To do this we can just use
config.action_controller.asset_host = 'http://yourdomain.tld'
in environments/production.rb. This way all links pointing to static assets will be generated with http://yourdomain.tld in front of them.
Taken care of the static assets, we have to deal with the cookies. If a user logs in on http://yourdomain.tld/login, we want him to be logged in on http://username1.yourdomain.tld/ as well, and on http://username2.yourdomain.tld/ and so on. To archive this we have to set the domain of the session cookie to .yourdomain.tld.
Now we have almost everything in place. What we still need is some helpers to play nice with the subdomains. Wouldn’t it be nice if you could
<%= link_to 'Home', :controller => 'home', :action => 'index', :subdomain => 'user1' %>?
Yes, it would. And here comes the ugly part. To do this I had to monkeypatch the UrlRewriter
#I took this from somewhere on the net,
#i don't remember who wrote it so I can't
#really give credit, but I did not write it!
module ActionController
class UrlRewriter
def rewrite(options = {})
unless options[:subdomain].nil?
if options[:subdomain] == false
newhost = @request.server_name #domain(options[:tld_length] || 1)
elsif @request.vhost != options[:subdomain]
newhost = "#{options[:subdomain]}." + @request.server_name
end
unless newhost.nil?
options[:host] = @request.port == @request.standard_port ? "#{newhost}" : "#{newhost}:#{@request.port}"
options[:only_path] = false
end
end
options.delete(:subdomain)
rewrite_url(options)
end
end
end
Just make a plugin out of this code, or put it in a file in the inizializers directory. But, where does that @request.server_name and @request.vhost come from I hear you all (the 2 users reading this post) scream? I patched the AbstractRequest too, to add this two methods:
module ActionController
class AbstractRequest
def server_name
@env['HTTP_SERVER_NAME']
end
def vhost
subdomains.first
end
end
end
Remember the
RequestHeader set SERVER_NAME yourdomain.tld
part in the apache config file? Here we use it to get the root domain name.
Well, done. Now we should have everything in place to get going.
well dunn, dude!
sounds very useful :)
Thanks for the writeup! That’s a bookmark.
The only thing I’d like to add is that you might want to consider filtering usernames for terms like ‘www’, ‘ftp’ etc when using subdomains as identifiers in order to prevent your users from monkeying around too much :)
Thanks! My team was just about to embark on this exact task. Great writeup.
Could this be used to support multiple domains with one application?
Each user getting their own URL if they wanted it?
Nice writeup.
Also see Dan Webb’s routing plugin
@Todd, yes of course. That’s the point.
It’s much easier to implement subdomains in Rails 3 than in Rails 2 (no plugin required). For anyone looking for a complete example implementation of Rails 3 subdomains with authentication (and a detailed tutorial) here’s my repo on Github:
http://github.com/fortuity/rails3-subdomain-devise
also Ryan Bates has a Railscast on Subdomains in Rails 3: http://railscasts.com/episodes/221-subdomains-in-rails-3