Skip to content

Commit

Permalink
Merge branch 'arbesulo-feat/add_ip_whitelisting'
Browse files Browse the repository at this point in the history
  • Loading branch information
ianheggie committed Nov 29, 2016
2 parents c6f04c1 + 9652210 commit 6081a1e
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 375 deletions.
25 changes: 19 additions & 6 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ To change the configuration of health_check, create a file `config/initializers/

HealthCheck.setup do |config|

# uri prefix
# uri prefix (no leading slash)
config.uri = 'health_check'

# Text output upon success
Expand Down Expand Up @@ -124,6 +124,13 @@ To change the configuration of health_check, create a file `config/initializers/
# These default to nil and the endpoint is not protected
config.basic_auth_username = 'my_username'
config.basic_auth_password = 'my_password'

# Whitelist requesting IPs
# Defaults to blank and allows any IP
config.origin_ip_whitelist = %w(123.123.123.123)

# http status code used when the ip is not allowed for the request
config.http_status_for_ip_whitelist_error = 403
end

You may call add_custom_check multiple times with different tests. These tests will be included in the default list ("standard").
Expand All @@ -133,15 +140,21 @@ If you have a catchall route then add the following line above the catch all rou

=== Installing As Middleware

Install health_check as middleware if you want to ignore exceptions from later parts of the Rails middleware stack,
eg DB connection errors from QueryCache. Include "middleware" in the list of checks to have the checks processed by the middleware
health check rather than the full stack check. The "middleware" check will fail if you have not installed health_check as middleware.
Install health_check as middleware if you want to sometimes ignore exceptions from later parts of the Rails middleware stack,
eg DB connection errors from QueryCache. The "middleware" check will fail if you have not installed health_check as middleware.

To install health_check as middleware add the following line to the config/application.rb:
config.middleware.insert_after "Rails::Rack::Logger", HealthCheck::MiddlewareHealthcheck

Note: health_check is installed as a full rails engine even if it has been installed as middleware. This is so the
standard checks continue to test the complete rails stack.
remaining checks continue to run through the complete rails stack.

You can also adjust what checks are run from middleware, eg if you want to exclude the checking of the database etc, then set
config.middleware_checks = ['middleware', 'standard', 'custom']
config.standard_checks = ['middleware', 'custom']

Middleware checks are run first, and then full stack checks.
When installed as middleware, exceptions thrown when running the full stack tests are formatted in the standard way.

== Uptime Monitoring

Expand Down Expand Up @@ -277,7 +290,7 @@ The command `rake test` will also launch these tests, except it cannot install t

== Copyright

Copyright (c) 2010-2014 Ian Heggie, released under the MIT license.
Copyright (c) 2010-2016 Ian Heggie, released under the MIT license.
See MIT-LICENSE for details.

== Contributors
Expand Down
35 changes: 3 additions & 32 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,9 @@ require "bundler/gem_tasks"
#require 'rubygems'
require 'rake'

# Tests are conducted with health_test as a plugin
environment_file = File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'environment.rb')
plugin_dir = File.join(File.dirname(__FILE__), '..', 'plugins')

if File.exists?(environment_file) and File.directory?(plugin_dir)
# test as plugin

require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end

begin
require 'rcov/rcovtask'
Rcov::RcovTask.new do |test|
test.libs << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
rescue LoadError
task :rcov do
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
end
end

else
#tests as gem
task :test do
exec '/bin/bash', './test/test_with_railsapp'
end
#tests as gem
task :test do
exec '/bin/bash', './test/test_with_railsapp'
end

task :default => :test
Expand Down
16 changes: 15 additions & 1 deletion lib/health_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class Engine < Rails::Engine
mattr_accessor :http_status_for_error_object
self.http_status_for_error_object = 500

# http status code used when the ip is not allowed for the request
mattr_accessor :http_status_for_ip_whitelist_error
self.http_status_for_ip_whitelist_error = 403

# ips allowed to perform requests
mattr_accessor :origin_ip_whitelist
self.origin_ip_whitelist = []

# max-age of response in seconds
# cache-control is public when max_age > 1 and basic authentication is used
mattr_accessor :max_age
Expand All @@ -32,7 +40,7 @@ class Engine < Rails::Engine
mattr_accessor :buckets
self.buckets = {}

# health check uri path for middleware check
# health check uri path
mattr_accessor :uri
self.uri = 'health_check'

Expand All @@ -49,6 +57,12 @@ class Engine < Rails::Engine
self.full_checks = ['database', 'migrations', 'custom', 'email', 'cache', 'redis-if-present', 'sidekiq-redis-if-present', 'resque-redis-if-present', 's3-if-present']
self.standard_checks = [ 'database', 'migrations', 'custom', 'emailconf' ]

# Middleware based checks
mattr_accessor :middleware_checks
self.middleware_checks = [ 'middleware' ]

mattr_accessor :installed_as_middleware

def self.add_custom_check(&block)
custom_checks << block
end
Expand Down
54 changes: 33 additions & 21 deletions lib/health_check/health_check_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module HealthCheck
class HealthCheckController < ActionController::Base

layout false if self.respond_to? :layout
before_action :check_origin_ip
before_action :authenticate

def index
Expand All @@ -13,34 +14,21 @@ def index
if max_age > 1
last_modified = Time.at((last_modified.to_f / max_age).floor * max_age).utc
end
public = (max_age > 1) && ! basic_auth_username
public = (max_age > 1) && ! HealthCheck.basic_auth_username
if stale?(:last_modified => last_modified, :public => public)
# Rails 4.0 doesn't have :plain, but it is deprecated later on
plain_key = Rails.version < '4.1' ? :text : :plain
checks = params[:checks] || 'standard'
checks = params[:checks] ? params[:checks].split('_') : ['standard']
checks -= HealthCheck.middleware_checks if HealthCheck.installed_as_middleware
begin
errors = HealthCheck::Utils.process_checks(checks)
rescue Exception => e
errors = e.message.blank? ? e.class.to_s : e.message.to_s
end
end
response.headers['Cache-control'] = (public ? 'public' : 'private') + ', no-cache, must-revalidate' + (max_age > 0 ? ", max-age=#{max_age}" : '')
if errors.blank?
obj = { :healthy => true, :message => HealthCheck.success }
respond_to do |format|
format.html { render plain_key => HealthCheck.success, :content_type => 'text/plain' }
format.json { render :json => obj }
format.xml { render :xml => obj }
format.any { render plain_key => HealthCheck.success, :content_type => 'text/plain' }
end
send_response nil, :ok, :ok
else
msg = "health_check failed: #{errors}"
obj = { :healthy => false, :message => msg }
respond_to do |format|
format.html { render plain_key => msg, :status => HealthCheck.http_status_for_error_text, :content_type => 'text/plain' }
format.json { render :json => obj, :status => HealthCheck.http_status_for_error_object}
format.xml { render :xml => obj, :status => HealthCheck.http_status_for_error_object }
format.any { render plain_key => msg, :status => HealthCheck.http_status_for_error_text, :content_type => 'text/plain' }
end
send_response msg, HealthCheck.http_status_for_error_text, HealthCheck.http_status_for_error_object
# Log a single line as some uptime checkers only record that it failed, not the text returned
if logger
logger.info msg
Expand All @@ -49,20 +37,44 @@ def index
end
end


protected

def send_response(msg, text_status, obj_status)
healthy = !msg
msg ||= HealthCheck.success
obj = { :healthy => healthy, :message => msg}
respond_to do |format|
format.html { render plain_key => msg, :status => text_status, :content_type => 'text/plain' }
format.json { render :json => obj, :status => obj_status }
format.xml { render :xml => obj, :status => obj_status }
format.any { render plain_key => msg, :status => text_status, :content_type => 'text/plain' }
end
end

def authenticate
return unless HealthCheck.basic_auth_username && HealthCheck.basic_auth_password
authenticate_or_request_with_http_basic do |username, password|
authenticate_or_request_with_http_basic('Health Check') do |username, password|
username == HealthCheck.basic_auth_username && password == HealthCheck.basic_auth_password
end
end

def check_origin_ip
unless HealthCheck.origin_ip_whitelist.blank? ||
HealthCheck.origin_ip_whitelist.include?(request.ip)
render plain_key => 'Health check is not allowed for the requesting IP',
:status => HealthCheck.http_status_for_ip_whitelist_error,
:content_type => 'text/plain'
end
end

# turn cookies for CSRF off
def protect_against_forgery?
false
end

def plain_key
# Rails 4.0 doesn't have :plain, but it is deprecated later on
Rails.version < '4.1' ? :text : :plain
end
end
end
2 changes: 1 addition & 1 deletion lib/health_check/health_check_routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def health_check_routes(prefix = nil)

def add_health_check_routes(prefix = nil)
HealthCheck.uri = prefix if prefix
match "#{HealthCheck.uri}(/:checks)(.:format)", :to => 'health_check/health_check#index', via: [:get, :post]
match "#{HealthCheck.uri}(/:checks)(.:format)", :to => 'health_check/health_check#index', via: [:get, :post], :defaults => { :format => 'txt' }
end

end
Expand Down
79 changes: 72 additions & 7 deletions lib/health_check/middleware_health_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ def initialize(app)
end

def call(env)
uri = env['PATH_INFO']
if uri =~ /^\/?#{HealthCheck.uri}\/([-_0-9a-zA-Z]*)middleware_?([-_0-9a-zA-Z]*)(\.(\w*))?/
checks = $1 + ($1 != '' && $2 != '' ? '_' : '') + $2
checks = 'standard' if checks == ''
response_type = $4
(response_type, middleware_checks, full_stack_checks) = parse_env(env)
if response_type
if error_response = (ip_blocked(env) || not_authenticated(env))
return error_response
end
HealthCheck.installed_as_middleware = true
errors = ''
begin
errors = HealthCheck::Utils.process_checks(checks)
# Process the checks to be run from middleware
errors = HealthCheck::Utils.process_checks(middleware_checks, true)
# Process remaining checks through the full stack if there are any
unless full_stack_checks.empty?
return @app.call(env)
end
rescue => e
errors = e.message.blank? ? e.class.to_s : e.message.to_s
end
Expand All @@ -21,17 +28,75 @@ def call(env)
if response_type == 'xml'
content_type = 'text/xml'
msg = { healthy: healthy, message: msg }.to_xml
error_code = HealthCheck.http_status_for_error_object
elsif response_type == 'json'
content_type = 'application/json'
msg = { healthy: healthy, message: msg }.to_json
error_code = HealthCheck.http_status_for_error_object
else
content_type = 'text/plain'
error_code = HealthCheck.http_status_for_error_text
end
[ (healthy ? 200 : 500), { 'Content-Type' => content_type }, [msg] ]
[ (healthy ? 200 : error_code), { 'Content-Type' => content_type }, [msg] ]
else
@app.call(env)
end
end

protected

def parse_env(env)
uri = env['PATH_INFO']
if uri =~ /^\/#{Regexp.escape HealthCheck.uri}(\/([-_0-9a-zA-Z]*))?(\.(\w*))?$/
checks = $2.to_s == '' ? ['standard'] : $2.split('_')
response_type = $4.to_s
middleware_checks = checks & HealthCheck.middleware_checks
full_stack_checks = (checks - HealthCheck.middleware_checks) - ['and']
[response_type, middleware_checks, full_stack_checks ]
end
end

def ip_blocked(env)
return false if HealthCheck.origin_ip_whitelist.blank?
req = Rack::Request.new(env)
unless HealthCheck.origin_ip_whitelist.include?(req.ip)
[ HealthCheck.http_status_for_ip_whitelist_error,
{ 'Content-Type' => 'text/plain' },
[ 'Health check is not allowed for the requesting IP' ]
]
end
end

def not_authenticated(env)
return false unless HealthCheck.basic_auth_username && HealthCheck.basic_auth_password
auth = MiddlewareHealthcheck::Request.new(env)
if auth.provided? && auth.basic? && Rack::Utils.secure_compare(HealthCheck.basic_auth_username, auth.username) && Rack::Utils.secure_compare(HealthCheck.basic_auth_password, auth.password)
env['REMOTE_USER'] = auth.username
return false
end
[ 401,
{ 'Content-Type' => 'text/plain', 'WWW-Authenticate' => 'Basic realm="Health Check"' },
[ ]
]
end

class Request < Rack::Auth::AbstractRequest
def basic?
"basic" == scheme
end

def credentials
@credentials ||= params.unpack("m*").first.split(/:/, 2)
end

def username
credentials.first
end

def password
credentials.last
end
end

end
end
13 changes: 8 additions & 5 deletions lib/health_check/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ class Utils

cattr_accessor :default_smtp_settings

def self.process_checks(checks)
# process an array containing a list of checks
def self.process_checks(checks, called_from_middleware = false)
errors = ''
checks.split('_').each do |check|
checks.each do |check|
case check
when 'and', 'site'
# do nothing
Expand Down Expand Up @@ -63,15 +64,17 @@ def self.process_checks(checks)
when 's3'
errors << HealthCheck::S3HealthCheck.check
when "standard"
errors << HealthCheck::Utils.process_checks(HealthCheck.standard_checks.join('_'))
errors << HealthCheck::Utils.process_checks(HealthCheck.standard_checks, called_from_middleware)
when "middleware"
errors << "Health check not called from middleware - probably not installed as middleware." unless called_from_middleware
when "custom"
HealthCheck.custom_checks.each do |custom_check|
errors << custom_check.call(self)
end
when "all", "full"
errors << HealthCheck::Utils.process_checks(HealthCheck.full_checks.join('_'))
errors << HealthCheck::Utils.process_checks(HealthCheck.full_checks, called_from_middleware)
else
return "invalid argument to health_test. "
return "invalid argument to health_test."
end
end
return errors
Expand Down
3 changes: 3 additions & 0 deletions test/setup_railsapp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ HealthCheck.setup do |config|
config.http_status_for_error_text = 550
config.http_status_for_error_object = 555
config.uri = '$route_prefix'
config.origin_ip_whitelist = ENV['IP_WHITELIST'].split(',') unless ENV['IP_WHITELIST'].blank?
config.basic_auth_username = ENV['AUTH_USER'] unless ENV['AUTH_USER'].blank?
config.basic_auth_password = ENV['AUTH_PASSWORD'] unless ENV['AUTH_PASSWORD'].blank?
config.add_custom_check do
File.exists?("$custom_file") ? '' : '$custom_file is missing!'
Expand Down
Loading

0 comments on commit 6081a1e

Please sign in to comment.