class Wrenchmode::Rack
Constants
- CLIENT_NAME
- HEROKU_JWT_VAR
The ENV var set on Heroku where we can retrieve the JWT
- IP_WHITELIST_KEY
- IS_SWITCHED_KEY
- REVERSE_PROXY_KEY
- SWITCH_URL_KEY
- TEST_MODE_KEY
- VERSION
Public Class Methods
new(app, opts = {})
click to toggle source
# File lib/wrenchmode/rack.rb, line 20 def initialize(app, opts = {}) @app = app # Symbolize keys opts = symbolize_keys(opts) opts = { force_open: false, ignore_test_mode: true, disable_local_wrench: false, # LocalWrench is our "brand name", want to avoid scaring people will talk of proxies status_protocol: "https", status_host: "wrenchmode.com", status_path: "/api/projects/status", check_delay_secs: 5, logging: false, read_timeout_secs: 3, trust_remote_ip: true }.merge(opts) # The JWT can be set either explicity, or implicitly if Wrenchmode is added as a Heroku add-on # The WRENCHMODE_PROJECT_JWT variable is set as part of the Heroku add-on provisioning process @jwt = opts[:jwt] || ENV[HEROKU_JWT_VAR] @ignore_test_mode = opts[:ignore_test_mode] @disable_reverse_proxy = opts[:disable_local_wrench] @force_open = opts[:force_open] @status_url = "#{opts[:status_protocol]}://#{opts[:status_host]}#{opts[:status_path]}" @check_delay_secs = opts[:check_delay_secs] @logging = opts[:logging] @read_timeout_secs = opts[:read_timeout_secs] @ip_whitelist = [] @logger = nil @trust_remote_ip = opts[:trust_remote_ip] @enable_reverse_proxy = false @made_contact = false # Use a queue with 0 or 1 items to allow the threads to communicate. When a response from the main Wrenchmode server is received, # parse the JSON and put the hash in the queue. Then, the main request thread will update the underlying middleware state # the next time a request is received. @queue = Queue.new end
Public Instance Methods
call(env)
click to toggle source
# File lib/wrenchmode/rack.rb, line 63 def call(env) @logger = env['rack.logger'] if @logging && !@logger unless @jwt log("[Wrenchmode] No JWT specified so bypassing Wrenchmode. Please configure Wrenchmode with a JWT.", Logger::ERROR) return @app.call(env) end # On startup, we need to give it a chance to make contact @check_thread ||= start_check_thread() sleep(0.01) while !@made_contact # If we've gotten a new response from the server, use it # to update local status json = begin @queue.pop(true) rescue ThreadError nil end update_status(json) if json should_display_wrenchmode = false if @switched should_display_wrenchmode = !@force_open should_display_wrenchmode &&= !ip_whitelisted?(env) end if should_display_wrenchmode if @enable_reverse_proxy reverse_proxy else redirect end else @app.call(env) end end
update_status(json)
click to toggle source
# File lib/wrenchmode/rack.rb, line 102 def update_status(json) @switch_url = json[SWITCH_URL_KEY] test_mode = json[TEST_MODE_KEY] || false @switched = json[IS_SWITCHED_KEY] && !(@ignore_test_mode && test_mode) @ip_whitelist = json[IP_WHITELIST_KEY] || [] @enable_reverse_proxy = false if json[REVERSE_PROXY_KEY] && !@disable_reverse_proxy @enable_reverse_proxy = json[REVERSE_PROXY_KEY]["enabled"] @reverse_proxy_config = symbolize_keys(json[REVERSE_PROXY_KEY]) end end
Private Instance Methods
build_update_package()
click to toggle source
# File lib/wrenchmode/rack.rb, line 206 def build_update_package { hostname: guess_hostname, ip_address: guess_ip_address, pid: guess_pid, client_name: CLIENT_NAME, client_version: VERSION } end
client_ips(env)
click to toggle source
# File lib/wrenchmode/rack.rb, line 196 def client_ips(env) request = ::Rack::Request.new(env) ips = request.ip ? [request.ip] : [] if @trust_remote_ip ips << env.remote_ip.to_s if env.respond_to?(:remote_ip) ips << env["action_dispatch.remote_ip"].to_s if Module.const_defined?("ActionDispatch::RemoteIp") && env["action_dispatch.remote_ip"] end ips end
fetch_status()
click to toggle source
# File lib/wrenchmode/rack.rb, line 117 def fetch_status inner_fetch rescue Net::HTTPError => e log("Wrenchmode Check HTTP Error: #{e.message}") @switched = false nil rescue JSON::JSONError => e log("Wrenchmode Check JSON Error: #{e.message}") @switched = false nil rescue StandardError => e log("Wrenchmode Check Unknown Error: #{e.message}") @switched = false nil ensure @made_contact = true end
guess_hostname()
click to toggle source
# File lib/wrenchmode/rack.rb, line 223 def guess_hostname Socket.gethostname rescue StandardError => e log("Wrenchmode error trying to guess the hostname: #{e.inspect}") nil end
guess_ip_address()
click to toggle source
# File lib/wrenchmode/rack.rb, line 230 def guess_ip_address address = Socket.ip_address_list.find { |addr| addr.ipv4? && !addr.ipv4_loopback? && !addr.ipv4_private? } address ? address.ip_address : nil rescue StandardError => e log("Wrenchmode error trying to guess the IP address: #{e.inspect}") nil end
guess_pid()
click to toggle source
# File lib/wrenchmode/rack.rb, line 216 def guess_pid Process.pid rescue StandardError => e log("Wrenchmode error trying to guess PID: #{e.inspect}") nil end
inner_fetch()
click to toggle source
Split this one out for easier mocking/stubbing in the specs
# File lib/wrenchmode/rack.rb, line 136 def inner_fetch payload = JSON.generate(build_update_package) body = nil uri = URI.parse(@status_url) use_ssl = uri.scheme == "https" Net::HTTP.start(uri.host, uri.port, open_timeout: @read_timeout_secs, read_timeout: @read_timeout_secs, use_ssl: use_ssl) do |http| response = http.post(uri, payload, post_headers) body = response.read_body end JSON.parse(body) end
ip_whitelisted?(env)
click to toggle source
# File lib/wrenchmode/rack.rb, line 188 def ip_whitelisted?(env) client_ips(env).any? do |client_ip| @ip_whitelist.any? do |ip_address| IPAddr.new(ip_address).include?(client_ip) end end end
log(message, level = nil)
click to toggle source
# File lib/wrenchmode/rack.rb, line 238 def log(message, level = nil) @logger.add(level || Logger::INFO, message) if @logging && @logger end
post_headers()
click to toggle source
# File lib/wrenchmode/rack.rb, line 150 def post_headers { "Content-Type" => "application/json", "Accept" => "application/json", "Authorization" => @jwt, "User-Agent" => "#{CLIENT_NAME}-#{VERSION}" } end
redirect()
click to toggle source
# File lib/wrenchmode/rack.rb, line 159 def redirect [ 302, {'Location' => @switch_url, 'Content-Type' => 'text/html', 'Content-Length' => '0'}, [] ] end
reverse_proxy()
click to toggle source
# File lib/wrenchmode/rack.rb, line 167 def reverse_proxy [ @reverse_proxy_config[:http_status], @reverse_proxy_config[:response_headers], [@reverse_proxy_config[:response_body]] ] end
start_check_thread()
click to toggle source
# File lib/wrenchmode/rack.rb, line 175 def start_check_thread Thread.new do while true do if json = fetch_status @queue.clear() @queue.push(json) end sleep(@check_delay_secs) end end end
symbolize_keys(hash)
click to toggle source
# File lib/wrenchmode/rack.rb, line 242 def symbolize_keys(hash) hash.each_with_object({}) { |(k,v), h| h[k.to_sym] = v } end