diff --git a/config.ru b/config.ru index 4e95b1d7..f3272ca3 100644 --- a/config.ru +++ b/config.ru @@ -25,7 +25,7 @@ use Travis::Web::ApiRedirect do |app| app.settings.api_endpoint = ENV['API_ENDPOINT'] if ENV['API_ENDPOINT'] end -run Travis::Web::App.new( +run Travis::Web::App.build( environment: ENV['RACK_ENV'] || 'development', api_endpoint: ENV['API_ENDPOINT'], pusher_key: ENV['PUSHER_KEY'], diff --git a/lib/travis/web/app.rb b/lib/travis/web/app.rb index da5f475b..6ec4f3df 100644 --- a/lib/travis/web/app.rb +++ b/lib/travis/web/app.rb @@ -8,6 +8,8 @@ require 'time' class Travis::Web::App autoload :MobileRedirect, 'travis/web/app/mobile_redirect' + S3_URL = 'https://s3.amazonaws.com/travis-web-production/assets' + # Simple Rack router that behaves like a hash. # Key is the path, value the response. class Router < DelegateClass(Hash) @@ -18,90 +20,110 @@ class Travis::Web::App end def call(env) - if main_app.custom_branch?(env) - main_app.response_for_custom_branch(env) - else - self[env['PATH_INFO']] - end + self[env['PATH_INFO']] end end - def self.new(options = {}) - return super unless options[:environment] == 'development' - proc { |e| super.call(e) } # poor man's reloader + class AltVersions + attr_reader :app + + def initialize(app) + @app = app + end + + def call(env) + alt = alt_from(env) + env['travis.alt'] = alt if alt + status, headers, body = app.call(env) + set_cookies(headers, env['travis.alt']) if env.key?('travis.alt') + [status, headers, body] + end + + def set_cookies(headers, alt) + headers['Set-Cookie'] = "alt=#{alt}; Max-Age=#{alt == 'default' ? 0 : 86400}" + end + + def alt_from(env) + alt_from_params(env) || alt_from_cookie(env) + end + + def alt_from_params(env) + alt_from_string env['QUERY_STRING'] + end + + def alt_from_cookie(env) + alt_from_string env['HTTP_COOKIE'] + end + + def alt_from_string(string) + $1 if string =~ /alt=([^&]*)/ + end end - attr_reader :app, :router, :environment, :version, :last_modified, :age, :options, :root + class << self + def new(options = {}) + return super unless options[:environment] == 'development' + proc { |e| super.call(e) } # poor man's reloader + end + + def build(options = {}) + builder = Rack::Builder.new + if options.fetch(:environment) == 'production' + builder.use Rack::SSL + builder.use Rack::Cache + end + builder.use Rack::Deflater + builder.use Rack::Head + builder.use Rack::Protection::XSSHeader + builder.use Rack::Protection::FrameOptions + builder.use Rack::Protection::PathTraversal + builder.use Rack::ConditionalGet + builder.use Travis::Web::App::AltVersions + builder.run new(options) + builder.to_app + end + end + + attr_reader :routers, :environment, :version, :last_modified, :age, :options, :root def initialize(options = {}) @options = options @environment = options.fetch(:environment) @root = options.fetch(:root) - @router = Router.new(self) - @app = builder.to_app @version = File.read File.expand_path('version', root) @last_modified = Time.now @age = 60 * 60 * 24 * 365 - load_routes + @routers = { default: create_router } end def call(env) - app.call(env) - end - - def response_for_custom_branch(env) - status, headers, body = response_for File.join(root, 'index.html'), custom_branch: custom_branch(env) - response = Rack::Response.new body, status, headers - - if disable_custom_branch?(env) - response.delete_cookie 'custom_branch' - elsif custom_branch_from_params(env) - response.set_cookie 'custom_branch', value: custom_branch_from_params(env), expires: Time.now + 31536000 - end - - response.finish - end - - def custom_branch?(env) - custom_branch(env) || disable_custom_branch?(env) + name = env['travis.alt'] || :default + routers[name] ||= create_router(alt: name) + routers[name].call(env) end private - def disable_custom_branch?(env) - env['QUERY_STRING'] =~ /disable[_-]custom[_-]branch/ + def create_router(options = {}) + router = Router.new(self) + load_routes(router, options) + router end - def custom_branch_from_params(env) - branch = custom_branch_from_string env['QUERY_STRING'] - end - - def custom_branch_from_cookie(env) - custom_branch_from_string env['HTTP_COOKIE'] - end - - def custom_branch_from_string(string) - $1 if string =~ /(? content.bytesize.to_s, - 'Content-Location' => route_for(file), + 'Content-Location' => path_for(file), 'Cache-Control' => cache_control(file), - 'Content-Location' => route_for(file), + 'Content-Location' => path_for(file), 'Content-Type' => mime_type(file), 'ETag' => version, 'Last-Modified' => last_modified.httpdate, @@ -109,7 +131,7 @@ class Travis::Web::App 'Vary' => vary_for(file) } - [ 200, headers, [ content ] ] + [ 200, headers, [content] ] end def each_file @@ -126,18 +148,11 @@ class Travis::Web::App end def index?(file) - file.end_with? 'index.html' - end - - def route_for(file) - file = file.sub("#{root}/", '') - file = File.join(version, file) if prefix? file - file = "" if index? file - "/#{file}" + file.end_with?('index.html') end def cache_control(file) - case route_for(file) + case path_for(file) when '/' then "public, must-revalidate" when '/version' then "no-cache" else "public, max-age=#{age}" @@ -145,13 +160,20 @@ class Travis::Web::App end def vary_for(file) - case route_for(file) + case path_for(file) when '/' then 'Accept' when '/version' then '*' else '' end end + def path_for(file) + file = file.sub("#{root}/", '') + file = File.join(version, file) if prefix?(file) + file = "" if index?(file) + "/#{file}" + end + def mime_type(file) Rack::Mime.mime_type File.extname(file) end @@ -162,28 +184,7 @@ class Travis::Web::App end string.gsub! %r{(src|href)="(?:\/?)((styles|scripts)\/[^"]*)"} do - if opts[:custom_branch] - url = "https://s3.amazonaws.com/travis-web-production/assets/#{opts[:custom_branch]}/#{$2}" - %(#$1="#{url}") - else - %(#$1="/#{version}/#$2") - end + %(#{$1}=#{opts[:alt] ? "#{S3_URL}/#{opts[:alt]}/#{$2}" : "/#{version}/#{$2}"}) end end - - def builder - builder = Rack::Builder.new - if environment == 'production' - builder.use Rack::SSL - builder.use Rack::Cache - end - builder.use Rack::Deflater - builder.use Rack::Head - builder.use Rack::Protection::XSSHeader - builder.use Rack::Protection::FrameOptions - builder.use Rack::Protection::PathTraversal - builder.use Rack::ConditionalGet - builder.run router - builder - end end diff --git a/spec/app_spec.rb b/spec/app_spec.rb index 584616ba..b50f687a 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -6,7 +6,7 @@ describe Travis::Web::App do end describe 'catch all' do - before { get('/foo/bar') } + before { get('/foo/bar') } example { last_response.should be_ok } example { headers['Content-Location'].should be == '/' } example { headers['Cache-Control'].should include('must-revalidate') } @@ -15,7 +15,7 @@ describe Travis::Web::App do end describe 'assets' do - before { get('/favicon.ico') } + before { get('/favicon.ico') } example { last_response.should be_ok } example { headers['Content-Location'].should be == '/favicon.ico' } example { headers['Cache-Control'].should_not include('must-revalidate') } @@ -24,30 +24,30 @@ describe Travis::Web::App do end describe 'version' do - before { get('/version') } + before { get('/version') } example { last_response.should be_ok } example { headers['Content-Location'].should be == '/version' } example { headers['Cache-Control'].should be == 'no-cache' } example { headers['Vary'].split(',').should_not include('Accept') } end - describe 'custom branch' do - context 'when passing custom branch as a param' do - before { get('/?custom-branch=foo') } + describe 'alternate asset versions' do + context 'not passing an alt param' do + before { get('/') } + example { headers['Set-Cookie'].should be_nil } + end + + context 'passing an alt param' do + before { get('/?alt=foo') } example { last_response.should be_ok } example { last_response.body.should include('/assets/foo/styles/app.css') } example { last_response.body.should include('/assets/foo/scripts/app.js') } - example { headers['Set-Cookie'].should include('custom_branch=foo') } + example { headers['Set-Cookie'].should == 'alt=foo; Max-Age=86400' } end - context 'disabling custom branch' do - before { get('/?disable-custom-branch=true') } - example { last_response.should be_ok } - example { last_response.body.should =~ %r{src="/[^\/]+/scripts/app.js} } - example { last_response.body.should_not include('/assets/true/styles/app.css') } - example { last_response.body.should_not include('/assets/foo/styles/app.css') } - example { last_response.body.should_not include('/assets/foo/scripts/app.js') } - example { headers['Set-Cookie'].should include('custom_branch=;') } + context 'passing default as an alt param' do + before { get('/?alt=default') } + example { headers['Set-Cookie'].should == 'alt=default; Max-Age=0' } end end end