Building VTS

The Official VTS Engineering Blog

Our Brand New Shiny Domain

By Karl Baum

We did it! We were finally able to nab the vts.com domain. In this post we are going to focus on some of the technical steps we took when changing over to vts.com.

One SSL Certificate Per Heroku App

First, I want to point out an important requirement. Unsurprisingly all of our old links to viewthespace.com had to continue to work properly. Since all of our requests were done over ssl, we now needed to support both vts.com and viewthespace.com over ssl. Here is where we ran into our first speed bump as Heroku only supports one ssl end point per application. Luckily for us, there was a simple workaround. SSL endpoints from one Heroku app can route to another Heroku app. So the solution was to create an additional empty Heroku app for the vts.com ssl endpoint and route requests for that endpoint to our original Heroku app. Here are the specific steps:

  1. Created an additional heroku app called vts-ssl
  2. Installed the heroku ssl add-on on the vts-ssl app
  3. Installed our new *.vts.com wildcard certificate on vts-ssl
  4. Added the domains www.vts.com and vts.com to our original vts app using the heroku domains:add command
  5. Added a cname for the www subdomain to point to our ssl endpoint. SSL endpoints can be viewed by running the heroku certs command

Dealing with the Apex Domain

After running through the above steps, we had both https://www.viewthespace.com and https://www.vts.com working. Unfortunately we weren’t yet done. The apex domain was not yet configured for https://vts.com. This is where we ran into our next speed bump.

Following the recent DNSimple outage, we changed over to Amazon Route 53 to manage our DNS. We figured it was more reliable and we were already depending on Amazon for everything else. Other DNS providers like DNSimple support alias records out of box. This allows for pointing the apex domain directly at a cname which is what Heroku recommends. Unfortunately, Amazon Route 53 does not support alias records out of box. They do support something more manual that in theory can accomplish the same. One can point the apex domain at an S3 bucket. That S3 bucket can then be configured to redirect to any URL or in our case, http://www.vts.com. This would allow http://vts.com to redirect to http://www.vts.com. Problem solved? No, unfortunately not. If one of our users made an apex ssl request as https://vts.com (and they will), the request would generate a browser security warning. Reason being our ssl certificate would not be configured on the S3 bucket issuing the redirect. You cannot install a custom ssl certificate on an Amazon S3 bucket, but you can install one using an Amazon Cloudfront Distrubution. And yes, you can point vts.com at the Cloudfront Distribution and theoretically redirect from https://viewthespace.com to https://www.viewthespace.com. That seemed like the correct solution to us, but we elected for a workaround to save time. Instead we are using an A record to point directly to our ssl-end point IP address. This is not ideal because Heroku can change the IP address of our SSL endpoint at anytime. To mitigate this problem, we have monitoring in place to tell us if the apex domain is not responding. So far, the IP address has not changed. To be continued…

Redirecting from viewthespace.com to vts.com

At this point all of our links to viewthespace.com and vts.com work which is fine, but it would be even better if all of our users were redirected from viewthespace.com to vts.com. Since we are on Heroku and we don’t have control of our own web server like nginx or apache, we are doing need to do all of our redirects within our application. In our case this means within Rails 3.2.X. This actually works very well as rails routing is robust and we can test our redirects using capybara and rspec.

First order of business is to redirect all requests to *.viewthespace.com to *.vts.com. We added the following to our rails routes.rb file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  def query_params_to_query(request)
    query_params = request.params.except(:path, :format)
    query_params.any? ? "?#{query_params.to_query}" : ""
  end

  redirect_action =  ->(params, request) do
    "http://#{request.host.split('.').first}.vts.com/#{params[:path]}#{query_params_to_query(request)}"
  end

  #redirect *.viewthespace.com to *.vts.com
  constraints(host: %r{.*.viewthespace.com}) do
    root to: redirect(redirect_action)
    match '/*path', to: redirect(redirect_action)
  end
end

First notice that our redirect works for any subdomain of viewthespace.com allowing to test in other environment such as staging.viewthespace.com. The contstraint ensures that the logic only comes into play when a request is made to the *.viewthespace.com domain which should eventually be the exception. The redirect action dynamically takes the subdomain and pops it on the front of vts.com and appends the path and query parameters. So www.viewthepaces.com/my_portfolio?hello=world becomes www.vts.com/my_portfolio?hello=world. The above snippet handles 99% of our use cases, but leaves out direct requests to the apex or naked domains. Let’s handle that now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  def query_params_to_query(request)
    query_params = request.params.except(:path, :format)
    query_params.any? ? "?#{query_params.to_query}" : ""
  end

  def create_redirect_action(subdomain = nil)
    ->(params, _equest) do
      _subdomain = subdomain || _request.host.split('.').first
      "http://#{_subdomain}.vts.com/#{params[:path]}#{query_params_to_query(_request)}"
    end
  end


  #redirect *.viewthespace.com to *.vts.com
  constraints(host: %r{.*.viewthespace.com}) do
    redirect_action = create_redirect_action
    #we dont redirect for api or iphone requests
    root to: redirect(redirect_action)
    match '/*path', to: redirect(redirect_action)
  end

  #redirect apex http://viewthespace.com http://vts.com domain to www.vts.com
  %w(viewthespace vts).each do |domain|
    constraints(host: %r{^#{domain}.com}) do
      redirect_action = create_redirect_action('www')
      root to: redirect(redirect_action)
      match '/*path', to: redirect(redirect_action)
    end
  end

In the above snippet we added two constraints using an array for both http://viewthespace.com and https://vts.com. That funny looking ‘^’ character indicates the beginning of a line in a regular expression. Notice we have also created a create_redirect_action method allowing us to dynamically create a lambda block hard coded to using ‘www’ when there is no subdomain present in the apex domain case. Now requests to both http://viewthespace.com and http://vts.com are redirected to http://www.vts.com leveraging our existing code.

Looking at this code, I probably could dry this up even more and have just one constraint that is able to default to ‘www’ if there is no subdomain present (apex domain), but I am leaving it like this for now. Better is the enemy of done?

Testing the redirects

I know what you are thinking.. I didn’t write my tests first. That is not true, you’re just saying that because the order in which I chose to write this post. As mentioned, the great part about doing this in rails is that the redirect logic is easily testable with capybara and rspec. Let’s show some examples:

1
2
3
4
5
6
7
8
   before do
     Capybara.stub :app_host => "http://viewthespace.com"
     visit "/"
   end

   specify do
     expect(page.current_url).to eq("http://www.vts.com/")
   end

Capybara lets us stub out the app host to viewthespace.com. We then visit the root of our app and see that the url has been redirected. Now let’s make sure request parameters are carried over:

1
2
3
4
5
6
7
8
   before do
     Capybara.stub :app_host => "http://viewthespace.com"
     visit "/?hello=world"
   end

   specify do
     expect(page.current_url).to eq("http://www.vts.com/?hello=world")
   end

Now let’s make sure this works for both http://vts.com and http://viewthespace.com apex domains.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
['vts', 'viewthespace.com'].each do |domain|

      context 'visit forgot my password' do

        before do
          Capybara.stub :app_host => "http://#{domain}.com"
        end

        its(:current_url){ should == "http://www.vts.com/users/password/new?some_param=true" }

      end

    end

  end

Conclusion

That’s it for now. Supporting a new domain was not overly challenging but there were some minor hiccups along the way. As always Heroku support was extremely helpful. There are still some improvements to be made especially in relation to our apex domain configuration. We will add another post once we get that sorted out.