225 lines
6.6 KiB
Ruby
225 lines
6.6 KiB
Ruby
require 'rack'
|
|
require 'rack/ssl'
|
|
require 'rack/protection'
|
|
require 'delegate'
|
|
require 'time'
|
|
require 'json'
|
|
require 'travis/utils/deep_merge'
|
|
|
|
class Travis::Web::App
|
|
autoload :AltVersions, 'travis/web/app/alt_versions'
|
|
autoload :MobileRedirect, 'travis/web/app/mobile_redirect'
|
|
include Travis::DeepMerge
|
|
|
|
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)
|
|
attr_reader :main_app
|
|
def initialize(main_app)
|
|
@main_app = main_app
|
|
super({})
|
|
end
|
|
|
|
def call(env)
|
|
self[env['PATH_INFO']]
|
|
end
|
|
end
|
|
|
|
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[:environment] == 'production' ||
|
|
options[:environment] == 'staging'
|
|
builder.use Rack::SSL, hsts: Travis.config.ssl.hsts
|
|
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 new(options)
|
|
builder.to_app
|
|
end
|
|
end
|
|
|
|
attr_reader :routers, :age, :options, :root, :server_start
|
|
|
|
def initialize(options = {})
|
|
@options = options
|
|
@server_start = options.fetch(:server_start)
|
|
@root = options.fetch(:root)
|
|
@age = 60 * 60 * 24 * 365
|
|
@routers = { default: create_router }
|
|
end
|
|
|
|
def call(env)
|
|
name = env['travis.alt'] || :default
|
|
routers[name] ||= create_router(alt: name)
|
|
route = routers[name].call(env)
|
|
route[1]["Date"] = Time.now.httpdate
|
|
route
|
|
end
|
|
|
|
private
|
|
|
|
def create_router(options = {})
|
|
router = Router.new(self)
|
|
load_routes(router, options)
|
|
router
|
|
end
|
|
|
|
def load_routes(router, options = {})
|
|
each_file { |file| router[path_for(file)] = response_for(file, options) }
|
|
router.default = router['/']
|
|
end
|
|
|
|
def response_for(file, options = {})
|
|
content = File.read(file)
|
|
if fingerprinted?(file)
|
|
headers = {
|
|
'Content-Length' => content.bytesize.to_s,
|
|
'Cache-Control' => cache_control(file),
|
|
'Content-Location' => path_for(file),
|
|
'Content-Type' => mime_type(file),
|
|
'Expires' => expires(file),
|
|
'ETag' => fingerprint(file)
|
|
}
|
|
else
|
|
set_config(content, options) if config_needed?(file)
|
|
set_title(content) if index?(file)
|
|
|
|
headers = {
|
|
'Content-Length' => content.bytesize.to_s,
|
|
'Cache-Control' => cache_control(file),
|
|
'Content-Location' => path_for(file),
|
|
'Content-Type' => mime_type(file),
|
|
'Last-Modified' => server_start.httpdate,
|
|
'Expires' => expires(file),
|
|
'Vary' => vary_for(file),
|
|
'ETag' => Digest::MD5.hexdigest(content)
|
|
}
|
|
end
|
|
|
|
[ 200, headers, [content] ]
|
|
end
|
|
|
|
def each_file
|
|
Dir.glob(File.join(root, '**/*')) { |file| yield file if File.file?(file) }
|
|
end
|
|
|
|
def config_needed?(file)
|
|
index?(file) || file.end_with?('spec.html')
|
|
end
|
|
|
|
def index?(file)
|
|
file == File.join(root, 'index.html') || file == 'index.html'
|
|
end
|
|
|
|
def fingerprint(file)
|
|
basename = File.basename(file)
|
|
extname = File.extname(file)
|
|
if result = basename.scan(/.+-([a-f0-9]{32})#{extname}$/)
|
|
result.flatten[0]
|
|
end
|
|
end
|
|
alias fingerprinted? fingerprint
|
|
|
|
def cache_control(file)
|
|
case path_for(file)
|
|
when '/' then "public, must-revalidate, max-age=0"
|
|
else "public, max-age=#{age}"
|
|
end
|
|
end
|
|
|
|
def expires(file)
|
|
if fingerprinted?(file)
|
|
(server_start + age).httpdate
|
|
else
|
|
'0'
|
|
end
|
|
end
|
|
|
|
def vary_for(file)
|
|
case path_for(file)
|
|
when '/' then 'Accept'
|
|
else ''
|
|
end
|
|
end
|
|
|
|
def path_for(file)
|
|
file = file.sub("#{root}/", '')
|
|
file = "" if index?(file)
|
|
"/#{file}"
|
|
end
|
|
|
|
def mime_type(file)
|
|
Rack::Mime.mime_type File.extname(file)
|
|
end
|
|
|
|
def set_title(content)
|
|
content.gsub!(/\{\{title\}\}/, title)
|
|
end
|
|
|
|
def title
|
|
default_title = "Travis CI - Test and Deploy Your Code with Confidence"
|
|
ENV['SITE_TITLE'] || default_title
|
|
end
|
|
|
|
def set_assets_host(content)
|
|
content.gsub!(/\{\{assets_host\}\}/, ENV['ASSETS_HOST'] || '')
|
|
end
|
|
|
|
def set_config(string, opts = {})
|
|
# TODO: clean up
|
|
config = {}
|
|
config['defaultTitle'] = title
|
|
config['apiEndpoint'] = options[:api_endpoint] if options[:api_endpoint]
|
|
config['pagesEndpoint'] = options[:pages_endpoint] if options[:pages_endpoint]
|
|
config['billingEndpoint'] = options[:billing_endpoint] if options[:billing_endpoint]
|
|
config['sourceEndpoint'] = options[:source_endpoint] if options[:source_endpoint]
|
|
pusher = {}
|
|
pusher['key'] = options[:pusher_key] if options[:pusher_key]
|
|
pusher['host'] = options[:pusher_host] if options[:pusher_host]
|
|
pusher['path'] = options[:pusher_path] if options[:pusher_path]
|
|
pusher['channelPrefix'] = options[:pusher_channel_prefix] if options[:pusher_channel_prefix]
|
|
Added Igor and Carmen to the team page
|
|
config['pusher'] = pusher
|
|
|
|
config['ember-cli-pendo'] = { apiKey: options[:pendo_key] }
|
|
|
|
config['gaCode'] = options[:ga_code] if options[:ga_code]
|
|
config['pro'] = options[:pro] if options[:pro]
|
|
config['enterprise'] = options[:enterprise] if options[:enterprise]
|
|
|
|
config['codeClimate'] = options[:code_climate] if options[:code_climate]
|
|
config['codeClimateUrl'] = options[:code_climate_url] if options[:code_climate_url]
|
|
config['charmKey'] = options[:charm_key] if options[:charm_key]
|
|
config['githubOrgsOauthAccessSettingsUrl'] = options[:github_orgs_oauth_access_settings_url]
|
|
config['ajaxPolling'] = true if options[:ajax_polling]
|
|
config['userlike'] = true if options[:userlike]
|
|
|
|
config['endpoints'] = {
|
|
'sshKey' => options[:ssh_key_enabled],
|
|
'caches' => options[:caches_enabled]
|
|
}
|
|
|
|
regexp = %r(<meta name="travis/config/environment"\s+content="([^"]+)")
|
|
string.gsub!(regexp) do
|
|
ember_config = JSON.parse(URI.unescape($1))
|
|
|
|
config = deep_merge ember_config, config
|
|
config = URI.escape config.to_json
|
|
|
|
%(<meta name="travis/config/environment" content="#{config}")
|
|
end
|
|
end
|
|
end
|