--- loncom/lond 2004/01/14 01:20:40 1.165.2.1 +++ loncom/lond 2004/07/23 16:14:19 1.211 @@ -2,7 +2,7 @@ # The LearningOnline Network # lond "LON Daemon" Server (port "LOND" 5663) # -# $Id: lond,v 1.165.2.1 2004/01/14 01:20:40 albertel Exp $ +# $Id: lond,v 1.211 2004/07/23 16:14:19 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -10,7 +10,7 @@ # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, @@ -45,24 +45,34 @@ use Authen::Krb4; use Authen::Krb5; use lib '/home/httpd/lib/perl/'; use localauth; +use localenroll; use File::Copy; +use LONCAPA::ConfigFileEdit; +use LONCAPA::lonlocal; +use LONCAPA::lonssl; my $DEBUG = 0; # Non zero to enable debug log entries. my $status=''; my $lastlog=''; -my $VERSION='$Revision: 1.165.2.1 $'; #' stupid emacs +my $VERSION='$Revision: 1.211 $'; #' stupid emacs my $remoteVERSION; my $currenthostid; my $currentdomainid; my $client; -my $clientip; -my $clientname; +my $clientip; # IP address of client. +my $clientdns; # DNS name of client. +my $clientname; # LonCAPA name of client. my $server; -my $thisserver; +my $thisserver; # DNS of us. + +my $keymode; + +my $cipher; # Cipher key negotiated with client +my $tmpsnum = 0; # Id of tmpputs. # # Connection type is: @@ -73,15 +83,30 @@ my $thisserver; my $ConnectionType; -my %hostid; -my %hostdom; -my %hostip; +my %hostid; # ID's for hosts in cluster by ip. +my %hostdom; # LonCAPA domain for hosts in cluster. +my %hostip; # IPs for hosts in cluster. +my %hostdns; # ID's of hosts looked up by DNS name. my %managers; # Ip -> manager names my %perlvar; # Will have the apache conf defined perl vars. # +# The hash below is used for command dispatching, and is therefore keyed on the request keyword. +# Each element of the hash contains a reference to an array that contains: +# A reference to a sub that executes the request corresponding to the keyword. +# A flag that is true if the request must be encoded to be acceptable. +# A mask with bits as follows: +# CLIENT_OK - Set when the function is allowed by ordinary clients +# MANAGER_OK - Set when the function is allowed to manager clients. +# +my $CLIENT_OK = 1; +my $MANAGER_OK = 2; +my %Dispatcher; + + +# # The array below are password error strings." # my $lastpwderror = 13; # Largest error number from lcpasswd. @@ -119,6 +144,195 @@ my @adderrors = ("ok", "lcuseradd Password mismatch"); + +# +# Statistics that are maintained and dislayed in the status line. +# +my $Transactions; # Number of attempted transactions. +my $Failures; # Number of transcations failed. + +# ResetStatistics: +# Resets the statistics counters: +# +sub ResetStatistics { + $Transactions = 0; + $Failures = 0; +} + + + +#------------------------------------------------------------------------ +# +# LocalConnection +# Completes the formation of a locally authenticated connection. +# This function will ensure that the 'remote' client is really the +# local host. If not, the connection is closed, and the function fails. +# If so, initcmd is parsed for the name of a file containing the +# IDEA session key. The fie is opened, read, deleted and the session +# key returned to the caller. +# +# Parameters: +# $Socket - Socket open on client. +# $initcmd - The full text of the init command. +# +# Implicit inputs: +# $clientdns - The DNS name of the remote client. +# $thisserver - Our DNS name. +# +# Returns: +# IDEA session key on success. +# undef on failure. +# +sub LocalConnection { + my ($Socket, $initcmd) = @_; + Debug("Attempting local connection: $initcmd client: $clientdns me: $thisserver"); + if($clientdns ne $thisserver) { + &logthis(' LocalConnection rejecting non local: ' + ."$clientdns ne $thisserver "); + close $Socket; + return undef; + } + else { + chomp($initcmd); # Get rid of \n in filename. + my ($init, $type, $name) = split(/:/, $initcmd); + Debug(" Init command: $init $type $name "); + + # Require that $init = init, and $type = local: Otherwise + # the caller is insane: + + if(($init ne "init") && ($type ne "local")) { + &logthis(' LocalConnection: caller is insane! ' + ."init = $init, and type = $type "); + close($Socket);; + return undef; + + } + # Now get the key filename: + + my $IDEAKey = lonlocal::ReadKeyFile($name); + return $IDEAKey; + } +} +#------------------------------------------------------------------------------ +# +# SSLConnection +# Completes the formation of an ssh authenticated connection. The +# socket is promoted to an ssl socket. If this promotion and the associated +# certificate exchange are successful, the IDEA key is generated and sent +# to the remote peer via the SSL tunnel. The IDEA key is also returned to +# the caller after the SSL tunnel is torn down. +# +# Parameters: +# Name Type Purpose +# $Socket IO::Socket::INET Plaintext socket. +# +# Returns: +# IDEA key on success. +# undef on failure. +# +sub SSLConnection { + my $Socket = shift; + + Debug("SSLConnection: "); + my $KeyFile = lonssl::KeyFile(); + if(!$KeyFile) { + my $err = lonssl::LastError(); + &logthis(" CRITICAL" + ."Can't get key file $err "); + return undef; + } + my ($CACertificate, + $Certificate) = lonssl::CertificateFile(); + + + # If any of the key, certificate or certificate authority + # certificate filenames are not defined, this can't work. + + if((!$Certificate) || (!$CACertificate)) { + my $err = lonssl::LastError(); + &logthis(" CRITICAL" + ."Can't get certificates: $err "); + + return undef; + } + Debug("Key: $KeyFile CA: $CACertificate Cert: $Certificate"); + + # Indicate to our peer that we can procede with + # a transition to ssl authentication: + + print $Socket "ok:ssl\n"; + + Debug("Approving promotion -> ssl"); + # And do so: + + my $SSLSocket = lonssl::PromoteServerSocket($Socket, + $CACertificate, + $Certificate, + $KeyFile); + if(! ($SSLSocket) ) { # SSL socket promotion failed. + my $err = lonssl::LastError(); + &logthis(" CRITICAL " + ."SSL Socket promotion failed: $err "); + return undef; + } + Debug("SSL Promotion successful"); + + # + # The only thing we'll use the socket for is to send the IDEA key + # to the peer: + + my $Key = lonlocal::CreateCipherKey(); + print $SSLSocket "$Key\n"; + + lonssl::Close($SSLSocket); + + Debug("Key exchange complete: $Key"); + + return $Key; +} +# +# InsecureConnection: +# If insecure connections are allowd, +# exchange a challenge with the client to 'validate' the +# client (not really, but that's the protocol): +# We produce a challenge string that's sent to the client. +# The client must then echo the challenge verbatim to us. +# +# Parameter: +# Socket - Socket open on the client. +# Returns: +# 1 - success. +# 0 - failure (e.g.mismatch or insecure not allowed). +# +sub InsecureConnection { + my $Socket = shift; + + # Don't even start if insecure connections are not allowed. + + if(! $perlvar{londAllowInsecure}) { # Insecure connections not allowed. + return 0; + } + + # Fabricate a challenge string and send it.. + + my $challenge = "$$".time; # pid + time. + print $Socket "$challenge\n"; + &status("Waiting for challenge reply"); + + my $answer = <$Socket>; + $answer =~s/\W//g; + if($challenge eq $answer) { + return 1; + } + else { + logthis("WARNING client did not respond to challenge"); + &status("No challenge reqply"); + return 0; + } + + +} + # # GetCertificate: Given a transaction that requires a certificate, # this function will extract the certificate from the transaction @@ -162,37 +376,41 @@ sub ReadManagerTable { # Clean out the old table first.. - foreach my $key (keys %managers) { - delete $managers{$key}; - } - - my $tablename = $perlvar{'lonTabDir'}."/managers.tab"; - if (!open (MANAGERS, $tablename)) { - logthis('No manager table. Nobody can manage!!'); - return; - } - while(my $host = ) { - chomp($host); - if (!defined $hostip{$host}) { # This is a non cluster member - + foreach my $key (keys %managers) { + delete $managers{$key}; + } + + my $tablename = $perlvar{'lonTabDir'}."/managers.tab"; + if (!open (MANAGERS, $tablename)) { + logthis('No manager table. Nobody can manage!!'); + return; + } + while(my $host = ) { + chomp($host); + if ($host =~ "^#") { # Comment line. + next; + } + if (!defined $hostip{$host}) { # This is a non cluster member # The entry is of the form: # cluname:hostname # cluname - A 'cluster hostname' is needed in order to negotiate # the host key. # hostname- The dns name of the host. # - - my($cluname, $dnsname) = split(/:/, $host); - open(MGRPIPE, "/usr/bin/host $dnsname |") || die "Can't make host pipeline"; - my $dnsinfo = ; - chomp $dnsinfo; - close MGRPIPE; - my($jname, $jhas, $jaddress, $hostip) = split(/ /, $dnsinfo); - $managers{$hostip} = $cluname; - } else { - $managers{$hostip{$host}} = $host; # Use info from cluster tab if clumemeber - } - } + my($cluname, $dnsname) = split(/:/, $host); + + my $ip = gethostbyname($dnsname); + if(defined($ip)) { # bad names don't deserve entry. + my $hostip = inet_ntoa($ip); + $managers{$hostip} = $cluname; + logthis(' registering manager '. + "$dnsname as $cluname with $hostip \n"); + } + } else { + logthis(' existing host'." $host\n"); + $managers{$hostip{$host}} = $host; # Use info from cluster tab if clumemeber + } + } } # @@ -219,8 +437,8 @@ sub ValidManager { # 1 - Success. # sub CopyFile { - my $oldfile = shift; - my $newfile = shift; + + my ($oldfile, $newfile) = @_; # The file must exist: @@ -279,42 +497,30 @@ sub AdjustHostContents { my $adjusted; my $me = $perlvar{'lonHostID'}; - foreach my $line (split(/\n/,$contents)) { + foreach my $line (split(/\n/,$contents)) { if(!(($line eq "") || ($line =~ /^ *\#/) || ($line =~ /^ *$/))) { chomp($line); my ($id,$domain,$role,$name,$ip,$maxcon,$idleto,$mincon)=split(/:/,$line); if ($id eq $me) { - open(PIPE, " /usr/bin/host $name |") || die "Cant' make host pipeline"; - my $hostinfo = ; - close PIPE; - - my ($hostname, $has, $address, $ipnew) = split(/ /,$hostinfo); - &logthis(''. - "hostname = $hostname me = $me, name = $name actual ip = $ipnew "); - - if ($hostname eq $name) { # Lookup succeeded.. - &logthis(' look up ok '); - $ip = $ipnew; - } else { - &logthis(' Lookup failed: ' - .$hostname." ne $name "); - } + my $ip = gethostbyname($name); + my $ipnew = inet_ntoa($ip); + $ip = $ipnew; # Reconstruct the host line and append to adjusted: - my $newline = "$id:$domain:$role:$name:$ip"; - if($maxcon ne "") { # Not all hosts have loncnew tuning params - $newline .= ":$maxcon:$idleto:$mincon"; - } - $adjusted .= $newline."\n"; + my $newline = "$id:$domain:$role:$name:$ip"; + if($maxcon ne "") { # Not all hosts have loncnew tuning params + $newline .= ":$maxcon:$idleto:$mincon"; + } + $adjusted .= $newline."\n"; - } else { # Not me, pass unmodified. - $adjusted .= $line."\n"; - } + } else { # Not me, pass unmodified. + $adjusted .= $line."\n"; + } } else { # Blank or comment never re-written. $adjusted .= $line."\n"; # Pass blanks and comments as is. } - } - return $adjusted; + } + return $adjusted; } # # InstallFile: Called to install an administrative file: @@ -332,8 +538,8 @@ sub AdjustHostContents { # 0 - failure and $! has an errno. # sub InstallFile { - my $Filename = shift; - my $Contents = shift; + + my ($Filename, $Contents) = @_; my $TempFile = $Filename.".tmp"; # Open the file for write: @@ -357,6 +563,32 @@ sub InstallFile { return 1; } + +# +# ConfigFileFromSelector: converts a configuration file selector +# (one of host or domain at this point) into a +# configuration file pathname. +# +# Parameters: +# selector - Configuration file selector. +# Returns: +# Full path to the file or undef if the selector is invalid. +# +sub ConfigFileFromSelector { + my $selector = shift; + my $tablefile; + + my $tabledir = $perlvar{'lonTabDir'}.'/'; + if ($selector eq "hosts") { + $tablefile = $tabledir."hosts.tab"; + } elsif ($selector eq "domain") { + $tablefile = $tabledir."domain.tab"; + } else { + return undef; + } + return $tablefile; + +} # # PushFile: Called to do an administrative push of a file. # - Ensure the file being pushed is one we support. @@ -386,12 +618,9 @@ sub PushFile { # part of some elaborate spoof that managed somehow to authenticate. # - my $tablefile = $perlvar{'lonTabDir'}.'/'; # need to precede with dir. - if ($filename eq "host") { - $tablefile .= "hosts.tab"; - } elsif ($filename eq "domain") { - $tablefile .= "domain.tab"; - } else { + + my $tablefile = ConfigFileFromSelector($filename); + if(! (defined $tablefile)) { return "refused"; } # @@ -483,6 +712,436 @@ sub ReinitProcess { } return 'ok'; } +# Validate a line in a configuration file edit script: +# Validation includes: +# - Ensuring the command is valid. +# - Ensuring the command has sufficient parameters +# Parameters: +# scriptline - A line to validate (\n has been stripped for what it's worth). +# +# Return: +# 0 - Invalid scriptline. +# 1 - Valid scriptline +# NOTE: +# Only the command syntax is checked, not the executability of the +# command. +# +sub isValidEditCommand { + my $scriptline = shift; + + # Line elements are pipe separated: + + my ($command, $key, $newline) = split(/\|/, $scriptline); + &logthis(' isValideditCommand checking: '. + "Command = '$command', Key = '$key', Newline = '$newline' \n"); + + if ($command eq "delete") { + # + # key with no newline. + # + if( ($key eq "") || ($newline ne "")) { + return 0; # Must have key but no newline. + } else { + return 1; # Valid syntax. + } + } elsif ($command eq "replace") { + # + # key and newline: + # + if (($key eq "") || ($newline eq "")) { + return 0; + } else { + return 1; + } + } elsif ($command eq "append") { + if (($key ne "") && ($newline eq "")) { + return 1; + } else { + return 0; + } + } else { + return 0; # Invalid command. + } + return 0; # Should not get here!!! +} +# +# ApplyEdit - Applies an edit command to a line in a configuration +# file. It is the caller's responsiblity to validate the +# edit line. +# Parameters: +# $directive - A single edit directive to apply. +# Edit directives are of the form: +# append|newline - Appends a new line to the file. +# replace|key|newline - Replaces the line with key value 'key' +# delete|key - Deletes the line with key value 'key'. +# $editor - A config file editor object that contains the +# file being edited. +# +sub ApplyEdit { + + my ($directive, $editor) = @_; + + # Break the directive down into its command and its parameters + # (at most two at this point. The meaning of the parameters, if in fact + # they exist depends on the command). + + my ($command, $p1, $p2) = split(/\|/, $directive); + + if($command eq "append") { + $editor->Append($p1); # p1 - key p2 null. + } elsif ($command eq "replace") { + $editor->ReplaceLine($p1, $p2); # p1 - key p2 = newline. + } elsif ($command eq "delete") { + $editor->DeleteLine($p1); # p1 - key p2 null. + } else { # Should not get here!!! + die "Invalid command given to ApplyEdit $command" + } +} +# +# AdjustOurHost: +# Adjusts a host file stored in a configuration file editor object +# for the true IP address of this host. This is necessary for hosts +# that live behind a firewall. +# Those hosts have a publicly distributed IP of the firewall, but +# internally must use their actual IP. We assume that a given +# host only has a single IP interface for now. +# Formal Parameters: +# editor - The configuration file editor to adjust. This +# editor is assumed to contain a hosts.tab file. +# Strategy: +# - Figure out our hostname. +# - Lookup the entry for this host. +# - Modify the line to contain our IP +# - Do a replace for this host. +sub AdjustOurHost { + my $editor = shift; + + # figure out who I am. + + my $myHostName = $perlvar{'lonHostID'}; # LonCAPA hostname. + + # Get my host file entry. + + my $ConfigLine = $editor->Find($myHostName); + if(! (defined $ConfigLine)) { + die "AdjustOurHost - no entry for me in hosts file $myHostName"; + } + # figure out my IP: + # Use the config line to get my hostname. + # Use gethostbyname to translate that into an IP address. + # + my ($id,$domain,$role,$name,$ip,$maxcon,$idleto,$mincon) = split(/:/,$ConfigLine); + my $BinaryIp = gethostbyname($name); + my $ip = inet_ntoa($ip); + # + # Reassemble the config line from the elements in the list. + # Note that if the loncnew items were not present before, they will + # be now even if they would be empty + # + my $newConfigLine = $id; + foreach my $item ($domain, $role, $name, $ip, $maxcon, $idleto, $mincon) { + $newConfigLine .= ":".$item; + } + # Replace the line: + + $editor->ReplaceLine($id, $newConfigLine); + +} +# +# ReplaceConfigFile: +# Replaces a configuration file with the contents of a +# configuration file editor object. +# This is done by: +# - Copying the target file to .old +# - Writing the new file to .tmp +# - Moving -> +# This laborious process ensures that the system is never without +# a configuration file that's at least valid (even if the contents +# may be dated). +# Parameters: +# filename - Name of the file to modify... this is a full path. +# editor - Editor containing the file. +# +sub ReplaceConfigFile { + + my ($filename, $editor) = @_; + + CopyFile ($filename, $filename.".old"); + + my $contents = $editor->Get(); # Get the contents of the file. + + InstallFile($filename, $contents); +} +# +# +# Called to edit a configuration table file +# Parameters: +# request - The entire command/request sent by lonc or lonManage +# Return: +# The reply to send to the client. +# +sub EditFile { + my $request = shift; + + # Split the command into it's pieces: edit:filetype:script + + my ($request, $filetype, $script) = split(/:/, $request,3); # : in script + + # Check the pre-coditions for success: + + if($request != "edit") { # Something is amiss afoot alack. + return "error:edit request detected, but request != 'edit'\n"; + } + if( ($filetype ne "hosts") && + ($filetype ne "domain")) { + return "error:edit requested with invalid file specifier: $filetype \n"; + } + + # Split the edit script and check it's validity. + + my @scriptlines = split(/\n/, $script); # one line per element. + my $linecount = scalar(@scriptlines); + for(my $i = 0; $i < $linecount; $i++) { + chomp($scriptlines[$i]); + if(!isValidEditCommand($scriptlines[$i])) { + return "error:edit with bad script line: '$scriptlines[$i]' \n"; + } + } + + # Execute the edit operation. + # - Create a config file editor for the appropriate file and + # - execute each command in the script: + # + my $configfile = ConfigFileFromSelector($filetype); + if (!(defined $configfile)) { + return "refused\n"; + } + my $editor = ConfigFileEdit->new($configfile); + + for (my $i = 0; $i < $linecount; $i++) { + ApplyEdit($scriptlines[$i], $editor); + } + # If the file is the host file, ensure that our host is + # adjusted to have our ip: + # + if($filetype eq "host") { + AdjustOurHost($editor); + } + # Finally replace the current file with our file. + # + ReplaceConfigFile($configfile, $editor); + + return "ok\n"; +} + +#--------------------------------------------------------------- +# +# Manipulation of hash based databases (factoring out common code +# for later use as we refactor. +# +# Ties a domain level resource file to a hash. +# If requested a history entry is created in the associated hist file. +# +# Parameters: +# domain - Name of the domain in which the resource file lives. +# namespace - Name of the hash within that domain. +# how - How to tie the hash (e.g. GDBM_WRCREAT()). +# loghead - Optional parameter, if present a log entry is created +# in the associated history file and this is the first part +# of that entry. +# logtail - Goes along with loghead, The actual logentry is of the +# form $loghead::logtail. +# Returns: +# Reference to a hash bound to the db file or alternatively undef +# if the tie failed. +# +sub tie_domain_hash { + my ($domain,$namespace,$how,$loghead,$logtail) = @_; + + # Filter out any whitespace in the domain name: + + $domain =~ s/\W//g; + + # We have enough to go on to tie the hash: + + my $user_top_dir = $perlvar{'lonUsersDir'}; + my $domain_dir = $user_top_dir."/$domain"; + my $resource_file = $domain_dir."/$namespace.db"; + my %hash; + if(tie(%hash, 'GDBM_File', $resource_file, $how, 0640)) { + if (defined($loghead)) { # Need to log the operation. + my $logFh = IO::File->new(">>$domain_dir/$namespace.hist"); + if($logFh) { + my $timestamp = time; + print $logFh "$loghead:$timestamp:$logtail\n"; + } + $logFh->close; + } + return \%hash; # Return the tied hash. + } else { + return undef; # Tie failed. + } +} + +# +# Ties a user's resource file to a hash. +# If necessary, an appropriate history +# log file entry is made as well. +# This sub factors out common code from the subs that manipulate +# the various gdbm files that keep keyword value pairs. +# Parameters: +# domain - Name of the domain the user is in. +# user - Name of the 'current user'. +# namespace - Namespace representing the file to tie. +# how - What the tie is done to (e.g. GDBM_WRCREAT(). +# loghead - Optional first part of log entry if there may be a +# history file. +# what - Optional tail of log entry if there may be a history +# file. +# Returns: +# hash to which the database is tied. It's up to the caller to untie. +# undef if the has could not be tied. +# +sub tie_user_hash { + my ($domain,$user,$namespace,$how,$loghead,$what) = @_; + + $namespace=~s/\//\_/g; # / -> _ + $namespace=~s/\W//g; # whitespace eliminated. + my $proname = propath($domain, $user); + + # Tie the database. + + my %hash; + if(tie(%hash, 'GDBM_File', "$proname/$namespace.db", + $how, 0640)) { + # If this is a namespace for which a history is kept, + # make the history log entry: + if (($namespace =~/^nohist\_/) && (defined($loghead))) { + my $args = scalar @_; + Debug(" Opening history: $namespace $args"); + my $hfh = IO::File->new(">>$proname/$namespace.hist"); + if($hfh) { + my $now = time; + print $hfh "$loghead:$now:$what\n"; + } + $hfh->close; + } + return \%hash; + } else { + return undef; + } + +} +#--------------------------------------------------------------- +# +# Getting, decoding and dispatching requests: +# + +# +# Get a Request: +# Gets a Request message from the client. The transaction +# is defined as a 'line' of text. We remove the new line +# from the text line. +# +sub get_request { + my $input = <$client>; + chomp($input); + + Debug("Request = $input\n"); + + &status('Processing '.$clientname.':'.$input); + + return $input; +} +# +# Decipher encoded traffic +# Parameters: +# input - Encoded data. +# Returns: +# Decoded data or undef if encryption key was not yet negotiated. +# Implicit input: +# cipher - This global holds the negotiated encryption key. +# +sub decipher { + my ($input) = @_; + my $output = ''; + + + if($cipher) { + my($enc, $enclength, $encinput) = split(/:/, $input); + for(my $encidx = 0; $encidx < length($encinput); $encidx += 16) { + $output .= + $cipher->decrypt(pack("H16", substr($encinput, $encidx, 16))); + } + return substr($output, 0, $enclength); + } else { + return undef; + } +} + +# +# Register a command processor. This function is invoked to register a sub +# to process a request. Once registered, the ProcessRequest sub can automatically +# dispatch requests to an appropriate sub, and do the top level validity checking +# as well: +# - Is the keyword recognized. +# - Is the proper client type attempting the request. +# - Is the request encrypted if it has to be. +# Parameters: +# $request_name - Name of the request being registered. +# This is the command request that will match +# against the hash keywords to lookup the information +# associated with the dispatch information. +# $procedure - Reference to a sub to call to process the request. +# All subs get called as follows: +# Procedure($cmd, $tail, $replyfd, $key) +# $cmd - the actual keyword that invoked us. +# $tail - the tail of the request that invoked us. +# $replyfd- File descriptor connected to the client +# $must_encode - True if the request must be encoded to be good. +# $client_ok - True if it's ok for a client to request this. +# $manager_ok - True if it's ok for a manager to request this. +# Side effects: +# - On success, the Dispatcher hash has an entry added for the key $RequestName +# - On failure, the program will die as it's a bad internal bug to try to +# register a duplicate command handler. +# +sub register_handler { + my ($request_name, + $procedure, + $must_encode, + $client_ok, + $manager_ok) = @_; + + # Don't allow duplication# + + if (defined $Dispatcher{$request_name}) { + die "Attempting to define a duplicate request handler for $request_name\n"; + } + # Build the client type mask: + + my $client_type_mask = 0; + if($client_ok) { + $client_type_mask |= $CLIENT_OK; + } + if($manager_ok) { + $client_type_mask |= $MANAGER_OK; + } + + # Enter the hash: + + my @entry = ($procedure, $must_encode, $client_type_mask); + + $Dispatcher{$request_name} = \@entry; + + +} + + +#------------------------------------------------------------------ + + + # # Convert an error return code from lcpasswd to a string value. @@ -514,7 +1173,7 @@ sub catchexception { $SIG{'QUIT'}='DEFAULT'; $SIG{__DIE__}='DEFAULT'; &status("Catching exception"); - &logthis("CRITICAL: " + &logthis("CRITICAL: " ."ABNORMAL EXIT. Child $$ for server $thisserver died through " ."a crash with this error msg->[$error]"); &logthis('Famous last words: '.$status.' - '.$lastlog); @@ -525,7 +1184,7 @@ sub catchexception { sub timeout { &status("Handling Timeout"); - &logthis("CRITICAL: TIME OUT ".$$.""); + &logthis("CRITICAL: TIME OUT ".$$.""); &catchexception('Timeout'); } # -------------------------------- Set signal handlers to record abnormal exits @@ -577,18 +1236,26 @@ $server = IO::Socket::INET->new(LocalPor # global variables my %children = (); # keys are current child process IDs -my $children = 0; # current number of children sub REAPER { # takes care of dead children $SIG{CHLD} = \&REAPER; &status("Handling child death"); - my $pid = wait; - if (defined($children{$pid})) { - &logthis("Child $pid died"); - $children --; - delete $children{$pid}; - } else { - &logthis("Unknown Child $pid died"); + my $pid; + do { + $pid = waitpid(-1,&WNOHANG()); + if (defined($children{$pid})) { + &logthis("Child $pid died"); + delete($children{$pid}); + } elsif ($pid > 0) { + &logthis("Unknown Child $pid died"); + } + } while ( $pid > 0 ); + foreach my $child (keys(%children)) { + $pid = waitpid($child,&WNOHANG()); + if ($pid > 0) { + &logthis("Child $child - $pid looks like we missed it's death"); + delete($children{$pid}); + } } &status("Finished Handling child death"); } @@ -600,7 +1267,7 @@ sub HUNTSMAN { # si &logthis("Free socket: ".shutdown($server,2)); # free up socket my $execdir=$perlvar{'lonDaemons'}; unlink("$execdir/logs/lond.pid"); - &logthis("CRITICAL: Shutting down"); + &logthis("CRITICAL: Shutting down"); &status("Done killing children"); exit; # clean up with dignity } @@ -610,7 +1277,7 @@ sub HUPSMAN { # sig &status("Killing children for restart (HUP)"); kill 'INT' => keys %children; &logthis("Free socket: ".shutdown($server,2)); # free up socket - &logthis("CRITICAL: Restarting"); + &logthis("CRITICAL: Restarting"); my $execdir=$perlvar{'lonDaemons'}; unlink("$execdir/logs/lond.pid"); &status("Restarting self (HUP)"); @@ -620,7 +1287,7 @@ sub HUPSMAN { # sig # # Kill off hashes that describe the host table prior to re-reading it. # Hashes affected are: -# %hostid, %hostdom %hostip +# %hostid, %hostdom %hostip %hostdns. # sub KillHostHashes { foreach my $key (keys %hostid) { @@ -632,6 +1299,9 @@ sub KillHostHashes { foreach my $key (keys %hostip) { delete $hostip{$key}; } + foreach my $key (keys %hostdns) { + delete $hostdns{$key}; + } } # # Read in the host table from file and distribute it into the various hashes: @@ -642,14 +1312,22 @@ sub KillHostHashes { sub ReadHostTable { open (CONFIG,"$perlvar{'lonTabDir'}/hosts.tab") || die "Can't read host file"; - + my $myloncapaname = $perlvar{'lonHostID'}; + Debug("My loncapa name is : $myloncapaname"); while (my $configline=) { - my ($id,$domain,$role,$name,$ip)=split(/:/,$configline); - chomp($ip); $ip=~s/\D+$//; - $hostid{$ip}=$id; - $hostdom{$id}=$domain; - $hostip{$id}=$ip; - if ($id eq $perlvar{'lonHostID'}) { $thisserver=$name; } + if (!($configline =~ /^\s*\#/)) { + my ($id,$domain,$role,$name,$ip)=split(/:/,$configline); + chomp($ip); $ip=~s/\D+$//; + $hostid{$ip}=$id; # LonCAPA name of host by IP. + $hostdom{$id}=$domain; # LonCAPA domain name of host. + $hostip{$id}=$ip; # IP address of host. + $hostdns{$name} = $id; # LonCAPA name of host by DNS. + + if ($id eq $perlvar{'lonHostID'}) { + Debug("Found me in the host table: $name"); + $thisserver=$name; + } + } } close(CONFIG); } @@ -770,9 +1448,8 @@ sub Debug { # request - Original request from client. # sub Reply { - my $fd = shift; - my $reply = shift; - my $request = shift; + + my ($fd, $reply, $request) = @_; print $fd $reply; Debug("Request was $request Reply was $reply"); @@ -785,13 +1462,14 @@ sub logstatus { my $docdir=$perlvar{'lonDocRoot'}; { my $fh=IO::File->new(">>$docdir/lon-status/londstatus.txt"); - print $fh $$."\t".$currenthostid."\t".$status."\t".$lastlog."\n"; + print $fh $$."\t".$clientname."\t".$currenthostid."\t" + .$status."\t".$lastlog."\t $keymode\n"; $fh->close(); } &status("Finished londstatus.txt"); { my $fh=IO::File->new(">$docdir/lon-status/londchld/$$.txt"); - print $fh $status."\n".$lastlog."\n".time; + print $fh $status."\n".$lastlog."\n".time."\n$keymode"; $fh->close(); } &status("Finished logging"); @@ -850,11 +1528,11 @@ sub reconlonc { kill USR1 => $loncpid; } else { &logthis( - "CRITICAL: " + "CRITICAL: " ."lonc at pid $loncpid not responding, giving up"); } } else { - &logthis('CRITICAL: lonc not running, giving up'); + &logthis('CRITICAL: lonc not running, giving up'); } } @@ -958,7 +1636,7 @@ my $execdir=$perlvar{'lonDaemons'}; open (PIDSAVE,">$execdir/logs/lond.pid"); print PIDSAVE "$$\n"; close(PIDSAVE); -&logthis("CRITICAL: ---------- Starting ----------"); +&logthis("CRITICAL: ---------- Starting ----------"); &status('Starting'); @@ -991,7 +1669,7 @@ while (1) { sub make_new_child { my $pid; - my $cipher; +# my $cipher; # Now global my $sigset; $client = shift; @@ -1013,15 +1691,27 @@ sub make_new_child { # the pid hash. # my $caller = getpeername($client); - my ($port,$iaddr)=unpack_sockaddr_in($caller); - $clientip=inet_ntoa($iaddr); + my ($port,$iaddr); + if (defined($caller) && length($caller) > 0) { + ($port,$iaddr)=unpack_sockaddr_in($caller); + } else { + &logthis("Unable to determine who caller was, getpeername returned nothing"); + } + if (defined($iaddr)) { + $clientip = inet_ntoa($iaddr); + Debug("Connected with $clientip"); + $clientdns = gethostbyaddr($iaddr, AF_INET); + Debug("Connected with $clientdns by name"); + } else { + &logthis("Unable to determine clientip"); + $clientip='Unavailable'; + } if ($pid) { # Parent records the child's birth and returns. sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock SIGINT for fork: $!\n"; $children{$pid} = $clientip; - $children++; &status('Started child '.$pid); return; } else { @@ -1038,7 +1728,7 @@ sub make_new_child { sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock SIGINT for fork: $!\n"; - my $tmpsnum=0; +# my $tmpsnum=0; # Now global #---------------------------------------------------- kerberos 5 initialization &Authen::Krb5::init_context(); &Authen::Krb5::init_ets(); @@ -1047,7 +1737,7 @@ sub make_new_child { # ============================================================================= # do something with the connection # ----------------------------------------------------------------------------- - # see if we know client and check for spoof IP by challenge + # see if we know client and 'check' for spoof IP by ineffective challenge ReadManagerTable; # May also be a manager!! @@ -1065,6 +1755,7 @@ sub make_new_child { $clientname = $managers{$clientip}; } my $clientok; + if ($clientrec || $ismanager) { &status("Waiting for init from $clientip $clientname"); &logthis('INFO: Connection, '. @@ -1072,34 +1763,95 @@ sub make_new_child { " ($clientname) connection type = $ConnectionType " ); &status("Connecting $clientip ($clientname))"); my $remotereq=<$client>; - $remotereq=~s/[^\w:]//g; + chomp($remotereq); + Debug("Got init: $remotereq"); + my $inikeyword = split(/:/, $remotereq); if ($remotereq =~ /^init/) { &sethost("sethost:$perlvar{'lonHostID'}"); - my $challenge="$$".time; - print $client "$challenge\n"; - &status( - "Waiting for challenge reply from $clientip ($clientname)"); - $remotereq=<$client>; - $remotereq=~s/\W//g; - if ($challenge eq $remotereq) { - $clientok=1; - print $client "ok\n"; + # + # If the remote is attempting a local init... give that a try: + # + my ($i, $inittype) = split(/:/, $remotereq); + + # If the connection type is ssl, but I didn't get my + # certificate files yet, then I'll drop back to + # insecure (if allowed). + + if($inittype eq "ssl") { + my ($ca, $cert) = lonssl::CertificateFile; + my $kfile = lonssl::KeyFile; + if((!$ca) || + (!$cert) || + (!$kfile)) { + $inittype = ""; # This forces insecure attempt. + &logthis(" Certificates not " + ."installed -- trying insecure auth"); + } + else { # SSL certificates are in place so + } # Leave the inittype alone. + } + + if($inittype eq "local") { + my $key = LocalConnection($client, $remotereq); + if($key) { + Debug("Got local key $key"); + $clientok = 1; + my $cipherkey = pack("H32", $key); + $cipher = new IDEA($cipherkey); + print $client "ok:local\n"; + &logthis('"); + $keymode = "local" + } else { + Debug("Failed to get local key"); + $clientok = 0; + shutdown($client, 3); + close $client; + } + } elsif ($inittype eq "ssl") { + my $key = SSLConnection($client); + if ($key) { + $clientok = 1; + my $cipherkey = pack("H32", $key); + $cipher = new IDEA($cipherkey); + &logthis('' + ."Successfull ssl authentication with $clientname "); + $keymode = "ssl"; + + } else { + $clientok = 0; + close $client; + } + } else { - &logthis( - "WARNING: $clientip did not reply challenge"); - &status('No challenge reply '.$clientip); + my $ok = InsecureConnection($client); + if($ok) { + $clientok = 1; + &logthis('' + ."Successful insecure authentication with $clientname "); + print $client "ok\n"; + $keymode = "insecure"; + } else { + &logthis('' + ."Attempted insecure connection disallowed "); + close $client; + $clientok = 0; + + } } } else { &logthis( - "WARNING: " + "WARNING: " ."$clientip failed to initialize: >$remotereq< "); &status('No init '.$clientip); } + } else { &logthis( - "WARNING: Unknown client $clientip"); + "WARNING: Unknown client $clientip"); &status('Hung up on '.$clientip); } + if ($clientok) { # ---------------- New known client connecting, could mean machine online again @@ -1111,7 +1863,7 @@ sub make_new_child { } &reconlonc("$perlvar{'lonSockDir'}/$id"); } - &logthis("Established connection: $clientname"); + &logthis("Established connection: $clientname"); &status('Will listen to '.$clientname); # ------------------------------------------------------------ Process requests while (my $userinput=<$client>) { @@ -1224,22 +1976,41 @@ sub make_new_child { } #--------------------------------------------------------------------- reinit } elsif($userinput =~ /^reinit/) { # Encoded and manager - if (($wasenc == 1) && isManager) { - my $cert = GetCertificate($userinput); - if(ValidManager($cert)) { - chomp($userinput); - my $reply = ReinitProcess($userinput); - print $client "$reply\n"; + if (($wasenc == 1) && isManager) { + my $cert = GetCertificate($userinput); + if(ValidManager($cert)) { + chomp($userinput); + my $reply = ReinitProcess($userinput); + print $client "$reply\n"; + } else { + print $client "refused\n"; + } } else { - print $client "refused\n"; + Reply($client, "refused\n", $userinput); } - } else { - Reply($client, "refused\n", $userinput); - - - } +#------------------------------------------------------------------------- edit + } elsif ($userinput =~ /^edit/) { # encoded and manager: + if(($wasenc ==1) && (isManager)) { + my $cert = GetCertificate($userinput); + if(ValidManager($cert)) { + my($command, $filetype, $script) = split(/:/, $userinput); + if (($filetype eq "hosts") || ($filetype eq "domain")) { + if($script ne "") { + Reply($client, EditFile($userinput)); + } else { + Reply($client,"refused\n",$userinput); + } + } else { + Reply($client,"refused\n",$userinput); + } + } else { + Reply($client,"refused\n",$userinput); + } + } else { + Reply($client,"refused\n",$userinput); + } # ------------------------------------------------------------------------ auth - } elsif ($userinput =~ /^auth/) { # Encoded and client only. + } elsif ($userinput =~ /^auth/) { # Encoded and client only. if (($wasenc==1) && isClient) { my ($cmd,$udom,$uname,$upass)=split(/:/,$userinput); chomp($upass); @@ -1289,7 +2060,7 @@ sub make_new_child { $pwdcorrect=0; # log error if it is not a bad password if ($krb4_error != 62) { - &logthis('krb4:'.$uname.','.$contentpwd.','. + &logthis('krb4:'.$uname.','. &Authen::Krb4::get_err_txt($Authen::Krb4::error)); } } @@ -1555,12 +2326,21 @@ sub make_new_child { } elsif ($userinput =~ /^fetchuserfile/) { # Client clear or enc. if(isClient) { my ($cmd,$fname)=split(/:/,$userinput); - my ($udom,$uname,$ufile)=split(/\//,$fname); + my ($udom,$uname,$ufile) = ($fname =~ m|^([^/]+)/([^/]+)/(.+)$|); my $udir=propath($udom,$uname).'/userfiles'; unless (-e $udir) { mkdir($udir,0770); } if (-e $udir) { - $ufile=~s/^[\.\~]+//; - $ufile=~s/\///g; + $ufile=~s/^[\.\~]+//; + my $path = $udir; + if ($ufile =~m|(.+)/([^/]+)$|) { + my @parts=split('/',$1); + foreach my $part (@parts) { + $path .= '/'.$part; + if ((-e $path)!=1) { + mkdir($path,0770); + } + } + } my $destname=$udir.'/'.$ufile; my $transname=$udir.'/'.$ufile.'.in.transit'; my $remoteurl='http://'.$clientip.'/userfiles/'.$fname; @@ -1589,7 +2369,37 @@ sub make_new_child { } } else { Reply($client, "refused\n", $userinput); - + } +# --------------------------------------------------------- remove a user file + } elsif ($userinput =~ /^removeuserfile/) { # Client clear or enc. + if(isClient) { + my ($cmd,$fname)=split(/:/,$userinput); + my ($udom,$uname,$ufile) = ($fname =~ m|^([^/]+)/([^/]+)/(.+)$|); + &logthis("$udom - $uname - $ufile"); + if ($ufile =~m|/\.\./|) { + # any files paths with /../ in them refuse + # to deal with + print $client "refused\n"; + } else { + my $udir=propath($udom,$uname); + if (-e $udir) { + my $file=$udir.'/userfiles/'.$ufile; + if (-e $file) { + unlink($file); + if (-e $file) { + print $client "failed\n"; + } else { + print $client "ok\n"; + } + } else { + print $client "not_found\n"; + } + } else { + print $client "not_home\n"; + } + } + } else { + Reply($client, "refused\n", $userinput); } # ------------------------------------------ authenticate access to a user file } elsif ($userinput =~ /^tokenauthuserfile/) { # Client only @@ -1600,7 +2410,7 @@ sub make_new_child { if (open(ENVIN,$perlvar{'lonIDsDir'}.'/'. $session.'.id')) { while (my $line=) { - if ($line=~/userfile\.$fname\=/) { $reply='ok'; } + if ($line=~ m|userfile\.\Q$fname\E\=|) { $reply='ok'; } } close(ENVIN); print $client $reply."\n"; @@ -1616,7 +2426,7 @@ sub make_new_child { if(isClient) { my ($cmd,$fname)=split(/:/,$userinput); if (-e $fname) { - print $client &unsub($client,$fname,$clientip); + print $client &unsub($fname,$clientip); } else { print $client "not_found\n"; } @@ -1667,24 +2477,23 @@ sub make_new_child { } elsif ($userinput =~ /^put/) { if(isClient) { my ($cmd,$udom,$uname,$namespace,$what) - =split(/:/,$userinput); + =split(/:/,$userinput,5); $namespace=~s/\//\_/g; $namespace=~s/\W//g; if ($namespace ne 'roles') { chomp($what); my $proname=propath($udom,$uname); my $now=time; - unless ($namespace=~/^nohist\_/) { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname/$namespace.hist") - ) { print $hfh "P:$now:$what\n"; } - } my @pairs=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File', "$proname/$namespace.db", &GDBM_WRCREAT(),0640)) { + unless ($namespace=~/^nohist\_/) { + my $hfh; + if ($hfh=IO::File->new(">>$proname/$namespace.hist")) { print $hfh "P:$now:$what\n"; } + } + foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); $hash{$key}=$value; @@ -1719,17 +2528,15 @@ sub make_new_child { chomp($what); my $proname=propath($udom,$uname); my $now=time; - unless ($namespace=~/^nohist\_/) { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname/$namespace.hist") - ) { print $hfh "P:$now:$what\n"; } - } my @pairs=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File', "$proname/$namespace.db", &GDBM_WRCREAT(),0640)) { + unless ($namespace=~/^nohist\_/) { + my $hfh; + if ($hfh=IO::File->new(">>$proname/$namespace.hist")) { print $hfh "P:$now:$what\n"; } + } foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); # We could check that we have a number... @@ -1743,12 +2550,12 @@ sub make_new_child { } else { print $client "error: ".($!+0) ." untie(GDBM) failed ". - "while attempting put\n"; + "while attempting inc\n"; } } else { print $client "error: ".($!) ." tie(GDBM) Failed ". - "while attempting put\n"; + "while attempting inc\n"; } } else { print $client "refused\n"; @@ -1771,17 +2578,16 @@ sub make_new_child { chomp($what); my $proname=propath($udom,$uname); my $now=time; - { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname/$namespace.hist") - ) { - print $hfh "P:$now:$exedom:$exeuser:$what\n"; - } - } my @pairs=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_WRCREAT(),0640)) { + { + my $hfh; + if ($hfh=IO::File->new(">>$proname/$namespace.hist")) { + print $hfh "P:$now:$exedom:$exeuser:$what\n"; + } + } + foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); &ManagePermissions($key, $udom, $uname, @@ -1822,17 +2628,15 @@ sub make_new_child { chomp($what); my $proname=propath($udom,$uname); my $now=time; - { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname/$namespace.hist") - ) { - print $hfh "D:$now:$exedom:$exeuser:$what\n"; - } - } my @rolekeys=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_WRCREAT(),0640)) { + { + my $hfh; + if ($hfh=IO::File->new(">>$proname/$namespace.hist")) { + print $hfh "D:$now:$exedom:$exeuser:$what\n"; + } + } foreach my $key (@rolekeys) { delete $hash{$key}; } @@ -1949,17 +2753,15 @@ sub make_new_child { chomp($what); my $proname=propath($udom,$uname); my $now=time; - unless ($namespace=~/^nohist\_/) { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname/$namespace.hist") - ) { print $hfh "D:$now:$what\n"; } - } my @keys=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_WRCREAT(),0640)) { + unless ($namespace=~/^nohist\_/) { + my $hfh; + if ($hfh=IO::File->new(">>$proname/$namespace.hist")) { print $hfh "D:$now:$what\n"; } + } foreach my $key (@keys) { - delete($hash{&unescape($key)}); + delete($hash{$key}); } if (untie(%hash)) { print $client "ok\n"; @@ -2074,7 +2876,6 @@ sub make_new_child { my $proname=propath($udom,$uname); my %hash; if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_READER(),0640)) { - study($regexp); while (my ($key,$value) = each(%hash)) { if ($regexp eq '.') { $qresult.=$key.'='.$value.'&'; @@ -2113,15 +2914,15 @@ sub make_new_child { chomp($what); my $proname=propath($udom,$uname); my $now=time; - unless ($namespace=~/^nohist\_/) { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname/$namespace.hist") - ) { print $hfh "P:$now:$rid:$what\n"; } - } my @pairs=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_WRCREAT(),0640)) { + unless ($namespace=~/^nohist\_/) { + my $hfh; + if ($hfh=IO::File->new(">>$proname/$namespace.hist")) { + print $hfh "P:$now:$rid:$what\n"; + } + } my @previouskeys=split(/&/,$hash{"keys:$rid"}); my $key; $hash{"version:$rid"}++; @@ -2222,7 +3023,7 @@ sub make_new_child { } # ------------------------------------------------------------------- querysend } elsif ($userinput =~ /^querysend/) { - if(isClient) { + if (isClient) { my ($cmd,$query, $arg1,$arg2,$arg3)=split(/\:/,$userinput); $query=~s/\n*$//g; @@ -2270,8 +3071,8 @@ sub make_new_child { my %hash; if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_WRCREAT(),0640)) { foreach my $pair (@pairs) { - my ($key,$value)=split(/=/,$pair); - $hash{$key}=$value.':'.$now; + my ($key,$descr,$inst_code)=split(/=/,$pair); + $hash{$key}=$descr.':'.$inst_code.':'.$now; } if (untie(%hash)) { print $client "ok\n"; @@ -2306,14 +3107,19 @@ sub make_new_child { my %hash; if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_READER(),0640)) { while (my ($key,$value) = each(%hash)) { - my ($descr,$lasttime)=split(/\:/,$value); + my ($descr,$lasttime,$inst_code); + if ($value =~ m/^([^\:]*):([^\:]*):(\d+)$/) { + ($descr,$inst_code,$lasttime)=($1,$2,$3); + } else { + ($descr,$lasttime) = split(/\:/,$value); + } if ($lasttime<$since) { next; } if ($description eq '.') { - $qresult.=$key.'='.$descr.'&'; + $qresult.=$key.'='.$descr.':'.$inst_code.'&'; } else { my $unescapeVal = &unescape($descr); - if (eval('$unescapeVal=~/$description/i')) { - $qresult.="$key=$descr&"; + if (eval('$unescapeVal=~/\Q$description\E/i')) { + $qresult.=$key.'='.$descr.':'.$inst_code.'&'; } } } @@ -2342,15 +3148,15 @@ sub make_new_child { $udom=~s/\W//g; my $proname="$perlvar{'lonUsersDir'}/$udom/ids"; my $now=time; - { - my $hfh; - if ( - $hfh=IO::File->new(">>$proname.hist") - ) { print $hfh "P:$now:$what\n"; } - } my @pairs=split(/\&/,$what); my %hash; if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_WRCREAT(),0640)) { + { + my $hfh; + if ($hfh=IO::File->new(">>$proname.hist")) { + print $hfh "P:$now:$what\n"; + } + } foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); $hash{$key}=$value; @@ -2467,9 +3273,29 @@ sub make_new_child { Reply($client, "refused\n", $userinput); } +# ----------------------------------------- portfolio directory list (portls) + } elsif ($userinput =~ /^portls/) { + if(isClient) { + my ($cmd,$uname,$udom)=split(/:/,$userinput); + my $udir=propath($udom,$uname).'/userfiles/portfolio'; + my $dirLine=''; + my $dirContents=''; + if (opendir(LSDIR,$udir.'/')){ + while ($dirLine = readdir(LSDIR)){ + $dirContents = $dirContents.$dirLine.'
'; + } + } else { + $dirContents = "No directory found\n"; + } + print $client $dirContents."\n"; + } else { + Reply($client, "refused\n", $userinput); + } # -------------------------------------------------------------------------- ls } elsif ($userinput =~ /^ls/) { if(isClient) { + my $obs; + my $rights; my ($cmd,$ulsdir)=split(/:/,$userinput); my $ulsout=''; my $ulsfn; @@ -2477,9 +3303,22 @@ sub make_new_child { if(-d $ulsdir) { if (opendir(LSDIR,$ulsdir)) { while ($ulsfn=readdir(LSDIR)) { + undef $obs, $rights; my @ulsstats=stat($ulsdir.'/'.$ulsfn); - $ulsout.=$ulsfn.'&'. - join('&',@ulsstats).':'; + #We do some obsolete checking here + if(-e $ulsdir.'/'.$ulsfn.".meta") { + open(FILE, $ulsdir.'/'.$ulsfn.".meta"); + my @obsolete=; + foreach my $obsolete (@obsolete) { + if($obsolete =~ m|()(on)|) { $obs = 1; } + if($obsolete =~ m|()(default)|) { $rights = 1; } + } + } + $ulsout.=$ulsfn.'&'.join('&',@ulsstats); + if($obs eq '1') { $ulsout.="&1"; } + else { $ulsout.="&0"; } + if($rights eq '1') { $ulsout.="&1:"; } + else { $ulsout.="&0:"; } } closedir(LSDIR); } @@ -2538,6 +3377,105 @@ sub make_new_child { } else { print $client "refused\n"; } +#------------------------------- is auto-enrollment enabled? + } elsif ($userinput =~/^autorun:/) { + if (isClient) { + my ($cmd,$cdom) = split(/:/,$userinput); + my $outcome = &localenroll::run($cdom); + print $client "$outcome\n"; + } else { + print $client "0\n"; + } +#------------------------------- get official sections (for auto-enrollment). + } elsif ($userinput =~/^autogetsections:/) { + if (isClient) { + my ($cmd,$coursecode,$cdom)=split(/:/,$userinput); + my @secs = &localenroll::get_sections($coursecode,$cdom); + my $seclist = &escape(join(':',@secs)); + print $client "$seclist\n"; + } else { + print $client "refused\n"; + } +#----------------------- validate owner of new course section (for auto-enrollment). + } elsif ($userinput =~/^autonewcourse:/) { + if (isClient) { + my ($cmd,$inst_course_id,$owner,$cdom)=split(/:/,$userinput); + my $outcome = &localenroll::new_course($inst_course_id,$owner,$cdom); + print $client "$outcome\n"; + } else { + print $client "refused\n"; + } +#-------------- validate course section in schedule of classes (for auto-enrollment). + } elsif ($userinput =~/^autovalidatecourse:/) { + if (isClient) { + my ($cmd,$inst_course_id,$cdom)=split(/:/,$userinput); + my $outcome=&localenroll::validate_courseID($inst_course_id,$cdom); + print $client "$outcome\n"; + } else { + print $client "refused\n"; + } +#--------------------------- create password for new user (for auto-enrollment). + } elsif ($userinput =~/^autocreatepassword:/) { + if (isClient) { + my ($cmd,$authparam,$cdom)=split(/:/,$userinput); + my ($create_passwd,$authchk); + ($authparam,$create_passwd,$authchk) = &localenroll::create_password($authparam,$cdom); + print $client &escape($authparam.':'.$create_passwd.':'.$authchk)."\n"; + } else { + print $client "refused\n"; + } +#--------------------------- read and remove temporary files (for auto-enrollment). + } elsif ($userinput =~/^autoretrieve:/) { + if (isClient) { + my ($cmd,$filename) = split(/:/,$userinput); + my $source = $perlvar{'lonDaemons'}.'/tmp/'.$filename; + if ( (-e $source) && ($filename ne '') ) { + my $reply = ''; + if (open(my $fh,$source)) { + while (<$fh>) { + chomp($_); + $_ =~ s/^\s+//g; + $_ =~ s/\s+$//g; + $reply .= $_; + } + close($fh); + print $client &escape($reply)."\n"; +# unlink($source); + } else { + print $client "error\n"; + } + } else { + print $client "error\n"; + } + } else { + print $client "refused\n"; + } +#--------------------- read and retrieve institutional code format (for support form). + } elsif ($userinput =~/^autoinstcodeformat:/) { + if (isClient) { + my $reply; + my($cmd,$cdom,$course) = split(/:/,$userinput); + my @pairs = split/\&/,$course; + my %instcodes = (); + my %codes = (); + my @codetitles = (); + my %cat_titles = (); + my %cat_order = (); + foreach (@pairs) { + my ($key,$value) = split/=/,$_; + $instcodes{&unescape($key)} = &unescape($value); + } + my $formatreply = &localenroll::instcode_format($cdom,\%instcodes,\%codes,\@codetitles,\%cat_titles,\%cat_order); + if ($formatreply eq 'ok') { + my $codes_str = &hash2str(%codes); + my $codetitles_str = &array2str(@codetitles); + my $cat_titles_str = &hash2str(%cat_titles); + my $cat_order_str = &hash2str(%cat_order); + print $client $codes_str.':'.$codetitles_str.':'.$cat_titles_str.':'.$cat_order_str."\n"; + } + } else { + print $client "refused\n"; + } # ------------------------------------------------------------- unknown command } else { @@ -2546,20 +3484,20 @@ sub make_new_child { } # -------------------------------------------------------------------- complete alarm(0); - &status('Listening to '.$clientname); + &status('Listening to '.$clientname." ($keymode)"); } # --------------------------------------------- client unknown or fishy, refuse } else { print $client "refused\n"; $client->close(); - &logthis("WARNING: " + &logthis("WARNING: " ."Rejected client $clientip, closing connection"); } } # ============================================================================= - &logthis("CRITICAL: " + &logthis("CRITICAL: " ."Disconnect from $clientip ($clientname)"); @@ -2584,13 +3522,10 @@ sub make_new_child { # sub ManagePermissions { - my $request = shift; - my $domain = shift; - my $user = shift; - my $authtype= shift; + + my ($request, $domain, $user, $authtype) = @_; # See if the request is of the form /$domain/_au - &logthis("ruequest is $request"); if($request =~ /^(\/$domain\/_au)$/) { # It's an author rolesput... my $execdir = $perlvar{'lonDaemons'}; my $userhome= "/home/$user" ; @@ -2605,8 +3540,8 @@ sub ManagePermissions # sub GetAuthType { - my $domain = shift; - my $user = shift; + + my ($domain, $user) = @_; Debug("GetAuthType( $domain, $user ) \n"); my $proname = &propath($domain, $user); @@ -2715,17 +3650,36 @@ sub chatadd { sub unsub { my ($fname,$clientip)=@_; my $result; + my $unsubs = 0; # Number of successful unsubscribes: + + + # An old way subscriptions were handled was to have a + # subscription marker file: + + Debug("Attempting unlink of $fname.$clientname"); if (unlink("$fname.$clientname")) { - $result="ok\n"; - } else { - $result="not_subscribed\n"; - } + $unsubs++; # Successful unsub via marker file. + } + + # The more modern way to do it is to have a subscription list + # file: + if (-e "$fname.subscription") { my $found=&addline($fname,$clientname,$clientip,''); - if ($found) { $result="ok\n"; } + if ($found) { + $unsubs++; + } + } + + # If either or both of these mechanisms succeeded in unsubscribing a + # resource we can return ok: + + if($unsubs) { + $result = "ok\n"; } else { - if ($result != "ok\n") { $result="not_subscribed\n"; } + $result = "not_subscribed\n"; } + return $result; } @@ -2847,6 +3801,16 @@ sub make_passwd_file { } } elsif ($umode eq 'unix') { { + # + # Don't allow the creation of privileged accounts!!! that would + # be real bad!!! + # + my $uid = getpwnam($uname); + if((defined $uid) && ($uid == 0)) { + &logthis(">>>Attempted to create privilged account blocked"); + return "no_priv_account_error\n"; + } + my $execpath="$perlvar{'lonDaemons'}/"."lcuseradd"; { &Debug("Executing external: ".$execpath); @@ -2879,7 +3843,7 @@ sub sethost { my (undef,$hostid)=split(/:/,$remotereq); if (!defined($hostid)) { $hostid=$perlvar{'lonHostID'}; } if ($hostip{$perlvar{'lonHostID'}} eq $hostip{$hostid}) { - $currenthostid=$hostid; + $currenthostid =$hostid; $currentdomainid=$hostdom{$hostid}; &logthis("Setting hostid to $hostid, and domain to $currentdomainid"); } else { @@ -2919,6 +3883,74 @@ sub userload { return $userloadpercent; } +# Routines for serializing arrays and hashes (copies from lonnet) + +sub array2str { + my (@array) = @_; + my $result=&arrayref2str(\@array); + $result=~s/^__ARRAY_REF__//; + $result=~s/__END_ARRAY_REF__$//; + return $result; +} + +sub arrayref2str { + my ($arrayref) = @_; + my $result='__ARRAY_REF__'; + foreach my $elem (@$arrayref) { + if(ref($elem) eq 'ARRAY') { + $result.=&arrayref2str($elem).'&'; + } elsif(ref($elem) eq 'HASH') { + $result.=&hashref2str($elem).'&'; + } elsif(ref($elem)) { + #print("Got a ref of ".(ref($elem))." skipping."); + } else { + $result.=&escape($elem).'&'; + } + } + $result=~s/\&$//; + $result .= '__END_ARRAY_REF__'; + return $result; +} + +sub hash2str { + my (%hash) = @_; + my $result=&hashref2str(\%hash); + $result=~s/^__HASH_REF__//; + $result=~s/__END_HASH_REF__$//; + return $result; +} + +sub hashref2str { + my ($hashref)=@_; + my $result='__HASH_REF__'; + foreach (sort(keys(%$hashref))) { + if (ref($_) eq 'ARRAY') { + $result.=&arrayref2str($_).'='; + } elsif (ref($_) eq 'HASH') { + $result.=&hashref2str($_).'='; + } elsif (ref($_)) { + $result.='='; + #print("Got a ref of ".(ref($_))." skipping."); + } else { + if ($_) {$result.=&escape($_).'=';} else { last; } + } + + if(ref($hashref->{$_}) eq 'ARRAY') { + $result.=&arrayref2str($hashref->{$_}).'&'; + } elsif(ref($hashref->{$_}) eq 'HASH') { + $result.=&hashref2str($hashref->{$_}).'&'; + } elsif(ref($hashref->{$_})) { + $result.='&'; + #print("Got a ref of ".(ref($hashref->{$_}))." skipping."); + } else { + $result.=&escape($hashref->{$_}).'&'; + } + } + $result=~s/\&$//; + $result .= '__END_HASH_REF__'; + return $result; +} + # ----------------------------------- POD (plain old documentation, CPAN style) =head1 NAME