# encoding: utf-8
# Phusion Passenger - https://www.phusionpassenger.com/
# Copyright (c) 2014-2015 Phusion
#
# "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'optparse'
PhusionPassenger.require_passenger_lib 'constants'
PhusionPassenger.require_passenger_lib 'config/command'
module PhusionPassenger
module Config
class ValidateInstallCommand < Command
# Signifies that there is at least 1 error.
FAIL_EXIT_CODE = 1
# Signifies that there are no error, but at least 1 warning.
WARN_EXIT_CODE = 2
# Internal error occurred.
INTERNAL_ERROR_CODE = 9
def run
@orig_argv = @argv.dup
parse_options
prepare
begin
if !@options[:auto] && !@options[:invoked_from_installer]
ask_what_to_validate
end
if @options[:validate_apache2]
initialize_apache_envvars
if !@options[:auto] && !@options[:invoked_from_installer]
check_whether_there_are_multiple_apache_installs
end
end
if @options[:validate_passenger]
check_tools_in_path
check_no_other_installs_in_path
end
if @options[:validate_apache2]
if check_apache2_installed
check_apache2_load_module_config
end
end
if @options[:summary]
summarize
end
exit(FAIL_EXIT_CODE) if @error_count > 0
exit(WARN_EXIT_CODE) if @warning_count > 0
ensure
reset_terminal
end
end
private
def self.create_default_options
return {
:auto => !STDIN.tty?,
:validate_passenger => true,
:colors => STDOUT.tty?,
:summary => true
}
end
def self.create_option_parser(options)
OptionParser.new do |opts|
nl = "\n" + ' ' * 37
opts.banner = "Usage: passenger-config validate-install [OPTIONS]\n"
opts.separator ""
opts.separator " Validate this #{SHORT_PROGRAM_NAME} installation and/or its integration with web servers."
opts.separator " If you run this command in a terminal, it will run interactively and ask you questions."
opts.separator " You can run this command non-interactively either by running it without a terminal, "
opts.separator " or by passing --auto."
opts.separator ""
opts.separator " When running non-interactively, the default is to validate the #{SHORT_PROGRAM_NAME}"
opts.separator " installation only (so e.g. Apache is not validated). You can customize this"
opts.separator " using the appropriate command line options."
opts.separator ""
opts.separator " The exit codes are as follows:"
opts.separator " 0 - All checks passed. No errors, no warnings."
opts.separator " #{FAIL_EXIT_CODE} - Some checks failed with an error."
opts.separator " #{WARN_EXIT_CODE} - No checks failed with an error, but some failed produced warnings."
opts.separator " #{INTERNAL_ERROR_CODE} - Some internal error occurred."
opts.separator ""
opts.separator "Options:"
opts.on("--auto", "Run non-interactively") do
options[:auto] = true
end
opts.on("--no-validate-passenger", "Do not validate the #{SHORT_PROGRAM_NAME} installation#{nl}" +
"itself") do
options[:validate_passenger] = false
end
opts.on("--validate-apache2", "Validate Apache 2 integration") do
options[:validate_apache2] = true
end
opts.on("--apxs2-path=PATH", String, "Apache installation to validate") do |val|
ENV['APXS2'] = val
end
opts.separator ""
opts.on("--no-colors", "Never output colors") do
options[:colors] = false
end
opts.on("--no-summary", "Do not display a summary") do
options[:summary] = false
end
opts.separator ""
opts.on("-h", "--help", "Show this help") do
options[:help] = true
end
opts.separator ""
opts.separator "Internal options:"
opts.on("--invoked-from-installer", "Indicate that this program is invoked from#{nl}" +
"passenger-install-apache2-module") do
options[:invoked_from_installer] = true
end
end
end
def prepare
begin
require 'rubygems'
rescue LoadError
end
PhusionPassenger.require_passenger_lib 'utils/ansi_colors'
PhusionPassenger.require_passenger_lib 'utils/terminal_choice_menu'
PhusionPassenger.require_passenger_lib 'platform_info'
PhusionPassenger.require_passenger_lib 'platform_info/ruby'
PhusionPassenger.require_passenger_lib 'platform_info/apache'
PhusionPassenger.require_passenger_lib 'platform_info/apache_detector'
PhusionPassenger.require_passenger_lib 'platform_info/depcheck'
require 'stringio'
require 'pathname'
@error_count = 0
@warning_count = 0
@colors = Utils::AnsiColors.new(@options[:colors])
prepare_terminal
end
def prepare_terminal
STDOUT.write(@colors.default_terminal_color)
STDOUT.flush
end
def reset_terminal
STDOUT.write(@colors.reset)
STDOUT.flush
end
def ask_what_to_validate
log "What would you like to validate?"
log "Use to select."
log "If the menu doesn't display correctly, press '!'"
puts
menu = Utils::TerminalChoiceMenu.new([
"#{SHORT_PROGRAM_NAME} itself",
"Apache"
])
menu["#{SHORT_PROGRAM_NAME} itself"].checked = @options[:validate_passenger]
menu["Apache"].checked = @options[:validate_apache2]
begin
menu.query
rescue Interrupt
exit(INTERNAL_ERROR_CODE)
end
display_separator
@options[:validate_passenger] = menu.selected_choices.include?("#{SHORT_PROGRAM_NAME} itself")
@options[:validate_apache2] = menu.selected_choices.include?("Apache")
end
def initialize_apache_envvars
# The Apache executable may be located in an 'sbin' folder. We add
# the 'sbin' folders to $PATH just in case. On some systems
# 'sbin' isn't in $PATH unless the user is logged in as root from
# the start (i.e. not via 'su' or 'sudo').
ENV["PATH"] += ":/usr/sbin:/sbin:/usr/local/sbin"
end
def check_tools_in_path
checking "whether this #{SHORT_PROGRAM_NAME} install is in PATH"
paths = ENV['PATH'].to_s.split(':')
if paths.include?(gem_bindir) ||
paths.include?(homebrew_bindir) ||
paths.include?(PhusionPassenger.bin_dir)
check_ok
else
check_warning
suggest %Q{
Please add #{PhusionPassenger.bin_dir} to PATH.
Otherwise you will get "command not found" errors upon running
any Passenger commands.
Learn more at about PATH at:
https://www.phusionpassenger.com/library/indepth/environment_variables.html#the-path-environment-variable
}
end
end
def check_no_other_installs_in_path
checking "whether there are no other #{SHORT_PROGRAM_NAME} installations"
paths = ENV['PATH'].to_s.split(':')
if Process.uid == 0 &&
(sudo_user = ENV['SUDO_USER']) &&
(bash = PlatformInfo.find_command("bash")) &&
PlatformInfo.find_command("sudo")
# If we were invoked through sudo then we need to check the original user's PATH too.
output = `sudo -u #{sudo_user} #{bash} -lc 'echo; echo PATH FOLLOWS; echo "$PATH"' 2>&1`
output.sub!(/.*\nPATH FOLLOWS\n/m, '')
output.strip!
paths.concat(output.split(':'))
end
# These may not be in PATH if the user did not run this command through sudo.
paths << "/usr/bin"
paths << "/usr/sbin"
# Some of the paths may be symlinks, so we take the realpaths when
# possible and remove duplicates. This is especially important on
# Red Hat 7, where /bin is a symlink to /usr/bin.
paths.map! do |path|
try_realpath(path)
end
paths.delete(try_realpath(gem_bindir))
paths.delete(try_realpath(homebrew_bindir))
paths.delete(try_realpath(PhusionPassenger.bin_dir))
paths.uniq!
other_installs = []
paths.each do |path|
filename = "#{path}/passenger"
if File.exist?(filename)
other_installs << filename
end
end
if other_installs.empty?
check_ok
else
check_warning
suggest %Q{
You are currently validating against #{PROGRAM_NAME} #{VERSION_STRING}, located in:
#{PhusionPassenger.bin_dir}/passenger
Besides this #{SHORT_PROGRAM_NAME} installation, the following other
#{SHORT_PROGRAM_NAME} installations have also been detected:
#{other_installs.join("\n ")}
Please uninstall these other #{SHORT_PROGRAM_NAME} installations to avoid
confusion or conflicts.
}
end
end
def check_whether_there_are_multiple_apache_installs
if PlatformInfo.httpd.nil? || PlatformInfo.apxs2.nil?
# check_apache2_installed will handle this.
return
end
log 'Checking whether there are multiple Apache installations...'
output = StringIO.new
detector = PlatformInfo::ApacheDetector.new(output)
begin
detector.detect_all
detector.report
apache2 = detector.result_for(PlatformInfo.apxs2)
if apache2.nil?
# Print an extra newline because the autodetection routines
# may have run some commands which printed stuff to stderr.
puts
if Process.uid == 0
# More information will be displayed in #check_no_duplicate_apache2_load_module_config,
# which should also fail.
log "Your Apache installation appears to be broken. More information will be displayed later."
else
whoami = `whoami`.strip
sudo = PhusionPassenger::PlatformInfo.ruby_sudo_command
selfcommand = "#{PhusionPassenger.bin_dir}/passenger-config validate-install #{@orig_argv.join(' ')}"
log "Permission problems"
log "This program must be able to analyze your Apache installation. But it can't"
log "do that, because you're running the installer as #{whoami}."
log "Please give this program root privileges, by re-running it with #{sudo}:"
log ""
log " export ORIG_PATH=\"$PATH\""
log " #{sudo_s_e}"
log " export PATH=\"$ORIG_PATH\""
log " #{ruby_command} #{selfcommand}"
exit(INTERNAL_ERROR_CODE)
end
elsif detector.results.size > 1
other_installs = detector.results - [apache2]
log "Multiple Apache installations detected!"
log ""
log "You are about to validate #{SHORT_PROGRAM_NAME} against the following"
log "Apache installation:"
log ""
log " Apache #{apache2.version}"
log " apxs2 : #{apache2.apxs2}"
log " Executable: #{apache2.httpd}"
log ""
log "However, #{other_installs.size} other Apache installation(s) have been found on your system:"
log ""
other_installs.each do |result|
log " Apache #{result.version}"
log " apxs2 : #{result.apxs2}"
log " Executable: #{result.httpd}"
log ""
end
result = prompt_confirmation "Are you sure you want to validate " +
"against Apache #{apache2.version} (#{apache2.apxs2})?"
if !result
puts
display_separator
other_installs.each do |result|
log " * To validate against Apache #{result.version} (#{result.apxs2}):"
log " Re-run this program with: --apxs2-path '#{result.apxs2}'"
end
log ""
log "You may also want to read the \"Installation\" section of Passenger Library"
log "installation troubleshooting:"
log ""
log " https://www.phusionpassenger.com/library/install/apache/"
log ""
log "If you keep having problems installing, please visit the following website for"
log "support:"
log ""
log " #{SUPPORT_URL}"
exit(INTERNAL_ERROR_CODE)
end
else
log 'Only a single installation detected. This is good.'
end
display_separator
ensure
detector.finish
end
end
def check_apache2_installed
checking "whether Apache is installed"
if PlatformInfo.httpd
if PlatformInfo.apxs2
check_ok
true
else
check_error
PlatformInfo::Depcheck.load("depcheck_specs/apache2")
dep = PlatformInfo::Depcheck.find("apache2-dev")
install_instructions = dep.install_instructions.split("\n").join("\n ")
if !@options[:invoked_from_installer]
next_step = "When done, please re-run this program."
end
suggest %Q{
Unable to validate your Apache installation: more software required
This program requires the apxs2 tool in order to be able to validate your
Apache installation. This tool is currently not installed. You can solve this
as follows:
#{install_instructions}
#{next_step}
}
false
end
else
check_error
PlatformInfo::Depcheck.load("depcheck_specs/apache2")
dep = PlatformInfo::Depcheck.find("apache2")
install_instructions = dep.install_instructions.split("\n").join("\n ")
suggest %Q{
Apache is not installed. You can solve this as follows:
#{install_instructions}
}
false
end
end
def check_apache2_load_module_config
checking "whether the Passenger module is correctly configured in Apache"
if PlatformInfo.httpd_default_config_file.nil?
check_error
passenger_config = "#{PhusionPassenger.bin_dir}/passenger-config"
suggest %Q{
Your Apache installation might be broken
You are about to validate #{PROGRAM_NAME} against the following
Apache installation:
apxs2: #{PlatformInfo.apxs2}
However, this Apache installation appears to be broken, so this program
cannot continue. To find out why this program thinks the above Apache
installation is broken, run:
export ORIG_PATH="$PATH"
#{sudo_s_e}
export PATH="$ORIG_PATH"
#{ruby_command} #{passenger_config} --detect-apache2
}
return
end
result = PlatformInfo.httpd_included_config_files(
PlatformInfo.httpd_default_config_file)
if !result[:unreadable_files].empty?
check_error
if Process.uid == 0
suggest %Q{
Permission problems
This program must be able to analyze your Apache installation. But it can't
do that despite running with root privileges. In particular, it failed to
read the following files:
#{result[:unreadable_files].join("\n ")}
This program doesn't know why this error occurred. Your system is probably
secured using some mechanism that this program is not familiar with. Please
consult your operating system's manual to learn which security mechanisms
may be preventing this program from accessing the above files. On Linux
systems, SELinux and AppArmor might be responsible.
When you've solved the problem, please re-run this program.
}
else
whoami = `whoami`.strip
sudo = PhusionPassenger::PlatformInfo.ruby_sudo_command
selfcommand = "#{PhusionPassenger.bin_dir}/passenger-config validate-install #{@orig_argv.join(' ')}"
suggest %Q{
Permission problems
This program must be able to analyze your Apache installation. But it can't
do that, because you're running the installer as '#{whoami}'. In particular,
it failed to read the following files:
#{result[:unreadable_files].join("\n ")}
Please give this program root privileges, by re-running it with '#{sudo}':
export ORIG_PATH=\"$PATH\"
#{sudo_s_e}
export PATH=\"$ORIG_PATH\"
#{ruby_command} #{selfcommand}
}
end
return
end
occurrences = 0
occurrence_files = []
module_path = nil
result[:files].each do |path|
lines = File.open(path, "rb") do |f|
f.read.split("\n")
end
lines.each do |line|
if line !~ /^[\s\t]*#/ && line =~ /LoadModule[\s\t]+passenger_module[\s\t]+(.*)/
module_path = $1
occurrences += 1
occurrence_files << path
end
end
end
if occurrences == 1
if module_path !~ /^\//
# Non-absolute path. Absolutize using ServerRoot.
module_path = "#{PlatformInfo.httpd_default_root}/#{module_path}"
end
# Resolve symlinks.
module_path = try_realpath(module_path)
expected_module_path = try_realpath(PhusionPassenger.apache2_module_path)
if module_path == expected_module_path
check_ok
else
check_error
suggest %Q{
Incorrect #{SHORT_PROGRAM_NAME} module path detected
#{PROGRAM_NAME} for Apache requires a 'LoadModule passenger_module'
directive inside an Apache configuration file. This directive has been
detected in the following config file:
#{occurrence_files[0]}
However, the directive refers to the following Apache module, which is wrong:
#{module_path}
Please edit the config file and change the directive to this instead:
LoadModule passenger_module #{PhusionPassenger.apache2_module_path}
}
end
elsif occurrences == 0
if @options[:invoked_from_installer]
check_warning
suggest %Q{
You did not specify 'LoadModule passenger_module' in any of your Apache
configuration files. Please paste the configuration snippet that this
installer printed earlier, into one of your Apache configuration files, such
as #{PlatformInfo.httpd_default_config_file}.
}
else
check_error
installer_command = "#{PhusionPassenger.bin_dir}/passenger-install-apache2-module"
suggest %Q{
You did not specify 'LoadModule passenger_module' in any of your
Apache configuration files. This means that #{PROGRAM_NAME}
for Apache is not installed or not active. Please run the
#{PROGRAM_NAME} Apache module installer:
#{ruby_command} #{installer_command} --apxs2=#{PlatformInfo.apxs2}
}
end
else
check_error
suggest %Q{
You have #{occurrences} 'LoadModule passenger_module' directives in your Apache
configuration files. However, you are only supposed to have one such
directive.
Please fix this by removing all 'LoadModule passenger_module' directives
besides the one for #{SHORT_PROGRAM_NAME} version #{VERSION_STRING}. The directives were
found in these files:
#{occurrence_files.uniq.join("\n ")}
Note: 'LoadModule passenger_module' may be placed inside the global context
only (so not within a VirtualHost).
}
end
end
def summarize
puts
if @error_count == 0 && @warning_count == 0
log "Everything looks good. :-)"
elsif @error_count == 0
log "Detected 0 error(s), #{@warning_count} warning(s)."
else
log "Detected #{@error_count} error(s), #{@warning_count} warning(s)."
end
end
# Returns the RubyGems bin dir, if Phusion Passenger is installed through RubyGems.
def gem_bindir
if defined?(Gem) &&
PhusionPassenger.originally_packaged? &&
PhusionPassenger.build_system_dir =~ /^#{Regexp.escape Gem.dir}\// &&
File.exist?("#{Gem.bindir}/passenger-config")
return Gem.bindir
else
return nil
end
end
# Returns the Homebrew bin dir, if Phusion Passenger is installed through Homebrew.
def homebrew_bindir
if PhusionPassenger.packaging_method == "homebrew"
return "/usr/local/bin"
else
return nil
end
end
def logn(message)
STDOUT.write(@colors.ansi_colorize(message))
STDOUT.flush
end
def log(message)
STDOUT.puts(@colors.ansi_colorize(message))
end
def display_separator
puts
puts "-------------------------------------------------------------------------"
puts
end
def checking(message)
logn " * Checking #{message}... "
end
def check_ok(message = "✓")
log "#{message}"
end
def check_error(message = "✗")
log "#{message}"
@error_count += 1
end
def check_warning(message = "(!)")
log "#{message}"
@warning_count += 1
end
def suggest(message)
puts
log reindent(unindent(message), 3)
puts
end
def prompt_confirmation(message)
result = prompt("#{message} [y/n]") do |value|
if value.downcase == 'y' || value.downcase == 'n'
true
else
log "Invalid input '#{value}'; please enter either 'y' or 'n'."
false
end
end
result.downcase == 'y'
rescue Interrupt
exit(INTERNAL_ERROR_CODE)
end
def prompt(message, default_value = nil)
done = false
while !done
logn "#{message}: "
if default_value
puts default_value
return default_value
end
begin
result = STDIN.readline
rescue EOFError
exit(INTERNAL_ERROR_CODE)
end
result.strip!
if result.empty?
if default_value
result = default_value
done = true
else
done = !block_given? || yield(result)
end
else
done = !block_given? || yield(result)
end
end
result
rescue Interrupt
exit(INTERNAL_ERROR_CODE)
end
def unindent(text)
return PlatformInfo.send(:unindent, text)
end
def reindent(text, level)
return PlatformInfo.send(:reindent, text, level)
end
def sudo_s_e
PlatformInfo.ruby_sudo_shell_command("-E")
end
def ruby_command
PlatformInfo.ruby_command
end
def try_realpath(path)
if path
begin
Pathname.new(path).realpath.to_s
rescue Errno::ENOENT, Errno::EACCES
path
end
else
nil
end
end
end
end # module Config
end # module PhusionPassenger