1#!${PERLPATH} 2#! $Id: sudoscriptd-in,v 1.9 2004/11/12 05:27:54 hbo Exp $ 3use strict; 4use warnings; 5use POSIX qw(mkfifo setsid); 6use POSIX qw(:sys_wait_h); 7use Fcntl qw(O_RDWR O_RDONLY O_WRONLY); 8use File::Path; 9use Sys::Syslog qw(:DEFAULT setlogsock); 10use File::Basename qw(dirname basename); 11use Getopt::Long; 12use lib ${SSLIBDIR}; 13use Sudoscript; 14 15our $ss=Sudoscript->new(); 16exit if (! defined $ss); 17 18# We take one option: --datefmt|-d which controls how dates are written in the log. 19my ($DATEFMT,$killme); 20GetOptions ( 21 "datefmt:s" => \$DATEFMT, 22 "kill" => \$killme, 23 ); 24 25$DATEFMT='short' if (! defined $DATEFMT); 26 27my $dpid=$ss->checkpid(); 28if ($killme){ 29 if ($dpid){ 30 `kill -HUP $dpid`; 31 print "Sudoscriptd at $dpid killed\n"; 32 } else { 33 print "Can't find a sudoscriptd to kill!\n"; 34 } 35 exit; 36} 37 38if ($dpid){ 39 print STDERR "sudoscriptd already running at $dpid\n"; 40 print STDERR "Can't start new sudoscriptd\n"; 41 exit 1; 42} 43 44# Become a daemon 45my $pid=fork; 46exit if $pid; 47die "Couldn't fork $!" unless defined($pid); 48 49our $rundir="/var/run"; 50our $fifodir="$rundir/sudoscript"; 51our $logdir="/var/log"; 52 53foreach my $tag ('','log','merge','comp'){ 54 unlink "$fifodir/stderr$tag" if (-e "$fifodir/stderr$tag"); 55} 56$ss->daemon_io(); 57 58die "Couldn't start new session $!" unless POSIX::setsid(); 59 # Change our ps id, if supported by OS 60$0="sudoscriptd: main"; 61 62# 2 MiByte limit on log size before compression/rotation. 63my $MAXLOGSIZE=1024*1024*2; 64 65 66# Open syslog 67my $facility='AUTHPRIV'; 68setlogsock 'unix'; 69openlog("sudoscriptd",'pid',$facility) || die "Can't open syslog $!"; 70syslog('info',"Master sudoscriptd starting"); 71 72# Declare these for the interrupt handler below. 73# Hash of outstanding logger PIDs by session tag 74my %LOGGERS; 75# PID of the log merger 76my $mergerpid; 77# interrupt handler to reap outstanding children 78 79sub main_exit_handler{ 80 # We could be called directly, or through a signal 81 # If the former, we'll have parameters. 82 my $exitseverity=shift; 83 my $exitmsg=shift; 84 $exitseverity="info" if (!$exitseverity || $exitseverity eq 'HUP'); 85 $exitmsg="Master sudoscriptd caught signal. Exiting" if (!$exitmsg); 86 syslog($exitseverity,$exitmsg); 87 # This is main daemon shutdown. Kill all other daemons we know about. 88 foreach (keys %LOGGERS){ 89 kill 1,$LOGGERS{$_}; 90 waitpid $LOGGERS{$_},&WNOHANG; 91 } 92 if ($mergerpid){ 93 kill 1, $mergerpid; 94 waitpid $mergerpid,&WNOHANG; 95 } 96 closelog(); 97 exit; 98}; 99$SIG{'HUP'} = \&main_exit_handler; 100 101# Check to see if we have a ssers group 102our ($grname,$grpasswd,$gid) = getgrnam 'ssers'; 103$grpasswd=""; # make strict happy 104# Set up state directories 105create_state_dirs(); 106 107# Record our PID for those who would wish us ill 8) 108if (! (open PID,">$rundir/sudoscriptd.pid")){ 109 syslog('crit',"Can't open $rundir/sudoscriptd.pid %m"); 110 closelog(); 111 die "Couldn't record pid, $!"; 112} 113 114print PID "$$\n"; 115close PID; 116 117 118# The front-end FIFO 119my $fifo="$fifodir/rendezvous"; 120# Create the FIFO that sudoshell will rendezvous on 121# FIFO must me writable by group if group exists 122if ($grname){ 123 mktypescript($fifo,0,0620,$gid); 124 } else { 125 mktypescript($fifo,0,0600); 126} 127 128# Create the back-end merge daemon 129$mergerpid=log_merger($DATEFMT); 130 131my $handshake; # sudoshell's id string 132while (1){ 133 # Open the rendezvous FIFO or exit 134 main_exit_handler('crit',"Couldn't open FIFO ($fifo), %m") if (!sysopen(FIFO, $fifo, O_RDONLY)); 135 136 # Main input loop. ss is on the writing end. 137 while (<FIFO>){ 138 139 if (/^HELO (.*)/){ # HELO handshake. Fork a new logger 140 $handshake=$1; 141 syslog('info',"Forking new logger for sudoshell $handshake"); 142 $pid=new_logger(split /\s+/,$handshake); 143 # Recored the new logger's PID 144 $LOGGERS{$handshake}=$pid; 145 } 146 if (/^GDBY (.*)/){ # GDBY handshake. Signal and reap the old logger 147 $handshake=$1; 148 $pid=$LOGGERS{$handshake}; 149 if ($pid){ 150 my $count= kill 1,$pid; 151 waitpid $pid,0; 152 delete $LOGGERS{$handshake}; 153 syslog('info',"session $handshake closed"); 154 } else { 155 syslog('err',"No PID recorded for handshake $handshake. Can't clean up session!"); 156 } 157 } 158 } 159 # End of master daemon 160} 161 162sub create_state_dirs{ 163 # Set up logging and fifo directories 164 # Create directories as needed 165 if (!-d $rundir){ 166 if (!mkdir $rundir,0755){ 167 syslog('crit',"Can't mkdir $rundir %m"); 168 closelog(); 169 die "Can't mkdir $rundir $!"; 170 } 171 } 172 173 # FIFO dir has to be group writeable if ss is to become a user other than root 174 my $fifodirmode; 175 if ($grname){ 176 $fifodirmode=0770; 177 } else { 178 $fifodirmode=0700; 179 } 180 181 `rm -fr $fifodir`; 182 if (!mkdir $fifodir,$fifodirmode){ 183 syslog('crit',"Can't mkdir $fifodir %m"); 184 closelog(); 185 die "Can't mkdir $fifodir $!"; 186 } 187 188 if ($grname){ 189 chown 0,$gid,$fifodir; 190 } 191 192 if (!-d $logdir){ 193 if(!mkdir $logdir,0700){ 194 syslog('crit',"Can't mkdir $logdir %m"); 195 closelog(); 196 die "Can't mkdir $logdir $!"; 197 } 198 } 199 # Let caller know where the fifo dir is 200 return $fifodir; 201} 202# 203# 204# merger daemon - 205# Collect all session's data (pre tagged by the loggers) into 206# a single log file. Rotate the log if it exceeds $MAXLOGSIZE in size. 207sub log_merger{ 208 my $ss=Sudoscript->new(); 209 my $pid=fork; 210 return $pid if ($pid); # caller needs pid to signal us 211 if (!defined $pid){ 212 syslog ('crit','Log merger encountered fork error %m'); 213 closelog(); 214 die "Couldn't fork $!"; 215 } 216 $ss->daemon_io('merge'); 217 my $session=POSIX::setsid(); 218 if (!$session){ 219 syslog ('crit','Log merger Couldn\'t setsid() %m'); 220 closelog(); 221 die "Couldn't start new session $!"; 222 } 223 # Change our ps id, if supported by OS 224 $0="sudoscriptd: merger"; 225 226 # close parent's syslog 227 closelog(); 228 229 # close parent's rendezvous FIFO 230 close FIFO; 231 232 # Open our own syslog 233 my $facility='AUTHPRIV'; 234 openlog("sudoscriptd-merger",'pid',$facility) || die "Can't open syslog $!"; 235 236 # Predeclares for the signal handler 237 # Our FIFO 238 my $fifo=$fifodir."/merge"; 239 # List of compressor PIDs 240 my @comppids; 241 242 # Merger signal handler 243 # Close and unlink FIFO 244 # Reap any outstanding compressors 245 $SIG{'HUP'} = 246 sub { 247 syslog('info',"Merger caught signal. Exiting"); 248 print LOG "Merger caught signal. Exiting\n"; 249 close LOG; 250 close MYFIFO; 251 unlink $fifo; 252 closelog(); 253 foreach(@comppids){ 254 kill 1,$_; 255 waitpid $_,&WNOHANG; 256 } 257 exit; 258 }; 259 260 # Announce ourselves to syslog 261 syslog('info',"New merger"); 262 263 # Open the log file, obtaining the current log size and possibly a compressor PID 264 my ($size,$comppid)=openmylog("$logdir/sudoscript"); 265 # Save PID, if any 266 push @comppids,$comppid if($comppid); 267 268 # Announce ourselves in the log file 269 print LOG $ss->datestamp($DATEFMT)." New Merger\n"; 270 271 # Create out merge FIFO 272 mktypescript($fifo); 273 # Announce success 274 print LOG "opened FIFO $fifo\n"; 275 276 # Merger input loop 277 while(1){ 278 # open the FIFO 279 sysopen(MYFIFO, $fifo,O_RDONLY) or die "Couldn't open FIFO ($fifo), $!"; 280 281 while (<MYFIFO>){ 282 283 # Datestamp the input 284 $_=$ss->datestamp($DATEFMT)." $_"; 285 # Keep track of the size 286 $size += length($_) +1; 287 # Log the input 288 print LOG; 289 # Rotate the log if $MAXLOGSIZE is exceeded 290 if ($size > $MAXLOGSIZE){ 291 # as above 292 ($size,$comppid)=openmylog("$logdir/sudoscript"); 293 push @comppids,$comppid if($comppid); 294 } 295 296 # if we have outstanding compressor kids, wait for them here 297 if ($#comppids >=0){ 298 if (waitpid $comppids[0],&WNOHANG >0){ 299 shift @comppids; 300 } 301 } 302 } 303 } 304 close MYFIFO; 305 # End of merger daemon 306} 307# 308# 309# Logger daemon 310# Creates a session FIFO for sudoshell 311# Accepts script(1) output from sudoshell 312# Tags it with session ID, and passes it on to the merger daemon 313# 314sub new_logger{ 315 # real user, sudoshell pid, effective user (or root if undef) 316 my ($user,$sspid,$runas)=@_; 317 my $ss=Sudoscript->new(); 318 my $pid=fork; 319 return $pid if ($pid); 320 if (!defined $pid){ 321 syslog ('crit','Child logger encountered fork error %m'); 322 closelog(); 323 die "Couldn't fork $!"; 324 } 325 $ss->daemon_io('log'); 326 my $session=POSIX::setsid(); 327 if (!$session){ 328 syslog ('crit','Child logger Couldn\'t setsid() %m'); 329 closelog(); 330 die "Couldn't start new session $!"; 331 } 332 # Close parent's syslog. 333 closelog(); 334 # Close parent's rendezvous FIFO 335 close FIFO; 336 337 # Change our ps id, if supported by OS 338 $0="sudoscriptd: logger"; 339 340 # Open our own syslog 341 my $facility='AUTHPRIV'; 342 openlog("sudoscriptd-logger",'pid',$facility) || die "Can't open syslog $!"; 343 344 # Set up the session FIFO directory 345 # We create a subdirectory in case we are granting access to the run 346 # directory to the ssers group. If we are, that group must have 347 # read/write access. The directory we create here has no group or other access, 348 # thereby limiting access to the user only. (This is still a security hole if we are enabling 349 # a role account shared by two or more real users.) 350 my $sessdir; 351 $sessdir=$fifodir."/ssd.${user}_"; 352 if ($runas){ 353 $sessdir .= "${runas}_$sspid"; 354 } else { 355 $sessdir .= "root_$sspid"; 356 } 357 358 if (!-d $sessdir){ 359 `rm -fr $sessdir` if (-e $sessdir); 360 mkdir $sessdir,0700; 361 } 362 # If we have an "effective" username, get the UID and chown the session dir to that user. 363 my ($tuname,$tpasswd,$tuid); # "target" user 364 if (defined $runas){ 365 ($tuname,$tpasswd,$tuid)=getpwnam $runas; 366 } else { 367 $tuid=0; 368 } 369 chown $tuid,0,$sessdir; 370 371 # Predeclare the merge FIFO name for the signal handler. 372 my $mergefifo="$fifodir/merge"; 373 374 $SIG{'HUP'} = 375 # Remove the session directory and FIFO and exit 376 sub { 377 syslog('info',"logger ($user,$sspid) caught signal. Exiting"); 378 print MERGEFIFO "logger ($user,$sspid) caught signal. Exiting\n"; 379 close MERGEFIFO; 380 close SESSFIFO; 381 `/bin/rm -fr $sessdir`; 382 closelog(); 383 exit; 384 }; 385 386 # Announce ourselves to syslog 387 syslog('info',"new_logger for user $user with pid $sspid"); 388 389 # Open the merge FIFO for output 390 if (!sysopen(MERGEFIFO, $mergefifo,O_WRONLY)){ 391 syslog('crit',"Couldn't open FIFO ($mergefifo) %m"); 392 closelog(); 393 die "Couldn't open FIFO ($mergefifo), $!"; 394 } 395 # Unbuffer our OUTPUT 396 my $foo=select MERGEFIFO; 397 $|=1; 398 select $foo; 399 400 # Announce ourselves on the merge FIFO 401 print MERGEFIFO "New logger for $user with pid $sspid\n"; 402 403 # Create the session FIFO 404 my $sessfifo="$sessdir/$user$sspid.fifo"; 405 mktypescript($sessfifo,$tuid,0200); 406 407 # Give ss time to Posix::pause 408 sleep 1; 409 # Signal sudoshell that we are ready 410 kill "WINCH",$sspid; 411 412 # Logger input/output loop 413 while(1){ 414 # Open the session FIFO for read 415 if (!sysopen(SESSFIFO, $sessfifo,O_RDONLY)){ 416 syslog('crit',"Couldn't open FIFO ($sessfifo) %m"); 417 close MERGEFIFO; 418 closelog(); 419 die "Couldn't open FIFO ($sessfifo), $!"; 420 } 421 while (<SESSFIFO>){ 422 # Tag the input with the session ID 423 $_="$user:$sspid $_"; 424 # And send it to the merge FIFO 425 print MERGEFIFO; 426 } 427 } 428 # End of logger daemon 429} 430# 431# 432# Create a FIFO with optional owner group and mode overriding defaults 433sub mktypescript { 434 my ($fifo,$owner,$mode,$group)=@_; 435 436 # Delete the FIFO if it exists. 437 if (-e $fifo){ 438 unlink $fifo; 439 } 440 # Default mode is r/w to owner 441 $mode=0600 if (! defined $mode); 442 443 if (!mkfifo($fifo,$mode)){ 444 syslog('crit',"mktypescript() couldn't make new fifo $fifo %m"); 445 closelog(); 446 die "Can't make fifo $fifo $!"; 447 } 448 449 # Default owner is root 450 $owner=0 if (!defined $owner); 451 452 # Default group is wheel/root 453 $group=0 if (!defined $group); 454 chown $owner,$group,$fifo; 455 chmod $mode,$fifo; 456} 457 458 459# 460# (re)open the log file 461# Rotate the logs if larger than $MAXLOGSIZE 462sub openmylog{ 463 # Passed the log file name 464 my $log=shift; 465 466 # compressor PID 467 my $pid; 468 469 # File stat values 470 my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size); 471 472 # Get the logfile size if it exists 473 if (-e $log){ 474 ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size)=stat $log; 475 476 # If te size exceeds $MAXLOGSIZE, fork a rotator/compressor 477 if ($size>$MAXLOGSIZE){ 478 $pid=rotate_log($log); 479 480 # rotator/compressor moves old log out of our way 481 $size=0; 482 } 483 } 484 485 # Open the log file 486 # Default is create new .. 487 my $op = ">"; 488 #.. but append if it already existts 489 $op.=">" if (-e $log); 490 chmod 0600,$log; 491 if (!open LOG, "$op$log"){ 492 syslog('crit',"openmylog() couldn't open new log $op$log %m"); 493 die "Couldn't open log ($op$log), $!"; 494 } 495 my $foo=select LOG; 496 $|=1; # Unbuffered 497 select $foo; 498 # Return the current log size and any compressor PID that we may have spawned. 499 return ($size,$pid); 500} 501# 502# 503# Rotate and compress the log files. 504# Called by openmylog() when log file size exceeds $MAXLOGSIZE bytes 505sub rotate_log { 506 # We are passed the log file name 507 my $log =shift; 508 # We compute the dirname and basename 509 my $logdir=dirname($log); 510 my $logbase=basename($log); 511 512 # Temp log file name 513 my $tlog="$log.$$"; 514 515 # Move the old log out of the way 516 link $log, $tlog; 517 unlink $log; 518 519 my $ss=Sudoscript->new(); 520 # Fork a child to do the rotation/compression 521 my ($pid)=fork; 522 # Parent gets the child PID so we can interrupt if necessary 523 # We've moved the old log out of the way so the parent can create a new one. 524 return $pid if $pid; 525 526 527 # CHILD 528 529 $ss->daemon_io('comp'); 530 531 die "Couldn't start new session $!" unless POSIX::setsid(); 532 533 # Fix up our ps name if OS supports it 534 $0="sudoscriptd: compressor"; 535 536 my $MAXLOGS=10; 537 # Rename the old log files incrementing numeric extensions 538 # The $MAXLOGth one goes in the bit bucket 539 my ($currlog,$lastlog); 540 for (my $i=$MAXLOGS-1;$i;$i--){ 541 $currlog="$logdir/$logbase.$i.gz"; 542 $lastlog="$logdir/$logbase.".($i+1).".gz"; 543 if (-e $currlog){ 544 unlink $lastlog; 545 link $currlog,$lastlog; 546 } 547 } 548 # dot one is now linked with dot two 549 # clean it up 550 unlink "$logdir/$logbase.1.gz"; 551 552 # compress the temp log onto the dot one 553 my $cmd="gzip -c $tlog >$logdir/$logbase.1.gz"; 554 `$cmd`; 555 chmod 0600,"$logdir/$logbase.1.gz"; 556 unlink $tlog; 557 exit 0; 558} 559 560=pod 561 562=head1 NAME 563 564 sudoscriptd - logging daemons for sudoshell(1) 565 566=head1 SYNOPSIS 567 568 sudoscriptd [-d|--datefmt long|short|sortable] 569 570=head1 VERSION 571 572This manpage documents version ${VERSION} of sudoscriptd 573 574=head1 DESCRIPTION 575 576I<sudoscriptd> is a daemon for logging output from L<sudoshell(8)>. 577Used with that script, it provides an audit trail for shells run under 578sudo. 579 580=head1 README 581 582When I<sudoscriptd> starts, it creates a named pipe (FIFO) in a spool 583area. Then it forks a log management daemon that opens another FIFO 584and hangs around waiting for someone to write to it. When a new sudoshell 585starts, it writes the name of the user who ran it (from SUDO_UID) and its 586own PID to the first FIFO, then pauses waiting for a signal. 587Sudoscriptd forks a logger with the information given by sudoshell, 588which opens yet another FIFO, whose name is derived from the username and 589PID. The logger then sends the signal that sudoshell is waiting for. 590Sudoshell then runs script(1) on the session FIFO. The logger takes the 591output thus produced, tags it with a session ID, and writes it to the 592log management daemon's (remember him?) FIFO. The log daemon tags the data 593with a datestamp and writes it to a log file. It also manages the logs so 594they don't overflow the logging partition. When the user ends her script(1) 595session, sudoshell tells the front end daemon that it is done. The daemon 596signals the session logger to wrap up its work, which it does by deleting 597the session FIFO and exiting. 598 599=head1 CONFIGURATION 600 601I<sudoshell> uses L<sudo(8)> to perform all its authentication and 602privilege escalation. The I<sudoshell> user must therefore be in the 603I<sudoers> file (See L<sudoers(5)>.) with an entry that allows 604running I<sudoshell> as the desired user. See the SUDOCONFIG file in 605the distribution for details. (On Linux, this will be in 606/usr/share/doc/sudoscript-VERSION. Everywhere else, it's in 607/usr/local/doc/sudoscript-VERSION.) 608 609=head1 IS THIS SECURE? 610 611In a word, no. Giving a user a root shell is a bad idea if you don't trust him 612or her. There are countless ways to evade the audit trail provided by sudoscript, 613even without root privilege. Let me highlight the last part of that sentence: I<even 614without root privilege!> (Think about the implications of the fact that a user must have 615write access to the logging FIFO to see what I mean.) That means you can't rely on 616this tool to maintain security for you. So, what good is sudoscript? It's useful in an 617at least two environments. First, you trust your users, but need a record of what they 618do for auditing purposes. Second, you may or may not trust your users, but they have 619successfully agitated for a root (or other) shell. Sudoscript then provides an audit trail as 620long as your users don't try to evade it. 621 622See the file SECURITY (in the same place as SUDOCONFIG, above) for more on sudoscript's 623security assumptions. 624 625=head1 SWITCHES 626 627One optional switch, C<--datefmt>, is accepted by C<sudoscriptd>. This 628controls the format of the datestamps in the log file. Three options 629are available. 630 631=over 4 632 633=item long 634 635This selects a long date format of 'wdy mon dd hh:mm:dd ZZZ YYYY' where 636'wdy' is the weekday name, 'mon' is the three letter month name, 'dd' 637is the day of the month, hh:mm:ss' is the local time, 'ZZZ' is the local time 638zone name and 'YYYY' is the four digit year. 639 640=item short 641 642This selects a shorter date format of 'wdy mon dd hh:mm:dd'. This is 643just the long with the time zone and year removed. C<short> is the default 644format if no C<--datefmt> is given. 645 646=item sortable 647 648This selects a compressed and numerically sortable format of 'yyyymmddhhmmss'. 649 650=back 651 652=head1 FILES 653 654The front end fifo is /var/run/sudocript/rendezvous. The backend FIFO 655is /var/run/sudocript/merge. These two are semi-permanent. The session 656FIFOs are named /var/run/sudocript/ssd{username}{pid}. They go away once 657the session closes. 658 659The log file is named /var/log/sudoscript. When the backend daemon 660rotates the log, it forks a compressor that creates files called 661/var/log/sudoscript.{n}.gz, where {n} is one through ten. 662Sudoscriptd stores its PID in /var/run/sudoscriptd.pid. 663 664=head1 BUGS 665 666The script(1) output is pretty ugly. All control characters are preserved 667exactly as typed, or worse, as displayed by curses based console apps like 668vi. The content of such logs can look completely unintelligible unless 669they are cleaned up first. A shell script from the "Unix Power Tools" book 670that uses sed(1) to do a first pass over such logs is available at 671L<ftp://ftp.oreilly.com/pub/examples/power_tools/unix/split/script.tidy>. 672I considered building something like that into sudoscriptd, but rejected it 673for two reasons. First, the daemon needs to get back to reading the FIFO 674as quickly as possible to avoid losing data to an over-full buffer. Second, 675any cleanup of the logs would I<remove information>. This could be bad if 676I were over-zealous in my clean up. As it stands, you can run your own 677clean up on the log data without destroying the original log. 678 679The datestamp() routine is not locale aware and returns American 680English values. 681 682=head1 SEE ALSO 683 684sudoscript(8) 685 686sudoshell(1) 687 688Sudoscript(3pm) 689 690sudo(8) 691 692sudoers(5) 693 694 695=head1 PREREQUISITES 696 697sudo - L<http://www.courtesan.com/sudo/index.html> 698 699=head1 OSNAMES 700 701C<Solaris> 702 703C<Linux> 704 705C<FreeBSD> 706 707C<OpenBSD> 708 709C<HP-UX> 710 711=head1 SCRIPT CATEGORIES 712 713UNIX/System_administration 714 715=head1 CONTRIBUTORS 716 717The following people offered helpful advice and/or code: 718 719 Dan Rich (drich@emplNOoyeeSPAMs.org) 720 Alex Griffiths (dag@unifiedNOcomputingSPAM.com) 721 Bruce Gray (bruce.gray@aNOcSPAMm.org) 722 Chan Wilson (cwilson@coNrOp.sSgPi.cAoMm> 723 Tommy Smith (tsNmOith@eSaPtAeMl.net) 724 Donny Jekels (donny@jNOeSkPeAlMs.com 725 726=head1 AUTHOR 727 728Howard Owen, E<lt>hbo@egbok.comE<gt> 729 730=head1 COPYRIGHT AND LICENSE 731 732Copyright 2002,2003 by Howard Owen 733 734sudoscript is free software; you can redistribute it and/or modify 735it under the same terms as Perl itself. 736 737=cut 738