Showing posts with label subdomains. Show all posts
Showing posts with label subdomains. Show all posts

20130529

HTTPS in local development environment (with subdomains, stunnel and [optionally] pow and [bonus] iPad)

In every web developer's life there's a moment your baby wants to go secure, and this means putting your web app on HTTPS. That is, simply put, adding SSL on top of everything. Sounds simple? Well it should be. For production/staging solution you should read your own app hosting provider's docs, e.g. SSL Endpoint | Heroku Dev Center. And for your local dev env, read on (well if you're on OSX or *nix at least).

First, I've found no reason not to use pow, both for same-machine (via http://somesub.yourbaby.dev) and same-network (via http://somesub.yourbaby.192.168.1.111.xip.io) testing.
Tip: even if you don't fancy pow, and use different ports for running stuff instead, you can still use this technique with http://somesub.lvh.me:3000 (same-machine, resolves to 127.0.0.1:3000) or http://somesub.192.168.1.111.xip.io:3000 (same-network, resolves to 192.168.1.111:3000), somesub being any subdomain.

Then, we're going to use stunnel to put that SSL on top of our fine-running web app. So, I've compiled a little script to automate the stunnel certificate + configuration generation, here it is (any patches/comments are welcome). Install/copy it somewhere, then go to your project's directory and run:

stunnelo.sh yourbaby.192.168.1.111.xip.io

Hopefully, it will produce all the necessary files in ./var/stunnel/yourbaby.192.168.1.111.xip.io/

Finally, you may run (in a dedicated terminal window):

stunnel var/stunnel/yourbaby.192.168.1.111.xip.io/stunnel.cnf

then hopefully go to https://anysub.yourbaby.192.168.1.111.xip.io and see your same old app running in total security.

Wait! In a moment you'll notice your browser not being happy about the certificate we've just created, so here's a...

Bonus:
The self-signed certificates this technique creates are obviously will not be trusted by any browser on any device by default. This will get even more messy if your app involves a number of services you're running in the same dev env.
For computers, a browser will usually allow you to install the certificate in question when it sees it, so you will be annoyed just once. For iPads/iPhones — that's what same-network testing is for — you will have to take var/stunnel/yourbaby.192.168.1.111.xip.io/stunnel.crt and mail it to yourself, then open the attachment on your iPad and that's how you install the certificate there and go debugging.
Obviously, you can have any number of such stunnel configs in ./var/stunnel/ — go wild with the domains you are to test.

p.s. I've tried to use tunnels, but it provides no means of control over the certificates and thus, complicated scenarios bring... uh... complications.
p.p.s. Thanks go to xip.io-cert for outlining self-signed certificate generation.

20121119

subdomains in rails apps: a current brief

Subdomains (or second level domains) are still a nice way to present separate interfaces to a single web app — commonly to give a certain kind of customers the feel-n-touch of a dedicated app install.

Here are most of the related aspects based on a particular Rails3 project:

  • some model Group, representing a customer, or rather a group of users, has a string column domain (migration not shown, but don’t forget to index by that column)

    in app/models/group.rb:

    has_many :users
    before_validation :downcase_domain, :if => :domain_changed?
    validates :domain, presence: true, uniqueness: true, length: {maximum: 255}, format: /^[0-9a-z-]+$/
    
    
    def host
      "#{domain}.#{ROOT_DOMAIN}"
    end
    
    
    protected
    def downcase_domain
      self.domain = domain.to_s.downcase
    end
    
  • under assumption that you’re using devise and your (group’s) user identity model is User (updated for everchanging devise 2.0.4)

    in app/models/user.rb:

    belongs_to :group
    devise :database_authenticatable, ..., :request_keys => [:app_subdomain]
    
    
    def self.find_for_authentication(conditions={})
      group = Group.find_by_domain(conditions.delete(:app_subdomain))
      return nil unless group.present?
      conditions[:group_id] = group.id
      super
    end
    
  • you’ll have some admin interface where the groups are managed (e.g. active_admin, not recommended)

    in app/admin/groups.rb:

    link_to group.domain, root_url(host: group.host)
    
  • you will certainly want to do something specific in config/routes.rb

  • you’ll want some handy helper method, to know which guvnor you’re serving

    in app/controllers/application_controller.rb:

    helper_method :current_group
    def current_group
      return @current_group if defined? @current_group
      @current_group = request.app_subdomain && Group.find_by_domain(request.app_subdomain)
    end
    
  • you’ll need an initializer of some sort to set a constant and monkey-patch the request class, so…

    in config/initializers/subdomain.rb:

    ROOT_DOMAIN ||= ENV['ROOT_DOMAIN'] or raise "ROOT_DOMAIN must be set"
    
    
    # (): paranoid monkey patching :()
    class ActionDispatch::Request
      def app_subdomain
        return @app_subdomain if defined? @app_subdomain
        @@app_hostname_regex ||= /^(?:([0-9a-z-]+).)?#{Regexp.escape(ROOT_DOMAIN)}$/
        raise 'Wrong domain' unless host.downcase =~ @@app_hostname_regex
        @app_subdomain = $1
      end
    end
    
  • then, for your production (and staging) environment on, say, heroku, you’ll have to setup your lovely app domain name (with wildcard subdomains) and set the environment variable ROOT_DOMAIN to it

  • for test environment, which is also good for the handy circleci

    in config/environments/test.rb:

    ROOT_DOMAIN = 'lvh.me'  # yes, it's a magic domain for 127.0.0.1 //smackaho.st RIP
    
  • for other environment cases, be sure to set either ROOT_DOMAIN or ENV['ROOT_DOMAIN'] as appropriate

  • if you use factories (and girls, factory_girl)

    in spec/factories.rb:

    factory :group do
      sequence(:domain) {|n| "dom-#{n}"}  # or better still, use `forgery` with some smart randomness
      ...
    end
    
  • if you use capybara (2.0.0 at least, recommended) and rspec (rspec-rails 2.12.0 at least)

    in spec/spec_helper.rb: (in Spork.prefork block if you use spork, recommended)

    Capybara.always_include_port = true  # unless you `visit` external sites in your feature specs
    

    and then in some spec/features/..._spec.rb:

    visit("http://some_domain.#{ROOT_DOMAIN}/some_path")
    # or
    visit(some_url host: @group.host)  # if you're playing dirty, using pre-fabricated data and route helpers, recommended
    
  • in some spec/controllers/..._spec.rb you’ll have to include something like this:

    before(:each) do
      request.host = @group.host
    end
    
  • don’t forget the specs for domains in spec/models/group_spec.rb and other relevant places

May your sub-domains be obedient to their master.