#!/usr/bin/perl -w # Copyright © 2001-2008 Jamie Zawinski # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. No representations are made about the suitability of this # software for any purpose. It is provided "as is" without express or # implied warranty. # # Created: 9-Jun-2001 # # This script kills any processes owned by the "guest" user that don't match # a list of "allowed" executables. This is not so much for security reasons, # as for cleanliness: if we run this script when the screensaver activates, # we can kill off any apps that other users have left cluttering up the # screen. # require 5; use diagnostics; use strict; use POSIX; my $progname = $0; $progname =~ s@.*/@@g; my $version = q{ $Revision: 1.4 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/; my $verbose = 0; my $debug = 0; $ENV{PATH} = "/usr/local/bin:/usr/local/sbin:$ENV{PATH}"; my $target_owner = "guest"; my @kiosk_users = ("guest01", "guest02", "guest03", "guest04", "guest05", "guest06", "guest07"); my $process_death_grace_period = 2; # seconds between SIGTERM and SIGKILL # If any of these processes are not running, then things are fucked. # my %required_programs = ( "/usr/bin/metacity", "/usr/bin/xscreensaver", "/usr/bin/gnome-panel", "/usr/libexec/multiload-applet-2", # "/usr/lib/gnome-applets/multiload-applet-2", ); # When the user is idle, we kill any programs that are not on this list. # Elements can also be directories, in which case they match all executables # in that directory. # my %allowed_programs = ( "/bin/bash" => 1, "/bin/cat" => 1, "/bin/dash" => 1, "/bin/sed" => 1, "/bin/sleep" => 1, "/bin/tcsh" => 1, "/home/local/bin/chbg" => 1, "/usr/bin/dbus-daemon" => 1, "/usr/bin/dbus-launch" => 1, "/usr/bin/gnome-name-service" => 1, "/usr/bin/gnome-panel" => 1, "/usr/bin/gnome-settings-daemon" => 1, "/usr/bin/metacity" => 1, "/usr/bin/perl" => 1, "/usr/bin/ssh-agent" => 1, "/usr/bin/xscreensaver" => 1, "/usr/bin/xscreensaver-getimage" => 1, "/usr/bin/xscreensaver-getimage-file" => 1, "/usr/lib/bonobo-activation/bonobo-activation-server" => 1, "/usr/lib/firefox/firefox-bin" => 1, "/usr/lib/gnome-applets/multiload-applet-2" => 1, "/usr/lib/gnome-control-center/gnome-settings-daemon" => 1, "/usr/lib/libgconf2-4/gconfd-2" => 1, "/usr/lib/xscreensaver/" => 1, # whole dir "/usr/libexec/bonobo-activation-server" => 1, "/usr/libexec/gconfd-2" => 1, "/usr/libexec/multiload-applet-2" => 1, "/usr/libexec/xscreensaver/" => 1, # whole dir ); # These are programs that we kill, but that we don't bother logging, # since we're a-killin' them all the time... # my %boring_programs = ( "/usr/bin/gaim" => 1, "usr/bin/xchat" => 1, "usr/bin/gsendmail" => 1, "usr/bin/gnome-terminal" => 1, ); sub error { ($_) = @_; print STDERR "$progname: $_\n"; exit 1; } # like system() but checks errors, and calls cleanup if there was one. # sub clean_system(@) { my (@cmd) = @_; print STDERR "$progname: executing " . join(' ', @cmd) . "\n" if ($verbose > 1); system @cmd; my $exit_value = $? >> 8; my $signal_num = $? & 127; my $dumped_core = $? & 128; error ("$cmd[0]: core dumped!") if ($dumped_core); error ("$cmd[0]: signal $signal_num!") if ($signal_num); error ("$cmd[0]: exited with $exit_value!") if ($exit_value); } # Sets $HOME, $DISPLAY and $XAUTHORITY in %ENV to be that of the # given kiosk user. Gets the proper values for those by extracting # them from that user's running "metacity" process, if there is one. # sub set_kiosk_env($%) { my ($user, %procs) = @_; my $kiosk = $user; $user =~ s/kiosk/guest/; $kiosk =~ s/guest/kiosk/; my $target_proc = "/usr/bin/metacity"; my $target_pid = @{$procs{$target_proc}}[0]; if (! $target_pid) { print STDERR "$progname: $user: env: $target_proc not running!\n"; } my %tenv; if ($target_pid) { foreach (split(/\000/, `cat "/proc/$target_pid/environ"`)) { next unless (m/^([^=]+)=(.*)$/s); $tenv{$1} = $2; print STDERR "$progname: $user: getenv: $1 = $2\n" if ($verbose > 4); } } foreach ("HOME", "DISPLAY", "XAUTHORITY") { $ENV{$_} = $tenv{$_} || 'UNKNOWN'; print STDERR "$progname: $user: setenv: $_ = $ENV{$_}\n" if ($verbose > 3); } } sub exec_with_timeout($$$) { my ($user, $cmd, $timeout) = @_; my $result; eval { local $SIG{ALRM} = sub { die; }; my $oalarm = alarm($timeout); $result = `$cmd`; alarm($oalarm); }; print STDERR "$progname: $user: timed out: $cmd\n" if (!defined($result) && $verbose > 3); return $result; } sub kiosk_idleness($) { my ($user) = @_; my $kiosk = $user; $user =~ s/kiosk/guest/; $kiosk =~ s/guest/kiosk/; my $cmd = 'xscreensaver-command -time 2>&1'; print STDERR "$progname: $user: exec: $cmd\n" if ($verbose > 3); my $result = (exec_with_timeout ($user, $cmd, 10) || 'X11 connection timed out'); print STDERR "$progname: $user: result: $result\n" if ($verbose > 3); if ($result =~ m/ screen blanked /) { return 'idle'; } elsif ($result =~ m/ screen non-blanked /) { return 'active'; } elsif ($result =~ m/\bno saver status /) { # never blanked return 'active'; } else { $result = "null response" unless ($result); return ("dead", $result); } } sub kiosk_nanny($$) { my ($user, $force_reset_p) = @_; $user =~ s/^kiosk/guest/; my %procs; # executables that this user is running my @all_pids = (); my %pid_names; foreach () { my $file = $_; my ($pid) = m@/(\d+)/exe$@; my @st = lstat($file); next unless defined ($st[4]); my $owner = getpwuid($st[4]); my $exe = readlink($file) || '???'; if ($owner eq 'root' && $exe =~ m@/xscreensaver$@s) { print STDERR "$progname: WARNING: $exe (pid $pid) running as $owner!\n"; } if ($user ne $owner) { print STDERR "$progname: $user: skip $owner pid $pid ($exe)\n" if ($verbose > 4); next; } my @ppids = ($pid); if (defined($procs{$exe})) { my $pidsP = $procs{$exe}; my @pids2 = @$pidsP; push @ppids, @pids2; } $procs{$exe} = \@ppids; push @all_pids, $pid; $pid_names{$pid} = $exe; } if ($verbose > 2) { foreach my $prog (sort keys (%procs)) { my $pidsP = $procs{$prog}; my @ppids = @$pidsP; print STDERR "$progname: $user: $prog: " . join(' ', @ppids) . "\n"; } } if ($#all_pids < 0) { print STDERR "$progname: $user: no pids!\n" if ($verbose); return; } my $reap_p = 0; my $reset_p = 0; local %ENV = %ENV; set_kiosk_env($user, %procs); my ($idleness, $err) = kiosk_idleness($user); if ($idleness eq 'active') { print STDERR "$progname: $user is active.\n" if ($verbose); } elsif ($idleness eq 'idle') { print STDERR "$progname: $user is idle.\n" if ($verbose); $reap_p = 1; } else { print STDERR "$progname: $user is $idleness\n" if ($verbose); $err =~ s/\n/\n\t/; print STDERR "$progname: $user: result was: $err\n" if ($verbose); $reset_p = 1; } foreach my $prog (%required_programs) { if (defined ($procs{$prog})) { print STDERR "$progname: $user: $prog is running.\n" if ($verbose > 1); } else { print STDERR "$progname: $user: $prog is not running!\n" if ($verbose); $reset_p = 1; } } if ($force_reset_p) { print STDERR "$progname: $user: force reset.\n" if ($verbose); $reset_p = 1; } my @pids_to_kill = (); if ($reset_p || $reap_p) { clean_system ("kiosk-home-reset-ltsp", $user); } if ($reset_p) { print STDERR "$progname: $user: killing everything.\n" if ($verbose); @pids_to_kill = @all_pids; } elsif ($reap_p) { foreach my $prog (keys (%procs)) { my $pidsP = $procs{$prog}; my @ppids = @$pidsP; if (defined ($allowed_programs{$prog})) { print STDERR "$progname: $user: $prog: allowed.\n" if ($verbose > 1); } else { # if the executable didn't match, check the excutable's directory. my $dir = $prog; $dir =~ s@/[^/]+$@/@; if (defined($allowed_programs{$dir})) { print STDERR "$progname: $user: $prog: allowed dir.\n" if ($verbose > 1); } else { print STDERR "$progname: $user: $prog: not allowed.\n" if ($verbose); push @pids_to_kill, @ppids; } } } } if ($#pids_to_kill >= 0) { if ($verbose > 1) { print STDERR "$progname: $user: killing:\n"; foreach my $p (@pids_to_kill) { print STDERR " $p $pid_names{$p}\n"; } } if ($debug) { print STDERR "$progname: $user: debug: not killing\n"; } else { kill ('TERM', @pids_to_kill); sleep $process_death_grace_period; kill ('KILL', @pids_to_kill); } } # And as a final special case: if firefox isn't running, launch it. # if (! $reset_p) { my $ff0 = "/usr/bin/firefox"; my $ff1 = "/usr/lib/firefox/firefox-bin"; if (! defined ($procs{$ff1})) { print STDERR "$progname: $user: launching $ff0\n" if ($verbose); if (! $debug) { clean_system ("su $user -c $ff0 /dev/null 2>&1 &"); } } } } # ssh'es to each kiosk as user "reboot" in turn. # kills off the ssh processes after a few seconds, since they will hang. # sub reboot_kiosks(@) { my (@kiosks) = @_; my @pids = (); foreach my $kiosk (@kiosks) { $kiosk =~ s/^guest/kiosk/; my @cmd = ("ssh", "-qnTx", "reboot\@$kiosk"); print STDERR "$progname: rebooting $kiosk\n" if ($verbose); print STDERR "$progname: cmd: " . join(' ', @cmd) . "\n" if ($verbose > 2); my $pid = fork(); if ($pid) { push @pids, $pid; } elsif (!defined ($pid)) { error ("can't fork: $!"); } else { close STDIN; close STDOUT unless ($verbose > 2); close STDERR unless ($verbose > 2); exec (@cmd); } sleep (1); } sleep (5); waitpid (-1, WNOHANG); print STDERR "$progname: killing ssh pids @pids\n" if ($verbose > 2); kill ('TERM', @pids); } sub usage { print STDERR "usage: $progname [--verbose] [--debug] [--reset] [--reboot] " . "guestNN ...\n"; exit 1; } sub main { my $force_reset_p = 0; my $reboot_p = 0; my @users = (); while ($_ = $ARGV[0]) { shift @ARGV; if ($_ eq "--verbose") { $verbose++; } elsif ($_ eq "--debug") { $debug++; } elsif ($_ eq "--reset") { $force_reset_p++; } elsif ($_ eq "--reboot") { $reboot_p++; } elsif (m/^-v+$/) { $verbose += length($_)-1; } elsif (m/^-d+$/) { $debug += length($_)-1; } elsif (m/^-./) { usage; } elsif (m/^(guest|kiosk)\d\d$/) { push @users, $_; } else { usage; } } @users = @kiosk_users if ($#users < 0); if ($reboot_p) { reboot_kiosks (@users); } else { foreach (@users) { kiosk_nanny ($_, $force_reset_p); } } } main; exit 0;