Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
tree: 6ec77e4c30
Fetching contributors…

Cannot retrieve contributors at this time

749 lines (652 sloc) 22.184 kb
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
require 'tmpdir'
require 'fileutils'
require 'stringio'
require 'etc'
require 'phusion_passenger/message_channel'
require 'phusion_passenger/platform_info/ruby'
require 'phusion_passenger/utils'
require 'ruby/shared/utils/pseudo_io_spec'
module PhusionPassenger
describe Utils do
include Utils
specify "#close_all_io_objects_for_fds closes all IO objects that are associated with the given file descriptors" do
filename = "#{Dir.tmpdir}/passenger_test.#{Process.pid}.txt"
begin
pid = safe_fork('utils_spec') do
a, b = IO.pipe
close_all_io_objects_for_fds([0, 1, 2])
File.open(filename, "w") do |f|
f.write("#{a.closed?}, #{b.closed?}")
end
end
Process.waitpid(pid) rescue nil
File.read(filename).should == "true, true"
ensure
File.unlink(filename) rescue nil
end
end
describe "#report_app_init_status" do
it "reports normal errors, which #unmarshal_and_raise_errors raises" do
a, b = IO.pipe
begin
pid = safe_fork('utils_spec') do
a.close
report_app_init_status(MessageChannel.new(b)) do
raise RuntimeError, "hello world"
end
end
b.close
lambda { unmarshal_and_raise_errors(MessageChannel.new(a)) }.should raise_error(/hello world/)
ensure
a.close rescue nil
b.close rescue nil
end
end
it "reports SystemExit errors, which #unmarshal_and_raise_errors raises" do
a, b = IO.pipe
begin
pid = safe_fork('utils_spec') do
a.close
report_app_init_status(MessageChannel.new(b)) do
exit
end
end
b.close
lambda { unmarshal_and_raise_errors(MessageChannel.new(a)) }.should raise_error(/exited during startup/)
ensure
a.close rescue nil
b.close rescue nil
end
end
it "returns whether the block succeeded" do
channel = MessageChannel.new(StringIO.new)
success = report_app_init_status(channel) do
false
end
success.should be_true
success = report_app_init_status(channel) do
raise StandardError, "hi"
end
success.should be_false
end
it "reports all data written to STDERR and $stderr" do
a, b = IO.pipe
begin
pid = safe_fork('utils_spec') do
a.close
report_app_init_status(MessageChannel.new(b), nil) do
STDERR.puts "Something went wrong!"
$stderr.puts "Something went wrong again!"
exit
end
end
b.close
begin
unmarshal_and_raise_errors(MessageChannel.new(a))
violated "No exception raised"
rescue AppInitError => e
e.stderr.should =~ /Something went wrong!/
e.stderr.should =~ /Something went wrong again!/
end
ensure
a.close rescue nil
b.close rescue nil
end
end
it "reports all data written to STDERR and $stderr even if it was reopened" do
a, b = IO.pipe
begin
pid = safe_fork('utils_spec') do
a.close
report_app_init_status(MessageChannel.new(b), nil) do
STDERR.puts "Something went wrong!"
STDERR.reopen("output.tmp", "w")
STDERR.puts "Something went wrong again!"
STDERR.flush
$stderr.puts "Something went wrong yet again!"
$stderr.flush
exit
end
end
b.close
begin
unmarshal_and_raise_errors(MessageChannel.new(a))
violated "No exception raised"
rescue AppInitError => e
e.stderr.should =~ /Something went wrong!/
e.stderr.should =~ /Something went wrong again!/
e.stderr.should =~ /Something went wrong yet again!/
end
file_contents = File.read("output.tmp")
file_contents.should =~ /Something went wrong again!/
file_contents.should =~ /Something went wrong yet again!/
ensure
a.close rescue nil
b.close rescue nil
File.unlink("output.tmp") rescue nil
end
end
describe "if the block failed" do
before :each do
@raise_error = true
end
it_should_behave_like "a pseudo stderr created by #report_app_init_status"
end
describe "if the block succeeded" do
it_should_behave_like "a pseudo stderr created by #report_app_init_status"
end
end
specify "#safe_fork with double_fork == false reseeds the pseudo-random number generator" do
a, b = IO.pipe
begin
pid = safe_fork do
b.puts(rand)
end
Process.waitpid(pid) rescue nil
pid = safe_fork do
b.puts(rand)
end
Process.waitpid(pid) rescue nil
first_num = a.readline
second_num = a.readline
first_num.should_not == second_num
ensure
a.close rescue nil
b.close rescue nil
end
end
specify "#safe_fork with double_fork == true reseeds the pseudo-random number generator" do
a, b = IO.pipe
begin
# Seed the pseudo-random number generator here
# so that it doesn't happen in the child processes.
srand
safe_fork(self.class, true) do
b.puts(rand)
end
safe_fork(self.class, true) do
b.puts(rand)
end
first_num = a.readline
second_num = a.readline
first_num.should_not == second_num
ensure
a.close rescue nil
b.close rescue nil
end
end
describe "#unmarshal_and_raise_errors" do
before :each do
@a, @b = IO.pipe
@report_channel = MessageChannel.new(@a)
report_app_init_status(MessageChannel.new(@b)) do
raise StandardError, "Something went wrong!"
end
end
after :each do
@a.close rescue nil
@b.close rescue nil
end
it "prints the exception information to the 'print_exception' argument using #puts, if 'print_exception' responds to that" do
buffer = StringIO.new
lambda { unmarshal_and_raise_errors(@report_channel, buffer) }.should raise_error(AppInitError)
buffer.string.should =~ /Something went wrong!/
buffer.string.should =~ /utils\.rb/
buffer.string.should =~ /utils_spec\.rb/
end
it "appends the exception information to the file pointed to by 'print_exception', if 'print_exception' responds to #to_str" do
begin
lambda { unmarshal_and_raise_errors(@report_channel, "exception.txt") }.should raise_error(AppInitError)
data = File.read('exception.txt')
data.should =~ /Something went wrong!/
data.should =~ /utils\.rb/
data.should =~ /utils_spec\.rb/
ensure
File.unlink('exception.txt') rescue nil
end
end
end
specify "#to_boolean works" do
to_boolean(nil).should be_false
to_boolean(false).should be_false
to_boolean(true).should be_true
to_boolean(1).should be_true
to_boolean(0).should be_true
to_boolean("").should be_true
to_boolean("true").should be_true
to_boolean("false").should be_false
to_boolean("bla bla").should be_true
end
specify "#split_by_null_into_hash works" do
split_by_null_into_hash("").should == {}
split_by_null_into_hash("foo\0bar\0").should == { "foo" => "bar" }
split_by_null_into_hash("foo\0\0bar\0baz\0").should == { "foo" => "", "bar" => "baz" }
split_by_null_into_hash("foo\0bar\0baz\0\0").should == { "foo" => "bar", "baz" => "" }
split_by_null_into_hash("\0\0").should == { "" => "" }
end
describe "#passenger_tmpdir" do
before :each do
@old_passenger_tmpdir = Utils.passenger_tmpdir
Utils.passenger_tmpdir = nil
end
after :each do
Utils.passenger_tmpdir = @old_passenger_tmpdir
end
it "returns a directory under /tmp if Utils.passenger_tmpdir is nil" do
File.dirname(passenger_tmpdir(false)).should == "/tmp"
end
it "returns a directory under /tmp if Utils.passenger_tmpdir is an empty string" do
Utils.passenger_tmpdir = ''
File.dirname(passenger_tmpdir(false)).should == "/tmp"
end
it "returns Utils.passenger_tmpdir if it's set" do
Utils.passenger_tmpdir = '/foo'
passenger_tmpdir(false).should == '/foo'
end
it "creates the directory if it doesn't exist, if the 'create' argument is true" do
Utils.passenger_tmpdir = 'utils_spec.tmp'
passenger_tmpdir
begin
File.directory?('utils_spec.tmp').should be_true
ensure
FileUtils.chmod_R(0777, 'utils_spec.tmp')
FileUtils.rm_rf('utils_spec.tmp')
end
end
end
when_user_switching_possible do
describe "#lower_privilege" do
before :each do
@options = {
"default_user" => CONFIG["default_user"],
"default_group" => CONFIG["default_group"]
}
@startup_file = "tmp.startup_file"
@startup_file_target = "tmp.startup_file_target"
File.symlink(@startup_file_target, @startup_file)
File.touch(@startup_file_target)
end
after :each do
File.unlink(@startup_file) rescue nil
File.unlink(@startup_file_target) rescue nil
end
def run(options = {})
script = %q{
require 'phusion_passenger/utils'
include PhusionPassenger::Utils
options = Marshal.load(ARGV[0].unpack('m').first)
startup_file = ARGV[1]
begin
lower_privilege(startup_file, options)
puts "success"
puts Process.uid
puts Process.gid
puts `groups`
puts ENV["HOME"]
puts ENV["USER"]
rescue => e
puts "error"
puts e
end
}.strip
data = Marshal.dump(@options.merge(options))
output = run_script(script, [data].pack('m'), @startup_file)
lines = output.split("\n")
status = lines.shift
if status == "success"
@uid, @gid, @groups, @env_home, @env_user = lines
@uid = @uid.to_i
@gid = @gid.to_i
@username = Etc.getpwuid(@uid).name
@groupname = Etc.getgrgid(@gid).name
else
@error = lines[0]
end
end
def primary_group_for(username)
gid = Etc.getpwnam(username).gid
return Etc.getgrgid(gid).name
end
def uid_for(username)
return Etc.getpwnam(username).uid
end
def gid_for(group_name)
return Etc.getgrnam(group_name).gid
end
def group_name_for_gid(gid)
return Etc.getgrgid(gid).name
end
describe "if 'user' is given" do
describe "and 'user' is 'root'" do
before :each do
@options["user"] = "root"
end
it "changes the user to the value of 'default_user'" do
run
@username.should == CONFIG["default_user"]
end
specify "if 'group' is given, it changes group to the given group name" do
run("group" => CONFIG["normal_group_1"])
@groupname.should == CONFIG["normal_group_1"]
end
specify "if 'group' is set to the root group, it changes group to default_group" do
run("group" => group_name_for_gid(0))
@groupname.should == CONFIG["default_group"]
end
describe "and 'group' is set to '!STARTUP_FILE!'" do
before :each do
@options["group"] = "!STARTUP_FILE!"
end
it "changes the group to the startup file's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file)
run
@groupname.should == CONFIG["normal_group_1"]
end
specify "if the startup file is a symlink, then it uses the symlink's group, not the target's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_2"]),
@startup_file)
File.chown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file_target)
run
@groupname.should == CONFIG["normal_group_2"]
end
end
specify "if 'group' is not given, it changes the group to default_user's primary group" do
run
@groupname.should == primary_group_for(CONFIG["default_user"])
end
end
describe "and 'user' is not 'root'" do
before :each do
@options["user"] = CONFIG["normal_user_1"]
end
it "changes the user to the given username" do
run
@username.should == CONFIG["normal_user_1"]
end
specify "if 'group' is given, it changes group to the given group name" do
run("group" => CONFIG["normal_group_1"])
@groupname.should == CONFIG["normal_group_1"]
end
specify "if 'group' is set to the root group, it changes group to default_group" do
run("group" => group_name_for_gid(0))
@groupname.should == CONFIG["default_group"]
end
describe "and 'group' is set to '!STARTUP_FILE!'" do
before :each do
@options["group"] = "!STARTUP_FILE!"
end
it "changes the group to the startup file's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file)
run
@groupname.should == CONFIG["normal_group_1"]
end
specify "if the startup file is a symlink, then it uses the symlink's group, not the target's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_2"]),
@startup_file)
File.chown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file_target)
run
@groupname.should == CONFIG["normal_group_2"]
end
end
specify "if 'group' is not given, it changes the group to the user's primary group" do
run
@groupname.should == primary_group_for(CONFIG["normal_user_1"])
end
end
describe "and the given username does not exist" do
before :each do
@options["user"] = CONFIG["nonexistant_user"]
end
it "changes the user to the value of 'default_user'" do
run
@username.should == CONFIG["default_user"]
end
specify "if 'group' is given, it changes group to the given group name" do
run("group" => CONFIG["normal_group_1"])
@groupname.should == CONFIG["normal_group_1"]
end
specify "if 'group' is set to the root group, it changes group to default_group" do
run("group" => group_name_for_gid(0))
@groupname.should == CONFIG["default_group"]
end
describe "and 'group' is set to '!STARTUP_FILE!'" do
before :each do
@options["group"] = "!STARTUP_FILE!"
end
it "changes the group to the startup file's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file)
run
@groupname.should == CONFIG["normal_group_1"]
end
specify "if the startup file is a symlink, then it uses the symlink's group, not the target's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_2"]),
@startup_file)
File.chown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file_target)
run
@groupname.should == CONFIG["normal_group_2"]
end
end
specify "if 'group' is not given, it changes the group to default_user's primary group" do
run
@groupname.should == primary_group_for(CONFIG["default_user"])
end
end
end
describe "if 'user' is not given" do
describe "and the startup file's owner exists" do
before :each do
File.lchown(uid_for(CONFIG["normal_user_1"]),
-1,
@startup_file)
end
it "changes the user to the owner of the startup file" do
run
@username.should == CONFIG["normal_user_1"]
end
specify "if the startup file is a symlink, then it uses the symlink's owner, not the target's owner" do
File.lchown(uid_for(CONFIG["normal_user_2"]),
-1,
@startup_file)
File.chown(uid_for(CONFIG["normal_user_1"]),
-1,
@startup_file_target)
run
@username.should == CONFIG["normal_user_2"]
end
specify "if 'group' is given, it changes group to the given group name" do
run("group" => CONFIG["normal_group_1"])
@groupname.should == CONFIG["normal_group_1"]
end
specify "if 'group' is set to the root group, it changes group to default_group" do
run("group" => group_name_for_gid(0))
@groupname.should == CONFIG["default_group"]
end
describe "and 'group' is set to '!STARTUP_FILE!'" do
before :each do
@options["group"] = "!STARTUP_FILE!"
end
it "changes the group to the startup file's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file)
run
@groupname.should == CONFIG["normal_group_1"]
end
specify "if the startup file is a symlink, then it uses the symlink's group, not the target's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_2"]),
@startup_file)
File.chown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file_target)
run
@groupname.should == CONFIG["normal_group_2"]
end
end
specify "if 'group' is not given, it changes the group to the startup file's owner's primary group" do
run
@groupname.should == primary_group_for(CONFIG["normal_user_1"])
end
end
describe "and the startup file's owner doesn't exist" do
before :each do
File.lchown(CONFIG["nonexistant_uid"],
-1,
@startup_file)
end
it "changes the user to the value of 'default_user'" do
run
@username.should == CONFIG["default_user"]
end
specify "if 'group' is given, it changes group to the given group name" do
run("group" => CONFIG["normal_group_1"])
@groupname.should == CONFIG["normal_group_1"]
end
specify "if 'group' is set to the root group, it changes group to default_group" do
run("group" => group_name_for_gid(0))
@groupname.should == CONFIG["default_group"]
end
describe "and 'group' is set to '!STARTUP_FILE!'" do
before :each do
@options["group"] = "!STARTUP_FILE!"
end
describe "and the startup file's group doesn't exist" do
before :each do
File.lchown(-1,
CONFIG["nonexistant_gid"],
@startup_file)
end
it "changes the group to the value given by 'default_group'" do
run
@groupname.should == CONFIG["default_group"]
end
end
describe "and the startup file's group exists" do
before :each do
File.lchown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file)
end
it "changes the group to the startup file's group" do
run
@groupname.should == CONFIG["normal_group_1"]
end
specify "if the startup file is a symlink, then it uses the symlink's group, not the target's group" do
File.lchown(-1,
gid_for(CONFIG["normal_group_2"]),
@startup_file)
File.chown(-1,
gid_for(CONFIG["normal_group_1"]),
@startup_file_target)
run
@groupname.should == CONFIG["normal_group_2"]
end
end
end
specify "if 'group' is not given, it changes the group to default_user's primary group" do
run
@groupname.should == primary_group_for(CONFIG["default_user"])
end
end
end
it "raises an error if it tries to lower to 'default_user', but that user doesn't exist" do
run("user" => "root", "default_user" => CONFIG["nonexistant_user"])
@error.should =~ /Cannot determine a user to lower privilege to/
end
it "raises an error if it tries to lower to 'default_group', but that group doesn't exist" do
run("user" => CONFIG["normal_user_1"],
"group" => group_name_for_gid(0),
"default_group" => CONFIG["nonexistant_group"])
@error.should =~ /Cannot determine a group to lower privilege to/
end
it "changes supplementary groups to the owner's default supplementary groups" do
run("user" => CONFIG["normal_user_1"])
default_groups = `groups "#{CONFIG['normal_user_1']}"`.strip
default_groups.gsub!(/.*: */, '')
@groups.should == default_groups
end
it "sets $HOME to the user's home directory" do
run("user" => CONFIG["normal_user_1"])
@env_home.should == Etc.getpwnam(CONFIG["normal_user_1"]).dir
end
it "sets $USER to the user's name" do
run("user" => CONFIG["normal_user_1"])
@env_user.should == CONFIG["normal_user_1"]
end
end
end
describe "#check_directory_tree_permissions" do
before :each do
@root = PhusionPassenger::Utils.passenger_tmpdir
end
def primary_group_for(username)
gid = Etc.getpwnam(username).gid
return Etc.getgrgid(gid).name
end
it "raises an error for the top-most parent directory that has wrong permissions" do
FileUtils.mkdir_p("#{@root}/a/b/c/d")
when_running_as_root do
user = CONFIG['normal_user_1']
group = primary_group_for(user)
system("chown -R #{user} #{@root}/a")
system("chgrp -R #{group} #{@root}/a")
output = run_script(%q{
require 'phusion_passenger/utils'
include PhusionPassenger::Utils
@root = ARGV[0]
lower_privilege(nil,
"user" => ARGV[1],
"group" => ARGV[2])
File.chmod(0600, "#{@root}/a/b/c/d")
File.chmod(0600, "#{@root}/a/b/c")
File.chmod(0600, "#{@root}/a")
p check_directory_tree_permissions("#{@root}/a/b/c/d")
File.chmod(0700, "#{@root}/a")
p check_directory_tree_permissions("#{@root}/a/b/c/d")
File.chmod(0700, "#{@root}/a/b/c")
p check_directory_tree_permissions("#{@root}/a/b/c/d")
File.chmod(0700, "#{@root}/a/b/c/d")
p check_directory_tree_permissions("#{@root}/a/b/c/d")
}, @root, user, group)
lines = output.split("\n")
lines[0].should == ["#{@root}/a", true].inspect
lines[1].should == ["#{@root}/a/b/c", true].inspect
lines[2].should == ["#{@root}/a/b/c/d", false].inspect
lines[3].should == "nil"
end
when_not_running_as_root do
File.chmod(0000, "#{@root}/a/b/c/d")
File.chmod(0600, "#{@root}/a/b/c")
File.chmod(0600, "#{@root}/a")
check_directory_tree_permissions("#{@root}/a/b/c/d").should ==
["#{@root}/a", true]
File.chmod(0700, "#{@root}/a")
check_directory_tree_permissions("#{@root}/a/b/c/d").should ==
["#{@root}/a/b/c", true]
File.chmod(0700, "#{@root}/a/b/c")
check_directory_tree_permissions("#{@root}/a/b/c/d").should ==
["#{@root}/a/b/c/d", false]
File.chmod(0700, "#{@root}/a/b/c/d")
check_directory_tree_permissions("#{@root}/a/b/c/d").should be_nil
end
end
end
######################
end
end # module PhusionPassenger
Jump to Line
Something went wrong with that request. Please try again.