shellinabox.pl.in 7.48 KB
Newer Older
1 2
#!/usr/bin/perl -w
#
Leigh B Stoller's avatar
Leigh B Stoller committed
3
# Copyright (c) 2008-2014 University of Utah and the Flux Group.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
# 
# {{{GENIPUBLIC-LICENSE
# 
# GENI Public License
# 
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and/or hardware specification (the "Work") to
# deal in the Work without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Work, and to permit persons to whom the Work
# 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 Work.
# 
# THE WORK 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 WORK OR THE USE OR OTHER DEALINGS
# IN THE WORK.
# 
# }}}
#

#
# Simple CGI interface to shellinabox ...
#
use strict;
use English;
use Data::Dumper;
use CGI;
use JSON;
use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
use Sys::Syslog;
use IO::Handle;

# Yack. apache does not close fds before the exec, and if this dies
# we are left with a giant mess.
BEGIN {
    no warnings;
    for (my $i = 3; $i < 1024; $i++) {
      POSIX:close($i);
    }
}

# Configure variables
my $TB		     = "@prefix@";
my $MAINSITE 	     = @TBMAINSITE@;
my $TBOPS            = "@TBOPSEMAIL@";
my $TBLOGS           = "@TBLOGSEMAIL@";
my $TBBASE           = "@TBBASE@";
my $TBLOGFACIL       = "@TBLOGFACIL@";
Leigh B Stoller's avatar
Leigh B Stoller committed
59 60 61
my $USERNODE         = "@USERNODE@";
my $CERTFILE         = "/usr/local/etc/apache22/ssl.crt/${USERNODE}.crt";
my $KEYFILE          = "/usr/local/etc/apache22/ssl.key/${USERNODE}.key";
62
my $APTDIR           = "/var/apt/users";
63
my $CONSOLEBIN       = "$TB/bin/console.bin";
64 65 66 67 68 69 70 71 72 73 74 75

# Testbed libraries.
use lib '@prefix@/lib';

#
# Turn off line buffering on output
#
$| = 1;

#
# Untaint the path
#
76
$ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin:/usr/local/bin';
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

# Locals
my $debug      = 0;

# Watch for apache restart; must disconnect and continue.
my $disconnected = 0;

#
# Only apache ... 
#
if (getpwuid($UID) ne "nobody") {
    printf STDERR "You are not allowed to run this script!\n";
    exit(1);
}

sub info($)
{
    my ($msg) = @_;
    
    if ($debug) {
	print STDERR "$msg\n";
    }
    else {
	syslog("info", $msg);
    }
}
sub fatal($)
{
    my ($msg) = @_;
    info($msg);
    exit(1);
}

if ($debug) {
    open(STDERR, "> /tmp/foo.log");
}
else {
    # Set up syslog
    openlog("shellinabox", "pid", $TBLOGFACIL);
}

# The query holds the authentication object. 
my $query = new CGI();
my $authstuff = $query->param('auth');
if (!defined($authstuff)) {
    fatal("No auth object provided");
}
Leigh B Stoller's avatar
Leigh B Stoller committed
125 126 127
if (0) {
    info(Dumper($query));
}
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151

#
# We need the shared key to recreate the SHA1 signature.
#
open(KEY, "/usr/testbed/etc/sshauth.key") or
    fatal("Could not open sshauth.key");
my $sshauthkey = <KEY>;
chomp($sshauthkey);

#
# Dig out the authentication object. It is a json object.
#
my $auth = eval { decode_json($authstuff); };
if ($@) {
    fatal("Could not decode auth object");
}
if ($debug) {
    print STDERR Dumper($auth);
}
else {
    syslog("info", $auth->{'uid'} . "," . $auth->{'nodeid'});
}

#
152 153
# Recreate the signature and compare. We do a different check for
# a console vs ssh request.
154
#
155 156 157 158 159 160 161 162 163 164 165
my $sigtocheck = 
    $auth->{'uid'} . $auth->{'stuff'} .
    $auth->{'nodeid'} . $auth->{'timestamp'};
if (exists($auth->{'console'})) {
    $sigtocheck .= " " .
	$auth->{'console'}->{"server"} . "," .
	$auth->{'console'}->{"portnum"} . "," .
	$auth->{'console'}->{"keylen"} . "," .
	$auth->{'console'}->{"keydata"} . "," .
	$auth->{'console'}->{"certhash"};
}
166 167 168 169 170 171
my $signature = hmac_sha1_hex($sigtocheck, $sshauthkey);
if ($signature ne $auth->{'signature'}) {
    fatal("Bad signature: $signature");
}
my $uid      = $auth->{'uid'};
my $nodeid   = $auth->{'nodeid'};
172
my $port;
173 174 175 176 177 178 179 180 181 182
# Silly taint check stuff.
if ($uid =~ /^([-\w]*)$/) {
    $uid = $1;
}
# Watch for port number in nodeid. 
if ($nodeid =~ /^([-\.\w]*)$/) {
    $nodeid = $1;
}
elsif ($nodeid =~ /^([-\.\w]*):(\d*)$/) {
    $nodeid = $1;
183
    $port   = $2;
184 185
}
my $where    = "HOME";
186 187
my $tempfile;
my $command;
188 189

# shellinabox wants the gid to be the default for the user.
Leigh B Stoller's avatar
Leigh B Stoller committed
190
my (undef,undef,undef,$gid) = getpwnam($uid);
191 192 193 194

if (exists($auth->{'console'})) {
    if (!defined($gid)) {
	$where    = "/tmp";
195 196 197 198
	# Switch to nobody for below.
	$uid = "nobody";
	$gid = "nobody";
    }
199 200 201 202 203 204 205 206 207 208 209 210 211
    #
    # Make a temp file for the acl.
    #
    $ENV{'TMPDIR'} = "/tmp";

    $tempfile = `mktemp -t tipacl`;
    if ($?) {
	fatal("Could not create a temporary file!");
    }
    # Silly taint check for below.
    if ($tempfile =~ /^([-\w\/\.]*)$/) {
	$tempfile = $1;
    }
212
    else {
213 214 215 216 217 218 219 220 221 222 223 224 225 226
	fatal("Bad data in filensame: $tempfile");
    }
    open(TMP, ">$tempfile")
	or fatal("Could not open $tempfile for writing");
    foreach my $key (keys(%{ $auth->{'console'} })) {
	my $val = $auth->{'console'}->{$key};

	print TMP "$key:  $val\n";
    }
    close(TMP);
    system("chown $uid:$gid $tempfile");
    $command = "$CONSOLEBIN -e -a $tempfile $nodeid";
}
else {
Leigh B Stoller's avatar
Leigh B Stoller committed
227 228
    my $sshopts  = "";
    my $username = $uid;
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    
    # No gid, see if a phony user.
    if (!defined($gid)) {
	if (-e "$APTDIR/$uid") {
	    $sshopts  = "-i $APTDIR/$uid/id_rsa ";
	    $sshopts .= "-q -o BatchMode=yes -o StrictHostKeyChecking=no ";
	    $sshopts .= "-o UserKnownHostsFile=${APTDIR}/$uid/known_hosts";
	    $where    = "$APTDIR/$uid";
	    # Switch to nobody for below.
	    $uid = "nobody";
	    $gid = "nobody";
	}
	else {
	    fatal("$uid is not in the passwd file or $APTDIR");
	}
244
    }
Leigh B Stoller's avatar
Leigh B Stoller committed
245
    $command = "/usr/bin/ssh " . ($port ? "-p $port" : "") . " " .
Leigh B Stoller's avatar
Leigh B Stoller committed
246
	"$sshopts ${username}\@${nodeid}";
247 248 249 250 251 252 253 254 255 256 257
}

# Silly taint check stuff.
if ($gid =~ /^([-\w]*)$/) {
    $gid = $1;
}

# This is so shellinabox will not complain. 
$UID = $EUID;

# Shove this header out so that we can do cross site xmlrpc.
Leigh B Stoller's avatar
Leigh B Stoller committed
258
print "Access-Control-Allow-Origin: *\n";
259

260
my $cmd = "shellinaboxd --no-beep " . ($debug ? "-d" : "-v") . " " .
261
    "--certfile=${CERTFILE} --keyfile=${KEYFILE} ".
262
    "--cgi -c $TB/etc -s '/:$uid:$gid:$where:$command'";
263 264

info($cmd);
Leigh B Stoller's avatar
Leigh B Stoller committed
265 266 267 268
if ($debug) {
    system($cmd);
    exit(0);
}
269 270 271 272 273 274 275 276 277 278 279 280

#
# The point of this is to capture the initial STDERR of shellinaboxd
# and send it out to syslog or file.
#
pipe(READER, WRITER);
WRITER->autoflush(1);
my $pid = fork();
if ($pid) {
    close WRITER;
    close STDOUT;
    close STDIN;
281 282 283 284 285 286 287 288 289
    #
    # The read seems to get stuck, leaving a defunct child and this
    # script hanging out. Not sure why, so cancel the read after a
    # a bit and go on. 
    #
    local $SIG{ALRM} = sub {
	info("Timed out waiting for $pid to say something");
	waitpid($pid, 0);
	info("After waitpid: $?");
Leigh B Stoller's avatar
Leigh B Stoller committed
290 291
	unlink($tempfile)
	    if (defined($tempfile));
292 293 294
	exit($? >> 0);
    };
    alarm 30;
295 296 297
    while (<READER>) {
	info($_);
    }
298 299
    alarm 0;
    info("Before waitpid for $pid");
300 301
    waitpid($pid, 0);
    info("After waitpid: $?");
Leigh B Stoller's avatar
Leigh B Stoller committed
302 303
    unlink($tempfile)
	if (defined($tempfile));
304 305 306 307 308 309 310 311 312 313 314 315
    exit($? >> 0);
}
else {
    # Need the parent to run first so it can close STDOUT. Better
    # ways to do this of course.
    select(undef, undef, undef, 0.2);
    close READER;
    close STDERR;
    open(STDERR, ">&WRITER") || fatal("can't dup to stderr");
    close WRITER;
}
exec($cmd);