--- loncom/lond 2004/02/24 16:52:16 1.178.2.5 +++ loncom/lond 2007/01/19 02:58:17 1.357 @@ -2,7 +2,7 @@ # The LearningOnline Network # lond "LON Daemon" Server (port "LOND" 5663) # -# $Id: lond,v 1.178.2.5 2004/02/24 16:52:16 albertel Exp $ +# $Id: lond,v 1.357 2007/01/19 02:58:17 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -20,7 +20,7 @@ # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # @@ -31,42 +31,50 @@ use strict; use lib '/home/httpd/lib/perl/'; +use LONCAPA; use LONCAPA::Configuration; use IO::Socket; use IO::File; #use Apache::File; -use Symbol; use POSIX; use Crypt::IDEA; use LWP::UserAgent(); +use Digest::MD5 qw(md5_hex); use GDBM_File; use Authen::Krb4; use Authen::Krb5; -use lib '/home/httpd/lib/perl/'; use localauth; +use localenroll; +use localstudentphoto; use File::Copy; +use File::Find; use LONCAPA::ConfigFileEdit; +use LONCAPA::lonlocal; +use LONCAPA::lonssl; +use Fcntl qw(:flock); -my $DEBUG = 1; # Non zero to enable debug log entries. +my $DEBUG = 0; # Non zero to enable debug log entries. my $status=''; my $lastlog=''; -my $VERSION='$Revision: 1.178.2.5 $'; #' stupid emacs +my $VERSION='$Revision: 1.357 $'; #' stupid emacs my $remoteVERSION; -my $currenthostid; +my $currenthostid="default"; my $currentdomainid; my $client; -my $clientip; -my $clientname; - -my $cipher; # Cipher key negotiated with client. -my $tmpsnum = 0;; # Id of tmpputs. +my $clientip; # IP address 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: @@ -77,9 +85,11 @@ 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 %hostname; # DNSname -> ID's mapping. +my %hostip; # IPs for hosts in cluster. +my %hostdns; # ID's of hosts looked up by DNS name. my %managers; # Ip -> manager names @@ -98,23 +108,26 @@ 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. my @passwderrors = ("ok", - "lcpasswd must be run as user 'www'", - "lcpasswd got incorrect number of arguments", - "lcpasswd did not get the right nubmer of input text lines", - "lcpasswd too many simultaneous pwd changes in progress", - "lcpasswd User does not exist.", - "lcpasswd Incorrect current passwd", - "lcpasswd Unable to su to root.", - "lcpasswd Cannot set new passwd.", - "lcpasswd Username has invalid characters", - "lcpasswd Invalid characters in password", - "11", "12", - "lcpasswd Password mismatch"); + "pwchange_failure - lcpasswd must be run as user 'www'", + "pwchange_failure - lcpasswd got incorrect number of arguments", + "pwchange_failure - lcpasswd did not get the right nubmer of input text lines", + "pwchange_failure - lcpasswd too many simultaneous pwd changes in progress", + "pwchange_failure - lcpasswd User does not exist.", + "pwchange_failure - lcpasswd Incorrect current passwd", + "pwchange_failure - lcpasswd Unable to su to root.", + "pwchange_failure - lcpasswd Cannot set new passwd.", + "pwchange_failure - lcpasswd Username has invalid characters", + "pwchange_failure - lcpasswd Invalid characters in password", + "pwchange_failure - lcpasswd User already exists", + "pwchange_failure - lcpasswd Something went wrong with user addition.", + "pwchange_failure - lcpasswd Password mismatch", + "pwchange_failure - lcpasswd Error filename is invalid"); # The array below are lcuseradd error strings.: @@ -135,11 +148,13 @@ my @adderrors = ("ok", "lcuseradd Could not add user.", "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. +my $Transactions = 0; # Number of attempted transactions. +my $Failures = 0; # Number of transcations failed. # ResetStatistics: # Resets the statistics counters: @@ -149,6 +164,227 @@ sub ResetStatistics { $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: +# $thisserver - Our DNS name. +# +# Returns: +# IDEA session key on success. +# undef on failure. +# +sub LocalConnection { + my ($Socket, $initcmd) = @_; + Debug("Attempting local connection: $initcmd client: $clientip me: $thisserver"); + if($clientip ne "127.0.0.1") { + &logthis(' LocalConnection rejecting non local: ' + ."$clientip 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; + } + + +} +# +# Safely execute a command (as long as it's not a shel command and doesn +# not require/rely on shell escapes. The function operates by doing a +# a pipe based fork and capturing stdout and stderr from the pipe. +# +# Formal Parameters: +# $line - A line of text to be executed as a command. +# Returns: +# The output from that command. If the output is multiline the caller +# must know how to split up the output. +# +# +sub execute_command { + my ($line) = @_; + my @words = split(/\s/, $line); # Bust the command up into words. + my $output = ""; + + my $pid = open(CHILD, "-|"); + + if($pid) { # Parent process + Debug("In parent process for execute_command"); + my @data = ; # Read the child's outupt... + close CHILD; + foreach my $output_line (@data) { + Debug("Adding $output_line"); + $output .= $output_line; # Presumably has a \n on it. + } + + } else { # Child process + close (STDERR); + open (STDERR, ">&STDOUT");# Combine stderr, and stdout... + exec(@words); # won't return. + } + return $output; +} + + +# GetCertificate: Given a transaction that requires a certificate, +# this function will extract the certificate from the transaction +# request. Note that at this point, the only concept of a certificate +# is the hostname to which we are connected. +# +# Parameter: +# request - The request sent by our client (this parameterization may +# need to change when we really use a certificate granting +# authority. +# +sub GetCertificate { + my $request = shift; + + return $clientip; +} + # # Return true if client is a manager. # @@ -164,110 +400,595 @@ sub isClient { # -# 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 GetRequest { - my $input = <$client>; - chomp($input); +# ReadManagerTable: Reads in the current manager table. For now this is +# done on each manager authentication because: +# - These authentications are not frequent +# - This allows dynamic changes to the manager table +# without the need to signal to the lond. +# +sub ReadManagerTable { - Debug("Request = $input\n"); + # Clean out the old table first.. - &status('Processing '.$clientname.':'.$input); + 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); + + 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 + } + } +} - return $input; +# +# ValidManager: Determines if a given certificate represents a valid manager. +# in this primitive implementation, the 'certificate' is +# just the connecting loncapa client name. This is checked +# against a valid client list in the configuration. +# +# +sub ValidManager { + my $certificate = shift; + + return isManager; } # -# Decipher encoded traffic +# CopyFile: Called as part of the process of installing a +# new configuration file. This function copies an existing +# file to a backup file. +# Parameters: +# oldfile - Name of the file to backup. +# newfile - Name of the backup file. +# Return: +# 0 - Failure (errno has failure reason). +# 1 - Success. +# +sub CopyFile { + + my ($oldfile, $newfile) = @_; + + if (! copy($oldfile,$newfile)) { + return 0; + } + chmod(0660, $newfile); + return 1; +} +# +# Host files are passed out with externally visible host IPs. +# If, for example, we are behind a fire-wall or NAT host, our +# internally visible IP may be different than the externally +# visible IP. Therefore, we always adjust the contents of the +# host file so that the entry for ME is the IP that we believe +# we have. At present, this is defined as the entry that +# DNS has for us. If by some chance we are not able to get a +# DNS translation for us, then we assume that the host.tab file +# is correct. +# BUGBUGBUG - in the future, we really should see if we can +# easily query the interface(s) instead. +# Parameter(s): +# contents - The contents of the host.tab to check. +# Returns: +# newcontents - The adjusted contents. +# +# +sub AdjustHostContents { + my $contents = shift; + my $adjusted; + my $me = $perlvar{'lonHostID'}; + + 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) { + 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"; + + } 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; +} +# +# InstallFile: Called to install an administrative file: +# - The file is created with .tmp +# - The .tmp file is then mv'd to +# This lugubrious procedure is done to ensure that we are never without +# a valid, even if dated, version of the file regardless of who crashes +# and when the crash occurs. +# # Parameters: -# input - Encoded data. +# Name of the file +# File Contents. +# Return: +# nonzero - success. +# 0 - failure and $! has an errno. +# +sub InstallFile { + + my ($Filename, $Contents) = @_; + my $TempFile = $Filename.".tmp"; + + # Open the file for write: + + my $fh = IO::File->new("> $TempFile"); # Write to temp. + if(!(defined $fh)) { + &logthis(' Unable to create '.$TempFile.""); + return 0; + } + # write the contents of the file: + + print $fh ($Contents); + $fh->close; # In case we ever have a filesystem w. locking + + chmod(0660, $TempFile); + + # Now we can move install the file in position. + + move($TempFile, $Filename); + + 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: -# Decoded data or undef if encryption key was not yet negotiated. -# Implicit input: -# cipher - This global holds the negotiated encryption key. +# Full path to the file or undef if the selector is invalid. # -sub Decipher { - my $input = shift; - 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); +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. +# - Backup the old file to +# - Separate the contents of the new file out from the +# rest of the request. +# - Write the new file. +# Parameter: +# Request - The entire user request. This consists of a : separated +# string pushfile:tablename:contents. +# NOTE: The contents may have :'s in it as well making things a bit +# more interesting... but not much. +# Returns: +# String to send to client ("ok" or "refused" if bad file). +# +sub PushFile { + my $request = shift; + my ($command, $filename, $contents) = split(":", $request, 3); + + # At this point in time, pushes for only the following tables are + # supported: + # hosts.tab ($filename eq host). + # domain.tab ($filename eq domain). + # Construct the destination filename or reject the request. + # + # lonManage is supposed to ensure this, however this session could be + # part of some elaborate spoof that managed somehow to authenticate. + # + + + my $tablefile = ConfigFileFromSelector($filename); + if(! (defined $tablefile)) { + return "refused"; + } + # + # >copy< the old table to the backup table + # don't rename in case system crashes/reboots etc. in the time + # window between a rename and write. + # + my $backupfile = $tablefile; + $backupfile =~ s/\.tab$/.old/; + if(!CopyFile($tablefile, $backupfile)) { + &logthis(' CopyFile from '.$tablefile." to ".$backupfile." failed "); + return "error:$!"; + } + &logthis(' Pushfile: backed up ' + .$tablefile." to $backupfile"); + + # If the file being pushed is the host file, we adjust the entry for ourself so that the + # IP will be our current IP as looked up in dns. Note this is only 99% good as it's possible + # to conceive of conditions where we don't have a DNS entry locally. This is possible in a + # network sense but it doesn't make much sense in a LonCAPA sense so we ignore (for now) + # that possibilty. + + if($filename eq "host") { + $contents = AdjustHostContents($contents); + } + + # Install the new file: + + if(!InstallFile($tablefile, $contents)) { + &logthis(' Pushfile: unable to install ' + .$tablefile." $! "); + return "error:$!"; + } else { + &logthis(' Installed new '.$tablefile + .""); + + } + + + # Indicate success: + + return "ok"; + } # -# 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. +# Called to re-init either lonc or lond. +# +# Parameters: +# request - The full request by the client. This is of the form +# reinit: +# where is allowed to be either of +# lonc or lond +# +# Returns: +# The string to be sent back to the client either: +# ok - Everything worked just fine. +# error:why - There was a failure and why describes the reason. +# +# +sub ReinitProcess { + my $request = shift; + + + # separate the request (reinit) from the process identifier and + # validate it producing the name of the .pid file for the process. + # + # + my ($junk, $process) = split(":", $request); + my $processpidfile = $perlvar{'lonDaemons'}.'/logs/'; + if($process eq 'lonc') { + $processpidfile = $processpidfile."lonc.pid"; + if (!open(PIDFILE, "< $processpidfile")) { + return "error:Open failed for $processpidfile"; + } + my $loncpid = ; + close(PIDFILE); + logthis(' Reinitializing lonc pid='.$loncpid + .""); + kill("USR2", $loncpid); + } elsif ($process eq 'lond') { + logthis(' Reinitializing self (lond) '); + &UpdateHosts; # Lond is us!! + } else { + &logthis('"); + return "error:Invalid process identifier $process"; + } + 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: -# $RequestName - 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 -# $MustEncode - True if the request must be encoded to be good. -# $ClientOk - True if it's ok for a client to request this. -# $ManagerOk - 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. +# scriptline - A line to validate (\n has been stripped for what it's worth). # -sub RegisterHandler { - my $RequestName = shift; - my $Procedure = shift; - my $MustEncode = shift; - my $ClientOk = shift; - my $ManagerOk = shift; - - # Don't allow duplication# - - if (defined $Dispatcher{$RequestName}) { - die "Attempting to define a duplicate request handler for $RequestName\n"; +# 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. } - # Build the client type mask: + 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,$maxcon,$idleto,$mincon) = split(/:/,$ConfigLine); + # + # 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, $maxcon, $idleto, $mincon) { + $newConfigLine .= ":".$item; + } + # Replace the line: + + $editor->ReplaceLine($id, $newConfigLine); - my $ClientTypeMask = 0; - if($ClientOk) { - $ClientTypeMask |= $CLIENT_OK; +} +# +# 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 ($cmd, $filetype, $script) = split(/:/, $request,3); # : in script + + # Check the pre-coditions for success: + + if($cmd != "edit") { # Something is amiss afoot alack. + return "error:edit request detected, but request != 'edit'\n"; } - if($ManagerOk) { - $ClientTypeMask |= $MANAGER_OK; + if( ($filetype ne "hosts") && + ($filetype ne "domain")) { + return "error:edit requested with invalid file specifier: $filetype \n"; } - - # Enter the hash: - - my @entry = ($Procedure, $MustEncode, $ClientTypeMask); - - $Dispatcher{$RequestName} = \@entry; - - + + # 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"; } +# read_profile +# +# Returns a set of specific entries from a user's profile file. +# this is a utility function that is used by both get_profile_entry and +# get_profile_entry_encrypted. +# +# Parameters: +# udom - Domain in which the user exists. +# uname - User's account name (loncapa account) +# namespace - The profile namespace to open. +# what - A set of & separated queries. +# Returns: +# If all ok: - The string that needs to be shipped back to the user. +# If failure - A string that starts with error: followed by the failure +# reason.. note that this probabyl gets shipped back to the +# user as well. +# +sub read_profile { + my ($udom, $uname, $namespace, $what) = @_; + + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_READER()); + if ($hashref) { + my @queries=split(/\&/,$what); + my $qresult=''; + + for (my $i=0;$i<=$#queries;$i++) { + $qresult.="$hashref->{$queries[$i]}&"; # Presumably failure gives empty string. + } + $qresult=~s/\&$//; # Remove trailing & from last lookup. + if (&untie_user_hash($hashref)) { + return $qresult; + } else { + return "error: ".($!+0)." untie (GDBM) Failed"; + } + } else { + if ($!+0 == 2) { + return "error:No such file or GDBM reported bad block error"; + } else { + return "error: ".($!+0)." tie (GDBM) Failed"; + } + } + +} #--------------------- Request Handlers -------------------------------------------- # -# By convention each request handler registers itself prior to the sub declaration: +# By convention each request handler registers itself prior to the sub +# declaration: # +#++ +# # Handles ping requests. # Parameters: # $cmd - the actual keyword that invoked us. @@ -281,19 +1002,21 @@ sub RegisterHandler { # 0 - Program should exit. # Side effects: # Reply information is sent to the client. - -sub PingHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub ping_handler { + my ($cmd, $tail, $client) = @_; + Debug("$cmd $tail $client .. $currenthostid:"); Reply( $client,"$currenthostid\n","$cmd:$tail"); return 1; } -RegisterHandler("ping", \&PingHandler, 0, 1, 1); # Ping unencoded, client or manager. +®ister_handler("ping", \&ping_handler, 0, 1, 1); # Ping unencoded, client or manager. + +#++ +# +# Handles pong requests. Pong replies with our current host id, and +# the results of a ping sent to us via our lonc. # -# Handles pong reequests: # Parameters: # $cmd - the actual keyword that invoked us. # $tail - the tail of the request that invoked us. @@ -306,22 +1029,24 @@ RegisterHandler("ping", \&PingHandler, 0 # 0 - Program should exit. # Side effects: # Reply information is sent to the client. - -sub PongHandler { - my $cmd = shift; - my $tail = shift; - my $replyfd = shift; +sub pong_handler { + my ($cmd, $tail, $replyfd) = @_; my $reply=&reply("ping",$clientname); - Reply( $replyfd, "$currenthostid:$reply\n", "$cmd:$tail"); + &Reply( $replyfd, "$currenthostid:$reply\n", "$cmd:$tail"); return 1; } -RegisterHandler("pong", \&PongHandler, 0, 1, 1); # Pong unencoded, client or manager +®ister_handler("pong", \&pong_handler, 0, 1, 1); # Pong unencoded, client or manager -# -# EstablishKeyHandler: +#++ # Called to establish an encrypted session key with the remote client. -# +# Note that with secure lond, in most cases this function is never +# invoked. Instead, the secure session key is established either +# via a local file that's locked down tight and only lives for a short +# time, or via an ssl tunnel...and is generated from a bunch-o-random +# bits from /dev/urandom, rather than the predictable pattern used by +# by this sub. This sub is only used in the old-style insecure +# key negotiation. # Parameters: # $cmd - the actual keyword that invoked us. # $tail - the tail of the request that invoked us. @@ -337,10 +1062,8 @@ RegisterHandler("pong", \&PongHandler, 0 # Reply information is sent to the client. # $cipher is set with a reference to a new IDEA encryption object. # -sub EstablishKeyHandler { - my $cmd = shift; - my $tail = shift; - my $replyfd = shift; +sub establish_key_handler { + my ($cmd, $tail, $replyfd) = @_; my $buildkey=time.$$.int(rand 100000); $buildkey=~tr/1-6/A-F/; @@ -353,14 +1076,13 @@ sub EstablishKeyHandler { $key=substr($key,0,32); my $cipherkey=pack("H32",$key); $cipher=new IDEA $cipherkey; - Reply($replyfd, "$buildkey\n", "$cmd:$tail"); + &Reply($replyfd, "$buildkey\n", "$cmd:$tail"); return 1; } -RegisterHandler("ekey", \&EstablishKeyHandler, 0, 1,1); +®ister_handler("ekey", \&establish_key_handler, 0, 1,1); -# LoadHandler: # Handler for the load command. Returns the current system load average # to the requestor. # @@ -377,10 +1099,8 @@ RegisterHandler("ekey", \&EstablishKeyHa # 0 - Program should exit. # Side effects: # Reply information is sent to the client. -sub LoadHandler { - my $cmd = shift; - my $tail = shift; - my $replyfd = shift; +sub load_handler { + my ($cmd, $tail, $replyfd) = @_; # Get the load average from /proc/loadavg and calculate it as a percentage of # the allowed load limit as set by the perl global variable lonLoadLim @@ -393,12 +1113,11 @@ sub LoadHandler { my $loadpercent=100*$loadavg/$perlvar{'lonLoadLim'}; - Reply( $replyfd, "$loadpercent\n", "$cmd:$tail"); + &Reply( $replyfd, "$loadpercent\n", "$cmd:$tail"); return 1; } -RegisterHandler("load", \&LoadHandler, 0, 1, 0); - +®ister_handler("load", \&load_handler, 0, 1, 0); # # Process the userload request. This sub returns to the client the current @@ -420,17 +1139,15 @@ RegisterHandler("load", \&LoadHandler, 0 # Implicit outputs: # the reply is written to the client. # -sub UserLoadHandler { - my $cmd = shift; - my $tail = shift; - my $replyfd = shift; +sub user_load_handler { + my ($cmd, $tail, $replyfd) = @_; my $userloadpercent=&userload(); - Reply($replyfd, "$userloadpercent\n", "$cmd:$tail"); + &Reply($replyfd, "$userloadpercent\n", "$cmd:$tail"); return 1; } -RegisterHandler("userload", \&UserLoadHandler, 0, 1, 0); +®ister_handler("userload", \&user_load_handler, 0, 1, 0); # Process a request for the authorization type of a user: # (userauth). @@ -445,28 +1162,37 @@ RegisterHandler("userload", \&UserLoadHa # Implicit outputs: # The user authorization type is written to the client. # -sub UserAuthorizationType { - my $cmd = shift; - my $tail = shift; - my $replyfd = shift; +sub user_authorization_type { + my ($cmd, $tail, $replyfd) = @_; my $userinput = "$cmd:$tail"; # Pull the domain and username out of the command tail. - # and call GetAuthType to determine the authentication type. + # and call get_auth_type to determine the authentication type. my ($udom,$uname)=split(/:/,$tail); - my $result = GetAuthType($udom, $uname); + my $result = &get_auth_type($udom, $uname); if($result eq "nouser") { - Failure( $replyfd, "unknown_user\n", $userinput); + &Failure( $replyfd, "unknown_user\n", $userinput); } else { - Reply( $replyfd, "$result\n", $userinput); + # + # We only want to pass the second field from get_auth_type + # for ^krb.. otherwise we'll be handing out the encrypted + # password for internals e.g. + # + my ($type,$otherinfo) = split(/:/,$result); + if($type =~ /^krb/) { + $type = $result; + } else { + $type .= ':'; + } + &Reply( $replyfd, "$type\n", $userinput); } return 1; } -RegisterHandler("currentauth", \&UserAuthorizationType, 1, 1, 0); -# +®ister_handler("currentauth", \&user_authorization_type, 1, 1, 0); + # Process a request by a manager to push a hosts or domain table # to us. We pick apart the command and pass it on to the subs # that already exist to do this. @@ -480,11 +1206,8 @@ RegisterHandler("currentauth", \&UserAut # 0 - Program should exit # Implicit Output: # a reply is written to the client. - -sub PushFileHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub push_file_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; @@ -492,22 +1215,206 @@ sub PushFileHandler { # the code below is a hook to do further authentication (e.g. to resolve # spoofing). - my $cert = GetCertificate($userinput); - if(ValidManager($cert)) { + my $cert = &GetCertificate($userinput); + if(&ValidManager($cert)) { # Now presumably we have the bona fides of both the peer host and the # process making the request. - my $reply = PushFile($userinput); - Reply($client, "$reply\n", $userinput); + my $reply = &PushFile($userinput); + &Reply($client, "$reply\n", $userinput); } else { - Failure( $client, "refused\n", $userinput); + &Failure( $client, "refused\n", $userinput); } + return 1; +} +®ister_handler("pushfile", \&push_file_handler, 1, 0, 1); + +# +# du - list the disk usuage of a directory recursively. +# +# note: stolen code from the ls file handler +# under construction by Rick Banghart +# . +# Parameters: +# $cmd - The command that dispatched us (du). +# $ududir - The directory path to list... I'm not sure what this +# is relative as things like ls:. return e.g. +# no_such_dir. +# $client - Socket open on the client. +# Returns: +# 1 - indicating that the daemon should not disconnect. +# Side Effects: +# The reply is written to $client. +# +sub du_handler { + my ($cmd, $ududir, $client) = @_; + ($ududir) = split(/:/,$ududir); # Make 'telnet' testing easier. + my $userinput = "$cmd:$ududir"; + + if ($ududir=~/\.\./ || $ududir!~m|^/home/httpd/|) { + &Failure($client,"refused\n","$cmd:$ududir"); + return 1; + } + # Since $ududir could have some nasties in it, + # we will require that ududir is a valid + # directory. Just in case someone tries to + # slip us a line like .;(cd /home/httpd rm -rf*) + # etc. + # + if (-d $ududir) { + my $total_size=0; + my $code=sub { + if ($_=~/\.\d+\./) { return;} + if ($_=~/\.meta$/) { return;} + $total_size+=(stat($_))[7]; + }; + chdir($ududir); + find($code,$ududir); + $total_size=int($total_size/1024); + &Reply($client,"$total_size\n","$cmd:$ududir"); + } else { + &Failure($client, "bad_directory:$ududir\n","$cmd:$ududir"); + } + return 1; +} +®ister_handler("du", \&du_handler, 0, 1, 0); + +# +# The ls_handler routine should be considered obosolete and is retained +# for communication with legacy servers. Please see the ls2_handler. +# +# ls - list the contents of a directory. For each file in the +# selected directory the filename followed by the full output of +# the stat function is returned. The returned info for each +# file are separated by ':'. The stat fields are separated by &'s. +# Parameters: +# $cmd - The command that dispatched us (ls). +# $ulsdir - The directory path to list... I'm not sure what this +# is relative as things like ls:. return e.g. +# no_such_dir. +# $client - Socket open on the client. +# Returns: +# 1 - indicating that the daemon should not disconnect. +# Side Effects: +# The reply is written to $client. +# +sub ls_handler { + # obsoleted by ls2_handler + my ($cmd, $ulsdir, $client) = @_; + + my $userinput = "$cmd:$ulsdir"; + + my $obs; + my $rights; + my $ulsout=''; + my $ulsfn; + if (-e $ulsdir) { + if(-d $ulsdir) { + if (opendir(LSDIR,$ulsdir)) { + while ($ulsfn=readdir(LSDIR)) { + undef($obs); + undef($rights); + my @ulsstats=stat($ulsdir.'/'.$ulsfn); + #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|1)/) { $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); + } + } else { + my @ulsstats=stat($ulsdir); + $ulsout.=$ulsfn.'&'.join('&',@ulsstats).':'; + } + } else { + $ulsout='no_such_dir'; + } + if ($ulsout eq '') { $ulsout='empty'; } + &Reply($client, "$ulsout\n", $userinput); # This supports debug logging. + + return 1; + } -RegisterHandler("pushfile", \&PushFileHandler, 1, 0, 1); +®ister_handler("ls", \&ls_handler, 0, 1, 0); +# +# Please also see the ls_handler, which this routine obosolets. +# ls2_handler differs from ls_handler in that it escapes its return +# values before concatenating them together with ':'s. +# +# ls2 - list the contents of a directory. For each file in the +# selected directory the filename followed by the full output of +# the stat function is returned. The returned info for each +# file are separated by ':'. The stat fields are separated by &'s. +# Parameters: +# $cmd - The command that dispatched us (ls). +# $ulsdir - The directory path to list... I'm not sure what this +# is relative as things like ls:. return e.g. +# no_such_dir. +# $client - Socket open on the client. +# Returns: +# 1 - indicating that the daemon should not disconnect. +# Side Effects: +# The reply is written to $client. +# +sub ls2_handler { + my ($cmd, $ulsdir, $client) = @_; + my $userinput = "$cmd:$ulsdir"; + + my $obs; + my $rights; + my $ulsout=''; + my $ulsfn; + if (-e $ulsdir) { + if(-d $ulsdir) { + if (opendir(LSDIR,$ulsdir)) { + while ($ulsfn=readdir(LSDIR)) { + undef($obs); + undef($rights); + my @ulsstats=stat($ulsdir.'/'.$ulsfn); + #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|1)/) { $obs = 1; } + if($obsolete =~ m|()(default)|) { + $rights = 1; + } + } + } + my $tmp = $ulsfn.'&'.join('&',@ulsstats); + if ($obs eq '1') { $tmp.="&1"; } else { $tmp.="&0"; } + if ($rights eq '1') { $tmp.="&1"; } else { $tmp.="&0"; } + $ulsout.= &escape($tmp).':'; + } + closedir(LSDIR); + } + } else { + my @ulsstats=stat($ulsdir); + $ulsout.=$ulsfn.'&'.join('&',@ulsstats).':'; + } + } else { + $ulsout='no_such_dir'; + } + if ($ulsout eq '') { $ulsout='empty'; } + &Reply($client, "$ulsout\n", $userinput); # This supports debug logging. + return 1; +} +®ister_handler("ls2", \&ls2_handler, 0, 1, 0); # Process a reinit request. Reinit requests that either # lonc or lond be reinitialized so that an updated @@ -523,25 +1430,22 @@ RegisterHandler("pushfile", \&PushFileHa # Implicit output: # a reply is sent to the client. # -sub ReinitProcessHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub reinit_process_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; - my $cert = GetCertificate($userinput); - if(ValidManager($cert)) { + my $cert = &GetCertificate($userinput); + if(&ValidManager($cert)) { chomp($userinput); - my $reply = ReinitProcess($userinput); - Reply( $client, "$reply\n", $userinput); + my $reply = &ReinitProcess($userinput); + &Reply( $client, "$reply\n", $userinput); } else { - Failure( $client, "refused\n", $userinput); + &Failure( $client, "refused\n", $userinput); } return 1; } - -RegisterHandler("reinit", \&ReinitProcessHandler, 1, 0, 1); +®ister_handler("reinit", \&reinit_process_handler, 1, 0, 1); # Process the editing script for a table edit operation. # the editing operation must be encrypted and requested by @@ -557,35 +1461,32 @@ RegisterHandler("reinit", \&ReinitProces # Implicit output: # a reply is sent to the client. # -sub EditTableHandler { - my $command = shift; - my $tail = shift; - my $client = shift; +sub edit_table_handler { + my ($command, $tail, $client) = @_; my $userinput = "$command:$tail"; - my $cert = GetCertificate($userinput); - if(ValidManager($cert)) { + my $cert = &GetCertificate($userinput); + if(&ValidManager($cert)) { my($filetype, $script) = split(/:/, $tail); if (($filetype eq "hosts") || ($filetype eq "domain")) { if($script ne "") { - Reply($client, # BUGBUG - EditFile - EditFile($userinput), # could fail. + &Reply($client, # BUGBUG - EditFile + &EditFile($userinput), # could fail. $userinput); } else { - Failure($client,"refused\n",$userinput); + &Failure($client,"refused\n",$userinput); } } else { - Failure($client,"refused\n",$userinput); + &Failure($client,"refused\n",$userinput); } } else { - Failure($client,"refused\n",$userinput); + &Failure($client,"refused\n",$userinput); } return 1; } -RegisterHandler("edit", \&EditTableHandler, 1, 0, 1); - +®ister_handler("edit", \&edit_table_handler, 1, 0, 1); # # Authenticate a user against the LonCAPA authentication @@ -611,136 +1512,36 @@ RegisterHandler("edit", \&EditTableHandl # The authentication systems describe above have their own forms of implicit # input into the authentication process that are described above. # -sub AuthenticateHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; - +sub authenticate_handler { + my ($cmd, $tail, $client) = @_; + + # Regenerate the full input line - + my $userinput = $cmd.":".$tail; - + # udom - User's domain. # uname - Username. # upass - User's password. - + my ($udom,$uname,$upass)=split(/:/,$tail); - Debug(" Authenticate domain = $udom, user = $uname, password = $upass"); + &Debug(" Authenticate domain = $udom, user = $uname, password = $upass"); chomp($upass); - $upass=unescape($upass); - my $proname=propath($udom,$uname); - my $passfilename="$proname/passwd"; - - # The user's 'personal' loncapa passworrd file describes how to authenticate: - - if (-e $passfilename) { - Debug("Located password file: $passfilename"); + $upass=&unescape($upass); - my $pf = IO::File->new($passfilename); - my $realpasswd=<$pf>; - chomp($realpasswd); - my ($howpwd,$contentpwd)=split(/:/,$realpasswd); - my $pwdcorrect=0; + my $pwdcorrect = &validate_user($udom, $uname, $upass); + if($pwdcorrect) { + &Reply( $client, "authorized\n", $userinput); # - # Authenticate against password stored in the internal file. - # - Debug("Authenticating via $howpwd"); - if ($howpwd eq 'internal') { - &Debug("Internal auth"); - $pwdcorrect= (crypt($upass,$contentpwd) eq $contentpwd); - # - # Authenticate against the unix password file. - # - } elsif ($howpwd eq 'unix') { - &Debug("Unix auth"); - if((getpwnam($uname))[1] eq "") { #no such user! - $pwdcorrect = 0; - } else { - $contentpwd=(getpwnam($uname))[1]; - my $pwauth_path="/usr/local/sbin/pwauth"; - unless ($contentpwd eq 'x') { - $pwdcorrect= (crypt($upass,$contentpwd) eq $contentpwd); - } elsif (-e $pwauth_path) { - open PWAUTH, "|$pwauth_path" or - die "Cannot invoke authentication"; - print PWAUTH "$uname\n$upass\n"; - close PWAUTH; - $pwdcorrect=!$?; - } - } - # - # Authenticate against a Kerberos 4 server: - # - } elsif ($howpwd eq 'krb4') { - my $null=pack("C",0); - unless ($upass=~/$null/) { - my $krb4_error = &Authen::Krb4::get_pw_in_tkt($uname, - "", - $contentpwd, - 'krbtgt', - $contentpwd, - 1, - $upass); - if (!$krb4_error) { - $pwdcorrect = 1; - } else { - $pwdcorrect=0; - # log error if it is not a bad password - if ($krb4_error != 62) { - &logthis('krb4:'.$uname.','.$contentpwd.','. - &Authen::Krb4::get_err_txt($Authen::Krb4::error)); - } - } - } - # - # Authenticate against a Kerberos 5 server: - # - } elsif ($howpwd eq 'krb5') { - my $null=pack("C",0); - unless ($upass=~/$null/) { - my $krbclient=&Authen::Krb5::parse_name($uname.'@'.$contentpwd); - my $krbservice="krbtgt/".$contentpwd."\@".$contentpwd; - my $krbserver=&Authen::Krb5::parse_name($krbservice); - my $credentials=&Authen::Krb5::cc_default(); - $credentials->initialize($krbclient); - my $krbreturn = &Authen::Krb5::get_in_tkt_with_password($krbclient, - $krbserver, - $upass, - $credentials); - $pwdcorrect = ($krbreturn == 1); - } else { - $pwdcorrect=0; - } - # - # Finally, the user may have written in an authentication module. - # in that case, if requested, authenticate against it. - # - } elsif ($howpwd eq 'localauth') { - $pwdcorrect=&localauth::localauth($uname,$upass,$contentpwd); - } - # - # Successfully authorized. - # - if ($pwdcorrect) { - Reply( $client, "authorized\n", $userinput); - # - # Bad credentials: Failed to authorize - # - } else { - Failure( $client, "non_authorized\n", $userinput); - } - # - # User bad... note it may be bad security practice to - # differntiate to the caller a bad user from a bad - # passwd... since that supplies covert channel information - # (you have a good user but bad password e.g.) to guessers. + # Bad credentials: Failed to authorize # } else { - Failure( $client, "unknown_user\n", $userinput); + &Failure( $client, "non_authorized\n", $userinput); } + return 1; } -RegisterHandler("auth", \&AuthenticateHandler, 1, 1, 0); +®ister_handler("auth", \&authenticate_handler, 1, 1, 0); # # Change a user's password. Note that this function is complicated by @@ -763,11 +1564,9 @@ RegisterHandler("auth", \&AuthenticateHa # Implicit inputs: # The authentication systems describe above have their own forms of implicit # input into the authentication process that are described above. -sub ChangePasswordHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; - +sub change_password_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = $cmd.":".$tail; # Reconstruct client's string. # @@ -775,88 +1574,62 @@ sub ChangePasswordHandler { # uname - Username. # upass - Current password. # npass - New password. + # context - Context in which this was called + # (preferences or reset_by_email). - my ($udom,$uname,$upass,$npass)=split(/:/,$tail); - chomp($npass); + my ($udom,$uname,$upass,$npass,$context)=split(/:/,$tail); + $upass=&unescape($upass); $npass=&unescape($npass); &Debug("Trying to change password for $uname"); - my $proname=propath($udom,$uname); - my $passfilename="$proname/passwd"; - if (-e $passfilename) { - my $realpasswd; - { - my $pf = IO::File->new($passfilename); - $realpasswd=<$pf>; - } - chomp($realpasswd); + + # First require that the user can be authenticated with their + # old password unless context was 'reset_by_email': + + my $validated; + if ($context eq 'reset_by_email') { + $validated = 1; + } else { + $validated = &validate_user($udom, $uname, $upass); + } + if($validated) { + my $realpasswd = &get_auth_type($udom, $uname); # Defined since authd. + my ($howpwd,$contentpwd)=split(/:/,$realpasswd); if ($howpwd eq 'internal') { &Debug("internal auth"); - if (crypt($upass,$contentpwd) eq $contentpwd) { - my $salt=time; - $salt=substr($salt,6,2); - my $ncpass=crypt($npass,$salt); - { - my $pf = IO::File->new(">$passfilename"); - if ($pf) { - print $pf "internal:$ncpass\n"; - &logthis("Result of password change for " - ."$uname: pwchange_success"); - Reply($client, "ok\n", $userinput); - } else { - &logthis("Unable to open $uname passwd " - ."to change password"); - Failure( $client, "non_authorized\n",$userinput); - } - } - } else { - Failure($client, "non_authorized\n", $userinput); - } - } elsif ($howpwd eq 'unix') { - # Unix means we have to access /etc/password - # one way or another. - # First: Make sure the current password is - # correct - &Debug("auth is unix"); - $contentpwd=(getpwnam($uname))[1]; - my $pwdcorrect = "0"; - my $pwauth_path="/usr/local/sbin/pwauth"; - unless ($contentpwd eq 'x') { - $pwdcorrect= (crypt($upass,$contentpwd) eq $contentpwd); - } elsif (-e $pwauth_path) { - open PWAUTH, "|$pwauth_path" or - die "Cannot invoke authentication"; - print PWAUTH "$uname\n$upass\n"; - close PWAUTH; - &Debug("exited pwauth with $? ($uname,$upass) "); - $pwdcorrect=($? == 0); - } - if ($pwdcorrect) { - my $execdir=$perlvar{'lonDaemons'}; - &Debug("Opening lcpasswd pipeline"); - my $pf = IO::File->new("|$execdir/lcpasswd > " - ."$perlvar{'lonDaemons'}" - ."/logs/lcpasswd.log"); - print $pf "$uname\n$npass\n$npass\n"; - close $pf; - my $err = $?; - my $result = ($err>0 ? 'pwchange_failure' : 'ok'); - &logthis("Result of password change for $uname: ". - &lcpasswdstrerror($?)); - Reply($client, "$result\n", $userinput); + my $salt=time; + $salt=substr($salt,6,2); + my $ncpass=crypt($npass,$salt); + if(&rewrite_password_file($udom, $uname, "internal:$ncpass")) { + &logthis("Result of password change for " + ."$uname: pwchange_success"); + &Reply($client, "ok\n", $userinput); } else { - Reply($client, "non_authorized\n", $userinput); + &logthis("Unable to open $uname passwd " + ."to change password"); + &Failure( $client, "non_authorized\n",$userinput); } + } elsif ($howpwd eq 'unix' && $context ne 'reset_by_email') { + my $result = &change_unix_password($uname, $npass); + &logthis("Result of password change for $uname: ". + $result); + &Reply($client, "$result\n", $userinput); } else { - Reply( $client, "auth_mode_error\n", $userinput); + # this just means that the current password mode is not + # one we know how to change (e.g the kerberos auth modes or + # locally written auth handler). + # + &Failure( $client, "auth_mode_error\n", $userinput); } + } else { - Reply( $client, "unknown_user\n", $userinput); + &Failure( $client, "non_authorized\n", $userinput); } + return 1; } -RegisterHandler("passwd", \&ChangePasswordHandler, 1, 1, 0); +®ister_handler("passwd", \&change_password_handler, 1, 1, 0); # # Create a new user. User in this case means a lon-capa user. @@ -874,50 +1647,49 @@ RegisterHandler("passwd", \&ChangePasswo # Implicit inputs: # The authentication systems describe above have their own forms of implicit # input into the authentication process that are described above. -sub AddUserHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; - - my $userinput = $cmd.":".$tail; +sub add_user_handler { + + my ($cmd, $tail, $client) = @_; + - my $oldumask=umask(0077); my ($udom,$uname,$umode,$npass)=split(/:/,$tail); + my $userinput = $cmd.":".$tail; # Reconstruct the full request line. + &Debug("cmd =".$cmd." $udom =".$udom." uname=".$uname); - chomp($npass); - $npass=&unescape($npass); - my $proname=propath($udom,$uname); - my $passfilename="$proname/passwd"; - &Debug("Password file created will be:".$passfilename); - if (-e $passfilename) { - Failure( $client, "already_exists\n", $userinput); - } elsif ($udom ne $currentdomainid) { - Failure($client, "not_right_domain\n", $userinput); - } else { - my @fpparts=split(/\//,$proname); - my $fpnow=$fpparts[0].'/'.$fpparts[1].'/'.$fpparts[2]; - my $fperror=''; - for (my $i=3;$i<=$#fpparts;$i++) { - $fpnow.='/'.$fpparts[$i]; - unless (-e $fpnow) { - unless (mkdir($fpnow,0777)) { - $fperror="error: ".($!+0)." mkdir failed while attempting " - ."makeuser"; - } - } - } - unless ($fperror) { - my $result=&make_passwd_file($uname, $umode,$npass, $passfilename); - Reply($client, $result, $userinput); #BUGBUG - could be fail + + + if($udom eq $currentdomainid) { # Reject new users for other domains... + + my $oldumask=umask(0077); + chomp($npass); + $npass=&unescape($npass); + my $passfilename = &password_path($udom, $uname); + &Debug("Password file created will be:".$passfilename); + if (-e $passfilename) { + &Failure( $client, "already_exists\n", $userinput); } else { - Failure($client, "$fperror\n", $userinput); + my $fperror=''; + if (!&mkpath($passfilename)) { + $fperror="error: ".($!+0)." mkdir failed while attempting " + ."makeuser"; + } + unless ($fperror) { + my $result=&make_passwd_file($uname, $umode,$npass, $passfilename); + &Reply($client, $result, $userinput); #BUGBUG - could be fail + } else { + &Failure($client, "$fperror\n", $userinput); + } } + umask($oldumask); + } else { + &Failure($client, "not_right_domain\n", + $userinput); # Even if we are multihomed. + } - umask($oldumask); return 1; } -RegisterHandler("makeuser", \&AddUserHandler, 1, 1, 0); +®ister_handler("makeuser", \&add_user_handler, 1, 1, 0); # # Change the authentication method of a user. Note that this may @@ -940,29 +1712,70 @@ RegisterHandler("makeuser", \&AddUserHan # Implicit inputs: # The authentication systems describe above have their own forms of implicit # input into the authentication process that are described above. +# NOTE: +# This is also used to change the authentication credential values (e.g. passwd). +# # -sub ChangeAuthenticationHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub change_authentication_handler { + + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; # Reconstruct user input. my ($udom,$uname,$umode,$npass)=split(/:/,$tail); - chomp($npass); &Debug("cmd = ".$cmd." domain= ".$udom."uname =".$uname." umode= ".$umode); - $npass=&unescape($npass); - my $proname=&propath($udom,$uname); - my $passfilename="$proname/passwd"; if ($udom ne $currentdomainid) { - Failure( $client, "not_right_domain\n", $client); + &Failure( $client, "not_right_domain\n", $client); } else { - my $result=&make_passwd_file($uname, $umode,$npass,$passfilename); - Reply($client, $result, $userinput); + + chomp($npass); + + $npass=&unescape($npass); + my $oldauth = &get_auth_type($udom, $uname); # Get old auth info. + my $passfilename = &password_path($udom, $uname); + if ($passfilename) { # Not allowed to create a new user!! + # If just changing the unix passwd. need to arrange to run + # passwd since otherwise make_passwd_file will run + # lcuseradd which fails if an account already exists + # (to prevent an unscrupulous LONCAPA admin from stealing + # an existing account by overwriting it as a LonCAPA account). + + if(($oldauth =~/^unix/) && ($umode eq "unix")) { + my $result = &change_unix_password($uname, $npass); + &logthis("Result of password change for $uname: ".$result); + if ($result eq "ok") { + &Reply($client, "$result\n") + } else { + &Failure($client, "$result\n"); + } + } else { + my $result=&make_passwd_file($uname, $umode,$npass,$passfilename); + # + # If the current auth mode is internal, and the old auth mode was + # unix, or krb*, and the user is an author for this domain, + # re-run manage_permissions for that role in order to be able + # to take ownership of the construction space back to www:www + # + + + if( (($oldauth =~ /^unix/) && ($umode eq "internal")) || + (($oldauth =~ /^internal/) && ($umode eq "unix")) ) { + if(&is_author($udom, $uname)) { + &Debug(" Need to manage author permissions..."); + &manage_permissions("/$udom/_au", $udom, $uname, "$umode:"); + } + } + &Reply($client, $result, $userinput); + } + + + } else { + &Failure($client, "non_authorized\n", $userinput); # Fail the user now. + } } return 1; } -RegisterHandler("changeuserauth", \&ChangeAuthenticationHandler, 1,1, 0); +®ister_handler("changeuserauth", \&change_authentication_handler, 1,1, 0); # # Determines if this is the home server for a user. The home server @@ -980,24 +1793,23 @@ RegisterHandler("changeuserauth", \&Chan # The authentication systems describe above have their own forms of implicit # input into the authentication process that are described above. # -sub IsHomeHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub is_home_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; my ($udom,$uname)=split(/:/,$tail); chomp($uname); - my $proname=propath($udom,$uname); - if (-e $proname) { - Reply( $client, "found\n", $userinput); + my $passfile = &password_filename($udom, $uname); + if($passfile) { + &Reply( $client, "found\n", $userinput); } else { - Failure($client, "not_found\n", $userinput); + &Failure($client, "not_found\n", $userinput); } return 1; } -RegisterHandler("home", \&IsHomeHandler, 0,1,0); +®ister_handler("home", \&is_home_handler, 0,1,0); + # # Process an update request for a resource?? I think what's going on here is # that a resource has been modified that we hold a subscription to. @@ -1020,14 +1832,15 @@ RegisterHandler("home", \&IsHomeHandler, # The authentication systems describe above have their own forms of implicit # input into the authentication process that are described above. # -sub UpdateResourceHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub update_resource_handler { + + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; - my $fname=$tail; + my $fname= $tail; # This allows interactive testing + + my $ownership=ishome($fname); if ($ownership eq 'not_owner') { if (-e $fname) { @@ -1037,7 +1850,9 @@ sub UpdateResourceHandler { my $since=$now-$atime; if ($since>$perlvar{'lonExpire'}) { my $reply=&reply("unsub:$fname","$clientname"); + &devalidate_meta_cache($fname); unlink("$fname"); + unlink("$fname.meta"); } else { my $transname="$fname.in.transfer"; my $remoteurl=&reply("sub:$fname","$clientname"); @@ -1067,21 +1882,43 @@ sub UpdateResourceHandler { alarm(0); } rename($transname,$fname); + &devalidate_meta_cache($fname); } } - Reply( $client, "ok\n", $userinput); + &Reply( $client, "ok\n", $userinput); } else { - Failure($client, "not_found\n", $userinput); + &Failure($client, "not_found\n", $userinput); } } else { - Failure($client, "rejected\n", $userinput); + &Failure($client, "rejected\n", $userinput); } return 1; } -RegisterHandler("update", \&UpdateResourceHandler, 0 ,1, 0); +®ister_handler("update", \&update_resource_handler, 0 ,1, 0); +sub devalidate_meta_cache { + my ($url) = @_; + use Cache::Memcached; + my $memcache = new Cache::Memcached({'servers'=>['127.0.0.1:11211']}); + $url = &declutter($url); + $url =~ s-\.meta$--; + my $id = &escape('meta:'.$url); + $memcache->delete($id); +} + +sub declutter { + my $thisfn=shift; + $thisfn=~s/^\Q$perlvar{'lonDocRoot'}\E//; + $thisfn=~s/^\///; + $thisfn=~s|^adm/wrapper/||; + $thisfn=~s|^adm/coursedocs/showdoc/||; + $thisfn=~s/^res\///; + $thisfn=~s/\?.+$//; + return $thisfn; +} # -# Fetch a user file from a remote server: +# Fetch a user file from a remote server to the user's home directory +# userfiles subdir. # Parameters: # $cmd - The command that got us here. # $tail - Tail of the command (remaining parameters). @@ -1090,25 +1927,34 @@ RegisterHandler("update", \&UpdateResour # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub FetchUserFileHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; - +sub fetch_user_file_handler { + + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; my $fname = $tail; - my ($udom,$uname,$ufile)=split(/\//,$fname); - my $udir=propath($udom,$uname).'/userfiles'; + my ($udom,$uname,$ufile) = ($fname =~ m|^([^/]+)/([^/]+)/(.+)$|); + my $udir=&propath($udom,$uname).'/userfiles'; unless (-e $udir) { mkdir($udir,0770); } + Debug("fetch user file for $fname"); if (-e $udir) { $ufile=~s/^[\.\~]+//; - $ufile=~s/\///g; + + # IF necessary, create the path right down to the file. + # Note that any regular files in the way of this path are + # wiped out to deal with some earlier folly of mine. + + if (!&mkpath($udir.'/'.$ufile)) { + &Failure($client, "unable_to_create\n", $userinput); + } + my $destname=$udir.'/'.$ufile; my $transname=$udir.'/'.$ufile.'.in.transit'; my $remoteurl='http://'.$clientip.'/userfiles/'.$fname; my $response; + Debug("Remote URL : $remoteurl Transfername $transname Destname: $destname"); alarm(120); { my $ua=new LWP::UserAgent; @@ -1120,58 +1966,195 @@ sub FetchUserFileHandler { unlink($transname); my $message=$response->status_line; &logthis("LWP GET: $message for $fname ($remoteurl)"); - Failure($client, "failed\n", $userinput); + &Failure($client, "failed\n", $userinput); } else { + Debug("Renaming $transname to $destname"); if (!rename($transname,$destname)) { &logthis("Unable to move $transname to $destname"); unlink($transname); - Failure($client, "failed\n", $userinput); + &Failure($client, "failed\n", $userinput); } else { - Reply($client, "ok\n", $userinput); + &Reply($client, "ok\n", $userinput); } } } else { - Failure($client, "not_home\n", $userinput); + &Failure($client, "not_home\n", $userinput); } return 1; } -RegisterHandler("fetchuserfile", \&FetchUserFileHandler, 0, 1, 0); +®ister_handler("fetchuserfile", \&fetch_user_file_handler, 0, 1, 0); + # -# Authenticate access to a user file. Question? The token for athentication -# is allowed to be sent as cleartext is this really what we want? This token -# represents the user's session id. Once it is forged does this allow too much access?? +# Remove a file from a user's home directory userfiles subdirectory. +# Parameters: +# cmd - the Lond request keyword that got us here. +# tail - the part of the command past the keyword. +# client- File descriptor connected with the client. +# +# Returns: +# 1 - Continue processing. +sub remove_user_file_handler { + my ($cmd, $tail, $client) = @_; + + my ($fname) = split(/:/, $tail); # Get rid of any tailing :'s lonc may have sent. + + my ($udom,$uname,$ufile) = ($fname =~ m|^([^/]+)/([^/]+)/(.+)$|); + if ($ufile =~m|/\.\./|) { + # any files paths with /../ in them refuse + # to deal with + &Failure($client, "refused\n", "$cmd:$tail"); + } else { + my $udir = &propath($udom,$uname); + if (-e $udir) { + my $file=$udir.'/userfiles/'.$ufile; + if (-e $file) { + # + # If the file is a regular file unlink is fine... + # However it's possible the client wants a dir. + # removed, in which case rmdir is more approprate: + # + if (-f $file){ + unlink($file); + } elsif(-d $file) { + rmdir($file); + } + if (-e $file) { + # File is still there after we deleted it ?!? + + &Failure($client, "failed\n", "$cmd:$tail"); + } else { + &Reply($client, "ok\n", "$cmd:$tail"); + } + } else { + &Failure($client, "not_found\n", "$cmd:$tail"); + } + } else { + &Failure($client, "not_home\n", "$cmd:$tail"); + } + } + return 1; +} +®ister_handler("removeuserfile", \&remove_user_file_handler, 0,1,0); + # +# make a directory in a user's home directory userfiles subdirectory. # Parameters: -# $cmd - The command that got us here. -# $tail - Tail of the command (remaining parameters). -# $client - File descriptor connected to client. -# Returns -# 0 - Requested to exit, caller should shut down. -# 1 - Continue processing. -sub AuthenticateUserFileAccess { - my $cmd = shift; - my $tail = shift; - my $client = shift; - my $userinput = "$cmd:$tail"; +# cmd - the Lond request keyword that got us here. +# tail - the part of the command past the keyword. +# client- File descriptor connected with the client. +# +# Returns: +# 1 - Continue processing. +sub mkdir_user_file_handler { + my ($cmd, $tail, $client) = @_; + + my ($dir) = split(/:/, $tail); # Get rid of any tailing :'s lonc may have sent. + $dir=&unescape($dir); + my ($udom,$uname,$ufile) = ($dir =~ m|^([^/]+)/([^/]+)/(.+)$|); + if ($ufile =~m|/\.\./|) { + # any files paths with /../ in them refuse + # to deal with + &Failure($client, "refused\n", "$cmd:$tail"); + } else { + my $udir = &propath($udom,$uname); + if (-e $udir) { + my $newdir=$udir.'/userfiles/'.$ufile.'/'; + if (!&mkpath($newdir)) { + &Failure($client, "failed\n", "$cmd:$tail"); + } + &Reply($client, "ok\n", "$cmd:$tail"); + } else { + &Failure($client, "not_home\n", "$cmd:$tail"); + } + } + return 1; +} +®ister_handler("mkdiruserfile", \&mkdir_user_file_handler, 0,1,0); + +# +# rename a file in a user's home directory userfiles subdirectory. +# Parameters: +# cmd - the Lond request keyword that got us here. +# tail - the part of the command past the keyword. +# client- File descriptor connected with the client. +# +# Returns: +# 1 - Continue processing. +sub rename_user_file_handler { + my ($cmd, $tail, $client) = @_; - my ($fname,$session)=split(/:/,$tail); + my ($udom,$uname,$old,$new) = split(/:/, $tail); + $old=&unescape($old); + $new=&unescape($new); + if ($new =~m|/\.\./| || $old =~m|/\.\./|) { + # any files paths with /../ in them refuse to deal with + &Failure($client, "refused\n", "$cmd:$tail"); + } else { + my $udir = &propath($udom,$uname); + if (-e $udir) { + my $oldfile=$udir.'/userfiles/'.$old; + my $newfile=$udir.'/userfiles/'.$new; + if (-e $newfile) { + &Failure($client, "exists\n", "$cmd:$tail"); + } elsif (! -e $oldfile) { + &Failure($client, "not_found\n", "$cmd:$tail"); + } else { + if (!rename($oldfile,$newfile)) { + &Failure($client, "failed\n", "$cmd:$tail"); + } else { + &Reply($client, "ok\n", "$cmd:$tail"); + } + } + } else { + &Failure($client, "not_home\n", "$cmd:$tail"); + } + } + return 1; +} +®ister_handler("renameuserfile", \&rename_user_file_handler, 0,1,0); + +# +# Authenticate access to a user file by checking that the token the user's +# passed also exists in their session file +# +# Parameters: +# cmd - The request keyword that dispatched to tus. +# tail - The tail of the request (colon separated parameters). +# client - Filehandle open on the client. +# Return: +# 1. +sub token_auth_user_file_handler { + my ($cmd, $tail, $client) = @_; + + my ($fname, $session) = split(/:/, $tail); + chomp($session); - my $reply='non_auth'; - if (open(ENVIN,$perlvar{'lonIDsDir'}.'/'.$session.'.id')) { - while (my $line=) { - if ($line=~/userfile\.$fname\=/) { - $reply='ok'; + my $reply="non_auth\n"; + my $file = $perlvar{'lonIDsDir'}.'/'.$session.'.id'; + if (open(ENVIN,"$file")) { + flock(ENVIN,LOCK_SH); + tie(my %disk_env,'GDBM_File',"$file",&GDBM_READER(),0640); + if (exists($disk_env{"userfile.$fname"})) { + $reply="ok\n"; + } else { + foreach my $envname (keys(%disk_env)) { + if ($envname=~ m|^userfile\.\Q$fname\E|) { + $reply="ok\n"; + last; + } } } + untie(%disk_env); close(ENVIN); - Reply($client, $reply."\n", $userinput); + &Reply($client, $reply, "$cmd:$tail"); } else { - Failure($client, "invalid_token\n", $userinput); + &Failure($client, "invalid_token\n", "$cmd:$tail"); } return 1; - + } -RegisterHandler("tokenauthuserfile", \&AuthenticateUserFileAccess, 0, 1, 0); +®ister_handler("tokenauthuserfile", \&token_auth_user_file_handler, 0,1,0); + # # Unsubscribe from a resource. # @@ -1183,23 +2166,25 @@ RegisterHandler("tokenauthuserfile", \&A # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub UnsubscribeHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub unsubscribe_handler { + my ($cmd, $tail, $client) = @_; + my $userinput= "$cmd:$tail"; - my $fname = $tail; + my ($fname) = split(/:/,$tail); # Split in case there's extrs. + + &Debug("Unsubscribing $fname"); if (-e $fname) { - Reply($client, &unsub($client,$fname,$clientip), $userinput); + &Debug("Exists"); + &Reply($client, &unsub($fname,$clientip), $userinput); } else { - Failure($client, "not_found\n", $userinput); + &Failure($client, "not_found\n", $userinput); } return 1; } -RegisterHandler("unusb", \&UnsubscribeHandler, 0, 1, 0); +®ister_handler("unsub", \&unsubscribe_handler, 0, 1, 0); -# Subscribe to a resource. +# Subscribe to a resource # # Parameters: # $cmd - The command that got us here. @@ -1209,17 +2194,16 @@ RegisterHandler("unusb", \&UnsubscribeHa # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub SubscribeHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub subscribe_handler { + my ($cmd, $tail, $client)= @_; + my $userinput = "$cmd:$tail"; - Reply( $client, &subscribe($userinput,$clientip), $userinput); + &Reply( $client, &subscribe($userinput,$clientip), $userinput); return 1; } -RegisterHandler("sub", \&SubscribeHandler, 0, 1, 0); +®ister_handler("sub", \&subscribe_handler, 0, 1, 0); # # Determine the version of a resource (?) Or is it return @@ -1234,19 +2218,17 @@ RegisterHandler("sub", \&SubscribeHandle # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub CurrentVersionHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub current_version_handler { + my ($cmd, $tail, $client) = @_; + my $userinput= "$cmd:$tail"; my $fname = $tail; - Reply( $client, ¤tversion($fname)."\n", $userinput); + &Reply( $client, ¤tversion($fname)."\n", $userinput); return 1; } -RegisterHandler("currentversion", \&CurrentVersionHandler, 0, 1, 0); - +®ister_handler("currentversion", \¤t_version_handler, 0, 1, 0); # Make an entry in a user's activity log. # @@ -1258,29 +2240,30 @@ RegisterHandler("currentversion", \&Curr # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub ActivityLogEntryHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub activity_log_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput= "$cmd:$tail"; my ($udom,$uname,$what)=split(/:/,$tail); chomp($what); - my $proname=propath($udom,$uname); + my $proname=&propath($udom,$uname); my $now=time; my $hfh; if ($hfh=IO::File->new(">>$proname/activity.log")) { print $hfh "$now:$clientname:$what\n"; - Reply( $client, "ok\n", $userinput); + &Reply( $client, "ok\n", $userinput); } else { - Reply($client, "error: ".($!+0)." IO::File->new Failed " - ."while attempting log\n", - $userinput); + &Failure($client, "error: ".($!+0)." IO::File->new Failed " + ."while attempting log\n", + $userinput); } return 1; } -RegisterHandler("log", \&ActivityLogEntryHandler, 0, 1, 0); +®ister_handler("log", \&activity_log_handler, 0, 1, 0); + # # Put a namespace entry in a user profile hash. # My druthers would be for this to be an encrypted interaction too. @@ -1295,51 +2278,95 @@ RegisterHandler("log", \&ActivityLogEntr # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub PutUserProfileEntry { - my $cmd = shift; - my $tail = shift; - my $client = shift; - my $userinput = "$cmd:$tail"; +sub put_user_profile_entry { + my ($cmd, $tail, $client) = @_; - my ($udom,$uname,$namespace,$what) =split(/:/,$tail); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; + my $userinput = "$cmd:$tail"; + + my ($udom,$uname,$namespace,$what) =split(/:/,$tail,4); 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)) { + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(),"P",$what); + if($hashref) { + my @pairs=split(/\&/,$what); foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); - $hash{$key}=$value; + $hashref->{$key}=$value; } - if (untie(%hash)) { - Reply( $client, "ok\n", $userinput); + if (&untie_user_hash($hashref)) { + &Reply( $client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) failed ". + &Failure($client, "error: ".($!+0)." untie(GDBM) failed ". "while attempting put\n", $userinput); } } else { - Failure( $client, "error: ".($!)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting put\n", $userinput); } } else { - Failure( $client, "refused\n", $userinput); + &Failure( $client, "refused\n", $userinput); + } + + return 1; +} +®ister_handler("put", \&put_user_profile_entry, 0, 1, 0); + +# Put a piece of new data in hash, returns error if entry already exists +# Parameters: +# $cmd - The command that got us here. +# $tail - Tail of the command (remaining parameters). +# $client - File descriptor connected to client. +# Returns +# 0 - Requested to exit, caller should shut down. +# 1 - Continue processing. +# +sub newput_user_profile_entry { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + + my ($udom,$uname,$namespace,$what) =split(/:/,$tail,4); + if ($namespace eq 'roles') { + &Failure( $client, "refused\n", $userinput); + return 1; + } + + chomp($what); + + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(),"N",$what); + if(!$hashref) { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting put\n", $userinput); + return 1; + } + + my @pairs=split(/\&/,$what); + foreach my $pair (@pairs) { + my ($key,$value)=split(/=/,$pair); + if (exists($hashref->{$key})) { + &Failure($client, "key_exists: ".$key."\n",$userinput); + return 1; + } + } + + foreach my $pair (@pairs) { + my ($key,$value)=split(/=/,$pair); + $hashref->{$key}=$value; + } + + if (&untie_user_hash($hashref)) { + &Reply( $client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) failed ". + "while attempting put\n", + $userinput); } - return 1; } -RegisterHandler("put", \&PutUserProfileEntry, 0, 1, 0); +®ister_handler("newput", \&newput_user_profile_entry, 0, 1, 0); # # Increment a profile entry in the user history file. @@ -1356,54 +2383,51 @@ RegisterHandler("put", \&PutUserProfileE # 0 - Requested to exit, caller should shut down. # 1 - Continue processing. # -sub IncrementUserValueHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; - my $userinput = shift; - +sub increment_user_value_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + my ($udom,$uname,$namespace,$what) =split(/:/,$tail); - $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)) { + chomp($what); + my $hashref = &tie_user_hash($udom, $uname, + $namespace, &GDBM_WRCREAT(), + "P",$what); + if ($hashref) { + my @pairs=split(/\&/,$what); foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); + $value = &unescape($value); # We could check that we have a number... if (! defined($value) || $value eq '') { $value = 1; } - $hash{$key}+=$value; + $hashref->{$key}+=$value; + if ($namespace eq 'nohist_resourcetracker') { + if ($hashref->{$key} < 0) { + $hashref->{$key} = 0; + } + } } - if (untie(%hash)) { - Reply( $client, "ok\n", $userinput); + if (&untie_user_hash($hashref)) { + &Reply( $client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) failed ". - "while attempting put\n", $userinput); + &Failure($client, "error: ".($!+0)." untie(GDBM) failed ". + "while attempting inc\n", $userinput); } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". - "while attempting put\n", $userinput); + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting inc\n", $userinput); } } else { - Failure($client, "refused\n", $userinput); + &Failure($client, "refused\n", $userinput); } - + return 1; } -RegisterHandler("inc", \&IncrementUserValueHandler, 0, 1, 0); +®ister_handler("inc", \&increment_user_value_handler, 0, 1, 0); + # # Put a new role for a user. Roles are LonCAPA's packaging of permissions. # Each 'role' a user has implies a set of permissions. Adding a new role @@ -1424,52 +2448,49 @@ RegisterHandler("inc", \&IncrementUserVa # 1 - To continue processing. # # -sub RolesPutHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub roles_put_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($exedom,$exeuser,$udom,$uname,$what) =split(/:/,$tail); - &Debug("cmd = ".$cmd." exedom= ".$exedom."user = ".$exeuser." udom=".$udom. - "what = ".$what); + my ( $exedom, $exeuser, $udom, $uname, $what) = split(/:/,$tail); + + my $namespace='roles'; chomp($what); - my $proname=propath($udom,$uname); - my $now=time; + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(), "P", + "$exedom:$exeuser:$what"); # # Log the attempt to set a role. The {}'s here ensure that the file # handle is open for the minimal amount of time. Since the flush # is done on close this improves the chances the log will be an un- # corrupted ordered thing. - { - 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)) { + if ($hashref) { + my $pass_entry = &get_auth_type($udom, $uname); + my ($auth_type,$pwd) = split(/:/, $pass_entry); + $auth_type = $auth_type.":"; + my @pairs=split(/\&/,$what); foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); - &ManagePermissions($key, $udom, $uname, - &GetAuthType( $udom, $uname)); - $hash{$key}=$value; + &manage_permissions($key, $udom, $uname, + $auth_type); + $hashref->{$key}=$value; } - if (untie(%hash)) { - Reply($client, "ok\n", $userinput); + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); } else { - Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting rolesput\n", $userinput); } } else { - Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting rolesput\n", $userinput); } return 1; } -RegisterHandler("rolesput", \&RolesPutHandler, 1,1,0); # Encoded client only. +®ister_handler("rolesput", \&roles_put_handler, 1,1,0); # Encoded client only. + # # Deletes (removes) a role for a user. This is equivalent to removing # a permissions package associated with the role from the user's profile. @@ -1487,10 +2508,9 @@ RegisterHandler("rolesput", \&RolesPutHa # 1 - Continue processing # 0 - Exit. # -sub RolesDeleteHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub roles_delete_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; my ($exedom,$exeuser,$udom,$uname,$what)=split(/:/,$tail); @@ -1498,40 +2518,30 @@ sub RolesDeleteHandler { "what = ".$what); my $namespace='roles'; chomp($what); - my $proname=propath($udom,$uname); - my $now=time; - # - # Log the attempt. This {}'ing is done to ensure that the - # logfile is flushed and closed as quickly as possible. Hopefully - # this preserves both time ordering and reduces the probability that - # messages will be interleaved. - # - { - 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 $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(), "D", + "$exedom:$exeuser:$what"); + + if ($hashref) { + my @rolekeys=split(/\&/,$what); + foreach my $key (@rolekeys) { - delete $hash{$key}; + delete $hashref->{$key}; } - if (untie(%hash)) { - Reply($client, "ok\n", $userinput); + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); } else { - Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting rolesdel\n", $userinput); } } else { - Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting rolesdel\n", $userinput); } return 1; } -RegisterHandler("rolesdel", \&RolesDeleteHandler, 1,1, 0); # Encoded client only +®ister_handler("rolesdel", \&roles_delete_handler, 1,1, 0); # Encoded client only # Unencrypted get from a user's profile database. See # GetProfileEntryEncrypted for a version that does end-to-end encryption. @@ -1552,43 +2562,27 @@ RegisterHandler("rolesdel", \&RolesDelet # 1 - Continue processing. # 0 - Exit. # -sub GetProfileEntry { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub get_profile_entry { + my ($cmd, $tail, $client) = @_; + my $userinput= "$cmd:$tail"; my ($udom,$uname,$namespace,$what) = split(/:/,$tail); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; chomp($what); - my @queries=split(/\&/,$what); - my $proname=propath($udom,$uname); - my $qresult=''; - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db", &GDBM_READER(),0640)) { - for (my $i=0;$i<=$#queries;$i++) { - $qresult.="$hash{$queries[$i]}&"; # Presumably failure gives empty string. - } - if (untie(%hash)) { - $qresult=~s/\&$//; # Remove trailing & from last lookup. - Reply($client, "$qresult\n", $userinput); - } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". - "while attempting get\n", $userinput); - } + + my $replystring = read_profile($udom, $uname, $namespace, $what); + my ($first) = split(/:/,$replystring); + if($first ne "error") { + &Reply($client, "$replystring\n", $userinput); } else { - if ($!+0 == 2) { # +0 coerces errno -> number 2 is ENOENT - Failure($client, "error:No such file or ". - "GDBM reported bad block error\n", $userinput); - } else { # Some other undifferentiated err. - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". - "while attempting get\n", $userinput); - } + &Failure($client, $replystring." while attempting get\n", $userinput); } return 1; + + } -RegisterHandler("get", \&GetProfileEntry, 0,1,0); +®ister_handler("get", \&get_profile_entry, 0,1,0); + # # Process the encrypted get request. Note that the request is sent # in clear, but the reply is encrypted. This is a small covert channel: @@ -1608,51 +2602,39 @@ RegisterHandler("get", \&GetProfileEntry # Returns: # 1 - Continue processing # 0 - server should exit. -sub GetProfileEntryEncrypted { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub get_profile_entry_encrypted { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($cmd,$udom,$uname,$namespace,$what) = split(/:/,$userinput); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; + my ($udom,$uname,$namespace,$what) = split(/:/,$tail); chomp($what); - my @queries=split(/\&/,$what); - my $proname=propath($udom,$uname); - my $qresult=''; - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_READER(),0640)) { - for (my $i=0;$i<=$#queries;$i++) { - $qresult.="$hash{$queries[$i]}&"; - } - if (untie(%hash)) { - $qresult=~s/\&$//; - if ($cipher) { - my $cmdlength=length($qresult); - $qresult.=" "; - my $encqresult=''; - for(my $encidx=0;$encidx<=$cmdlength;$encidx+=8) { - $encqresult.= unpack("H16", $cipher->encrypt(substr($qresult, - $encidx, - 8))); - } - Reply( $client, "enc:$cmdlength:$encqresult\n", $userinput); - } else { - Failure( $client, "error:no_key\n", $userinput); + my $qresult = read_profile($udom, $uname, $namespace, $what); + my ($first) = split(/:/, $qresult); + if($first ne "error") { + + if ($cipher) { + my $cmdlength=length($qresult); + $qresult.=" "; + my $encqresult=''; + for(my $encidx=0;$encidx<=$cmdlength;$encidx+=8) { + $encqresult.= unpack("H16", + $cipher->encrypt(substr($qresult, + $encidx, + 8))); } + &Reply( $client, "enc:$cmdlength:$encqresult\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". - "while attempting eget\n", $userinput); - } + &Failure( $client, "error:no_key\n", $userinput); + } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". - "while attempting eget\n", $userinput); + &Failure($client, "$qresult while attempting eget\n", $userinput); + } return 1; } -RegisterHandler("eget", \&GetProfileEncrypted, 0, 1, 0); +®ister_handler("eget", \&get_profile_entry_encrypted, 0, 1, 0); # # Deletes a key in a user profile database. @@ -1672,43 +2654,35 @@ RegisterHandler("eget", \&GetProfileEncr # 0 - Exit server. # # -sub DeletProfileEntry { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub delete_profile_entry { + my ($cmd, $tail, $client) = @_; + my $userinput = "cmd:$tail"; my ($udom,$uname,$namespace,$what) = split(/:/,$tail); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; 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)) { + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(), + "D",$what); + if ($hashref) { + my @keys=split(/\&/,$what); foreach my $key (@keys) { - delete($hash{$key}); + delete($hashref->{$key}); } - if (untie(%hash)) { - Reply($client, "ok\n", $userinput); + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting del\n", $userinput); } } else { - Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting del\n", $userinput); } return 1; } -RegisterHandler("del", \&DeleteProfileEntry, 0, 1, 0); +®ister_handler("del", \&delete_profile_entry, 0, 1, 0); + # # List the set of keys that are defined in a profile database file. # A successful reply from this will contain an & separated list of @@ -1724,37 +2698,35 @@ RegisterHandler("del", \&DeleteProfileEn # 1 - Continue processing. # 0 - Exit the server. # -sub GetProfileKeys { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub get_profile_keys { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; my ($udom,$uname,$namespace)=split(/:/,$tail); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; - my $proname=propath($udom,$uname); my $qresult=''; - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db",&GDBM_READER(),0640)) { - foreach my $key (keys %hash) { + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_READER()); + if ($hashref) { + foreach my $key (keys %$hashref) { $qresult.="$key&"; } - if (untie(%hash)) { + if (&untie_user_hash($hashref)) { $qresult=~s/\&$//; - Reply($client, "$qresult\n", $userinput); + &Reply($client, "$qresult\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting keys\n", $userinput); } } else { - Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting keys\n", $userinput); } return 1; } -RegisterHandler("keys", \&GetProfileKeys, 0, 1, 0); +®ister_handler("keys", \&get_profile_keys, 0, 1, 0); + # # Dump the contents of a user profile database. # Note that this constitutes a very large covert channel too since @@ -1774,26 +2746,24 @@ RegisterHandler("keys", \&GetProfileKeys # 1 - Continue processing. # 0 - Exit the server. # -sub DumpProfileDatabase { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub dump_profile_database { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; my ($udom,$uname,$namespace) = split(/:/,$tail); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; - my $qresult=''; - my $proname=propath($udom,$uname); - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db", &GDBM_READER(),0640)) { + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_READER()); + if ($hashref) { # Structure of %data: # $data{$symb}->{$parameter}=$value; # $data{$symb}->{'v.'.$parameter}=$version; # since $parameter will be unescaped, we do not - # have to worry about silly parameter names... + # have to worry about silly parameter names... + + my $qresult=''; my %data = (); # A hash of anonymous hashes.. - while (my ($key,$value) = each(%hash)) { + while (my ($key,$value) = each(%$hashref)) { my ($v,$symb,$param) = split(/:/,$key); next if ($v eq 'version' || $symb eq 'keys'); next if (exists($data{$symb}) && @@ -1802,7 +2772,7 @@ sub DumpProfileDatabase { $data{$symb}->{$param}=$value; $data{$symb}->{'v.'.$param}=$v; } - if (untie(%hash)) { + if (&untie_user_hash($hashref)) { while (my ($symb,$param_hash) = each(%data)) { while(my ($param,$value) = each (%$param_hash)){ next if ($param =~ /^v\./); # Ignore versions... @@ -1813,19 +2783,20 @@ sub DumpProfileDatabase { } } chop($qresult); - Reply($client , "$qresult\n", $userinput); + &Reply($client , "$qresult\n", $userinput); } else { - Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting currentdump\n", $userinput); } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting currentdump\n", $userinput); } return 1; } -RegisterHandler("currentdump", \&DumpProfileDatabase, 0, 1, 0); +®ister_handler("currentdump", \&dump_profile_database, 0, 1, 0); + # # Dump a profile database with an optional regular expression # to match against the keys. In this dump, no effort is made @@ -1850,53 +2821,66 @@ RegisterHandler("currentdump", \&DumpPro # Side effects: # response is written to $client. # -sub DumpWithRegexp { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub dump_with_regexp { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($udom,$uname,$namespace,$regexp)=split(/:/,$tail); - $namespace=~s/\//\_/g; - $namespace=~s/\W//g; + my ($udom,$uname,$namespace,$regexp,$range)=split(/:/,$tail); if (defined($regexp)) { $regexp=&unescape($regexp); } else { $regexp='.'; } - my $qresult=''; - my $proname=propath($udom,$uname); - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db", - &GDBM_READER(),0640)) { - while (my ($key,$value) = each(%hash)) { + my ($start,$end); + if (defined($range)) { + if ($range =~/^(\d+)\-(\d+)$/) { + ($start,$end) = ($1,$2); + } elsif ($range =~/^(\d+)$/) { + ($start,$end) = (0,$1); + } else { + undef($range); + } + } + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_READER()); + if ($hashref) { + my $qresult=''; + my $count=0; + while (my ($key,$value) = each(%$hashref)) { if ($regexp eq '.') { + $count++; + if (defined($range) && $count >= $end) { last; } + if (defined($range) && $count < $start) { next; } $qresult.=$key.'='.$value.'&'; } else { my $unescapeKey = &unescape($key); if (eval('$unescapeKey=~/$regexp/')) { + $count++; + if (defined($range) && $count >= $end) { last; } + if (defined($range) && $count < $start) { next; } $qresult.="$key=$value&"; } } } - if (untie(%hash)) { + if (&untie_user_hash($hashref)) { chop($qresult); - Reply($client, "$qresult\n", $userinput); + &Reply($client, "$qresult\n", $userinput); } else { - Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting dump\n", $userinput); } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting dump\n", $userinput); } return 1; } -RegisterHandler("dump", \&DumpWithRegexp, 0, 1, 0); +®ister_handler("dump", \&dump_with_regexp, 0, 1, 0); -# Store an aitem in any database but the roles database. +# Store a set of key=value pairs associated with a versioned name. # # Parameters: # $cmd - Request command keyword. @@ -1914,62 +2898,135 @@ RegisterHandler("dump", \&DumpWithRegexp # 1 (keep on processing). # Side-Effects: # Writes to the client -sub StoreHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub store_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; my ($udom,$uname,$namespace,$rid,$what) =split(/:/,$tail); - $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:$rid:$what\n"; - } - } my @pairs=split(/\&/,$what); - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db", - &GDBM_WRCREAT(),0640)) { - my @previouskeys=split(/&/,$hash{"keys:$rid"}); + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(), "S", + "$rid:$what"); + if ($hashref) { + my $now = time; + my @previouskeys=split(/&/,$hashref->{"keys:$rid"}); my $key; - $hash{"version:$rid"}++; - my $version=$hash{"version:$rid"}; + $hashref->{"version:$rid"}++; + my $version=$hashref->{"version:$rid"}; my $allkeys=''; foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); $allkeys.=$key.':'; - $hash{"$version:$rid:$key"}=$value; + $hashref->{"$version:$rid:$key"}=$value; } - $hash{"$version:$rid:timestamp"}=$now; + $hashref->{"$version:$rid:timestamp"}=$now; $allkeys.='timestamp'; - $hash{"$version:keys:$rid"}=$allkeys; - if (untie(%hash)) { - Reply($client, "ok\n", $userinput); + $hashref->{"$version:keys:$rid"}=$allkeys; + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting store\n", $userinput); + } + } else { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting store\n", $userinput); + } + } else { + &Failure($client, "refused\n", $userinput); + } + + return 1; +} +®ister_handler("store", \&store_handler, 0, 1, 0); + +# Modify a set of key=value pairs associated with a versioned name. +# +# Parameters: +# $cmd - Request command keyword. +# $tail - Tail of the request. This is a colon +# separated list containing: +# domain/user - User and authentication domain. +# namespace - Name of the database being modified +# rid - Resource keyword to modify. +# v - Version item to modify +# what - new value associated with rid. +# +# $client - Socket open on the client. +# +# +# Returns: +# 1 (keep on processing). +# Side-Effects: +# Writes to the client +sub putstore_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + + my ($udom,$uname,$namespace,$rid,$v,$what) =split(/:/,$tail); + if ($namespace ne 'roles') { + + chomp($what); + my $hashref = &tie_user_hash($udom, $uname, $namespace, + &GDBM_WRCREAT(), "M", + "$rid:$v:$what"); + if ($hashref) { + my $now = time; + my %data = &hash_extract($what); + my @allkeys; + while (my($key,$value) = each(%data)) { + push(@allkeys,$key); + $hashref->{"$v:$rid:$key"} = $value; + } + my $allkeys = join(':',@allkeys); + $hashref->{"$v:keys:$rid"}=$allkeys; + + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting store\n", $userinput); } } else { - Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting store\n", $userinput); } } else { - Failure($client, "refused\n", $userinput); + &Failure($client, "refused\n", $userinput); } return 1; } -RegisterHandler("store", \&StoreHandler, 0, 1, 0); +®ister_handler("putstore", \&putstore_handler, 0, 1, 0); + +sub hash_extract { + my ($str)=@_; + my %hash; + foreach my $pair (split(/\&/,$str)) { + my ($key,$value)=split(/=/,$pair); + $hash{$key}=$value; + } + return (%hash); +} +sub hash_to_str { + my ($hash_ref)=@_; + my $str; + foreach my $key (keys(%$hash_ref)) { + $str.=$key.'='.$hash_ref->{$key}.'&'; + } + $str=~s/\&$//; + return $str; +} + # -# Restore a prior version of a resource. +# Dump out all versions of a resource that has key=value pairs associated +# with it for each version. These resources are built up via the store +# command. # # Parameters: # $cmd - Command keyword. @@ -1983,44 +3040,47 @@ RegisterHandler("store", \&StoreHandler, # 1 indicating the caller should not yet exit. # Side-effects: # Writes a reply to the client. +# The reply is a string of the following shape: +# version=current&version:keys=k1:k2...&1:k1=v1&1:k2=v2... +# Where the 1 above represents version 1. +# this continues for all pairs of keys in all versions. # -sub RestoreHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +# +# +# +sub restore_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; # Only used for logging purposes. - - my ($cmd,$udom,$uname,$namespace,$rid) = split(/:/,$userinput); + my ($udom,$uname,$namespace,$rid) = split(/:/,$tail); $namespace=~s/\//\_/g; - $namespace=~s/\W//g; + $namespace = &LONCAPA::clean_username($namespace); + chomp($rid); - my $proname=propath($udom,$uname); my $qresult=''; - my %hash; - if (tie(%hash,'GDBM_File',"$proname/$namespace.db", - &GDBM_READER(),0640)) { - my $version=$hash{"version:$rid"}; + my $hashref = &tie_user_hash($udom, $uname, $namespace, &GDBM_READER()); + if ($hashref) { + my $version=$hashref->{"version:$rid"}; $qresult.="version=$version&"; my $scope; for ($scope=1;$scope<=$version;$scope++) { - my $vkeys=$hash{"$scope:keys:$rid"}; + my $vkeys=$hashref->{"$scope:keys:$rid"}; my @keys=split(/:/,$vkeys); my $key; $qresult.="$scope:keys=$vkeys&"; foreach $key (@keys) { - $qresult.="$scope:$key=".$hash{"$scope:$rid:$key"}."&"; + $qresult.="$scope:$key=".$hashref->{"$scope:$rid:$key"}."&"; } } - if (untie(%hash)) { + if (&untie_user_hash($hashref)) { $qresult=~s/\&$//; - Reply( $client, "$qresult\n", $userinput); + &Reply( $client, "$qresult\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting restore\n", $userinput); } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting restore\n", $userinput); } @@ -2028,18 +3088,20 @@ sub RestoreHandler { } -RegisterHandler("restor", \&RestoreHandler, 0,1,0); +®ister_handler("restore", \&restore_handler, 0,1,0); # -# Add a chat message to to a discussion board. +# Add a chat message to a synchronous discussion board. # # Parameters: # $cmd - Request keyword. # $tail - Tail of the command. A colon separated list # containing: # cdom - Domain on which the chat board lives -# cnum - Identifier of the discussion group. -# post - Body of the posting. +# cnum - Course containing the chat board. +# newpost - Body of the posting. +# group - Optional group, if chat board is only +# accessible in a group within the course # $client - Socket open on the client. # Returns: # 1 - Indicating caller should keep on processing. @@ -2048,22 +3110,22 @@ RegisterHandler("restor", \&RestoreHandl # writes a reply to the client. # # -sub SendChatHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub send_chat_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($cdom,$cnum,$newpost)=split(/\:/,$tail); - &chatadd($cdom,$cnum,$newpost); - Reply($client, "ok\n", $userinput); + my ($cdom,$cnum,$newpost,$group)=split(/\:/,$tail); + &chat_add($cdom,$cnum,$newpost,$group); + &Reply($client, "ok\n", $userinput); return 1; } -RegisterHandler("chatsend", \&SendChatHandler, 0, 1, 0); +®ister_handler("chatsend", \&send_chat_handler, 0, 1, 0); + # -# Retrieve the set of chat messagss from a discussion board. +# Retrieve the set of chat messages from a discussion board. # # Parameters: # $cmd - Command keyword that initiated the request. @@ -2073,31 +3135,33 @@ RegisterHandler("chatsend", \&SendChatHa # chat id - Discussion thread(?) # domain/user - Authentication domain and username # of the requesting person. +# group - Optional course group containing +# the board. # $client - Socket open on the client program. # Returns: # 1 - continue processing # Side effects: # Response is written to the client. # -sub RetrieveChatHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub retrieve_chat_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($cdom,$cnum,$udom,$uname)=split(/\:/,$tail); + my ($cdom,$cnum,$udom,$uname,$group)=split(/\:/,$tail); my $reply=''; - foreach (&getchat($cdom,$cnum,$udom,$uname)) { + foreach (&get_chat($cdom,$cnum,$udom,$uname,$group)) { $reply.=&escape($_).':'; } $reply=~s/\:$//; - Reply($client, $reply."\n", $userinput); + &Reply($client, $reply."\n", $userinput); return 1; } -RegisterHandler("chatretr", \&RetrieveChatHandler, 0, 1, 0); +®ister_handler("chatretr", \&retrieve_chat_handler, 0, 1, 0); + # # Initiate a query of an sql database. SQL query repsonses get put in # a file for later retrieval. This prevents sql query results from @@ -2117,22 +3181,21 @@ RegisterHandler("chatretr", \&RetrieveCh # Side-effects: # a reply is written to $client. # -sub SendQueryHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub send_query_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; my ($query,$arg1,$arg2,$arg3)=split(/\:/,$tail); $query=~s/\n*$//g; - Reply($client, "". sqlreply("$clientname\&$query". + &Reply($client, "". &sql_reply("$clientname\&$query". "\&$arg1"."\&$arg2"."\&$arg3")."\n", $userinput); return 1; } -RegisterHandler("querysend", \&SendQueryHandler, 0, 1, 0); +®ister_handler("querysend", \&send_query_handler, 0, 1, 0); # # Add a reply to an sql query. SQL queries are done asyncrhonously. @@ -2159,14 +3222,13 @@ RegisterHandler("querysend", \&SendQuery # Side effects: # ok written to the client. # -sub ReplyQueryHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub reply_query_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($cmd,$id,$reply)=split(/:/,$userinput); + my ($id,$reply)=split(/:/,$tail); my $store; my $execdir=$perlvar{'lonDaemons'}; if ($store=IO::File->new(">$execdir/tmp/$id")) { @@ -2176,9 +3238,9 @@ sub ReplyQueryHandler { my $store2=IO::File->new(">$execdir/tmp/$id.end"); print $store2 "done\n"; close $store2; - Reply($client, "ok\n", $userinput); + &Reply($client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0) + &Failure($client, "error: ".($!+0) ." IO::File->new Failed ". "while attempting queryreply\n", $userinput); } @@ -2186,9 +3248,10 @@ sub ReplyQueryHandler { return 1; } -RegisterHandler("queryreply", \&ReplyQueryHandler, 0, 1, 0); +®ister_handler("queryreply", \&reply_query_handler, 0, 1, 0); + # -# Process the courseidput query. Not quite sure what this means +# Process the courseidput request. Not quite sure what this means # at the system level sense. It appears a gdbm file in the # /home/httpd/lonUsers/$domain/nohist_courseids is tied and # a set of entries made in that database. @@ -2198,6 +3261,14 @@ RegisterHandler("queryreply", \&ReplyQue # $tail - Tail of the command. In this case consists of a colon # separated list contaning the domain to apply this to and # an ampersand separated list of keyword=value pairs. +# Each value is a colon separated list that includes: +# description, institutional code and course owner. +# For backward compatibility with versions included +# in LON-CAPA 1.1.X (and earlier) and 1.2.X, institutional +# code and/or course owner are preserved from the existing +# record when writing a new record in response to 1.1 or +# 1.2 implementations of lonnet::flushcourselogs(). +# # $client - Socket open on the client. # Returns: # 1 - indicating that processing should continue @@ -2205,42 +3276,57 @@ RegisterHandler("queryreply", \&ReplyQue # Side effects: # reply is written to the client. # -sub PutCourseIdHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub put_course_id_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - my ($udom,$what)=split(/:/,$tail); + my ($udom, $what) = split(/:/, $tail,2); chomp($what); - $udom=~s/\W//g; - my $proname= - "$perlvar{'lonUsersDir'}/$udom/nohist_courseids"; my $now=time; my @pairs=split(/\&/,$what); - my %hash; - if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_WRCREAT(),0640)) { + + my $hashref = &tie_domain_hash($udom, "nohist_courseids", &GDBM_WRCREAT()); + if ($hashref) { foreach my $pair (@pairs) { - my ($key,$value)=split(/=/,$pair); - $hash{$key}=$value.':'.$now; + my ($key,$courseinfo) = split(/=/,$pair,2); + $courseinfo =~ s/=/:/g; + my @current_items = split(/:/,$hashref->{$key},-1); + shift(@current_items); # remove description + pop(@current_items); # remove last access + my $numcurrent = scalar(@current_items); + if ($numcurrent > 3) { + $numcurrent = 3; + } + my @new_items = split(/:/,$courseinfo,-1); + my $numnew = scalar(@new_items); + if ($numcurrent > 0) { + if ($numnew <= $numcurrent) { # flushcourselogs() from pre 2.2 + for (my $j=$numcurrent-$numnew; $j>=0; $j--) { + $courseinfo .= ':'.$current_items[$numcurrent-$j-1]; + } + } + } + $hashref->{$key}=$courseinfo.':'.$now; } - if (untie(%hash)) { - Reply($client, "ok\n", $userinput); + if (&untie_domain_hash($hashref)) { + &Reply( $client, "ok\n", $userinput); } else { - Failure( $client, "error: ".($!+0) + &Failure($client, "error: ".($!+0) ." untie(GDBM) Failed ". "while attempting courseidput\n", $userinput); } } else { - Failure( $client, "error: ".($!+0) + &Failure($client, "error: ".($!+0) ." tie(GDBM) Failed ". "while attempting courseidput\n", $userinput); } + return 1; } -RegisterHandler("courseidput", \&PutCourseIdHandler, 0, 1, 0); +®ister_handler("courseidput", \&put_course_id_handler, 0, 1, 0); # Retrieves the value of a course id resource keyword pattern # defined since a starting date. Both the starting date and the @@ -2260,59 +3346,262 @@ RegisterHandler("courseidput", \&PutCour # description - regular expression that is used to filter # the dump. Only keywords matching this regexp # will be used. +# institutional code - optional supplied code to filter +# the dump. Only courses with an institutional code +# that match the supplied code will be returned. +# owner - optional supplied username and domain of owner to +# filter the dump. Only courses for which the course +# owner matches the supplied username and/or domain +# will be returned. Pre-2.2.0 legacy entries from +# nohist_courseiddump will only contain usernames. # $client - The socket open on the client. # Returns: # 1 - Continue processing. # Side Effects: # a reply is written to $client. -sub DumpCourseIdHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub dump_course_id_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; - my ($udom,$since,$description) =split(/:/,$tail); + my ($udom,$since,$description,$instcodefilter,$ownerfilter,$coursefilter, + $typefilter,$regexp_ok) =split(/:/,$tail); if (defined($description)) { $description=&unescape($description); } else { $description='.'; } + if (defined($instcodefilter)) { + $instcodefilter=&unescape($instcodefilter); + } else { + $instcodefilter='.'; + } + my ($ownerunamefilter,$ownerdomfilter); + if (defined($ownerfilter)) { + $ownerfilter=&unescape($ownerfilter); + if ($ownerfilter ne '.' && defined($ownerfilter)) { + if ($ownerfilter =~ /^([^:]*):([^:]*)$/) { + $ownerunamefilter = $1; + $ownerdomfilter = $2; + } else { + $ownerunamefilter = $ownerfilter; + $ownerdomfilter = ''; + } + } + } else { + $ownerfilter='.'; + } + + if (defined($coursefilter)) { + $coursefilter=&unescape($coursefilter); + } else { + $coursefilter='.'; + } + if (defined($typefilter)) { + $typefilter=&unescape($typefilter); + } else { + $typefilter='.'; + } + if (defined($regexp_ok)) { + $regexp_ok=&unescape($regexp_ok); + } + unless (defined($since)) { $since=0; } my $qresult=''; - my $proname = "$perlvar{'lonUsersDir'}/$udom/nohist_courseids"; - my %hash; - if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_READER(),0640)) { - while (my ($key,$value) = each(%hash)) { - my ($descr,$lasttime)=split(/\:/,$value); - if ($lasttime<$since) { - next; - } - if ($description eq '.') { - $qresult.=$key.'='.$descr.'&'; - } else { - my $unescapeVal = &unescape($descr); - if (eval('$unescapeVal=~/$description/i')) { - $qresult.="$key=$descr&"; + my $hashref = &tie_domain_hash($udom, "nohist_courseids", &GDBM_WRCREAT()); + if ($hashref) { + while (my ($key,$value) = each(%$hashref)) { + my ($descr,$lasttime,$inst_code,$owner,$type); + my @courseitems = split(/:/,$value); + $lasttime = pop(@courseitems); + ($descr,$inst_code,$owner,$type)=@courseitems; + if ($lasttime<$since) { next; } + my $match = 1; + unless ($description eq '.') { + my $unescapeDescr = &unescape($descr); + unless (eval('$unescapeDescr=~/\Q$description\E/i')) { + $match = 0; } + } + unless ($instcodefilter eq '.' || !defined($instcodefilter)) { + my $unescapeInstcode = &unescape($inst_code); + if ($regexp_ok) { + unless (eval('$unescapeInstcode=~/$instcodefilter/')) { + $match = 0; + } + } else { + unless (eval('$unescapeInstcode=~/\Q$instcodefilter\E/i')) { + $match = 0; + } + } } + unless ($ownerfilter eq '.' || !defined($ownerfilter)) { + my $unescapeOwner = &unescape($owner); + if (($ownerunamefilter ne '') && ($ownerdomfilter ne '')) { + if ($unescapeOwner =~ /:/) { + if (eval('$unescapeOwner !~ + /\Q$ownerunamefilter\E:\Q$ownerdomfilter\E$/i')) { + $match = 0; + } + } else { + if (eval('$unescapeOwner!~/\Q$ownerunamefilter\E/i')) { + $match = 0; + } + } + } elsif ($ownerunamefilter ne '') { + if ($unescapeOwner =~ /:/) { + if (eval('$unescapeOwner!~/\Q$ownerunamefilter\E:[^:]+$/i')) { + $match = 0; + } + } else { + if (eval('$unescapeOwner!~/\Q$ownerunamefilter\E/i')) { + $match = 0; + } + } + } elsif ($ownerdomfilter ne '') { + if ($unescapeOwner =~ /:/) { + if (eval('$unescapeOwner!~/^[^:]+:\Q$ownerdomfilter\E/')) { + $match = 0; + } + } else { + if ($ownerdomfilter ne $udom) { + $match = 0; + } + } + } + } + unless ($coursefilter eq '.' || !defined($coursefilter)) { + my $unescapeCourse = &unescape($key); + unless (eval('$unescapeCourse=~/^$udom(_)\Q$coursefilter\E$/')) { + $match = 0; + } + } + unless ($typefilter eq '.' || !defined($typefilter)) { + my $unescapeType = &unescape($type); + if ($type eq '') { + if ($typefilter ne 'Course') { + $match = 0; + } + } else { + unless (eval('$unescapeType=~/^\Q$typefilter\E$/')) { + $match = 0; + } + } + } + if ($match == 1) { + $qresult.=$key.'='.$descr.':'.$inst_code.':'.$owner.'&'; + } } - if (untie(%hash)) { + if (&untie_domain_hash($hashref)) { chop($qresult); - Reply($client, "$qresult\n", $userinput); + &Reply($client, "$qresult\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting courseiddump\n", $userinput); } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting courseiddump\n", $userinput); } return 1; } -RegisterHandler("courseiddump", \&DumpCourseIdHandler, 0, 1, 0); +®ister_handler("courseiddump", \&dump_course_id_handler, 0, 1, 0); + +# +# Puts an unencrypted entry in a namespace db file at the domain level +# +# Parameters: +# $cmd - The command that got us here. +# $tail - Tail of the command (remaining parameters). +# $client - File descriptor connected to client. +# Returns +# 0 - Requested to exit, caller should shut down. +# 1 - Continue processing. +# Side effects: +# reply is written to $client. +# +sub put_domain_handler { + my ($cmd,$tail,$client) = @_; + + my $userinput = "$cmd:$tail"; + + my ($udom,$namespace,$what) =split(/:/,$tail,3); + chomp($what); + my @pairs=split(/\&/,$what); + my $hashref = &tie_domain_hash($udom, "$namespace", &GDBM_WRCREAT(), + "P", $what); + if ($hashref) { + foreach my $pair (@pairs) { + my ($key,$value)=split(/=/,$pair); + $hashref->{$key}=$value; + } + if (&untie_domain_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting putdom\n", $userinput); + } + } else { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting putdom\n", $userinput); + } + + return 1; +} +®ister_handler("putdom", \&put_domain_handler, 0, 1, 0); + +# Unencrypted get from the namespace database file at the domain level. +# This function retrieves a keyed item from a specific named database in the +# domain directory. +# +# Parameters: +# $cmd - Command request keyword (get). +# $tail - Tail of the command. This is a colon separated list +# consisting of the domain and the 'namespace' +# which selects the gdbm file to do the lookup in, +# & separated list of keys to lookup. Note that +# the values are returned as an & separated list too. +# $client - File descriptor open on the client. +# Returns: +# 1 - Continue processing. +# 0 - Exit. +# Side effects: +# reply is written to $client. +# + +sub get_domain_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$client:$tail"; + + my ($udom,$namespace,$what)=split(/:/,$tail,3); + chomp($what); + my @queries=split(/\&/,$what); + my $qresult=''; + my $hashref = &tie_domain_hash($udom, "$namespace", &GDBM_READER()); + if ($hashref) { + for (my $i=0;$i<=$#queries;$i++) { + $qresult.="$hashref->{$queries[$i]}&"; + } + if (&untie_domain_hash($hashref)) { + $qresult=~s/\&$//; + &Reply($client, "$qresult\n", $userinput); + } else { + &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting getdom\n",$userinput); + } + } else { + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting getdom\n",$userinput); + } + + return 1; +} +®ister_handler("getdom", \&get_id_handler, 0, 1, 0); + + # # Puts an id to a domains id database. # @@ -2329,46 +3618,37 @@ RegisterHandler("courseiddump", \&DumpCo # Side effects: # reply is written to $client. # -sub PutIdHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub put_id_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; my ($udom,$what)=split(/:/,$tail); chomp($what); - $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 $hashref = &tie_domain_hash($udom, "ids", &GDBM_WRCREAT(), + "P", $what); + if ($hashref) { foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); - $hash{$key}=$value; + $hashref->{$key}=$value; } - if (untie(%hash)) { - Reply($client, "ok\n", $userinput); + if (&untie_domain_hash($hashref)) { + &Reply($client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". - "while attempting idput\n", $userinput); + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting idput\n", $userinput); } } else { - Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". - "while attempting idput\n", $userinput); + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting idput\n", $userinput); } return 1; } +®ister_handler("idput", \&put_id_handler, 0, 1, 0); -RegisterHandler("idput", \&PutIdHandler, 0, 1, 0); # # Retrieves a set of id values from the id database. # Returns an & separated list of results, one for each requested id to the @@ -2388,41 +3668,290 @@ RegisterHandler("idput", \&PutIdHandler, # Side effects: # An & separated list of results is written to $client. # -sub GetIdHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub get_id_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$client:$tail"; - + my ($udom,$what)=split(/:/,$tail); chomp($what); - $udom=~s/\W//g; - my $proname="$perlvar{'lonUsersDir'}/$udom/ids"; my @queries=split(/\&/,$what); my $qresult=''; - my %hash; - if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_READER(),0640)) { + my $hashref = &tie_domain_hash($udom, "ids", &GDBM_READER()); + if ($hashref) { for (my $i=0;$i<=$#queries;$i++) { - $qresult.="$hash{$queries[$i]}&"; + $qresult.="$hashref->{$queries[$i]}&"; } - if (untie(%hash)) { + if (&untie_domain_hash($hashref)) { $qresult=~s/\&$//; - Reply($client, "$qresult\n", $userinput); + &Reply($client, "$qresult\n", $userinput); } else { - Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". - "while attempting idget\n",$userinput); + &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting idget\n",$userinput); } } else { - Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". - "while attempting idget\n",$userinput); + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting idget\n",$userinput); + } + + return 1; +} +®ister_handler("idget", \&get_id_handler, 0, 1, 0); + +# +# Puts broadcast e-mail sent by Domain Coordinator in nohist_dcmail database +# +# Parameters +# $cmd - Command keyword that caused us to be dispatched. +# $tail - Tail of the command. Consists of a colon separated: +# domain - the domain whose dcmail we are recording +# email Consists of key=value pair +# where key is unique msgid +# and value is message (in XML) +# $client - Socket open on the client. +# +# Returns: +# 1 - indicating processing should continue. +# Side effects +# reply is written to $client. +# +sub put_dcmail_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + + my ($udom,$what)=split(/:/,$tail); + chomp($what); + my $hashref = &tie_domain_hash($udom, "nohist_dcmail", &GDBM_WRCREAT()); + if ($hashref) { + my ($key,$value)=split(/=/,$what); + $hashref->{$key}=$value; + } + if (&untie_domain_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting dcmailput\n", $userinput); + } + return 1; +} +®ister_handler("dcmailput", \&put_dcmail_handler, 0, 1, 0); + +# +# Retrieves broadcast e-mail from nohist_dcmail database +# Returns to client an & separated list of key=value pairs, +# where key is msgid and value is message information. +# +# Parameters +# $cmd - Command keyword that caused us to be dispatched. +# $tail - Tail of the command. Consists of a colon separated: +# domain - the domain whose dcmail table we dump +# startfilter - beginning of time window +# endfilter - end of time window +# sendersfilter - & separated list of username:domain +# for senders to search for. +# $client - Socket open on the client. +# +# Returns: +# 1 - indicating processing should continue. +# Side effects +# reply (& separated list of msgid=messageinfo pairs) is +# written to $client. +# +sub dump_dcmail_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + my ($udom,$startfilter,$endfilter,$sendersfilter) = split(/:/,$tail); + chomp($sendersfilter); + my @senders = (); + if (defined($startfilter)) { + $startfilter=&unescape($startfilter); + } else { + $startfilter='.'; + } + if (defined($endfilter)) { + $endfilter=&unescape($endfilter); + } else { + $endfilter='.'; + } + if (defined($sendersfilter)) { + $sendersfilter=&unescape($sendersfilter); + @senders = map { &unescape($_) } split(/\&/,$sendersfilter); + } + + my $qresult=''; + my $hashref = &tie_domain_hash($udom, "nohist_dcmail", &GDBM_WRCREAT()); + if ($hashref) { + while (my ($key,$value) = each(%$hashref)) { + my $match = 1; + my ($timestamp,$subj,$uname,$udom) = + split(/:/,&unescape(&unescape($key)),5); # yes, twice really + $subj = &unescape($subj); + unless ($startfilter eq '.' || !defined($startfilter)) { + if ($timestamp < $startfilter) { + $match = 0; + } + } + unless ($endfilter eq '.' || !defined($endfilter)) { + if ($timestamp > $endfilter) { + $match = 0; + } + } + unless (@senders < 1) { + unless (grep/^$uname:$udom$/,@senders) { + $match = 0; + } + } + if ($match == 1) { + $qresult.=$key.'='.$value.'&'; + } + } + if (&untie_domain_hash($hashref)) { + chop($qresult); + &Reply($client, "$qresult\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting dcmaildump\n", $userinput); + } + } else { + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting dcmaildump\n", $userinput); } + return 1; +} + +®ister_handler("dcmaildump", \&dump_dcmail_handler, 0, 1, 0); + +# +# Puts domain roles in nohist_domainroles database +# +# Parameters +# $cmd - Command keyword that caused us to be dispatched. +# $tail - Tail of the command. Consists of a colon separated: +# domain - the domain whose roles we are recording +# role - Consists of key=value pair +# where key is unique role +# and value is start/end date information +# $client - Socket open on the client. +# +# Returns: +# 1 - indicating processing should continue. +# Side effects +# reply is written to $client. +# +sub put_domainroles_handler { + my ($cmd,$tail,$client) = @_; + + my $userinput = "$cmd:$tail"; + my ($udom,$what)=split(/:/,$tail); + chomp($what); + my @pairs=split(/\&/,$what); + my $hashref = &tie_domain_hash($udom, "nohist_domainroles", &GDBM_WRCREAT()); + if ($hashref) { + foreach my $pair (@pairs) { + my ($key,$value)=split(/=/,$pair); + $hashref->{$key}=$value; + } + if (&untie_domain_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting domroleput\n", $userinput); + } + } else { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting domroleput\n", $userinput); + } + return 1; } -RegisterHandler("idget", \&GetIdHandler, 0, 1, 0); +®ister_handler("domroleput", \&put_domainroles_handler, 0, 1, 0); + # +# Retrieves domain roles from nohist_domainroles database +# Returns to client an & separated list of key=value pairs, +# where key is role and value is start and end date information. +# +# Parameters +# $cmd - Command keyword that caused us to be dispatched. +# $tail - Tail of the command. Consists of a colon separated: +# domain - the domain whose domain roles table we dump +# $client - Socket open on the client. +# +# Returns: +# 1 - indicating processing should continue. +# Side effects +# reply (& separated list of role=start/end info pairs) is +# written to $client. +# +sub dump_domainroles_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + my ($udom,$startfilter,$endfilter,$rolesfilter) = split(/:/,$tail); + chomp($rolesfilter); + my @roles = (); + if (defined($startfilter)) { + $startfilter=&unescape($startfilter); + } else { + $startfilter='.'; + } + if (defined($endfilter)) { + $endfilter=&unescape($endfilter); + } else { + $endfilter='.'; + } + if (defined($rolesfilter)) { + $rolesfilter=&unescape($rolesfilter); + @roles = split(/\&/,$rolesfilter); + } + + my $hashref = &tie_domain_hash($udom, "nohist_domainroles", &GDBM_WRCREAT()); + if ($hashref) { + my $qresult = ''; + while (my ($key,$value) = each(%$hashref)) { + my $match = 1; + my ($start,$end) = split(/:/,&unescape($value)); + my ($trole,$uname,$udom,$runame,$rudom,$rsec) = split(/:/,&unescape($key)); + unless ($startfilter eq '.' || !defined($startfilter)) { + if ($start >= $startfilter) { + $match = 0; + } + } + unless ($endfilter eq '.' || !defined($endfilter)) { + if ($end <= $endfilter) { + $match = 0; + } + } + unless (@roles < 1) { + unless (grep/^$trole$/,@roles) { + $match = 0; + } + } + if ($match == 1) { + $qresult.=$key.'='.$value.'&'; + } + } + if (&untie_domain_hash($hashref)) { + chop($qresult); + &Reply($client, "$qresult\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting domrolesdump\n", $userinput); + } + } else { + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting domrolesdump\n", $userinput); + } + return 1; +} + +®ister_handler("domrolesdump", \&dump_domainroles_handler, 0, 1, 0); + + # Process the tmpput command I'm not sure what this does.. Seems to # create a file in the lonDaemons/tmp directory of the form $id.tmp # where Id is the client's ip concatenated with a sequence number. @@ -2439,32 +3968,38 @@ RegisterHandler("idget", \&GetIdHandler, # Side effects: # A file is created in the local filesystem. # A reply is sent to the client. -sub TmpPutHandler { - my $cmd = shift; - my $what = shift; - my $client = shift; +sub tmp_put_handler { + my ($cmd, $what, $client) = @_; my $userinput = "$cmd:$what"; # Reconstruct for logging. - - my $store; + my ($record,$context) = split(/:/,$what); + if ($context ne '') { + chomp($context); + $context = &unescape($context); + } + my ($id,$store); $tmpsnum++; - my $id=$$.'_'.$clientip.'_'.$tmpsnum; + if ($context eq 'resetpw') { + $id = &md5_hex(&md5_hex(time.{}.rand().$$)); + } else { + $id = $$.'_'.$clientip.'_'.$tmpsnum; + } $id=~s/\W/\_/g; - $what=~s/\n//g; + $record=~s/\n//g; my $execdir=$perlvar{'lonDaemons'}; if ($store=IO::File->new(">$execdir/tmp/$id.tmp")) { - print $store $what; + print $store $record; close $store; - Reply($client, "$id\n", $userinput); + &Reply($client, "$id\n", $userinput); } else { - Failure( $client, "error: ".($!+0)."IO::File->new Failed ". - "while attempting tmpput\n", $userinput); + &Failure( $client, "error: ".($!+0)."IO::File->new Failed ". + "while attempting tmpput\n", $userinput); } return 1; } -RegisterHandler("tmpput", \&TmpPutHandler, 0, 1, 0); +®ister_handler("tmpput", \&tmp_put_handler, 0, 1, 0); # Processes the tmpget command. This command returns the contents # of a temporary resource file(?) created via tmpput. @@ -2478,30 +4013,29 @@ RegisterHandler("tmpput", \&TmpPutHandle # 1 - Inidcating processing can continue. # Side effects: # A reply is sent to the client. - # -sub TmpGetHandler { - my $cmd = shift; - my $id = shift; - my $client = shift; +sub tmp_get_handler { + my ($cmd, $id, $client) = @_; + my $userinput = "$cmd:$id"; + - chomp($id); $id=~s/\W/\_/g; my $store; my $execdir=$perlvar{'lonDaemons'}; if ($store=IO::File->new("$execdir/tmp/$id.tmp")) { my $reply=<$store>; - Reply( $client, "$reply\n", $userinput); + &Reply( $client, "$reply\n", $userinput); close $store; } else { - Failure( $client, "error: ".($!+0)."IO::File->new Failed ". - "while attempting tmpget\n", $userinput); + &Failure( $client, "error: ".($!+0)."IO::File->new Failed ". + "while attempting tmpget\n", $userinput); } return 1; } -RegisterHandler("tmpget", \&TmpGetHandler, 0, 1, 0); +®ister_handler("tmpget", \&tmp_get_handler, 0, 1, 0); + # # Process the tmpdel command. This command deletes a temp resource # created by the tmpput command. @@ -2516,77 +4050,25 @@ RegisterHandler("tmpget", \&TmpGetHandle # Side Effects: # A file is deleted # A reply is sent to the client. -sub TmpDelHandler { - my $cmd = shift; - my $id = shift; - my $client = shift; - +sub tmp_del_handler { + my ($cmd, $id, $client) = @_; + my $userinput= "$cmd:$id"; - + chomp($id); $id=~s/\W/\_/g; my $execdir=$perlvar{'lonDaemons'}; if (unlink("$execdir/tmp/$id.tmp")) { - Reply($client, "ok\n", $userinput); + &Reply($client, "ok\n", $userinput); } else { - Failure( $client, "error: ".($!+0)."Unlink tmp Failed ". - "while attempting tmpdel\n", $userinput); + &Failure( $client, "error: ".($!+0)."Unlink tmp Failed ". + "while attempting tmpdel\n", $userinput); } - + return 1; } -RegisterHandler("tmpdel", \&TmpDelHandler, 0, 1, 0); -# -# ls - list the contents of a directory. For each file in the -# selected directory the filename followed by the full output of -# the stat function is returned. The returned info for each -# file are separated by ':'. The stat fields are separated by &'s. -# Parameters: -# $cmd - The command that dispatched us (ls). -# $ulsdir - The directory path to list... I'm not sure what this -# is relative as things like ls:. return e.g. -# no_such_dir. -# $client - Socket open on the client. -# Returns: -# 1 - indicating that the daemon should not disconnect. -# Side Effects: -# The reply is written to $client. -# -sub LsHandler { - my $cmd = shift; - my $ulsdir = shift; - my $client = shift; - - my $userinput = "$cmd:$ulsdir"; - - my $ulsout=''; - my $ulsfn; - if (-e $ulsdir) { - if(-d $ulsdir) { - if (opendir(LSDIR,$ulsdir)) { - while ($ulsfn=readdir(LSDIR)) { - my @ulsstats=stat($ulsdir.'/'.$ulsfn); - $ulsout.=$ulsfn.'&'. - join('&',@ulsstats).':'; - } - closedir(LSDIR); - } - } else { - my @ulsstats=stat($ulsdir); - $ulsout.=$ulsfn.'&'.join('&',@ulsstats).':'; - } - } else { - $ulsout='no_such_dir'; - } - if ($ulsout eq '') { $ulsout='empty'; } - Reply($client, "$ulsout\n", $userinput); - - - return 1; -} -RegisterHandler("ls", \&LsHandler, 0, 1, 0); - +®ister_handler("tmpdel", \&tmp_del_handler, 0, 1, 0); # # Processes the setannounce command. This command @@ -2607,10 +4089,8 @@ RegisterHandler("ls", \&LsHandler, 0, 1, # The file {DocRoot}/announcement.txt is created. # A reply is sent to $client. # -sub SetAnnounceHandler { - my $cmd = shift; - my $announcement = shift; - my $client = shift; +sub set_announce_handler { + my ($cmd, $announcement, $client) = @_; my $userinput = "$cmd:$announcement"; @@ -2620,14 +4100,14 @@ sub SetAnnounceHandler { '/announcement.txt')) { print $store $announcement; close $store; - Reply($client, "ok\n", $userinput); + &Reply($client, "ok\n", $userinput); } else { - Failure($client, "error: ".($!+0)."\n", $userinput); + &Failure($client, "error: ".($!+0)."\n", $userinput); } return 1; } -RegisterHandler("setannounce", \&SetAnnounceHandler, 0, 1, 0); +®ister_handler("setannounce", \&set_announce_handler, 0, 1, 0); # # Return the version of the daemon. This can be used to determine @@ -2642,18 +4122,17 @@ RegisterHandler("setannounce", \&SetAnno # 1 - continue processing requests. # Side Effects: # Replies with version to $client. -sub GetVersionHandler { - my $client = shift; - my $tail = shift; - my $client = shift; - my $userinput = $client; +sub get_version_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = $cmd.$tail; - Reply($client, &version($userinput)."\n", $userinput); + &Reply($client, &version($userinput)."\n", $userinput); return 1; } -RegisterHandler("version", \&GetVersionHandler, 0, 1, 0); +®ister_handler("version", \&get_version_handler, 0, 1, 0); # Set the current host and domain. This is used to support # multihomed systems. Each IP of the system, or even separate daemons @@ -2672,19 +4151,17 @@ RegisterHandler("version", \&GetVersionH # The default domain/system context is modified for this daemon. # a reply is sent to the client. # -sub SelectHostHandler { - my $cmd = shift; - my $tail = shift; - my $socket = shift; +sub set_virtual_host_handler { + my ($cmd, $tail, $socket) = @_; my $userinput ="$cmd:$tail"; - Reply($client, &sethost($userinput)."\n", $userinput); + &Reply($client, &sethost($userinput)."\n", $userinput); return 1; } -RegisterHandler("sethost", \&SelectHostHandler, 0, 1, 0); +®ister_handler("sethost", \&set_virtual_host_handler, 0, 1, 0); # Process a request to exit: # - "bye" is sent to the client. @@ -2697,698 +4174,694 @@ RegisterHandler("sethost", \&SelectHostH # Returns: # 0 - Indicating the program should exit!! # -sub ExitHandler { - my $cmd = shift; - my $tail = shift; - my $client = shift; +sub exit_handler { + my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; &logthis("Client $clientip ($clientname) hanging up: $userinput"); - Reply($client, "bye\n", $userinput); + &Reply($client, "bye\n", $userinput); $client->shutdown(2); # shutdown the socket forcibly. $client->close(); return 0; } -RegisterHandler("exit", \&ExitHandler, 0, 1,1); -RegisterHandler("init", \&ExitHandler, 0, 1,1); # RE-init is like exit. -RegisterHandler("quit", \&ExitHandler, 0, 1,1); # I like this too! -#------------------------------------------------------------------------------------ -# -# Process a Request. Takes a request from the client validates -# it and performs the operation requested by it. Returns -# a response to the client. -# -# Parameters: -# request - A string containing the user's request. -# Returns: -# 0 - Requested to exit, caller should shut down. -# 1 - Accept additional requests from the client. +®ister_handler("exit", \&exit_handler, 0,1,1); +®ister_handler("init", \&exit_handler, 0,1,1); +®ister_handler("quit", \&exit_handler, 0,1,1); + +# Determine if auto-enrollment is enabled. +# Note that the original had what I believe to be a defect. +# The original returned 0 if the requestor was not a registerd client. +# It should return "refused". +# Formal Parameters: +# $cmd - The command that invoked us. +# $tail - The tail of the command (Extra command parameters. +# $client - The socket open on the client that issued the request. +# Returns: +# 1 - Indicating processing should continue. # -sub ProcessRequest { - my $Request = shift; - my $KeepGoing = 1; # Assume we're not asked to stop. +sub enrollment_enabled_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = $cmd.":".$tail; # For logging purposes. + - my $wasenc=0; - my $userinput = $Request; # for compatibility with oldcode + my ($cdom) = split(/:/, $tail, 2); # Domain we're asking about. + my $outcome = &localenroll::run($cdom); + &Reply($client, "$outcome\n", $userinput); -# ------------------------------------------------------------ See if encrypted - - if($userinput =~ /^enc/) { - $wasenc = 1; - $userinput = Decipher($userinput); - if(! $userinput) { - Failure($client,"error:Encrypted data without negotiating key"); - return 0; # Break off with this imposter. - } - } - # Split off the request keyword from the rest of the stuff. - - my ($command, $tail) = split(/:/, $userinput, 2); + return 1; +} +®ister_handler("autorun", \&enrollment_enabled_handler, 0, 1, 0); - Debug("Command received: $command, encoded = $wasenc"); +# Get the official sections for which auto-enrollment is possible. +# Since the admin people won't know about 'unofficial sections' +# we cannot auto-enroll on them. +# Formal Parameters: +# $cmd - The command request that got us dispatched here. +# $tail - The remainder of the request. In our case this +# will be split into: +# $coursecode - The course name from the admin point of view. +# $cdom - The course's domain(?). +# $client - Socket open on the client. +# Returns: +# 1 - Indiciting processing should continue. +# +sub get_sections_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - -# ------------------------------------------------------------- Normal commands + my ($coursecode, $cdom) = split(/:/, $tail); + my @secs = &localenroll::get_sections($coursecode,$cdom); + my $seclist = &escape(join(':',@secs)); - # - # If the command is in the hash, then execute it via the hash dispatch: - # - if(defined $Dispatcher{$command}) { + &Reply($client, "$seclist\n", $userinput); + - my $DispatchInfo = $Dispatcher{$command}; - my $Handler = $$DispatchInfo[0]; - my $NeedEncode = $$DispatchInfo[1]; - my $ClientTypes = $$DispatchInfo[2]; - Debug("Matched dispatch hash: mustencode: $NeedEncode ClientType $ClientTypes"); - - # Validate the request: - - my $ok = 1; - my $requesterprivs = 0; - if(isClient()) { - $requesterprivs |= $CLIENT_OK; - } - if(isManager()) { - $requesterprivs |= $MANAGER_OK; - } - if($NeedEncode && (!$wasenc)) { - Debug("Must encode but wasn't: $NeedEncode $wasenc"); - $ok = 0; - } - if(($ClientTypes & $requesterprivs) == 0) { - Debug("Client not privileged to do this operation"); - $ok = 0; - } + return 1; +} +®ister_handler("autogetsections", \&get_sections_handler, 0, 1, 0); - if($ok) { - Debug("Dispatching to handler $command $tail"); - $KeepGoing = &$Handler($command, $tail, $client); - } else { - Debug("Refusing to dispatch because ok is false"); - Failure($client, "refused", $userinput); - } +# Validate the owner of a new course section. +# +# Formal Parameters: +# $cmd - Command that got us dispatched. +# $tail - the remainder of the command. For us this consists of a +# colon separated string containing: +# $inst - Course Id from the institutions point of view. +# $owner - Proposed owner of the course. +# $cdom - Domain of the course (from the institutions +# point of view?).. +# $client - Socket open on the client. +# +# Returns: +# 1 - Processing should continue. +# +sub validate_course_owner_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($inst_course_id, $owner, $cdom) = split(/:/, $tail); + $owner = &unescape($owner); + my $outcome = &localenroll::new_course($inst_course_id,$owner,$cdom); + &Reply($client, "$outcome\n", $userinput); -# ------------------------------------------------------------- unknown command - } else { - # unknown command - Failure($client, "unknown_cmd\n", $userinput); - } - return $KeepGoing; + return 1; } - +®ister_handler("autonewcourse", \&validate_course_owner_handler, 0, 1, 0); # -# GetCertificate: Given a transaction that requires a certificate, -# this function will extract the certificate from the transaction -# request. Note that at this point, the only concept of a certificate -# is the hostname to which we are connected. +# Validate a course section in the official schedule of classes +# from the institutions point of view (part of autoenrollment). # -# Parameter: -# request - The request sent by our client (this parameterization may -# need to change when we really use a certificate granting -# authority. +# Formal Parameters: +# $cmd - The command request that got us dispatched. +# $tail - The tail of the command. In this case, +# this is a colon separated set of words that will be split +# into: +# $inst_course_id - The course/section id from the +# institutions point of view. +# $cdom - The domain from the institutions +# point of view. +# $client - Socket open on the client. +# Returns: +# 1 - Indicating processing should continue. # -sub GetCertificate { - my $request = shift; +sub validate_course_section_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($inst_course_id, $cdom) = split(/:/, $tail); - return $clientip; -} + my $outcome=&localenroll::validate_courseID($inst_course_id,$cdom); + &Reply($client, "$outcome\n", $userinput); + return 1; +} +®ister_handler("autovalidatecourse", \&validate_course_section_handler, 0, 1, 0); # -# ReadManagerTable: Reads in the current manager table. For now this is -# done on each manager authentication because: -# - These authentications are not frequent -# - This allows dynamic changes to the manager table -# without the need to signal to the lond. +# Validate course owner's access to enrollment data for specific class section. +# +# +# Formal Parameters: +# $cmd - The command request that got us dispatched. +# $tail - The tail of the command. In this case this is a colon separated +# set of words that will be split into: +# $inst_class - Institutional code for the specific class section +# $courseowner - The escaped username:domain of the course owner +# $cdom - The domain of the course from the institution's +# point of view. +# $client - The socket open on the client. +# Returns: +# 1 - continue processing. # -sub ReadManagerTable { - - # Clean out the old table first.. - - foreach my $key (keys %managers) { - delete $managers{$key}; - } +sub validate_class_access_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($inst_class,$courseowner,$cdom) = split(/:/, $tail); + $courseowner = &unescape($courseowner); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome=&localenroll::check_section($inst_class,$courseowner,$cdom); + }; + &Reply($client,"$outcome\n", $userinput); - 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. - logthis(' Skipping line: '. "$host\n"); - 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); - - 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 - } - } + return 1; } +®ister_handler("autovalidateclass_sec", \&validate_class_access_handler, 0, 1, 0); # -# ValidManager: Determines if a given certificate represents a valid manager. -# in this primitive implementation, the 'certificate' is -# just the connecting loncapa client name. This is checked -# against a valid client list in the configuration. -# -# -sub ValidManager { - my $certificate = shift; - - return isManager; -} +# Create a password for a new LON-CAPA user added by auto-enrollment. +# Only used for case where authentication method for new user is localauth # -# CopyFile: Called as part of the process of installing a -# new configuration file. This function copies an existing -# file to a backup file. -# Parameters: -# oldfile - Name of the file to backup. -# newfile - Name of the backup file. -# Return: -# 0 - Failure (errno has failure reason). -# 1 - Success. +# Formal Parameters: +# $cmd - The command request that got us dispatched. +# $tail - The tail of the command. In this case this is a colon separated +# set of words that will be split into: +# $authparam - An authentication parameter (localauth parameter). +# $cdom - The domain of the course from the institution's +# point of view. +# $client - The socket open on the client. +# Returns: +# 1 - continue processing. # -sub CopyFile { - my $oldfile = shift; - my $newfile = shift; - - # The file must exist: - - if(-e $oldfile) { +sub create_auto_enroll_password_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; - # Read the old file. + my ($authparam, $cdom) = split(/:/, $userinput); - my $oldfh = IO::File->new("< $oldfile"); - if(!$oldfh) { - return 0; - } - my @contents = <$oldfh>; # Suck in the entire file. + my ($create_passwd,$authchk); + ($authparam, + $create_passwd, + $authchk) = &localenroll::create_password($authparam,$cdom); - # write the backup file: + &Reply($client, &escape($authparam.':'.$create_passwd.':'.$authchk)."\n", + $userinput); - my $newfh = IO::File->new("> $newfile"); - if(!(defined $newfh)){ - return 0; - } - my $lines = scalar @contents; - for (my $i =0; $i < $lines; $i++) { - print $newfh ($contents[$i]); - } - $oldfh->close; - $newfh->close; + return 1; +} +®ister_handler("autocreatepassword", \&create_auto_enroll_password_handler, + 0, 1, 0); - chmod(0660, $newfile); +# Retrieve and remove temporary files created by/during autoenrollment. +# +# Formal Parameters: +# $cmd - The command that got us dispatched. +# $tail - The tail of the command. In our case this is a colon +# separated list that will be split into: +# $filename - The name of the file to remove. +# The filename is given as a path relative to +# the LonCAPA temp file directory. +# $client - Socket open on the client. +# +# Returns: +# 1 - Continue processing. +sub retrieve_auto_file_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "cmd:$tail"; + + my ($filename) = split(/:/, $tail); + + 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); + &Reply($client, &escape($reply)."\n", $userinput); - return 1; - +# Does this have to be uncommented??!? (RF). +# +# unlink($source); + } else { + &Failure($client, "error\n", $userinput); + } } else { - return 0; + &Failure($client, "error\n", $userinput); } + + + return 1; } +®ister_handler("autoretrieve", \&retrieve_auto_file_handler, 0,1,0); + # -# Host files are passed out with externally visible host IPs. -# If, for example, we are behind a fire-wall or NAT host, our -# internally visible IP may be different than the externally -# visible IP. Therefore, we always adjust the contents of the -# host file so that the entry for ME is the IP that we believe -# we have. At present, this is defined as the entry that -# DNS has for us. If by some chance we are not able to get a -# DNS translation for us, then we assume that the host.tab file -# is correct. -# BUGBUGBUG - in the future, we really should see if we can -# easily query the interface(s) instead. -# Parameter(s): -# contents - The contents of the host.tab to check. +# Read and retrieve institutional code format (for support form). +# Formal Parameters: +# $cmd - Command that dispatched us. +# $tail - Tail of the command. In this case it conatins +# the course domain and the coursename. +# $client - Socket open on the client. # Returns: -# newcontents - The adjusted contents. -# +# 1 - Continue processing. # -sub AdjustHostContents { - my $contents = shift; - my $adjusted; - my $me = $perlvar{'lonHostID'}; +sub get_institutional_code_format_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + + my $reply; + my($cdom,$course) = split(/:/,$tail); + 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); + &Reply($client, + $codes_str.':'.$codetitles_str.':'.$cat_titles_str.':' + .$cat_order_str."\n", + $userinput); + } else { + # this else branch added by RF since if not ok, lonc will + # hang waiting on reply until timeout. + # + &Reply($client, "format_error\n", $userinput); + } + + return 1; +} +®ister_handler("autoinstcodeformat", + \&get_institutional_code_format_handler,0,1,0); - 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) { - 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"; - - } else { # Not me, pass unmodified. - $adjusted .= $line."\n"; - } - } else { # Blank or comment never re-written. - $adjusted .= $line."\n"; # Pass blanks and comments as is. - } +sub get_institutional_defaults_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + + my $dom = $tail; + my %defaults_hash; + my @code_order; + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::instcode_defaults($dom,\%defaults_hash, + \@code_order); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result=''; + while (my ($key,$value) = each(%defaults_hash)) { + $result.=&escape($key).'='.&escape($value).'&'; + } + $result .= 'code_order='.&escape(join('&',@code_order)); + &Reply($client,$result."\n",$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); } - return $adjusted; } +®ister_handler("autoinstcodedefaults", + \&get_institutional_defaults_handler,0,1,0); + + +# Get domain specific conditions for import of student photographs to a course # -# InstallFile: Called to install an administrative file: -# - The file is created with .tmp -# - The .tmp file is then mv'd to -# This lugubrious procedure is done to ensure that we are never without -# a valid, even if dated, version of the file regardless of who crashes -# and when the crash occurs. -# -# Parameters: -# Name of the file -# File Contents. -# Return: -# nonzero - success. -# 0 - failure and $! has an errno. +# Retrieves information from photo_permission subroutine in localenroll. +# Returns outcome (ok) if no processing errors, and whether course owner is +# required to accept conditions of use (yes/no). # -sub InstallFile { - my $Filename = shift; - my $Contents = shift; - my $TempFile = $Filename.".tmp"; - - # Open the file for write: - - my $fh = IO::File->new("> $TempFile"); # Write to temp. - if(!(defined $fh)) { - &logthis(' Unable to create '.$TempFile.""); - return 0; +# +sub photo_permission_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my $cdom = $tail; + my ($perm_reqd,$conditions); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::photo_permission($cdom,\$perm_reqd, + \$conditions); + }; + if (!$@) { + &Reply($client, &escape($outcome.':'.$perm_reqd.':'. $conditions)."\n", + $userinput); + } else { + &Failure($client,"unknown_cmd\n",$userinput); } - # write the contents of the file: - - print $fh ($Contents); - $fh->close; # In case we ever have a filesystem w. locking - - chmod(0660, $TempFile); + return 1; +} +®ister_handler("autophotopermission",\&photo_permission_handler,0,1,0); - # Now we can move install the file in position. - - move($TempFile, $Filename); +# +# Checks if student photo is available for a user in the domain, in the user's +# directory (in /userfiles/internal/studentphoto.jpg). +# Uses localstudentphoto:fetch() to ensure there is an up to date copy of +# the student's photo. +sub photo_check_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($udom,$uname,$pid) = split(/:/,$tail); + $udom = &unescape($udom); + $uname = &unescape($uname); + $pid = &unescape($pid); + my $path=&propath($udom,$uname).'/userfiles/internal/'; + if (!-e $path) { + &mkpath($path); + } + my $response; + my $result = &localstudentphoto::fetch($udom,$uname,$pid,\$response); + $result .= ':'.$response; + &Reply($client, &escape($result)."\n",$userinput); return 1; } +®ister_handler("autophotocheck",\&photo_check_handler,0,1,0); + # -# 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; +# Retrieve information from localenroll about whether to provide a button +# for users who have enbled import of student photos to initiate an +# update of photo files for registered students. Also include +# comment to display alongside button. - my $tabledir = $perlvar{'lonTabDir'}.'/'; - if ($selector eq "hosts") { - $tablefile = $tabledir."hosts.tab"; - } elsif ($selector eq "domain") { - $tablefile = $tabledir."domain.tab"; +sub photo_choice_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my $cdom = &unescape($tail); + my ($update,$comment); + eval { + local($SIG{__DIE__})='DEFAULT'; + ($update,$comment) = &localenroll::manager_photo_update($cdom); + }; + if (!$@) { + &Reply($client,&escape($update).':'.&escape($comment)."\n",$userinput); } else { - return undef; + &Failure($client,"unknown_cmd\n",$userinput); } - return $tablefile; - + return 1; } -# -# PushFile: Called to do an administrative push of a file. -# - Ensure the file being pushed is one we support. -# - Backup the old file to -# - Separate the contents of the new file out from the -# rest of the request. -# - Write the new file. -# Parameter: -# Request - The entire user request. This consists of a : separated -# string pushfile:tablename:contents. -# NOTE: The contents may have :'s in it as well making things a bit -# more interesting... but not much. -# Returns: -# String to send to client ("ok" or "refused" if bad file). -# -sub PushFile { - my $request = shift; - my ($command, $filename, $contents) = split(":", $request, 3); - - # At this point in time, pushes for only the following tables are - # supported: - # hosts.tab ($filename eq host). - # domain.tab ($filename eq domain). - # Construct the destination filename or reject the request. - # - # lonManage is supposed to ensure this, however this session could be - # part of some elaborate spoof that managed somehow to authenticate. - # +®ister_handler("autophotochoice",\&photo_choice_handler,0,1,0); +# +# Gets a student's photo to exist (in the correct image type) in the user's +# directory. +# Formal Parameters: +# $cmd - The command request that got us dispatched. +# $tail - A colon separated set of words that will be split into: +# $domain - student's domain +# $uname - student username +# $type - image type desired +# $client - The socket open on the client. +# Returns: +# 1 - continue processing. - my $tablefile = ConfigFileFromSelector($filename); - if(! (defined $tablefile)) { - return "refused"; +sub student_photo_handler { + my ($cmd, $tail, $client) = @_; + my ($domain,$uname,$ext,$type) = split(/:/, $tail); + + my $path=&propath($domain,$uname). '/userfiles/internal/'; + my $filename = 'studentphoto.'.$ext; + if ($type eq 'thumbnail') { + $filename = 'studentphoto_tn.'.$ext; } - # - # >copy< the old table to the backup table - # don't rename in case system crashes/reboots etc. in the time - # window between a rename and write. - # - my $backupfile = $tablefile; - $backupfile =~ s/\.tab$/.old/; - if(!CopyFile($tablefile, $backupfile)) { - &logthis(' CopyFile from '.$tablefile." to ".$backupfile." failed "); - return "error:$!"; - } - &logthis(' Pushfile: backed up ' - .$tablefile." to $backupfile"); - - # If the file being pushed is the host file, we adjust the entry for ourself so that the - # IP will be our current IP as looked up in dns. Note this is only 99% good as it's possible - # to conceive of conditions where we don't have a DNS entry locally. This is possible in a - # network sense but it doesn't make much sense in a LonCAPA sense so we ignore (for now) - # that possibilty. - - if($filename eq "host") { - $contents = AdjustHostContents($contents); + if (-e $path.$filename) { + &Reply($client,"ok\n","$cmd:$tail"); + return 1; } - - # Install the new file: - - if(!InstallFile($tablefile, $contents)) { - &logthis(' Pushfile: unable to install ' - .$tablefile." $! "); - return "error:$!"; + &mkpath($path); + my $file; + if ($type eq 'thumbnail') { + eval { + local($SIG{__DIE__})='DEFAULT'; + $file=&localstudentphoto::fetch_thumbnail($domain,$uname); + }; } else { - &logthis(' Installed new '.$tablefile - .""); - + $file=&localstudentphoto::fetch($domain,$uname); } + if (!$file) { + &Failure($client,"unavailable\n","$cmd:$tail"); + return 1; + } + if (!-e $path.$filename) { &convert_photo($file,$path.$filename); } + if (-e $path.$filename) { + &Reply($client,"ok\n","$cmd:$tail"); + return 1; + } + &Failure($client,"unable_to_convert\n","$cmd:$tail"); + return 1; +} +®ister_handler("studentphoto", \&student_photo_handler, 0, 1, 0); - - # Indicate success: - - return "ok"; - +# mkpath makes all directories for a file, expects an absolute path with a +# file or a trailing / if just a dir is passed +# returns 1 on success 0 on failure +sub mkpath { + my ($file)=@_; + my @parts=split(/\//,$file,-1); + my $now=$parts[0].'/'.$parts[1].'/'.$parts[2]; + for (my $i=3;$i<= ($#parts-1);$i++) { + $now.='/'.$parts[$i]; + if (!-e $now) { + if (!mkdir($now,0770)) { return 0; } + } + } + return 1; } +#--------------------------------------------------------------- # -# Called to re-init either lonc or lond. +# Getting, decoding and dispatching requests: # -# Parameters: -# request - The full request by the client. This is of the form -# reinit: -# where is allowed to be either of -# lonc or lond -# -# Returns: -# The string to be sent back to the client either: -# ok - Everything worked just fine. -# error:why - There was a failure and why describes the reason. # +# 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 ReinitProcess { - my $request = shift; +sub get_request { + my $input = <$client>; + chomp($input); + &Debug("get_request: Request = $input\n"); - # separate the request (reinit) from the process identifier and - # validate it producing the name of the .pid file for the process. - # - # - my ($junk, $process) = split(":", $request); - my $processpidfile = $perlvar{'lonDaemons'}.'/logs/'; - if($process eq 'lonc') { - $processpidfile = $processpidfile."lonc.pid"; - if (!open(PIDFILE, "< $processpidfile")) { - return "error:Open failed for $processpidfile"; - } - my $loncpid = ; - close(PIDFILE); - logthis(' Reinitializing lonc pid='.$loncpid - .""); - kill("USR2", $loncpid); - } elsif ($process eq 'lond') { - logthis(' Reinitializing self (lond) '); - &UpdateHosts; # Lond is us!! - } else { - &logthis('"); - return "error:Invalid process identifier $process"; - } - return 'ok'; + &status('Processing '.$clientname.':'.$input); + + return $input; } -# 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. +# Process a request. This sub should shrink as each action +# gets farmed out into a separat sub that is registered +# with the dispatch hash. # -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. +# Parameters: +# user_input - The request received from the client (lonc). +# Returns: +# true to keep processing, false if caller should exit. +# +sub process_request { + my ($userinput) = @_; # Easier for now to break style than to + # fix all the userinput -> user_input. + my $wasenc = 0; # True if request was encrypted. +# ------------------------------------------------------------ See if encrypted + # for command + # sethost: + # : + # we just send it to the processor + # for + # sethost::: + # we do the implict set host and then do the command + if ($userinput =~ /^sethost:/) { + (my $cmd,my $newid,$userinput) = split(':',$userinput,3); + if (defined($userinput)) { + &sethost("$cmd:$newid"); } else { - return 1; # Valid syntax. + $userinput = "$cmd:$newid"; } - } 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 { + } + + if ($userinput =~ /^enc/) { + $userinput = decipher($userinput); + $wasenc=1; + if(!$userinput) { # Cipher not defined. + &Failure($client, "error: Encrypted data without negotated key\n"); 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 = shift; - my $editor = shift; - - # 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); + Debug("process_request: $userinput\n"); + + # + # The 'correct way' to add a command to lond is now to + # write a sub to execute it and Add it to the command dispatch + # hash via a call to register_handler.. The comments to that + # sub should give you enough to go on to show how to do this + # along with the examples that are building up as this code + # is getting refactored. Until all branches of the + # if/elseif monster below have been factored out into + # separate procesor subs, if the dispatch hash is missing + # the command keyword, we will fall through to the remainder + # of the if/else chain below in order to keep this thing in + # working order throughout the transmogrification. - 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"; + my ($command, $tail) = split(/:/, $userinput, 2); + chomp($command); + chomp($tail); + $tail =~ s/(\r)//; # This helps people debugging with e.g. telnet. + $command =~ s/(\r)//; # And this too for parameterless commands. + if(!$tail) { + $tail =""; # defined but blank. } -} -# -# 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. + &Debug("Command received: $command, encoded = $wasenc"); - my $myHostName = $perlvar{'lonHostID'}; # LonCAPA hostname. + if(defined $Dispatcher{$command}) { - # Get my host file entry. + my $dispatch_info = $Dispatcher{$command}; + my $handler = $$dispatch_info[0]; + my $need_encode = $$dispatch_info[1]; + my $client_types = $$dispatch_info[2]; + Debug("Matched dispatch hash: mustencode: $need_encode " + ."ClientType $client_types"); + + # Validate the request: + + my $ok = 1; + my $requesterprivs = 0; + if(&isClient()) { + $requesterprivs |= $CLIENT_OK; + } + if(&isManager()) { + $requesterprivs |= $MANAGER_OK; + } + if($need_encode && (!$wasenc)) { + Debug("Must encode but wasn't: $need_encode $wasenc"); + $ok = 0; + } + if(($client_types & $requesterprivs) == 0) { + Debug("Client not privileged to do this operation"); + $ok = 0; + } - 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: + if($ok) { + Debug("Dispatching to handler $command $tail"); + my $keep_going = &$handler($command, $tail, $client); + return $keep_going; + } else { + Debug("Refusing to dispatch because client did not match requirements"); + Failure($client, "refused\n", $userinput); + return 1; + } - $editor->ReplaceLine($id, $newConfigLine); - + } + + print $client "unknown_cmd\n"; +# -------------------------------------------------------------------- complete + Debug("process_request - returning 1"); + return 1; } # -# 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. +# 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 ReplaceConfigFile { - my $filename = shift; - my $editor = shift; - - CopyFile ($filename, $filename.".old"); - - my $contents = $editor->Get(); # Get the contents of the file. - - InstallFile($filename, $contents); +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; + } } -# + # -# Called to edit a configuration table file +# 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 - The entire command/request sent by lonc or lonManage -# Return: -# The reply to send to the client. +# $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 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: +sub register_handler { + my ($request_name,$procedure,$must_encode, $client_ok,$manager_ok) = @_; - if($request != "edit") { # Something is amiss afoot alack. - return "error:edit request detected, but request != 'edit'\n"; + # Don't allow duplication# + + if (defined $Dispatcher{$request_name}) { + die "Attempting to define a duplicate request handler for $request_name\n"; } - if( ($filetype ne "hosts") && - ($filetype ne "domain")) { - return "error:edit requested with invalid file specifier: $filetype \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; + +} - # 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"; -} # # Convert an error return code from lcpasswd to a string value. # @@ -3419,22 +4892,22 @@ sub catchexception { $SIG{'QUIT'}='DEFAULT'; $SIG{__DIE__}='DEFAULT'; &status("Catching exception"); - &logthis("CRITICAL: " - ."ABNORMAL EXIT. Child $$ for server $thisserver died through " - ."a crash with this error msg->[$error]"); + &logthis("CRITICAL: " + ."ABNORMAL EXIT. Child $$ for server $thisserver died through " + ."a crash with this error msg->[$error]"); &logthis('Famous last words: '.$status.' - '.$lastlog); if ($client) { print $client "error: $error\n"; } $server->close(); die($error); } - sub timeout { &status("Handling Timeout"); - &logthis("CRITICAL: TIME OUT ".$$.""); + &logthis("CRITICAL: TIME OUT ".$$.""); &catchexception('Timeout'); } # -------------------------------- Set signal handlers to record abnormal exits + $SIG{'QUIT'}=\&catchexception; $SIG{__DIE__}=\&catchexception; @@ -3447,11 +4920,11 @@ undef $perlvarref; # ----------------------------- Make sure this process is running from user=www my $wwwid=getpwnam('www'); if ($wwwid!=$<) { - my $emailto="$perlvar{'lonAdmEMail'},$perlvar{'lonSysEMail'}"; - my $subj="LON: $currenthostid User ID mismatch"; - system("echo 'User ID mismatch. lond must be run as user www.' |\ + my $emailto="$perlvar{'lonAdmEMail'},$perlvar{'lonSysEMail'}"; + my $subj="LON: $currenthostid User ID mismatch"; + system("echo 'User ID mismatch. lond must be run as user www.' |\ mailto $emailto -s '$subj' > /dev/null"); - exit 1; + exit 1; } # --------------------------------------------- Check if other instance running @@ -3459,10 +4932,10 @@ if ($wwwid!=$<) { my $pidfile="$perlvar{'lonDaemons'}/logs/lond.pid"; if (-e $pidfile) { - my $lfh=IO::File->new("$pidfile"); - my $pide=<$lfh>; - chomp($pide); - if (kill 0 => $pide) { die "already running"; } + my $lfh=IO::File->new("$pidfile"); + my $pide=<$lfh>; + chomp($pide); + if (kill 0 => $pide) { die "already running"; } } # ------------------------------------------------------------- Read hosts file @@ -3475,25 +4948,33 @@ $server = IO::Socket::INET->new(LocalPor Proto => 'tcp', Reuse => 1, Listen => 10 ) - or die "making socket: $@\n"; + or die "making socket: $@\n"; # --------------------------------------------------------- Do global variables # 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"); } @@ -3505,7 +4986,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 } @@ -3515,7 +4996,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)"); @@ -3525,7 +5006,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) { @@ -3537,6 +5018,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: @@ -3547,14 +5031,36 @@ 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"); + my %name_to_ip; 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*\#/ && $configline !~ /^\s*$/ ) { + my ($id,$domain,$role,$name)=split(/:/,$configline); + $name=~s/\s//g; + my $ip; + if (!exists($name_to_ip{$name})) { + $ip = gethostbyname($name); + if (!$ip || length($ip) ne 4) { + &logthis("Skipping host $id name $name no IP found\n"); + next; + } + $ip=inet_ntoa($ip); + $name_to_ip{$name} = $ip; + } else { + $ip = $name_to_ip{$name}; + } + $hostid{$ip}=$id; # LonCAPA name of host by IP. + $hostdom{$id}=$domain; # LonCAPA domain name of host. + $hostname{$id}=$name; # LonCAPA name -> DNS name + $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); } @@ -3616,34 +5122,38 @@ sub checkchildren { &logthis('Going to check on the children'); my $docdir=$perlvar{'lonDocRoot'}; foreach (sort keys %children) { - sleep 1; + #sleep 1; unless (kill 'USR1' => $_) { &logthis ('Child '.$_.' is dead'); &logstatus($$.' is dead'); + delete($children{$_}); } } sleep 5; - $SIG{ALRM} = sub { die "timeout" }; + $SIG{ALRM} = sub { Debug("timeout"); + die "timeout"; }; $SIG{__DIE__} = 'DEFAULT'; &status("Checking on the children (waiting for reports)"); foreach (sort keys %children) { unless (-e "$docdir/lon-status/londchld/$_.txt") { - eval { - alarm(300); - &logthis('Child '.$_.' did not respond'); - kill 9 => $_; - #$emailto="$perlvar{'lonAdmEMail'},$perlvar{'lonSysEMail'}"; - #$subj="LON: $currenthostid killed lond process $_"; - #my $result=`echo 'Killed lond process $_.' | mailto $emailto -s '$subj' > /dev/null`; - #$execdir=$perlvar{'lonDaemons'}; - #$result=`/bin/cp $execdir/logs/lond.log $execdir/logs/lond.log.$_`; - alarm(0); - } + eval { + alarm(300); + &logthis('Child '.$_.' did not respond'); + kill 9 => $_; + #$emailto="$perlvar{'lonAdmEMail'},$perlvar{'lonSysEMail'}"; + #$subj="LON: $currenthostid killed lond process $_"; + #my $result=`echo 'Killed lond process $_.' | mailto $emailto -s '$subj' > /dev/null`; + #$execdir=$perlvar{'lonDaemons'}; + #$result=`/bin/cp $execdir/logs/lond.log $execdir/logs/lond.log.$_`; + delete($children{$_}); + alarm(0); + } } } $SIG{ALRM} = 'DEFAULT'; $SIG{__DIE__} = \&catchexception; &status("Finished checking children"); + &logthis('Finished Checking children'); } # --------------------------------------------------------------------- Logging @@ -3674,22 +5184,15 @@ sub Debug { # reply - Text to send to client. # request - Original request from client. # -# Note: This increments Transactions -# sub Reply { - alarm(120); - my $fd = shift; - my $reply = shift; - my $request = shift; - + my ($fd, $reply, $request) = @_; print $fd $reply; Debug("Request was $request Reply was $reply"); $Transactions++; - alarm(0); +} -} # # Sub to report a failure. # This function: @@ -3719,19 +5222,20 @@ sub logstatus { &status("Doing logging"); my $docdir=$perlvar{'lonDocRoot'}; { - my $fh=IO::File->new(">>$docdir/lon-status/londstatus.txt"); - print $fh $$."\t".$currenthostid."\t".$status."\t".$lastlog."\n"; - $fh->close(); + my $fh=IO::File->new(">$docdir/lon-status/londchld/$$.txt"); + print $fh $status."\n".$lastlog."\n".time."\n$keymode"; + $fh->close(); } - &status("Finished londstatus.txt"); + &status("Finished $$.txt"); { - my $fh=IO::File->new(">$docdir/lon-status/londchld/$$.txt"); - print $fh $status."\n".$lastlog."\n".time; - $fh->close(); + open(LOG,">>$docdir/lon-status/londstatus.txt"); + flock(LOG,LOCK_EX); + print LOG $$."\t".$clientname."\t".$currenthostid."\t" + .$status."\t".$lastlog."\t $keymode\n"; + flock(LOG,LOCK_UN); + close(LOG); } - ResetStatistics; &status("Finished logging"); - } sub initnewstatus { @@ -3753,27 +5257,8 @@ sub status { my $what=shift; my $now=time; my $local=localtime($now); - my $status = "lond: $what $local "; - if($Transactions) { - $status .= " Transactions: $Transactions Failed; $Failures"; - } - $0=$status; -} - -# -------------------------------------------------------- Escape Special Chars - -sub escape { - my $str=shift; - $str =~ s/(\W)/"%".unpack('H2',$1)/eg; - return $str; -} - -# ----------------------------------------------------- Un-Escape Special Chars - -sub unescape { - my $str=shift; - $str =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg; - return $str; + $status=$local.': '.$what; + $0='lond: '.$what.' '.$local; } # ----------------------------------------------------------- Send USR1 to lonc @@ -3789,11 +5274,12 @@ sub reconlonc { &logthis("lonc at pid $loncpid responding, sending USR1"); kill USR1 => $loncpid; } else { - &logthis("CRITICAL: " - ."lonc at pid $loncpid not responding, giving up"); + &logthis( + "CRITICAL: " + ."lonc at pid $loncpid not responding, giving up"); } } else { - &logthis('CRITICAL: lonc not running, giving up'); + &logthis('CRITICAL: lonc not running, giving up'); } } @@ -3801,12 +5287,12 @@ sub reconlonc { sub subreply { my ($cmd,$server)=@_; - my $peerfile="$perlvar{'lonSockDir'}/$server"; + my $peerfile="$perlvar{'lonSockDir'}/".$hostname{$server}; my $sclient=IO::Socket::UNIX->new(Peer =>"$peerfile", Type => SOCK_STREAM, Timeout => 10) - or return "con_lost"; - print $sclient "$cmd\n"; + or return "con_lost"; + print $sclient "sethost:$server:$cmd\n"; my $answer=<$sclient>; chomp($answer); if (!$answer) { $answer="con_lost"; } @@ -3814,60 +5300,48 @@ sub subreply { } sub reply { - my ($cmd,$server)=@_; - my $answer; - if ($server ne $currenthostid) { - $answer=subreply($cmd,$server); - if ($answer eq 'con_lost') { - $answer=subreply("ping",$server); - if ($answer ne $server) { - &logthis("sub reply: answer != server answer is $answer, server is $server"); - &reconlonc("$perlvar{'lonSockDir'}/$server"); - } - $answer=subreply($cmd,$server); - } - } else { - $answer='self_reply'; - } - return $answer; + my ($cmd,$server)=@_; + my $answer; + if ($server ne $currenthostid) { + $answer=subreply($cmd,$server); + if ($answer eq 'con_lost') { + $answer=subreply("ping",$server); + if ($answer ne $server) { + &logthis("sub reply: answer != server answer is $answer, server is $server"); + &reconlonc("$perlvar{'lonSockDir'}/".$hostname{$server}); + } + $answer=subreply($cmd,$server); + } + } else { + $answer='self_reply'; + } + return $answer; } # -------------------------------------------------------------- Talk to lonsql -sub sqlreply { +sub sql_reply { my ($cmd)=@_; - my $answer=subsqlreply($cmd); - if ($answer eq 'con_lost') { $answer=subsqlreply($cmd); } + my $answer=&sub_sql_reply($cmd); + if ($answer eq 'con_lost') { $answer=&sub_sql_reply($cmd); } return $answer; } -sub subsqlreply { +sub sub_sql_reply { my ($cmd)=@_; my $unixsock="mysqlsock"; my $peerfile="$perlvar{'lonSockDir'}/$unixsock"; my $sclient=IO::Socket::UNIX->new(Peer =>"$peerfile", Type => SOCK_STREAM, Timeout => 10) - or return "con_lost"; - print $sclient "$cmd\n"; + or return "con_lost"; + print $sclient "$cmd:$currentdomainid\n"; my $answer=<$sclient>; chomp($answer); if (!$answer) { $answer="con_lost"; } return $answer; } -# -------------------------------------------- Return path to profile directory - -sub propath { - my ($udom,$uname)=@_; - $udom=~s/\W//g; - $uname=~s/\W//g; - my $subdir=$uname.'__'; - $subdir =~ s/(.)(.)(.).*/$1\/$2\/$3/; - my $proname="$perlvar{'lonUsersDir'}/$udom/$subdir/$uname"; - return $proname; -} - # --------------------------------------- Is this the home server of an author? sub ishome { @@ -3897,7 +5371,7 @@ my $execdir=$perlvar{'lonDaemons'}; open (PIDSAVE,">$execdir/logs/lond.pid"); print PIDSAVE "$$\n"; close(PIDSAVE); -&logthis("CRITICAL: ---------- Starting ----------"); +&logthis("CRITICAL: ---------- Starting ----------"); &status('Starting'); @@ -3915,6 +5389,7 @@ $SIG{USR2} = \&UpdateHosts; ReadHostTable; +my $dist=`$perlvar{'lonDaemons'}/distprobe`; # -------------------------------------------------------------- # Accept connections. When a connection comes in, it is validated @@ -3931,6 +5406,7 @@ while (1) { sub make_new_child { my $pid; +# my $cipher; # Now global my $sigset; $client = shift; @@ -3941,7 +5417,7 @@ sub make_new_child { $sigset = POSIX::SigSet->new(SIGINT); sigprocmask(SIG_BLOCK, $sigset) or die "Can't block SIGINT for fork: $!\n"; - + die "fork: $!" unless defined ($pid = fork); $client->sockopt(SO_KEEPALIVE, 1); # Enable monitoring of @@ -3952,15 +5428,25 @@ 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"); + } 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 { @@ -3977,64 +5463,137 @@ sub make_new_child { sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock SIGINT for fork: $!\n"; - - +# my $tmpsnum=0; # Now global +#---------------------------------------------------- kerberos 5 initialization &Authen::Krb5::init_context(); - &Authen::Krb5::init_ets(); - + unless (($dist eq 'fedora5') || ($dist eq 'fedora4') + || ($dist eq 'suse9.3')) { + &Authen::Krb5::init_ets(); + } + &status('Accepted connection'); # ============================================================================= # 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!! - my $clientrec=($hostid{$clientip} ne undef); - my $ismanager=($managers{$clientip} ne undef); + my $outsideip=$clientip; + if ($clientip eq '127.0.0.1') { + $outsideip=$hostip{$perlvar{'lonHostID'}}; + } + + my $clientrec=($hostid{$outsideip} ne undef); + my $ismanager=($managers{$outsideip} ne undef); $clientname = "[unknonwn]"; if($clientrec) { # Establish client type. $ConnectionType = "client"; - $clientname = $hostid{$clientip}; + $clientname = $hostid{$outsideip}; if($ismanager) { $ConnectionType = "both"; } } else { $ConnectionType = "manager"; - $clientname = $managers{$clientip}; + $clientname = $managers{$outsideip}; } my $clientok; + if ($clientrec || $ismanager) { &status("Waiting for init from $clientip $clientname"); &logthis('INFO: Connection, '. $clientip. - " ($clientname) connection type = $ConnectionType " ); + " ($clientname) connection type = $ConnectionType " ); &status("Connecting $clientip ($clientname))"); my $remotereq=<$client>; - $remotereq=~s/[^\w:]//g; + chomp($remotereq); + Debug("Got init: $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: " + &logthis( + "WARNING: " ."$clientip failed to initialize: >$remotereq< "); &status('No init '.$clientip); } + } else { - &logthis("WARNING: Unknown client $clientip"); + &logthis( + "WARNING: Unknown client $clientip"); &status('Hung up on '.$clientip); } + if ($clientok) { # ---------------- New known client connecting, could mean machine online again @@ -4044,33 +5603,33 @@ sub make_new_child { # no need to try to do recon's to myself next; } - &reconlonc("$perlvar{'lonSockDir'}/$id"); + &reconlonc("$perlvar{'lonSockDir'}/".$hostname{$id}); } - &logthis("Established connection: $clientname"); + &logthis("Established connection: $clientname"); &status('Will listen to '.$clientname); - - ResetStatistics(); - # ------------------------------------------------------------ Process requests - my $KeepGoing = 1; - while ((my $userinput=GetRequest) && $KeepGoing) { - $KeepGoing = ProcessRequest($userinput); -# -------------------------------------------------------------------- complete - - &status('Listening to '.$clientname); + my $keep_going = 1; + my $user_input; + while(($user_input = get_request) && $keep_going) { + alarm(120); + Debug("Main: Got $user_input\n"); + $keep_going = &process_request($user_input); + alarm(0); + &status('Listening to '.$clientname." ($keymode)"); } + # --------------------------------------------- client unknown or fishy, refuse - } else { + } else { print $client "refused\n"; $client->close(); - &logthis("WARNING: " + &logthis("WARNING: " ."Rejected client $clientip, closing connection"); } - } + } # ============================================================================= - &logthis("CRITICAL: " + &logthis("CRITICAL: " ."Disconnect from $clientip ($clientname)"); @@ -4080,8 +5639,38 @@ sub make_new_child { exit; } +# +# Determine if a user is an author for the indicated domain. +# +# Parameters: +# domain - domain to check in . +# user - Name of user to check. +# +# Return: +# 1 - User is an author for domain. +# 0 - User is not an author for domain. +sub is_author { + my ($domain, $user) = @_; + + &Debug("is_author: $user @ $domain"); + + my $hashref = &tie_user_hash($domain, $user, "roles", + &GDBM_READER()); + # Author role should show up as a key /domain/_au + my $key = "/$domain/_au"; + my $value; + if (defined($hashref)) { + $value = $hashref->{$key}; + } + + if(defined($value)) { + &Debug("$user @ $domain is an author"); + } + + return defined($value); +} # # Checks to see if the input roleput request was to set # an author role. If so, invokes the lchtmldir script to set @@ -4093,31 +5682,98 @@ sub make_new_child { # user - Name of the user for which the role is being put. # authtype - The authentication type associated with the user. # -sub ManagePermissions { - my $request = shift; - my $domain = shift; - my $user = shift; - my $authtype= shift; +sub manage_permissions { + my ($request, $domain, $user, $authtype) = @_; + + &Debug("manage_permissions: $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... + if($request =~ /^(\/\Q$domain\E\/_au)$/) { # It's an author rolesput... my $execdir = $perlvar{'lonDaemons'}; my $userhome= "/home/$user" ; &logthis("system $execdir/lchtmldir $userhome $user $authtype"); + &Debug("Setting homedir permissions for $userhome"); system("$execdir/lchtmldir $userhome $user $authtype"); } } + + +# +# Return the full path of a user password file, whether it exists or not. +# Parameters: +# domain - Domain in which the password file lives. +# user - name of the user. +# Returns: +# Full passwd path: +# +sub password_path { + my ($domain, $user) = @_; + return &propath($domain, $user).'/passwd'; +} + +# Password Filename +# Returns the path to a passwd file given domain and user... only if +# it exists. +# Parameters: +# domain - Domain in which to search. +# user - username. +# Returns: +# - If the password file exists returns its path. +# - If the password file does not exist, returns undefined. +# +sub password_filename { + my ($domain, $user) = @_; + + Debug ("PasswordFilename called: dom = $domain user = $user"); + + my $path = &password_path($domain, $user); + Debug("PasswordFilename got path: $path"); + if(-e $path) { + return $path; + } else { + return undef; + } +} + +# +# Rewrite the contents of the user's passwd file. +# Parameters: +# domain - domain of the user. +# name - User's name. +# contents - New contents of the file. +# Returns: +# 0 - Failed. +# 1 - Success. +# +sub rewrite_password_file { + my ($domain, $user, $contents) = @_; + + my $file = &password_filename($domain, $user); + if (defined $file) { + my $pf = IO::File->new(">$file"); + if($pf) { + print $pf "$contents\n"; + return 1; + } else { + return 0; + } + } else { + return 0; + } + +} + # -# GetAuthType - Determines the authorization type of a user in a domain. +# get_auth_type - Determines the authorization type of a user in a domain. # Returns the authorization type or nouser if there is no such user. # -sub GetAuthType { - my $domain = shift; - my $user = shift; +sub get_auth_type +{ - Debug("GetAuthType( $domain, $user ) \n"); + my ($domain, $user) = @_; + + Debug("get_auth_type( $domain, $user ) \n"); my $proname = &propath($domain, $user); my $passwdfile = "$proname/passwd"; if( -e $passwdfile ) { @@ -4127,24 +5783,148 @@ sub GetAuthType { Debug("Password info = $realpassword\n"); my ($authtype, $contentpwd) = split(/:/, $realpassword); Debug("Authtype = $authtype, content = $contentpwd\n"); - my $availinfo = ''; - if($authtype eq 'krb4' or $authtype eq 'krb5') { - $availinfo = $contentpwd; - } - - return "$authtype:$availinfo"; + return "$authtype:$contentpwd"; } else { Debug("Returning nouser"); return "nouser"; } } +# +# Validate a user given their domain, name and password. This utility +# function is used by both AuthenticateHandler and ChangePasswordHandler +# to validate the login credentials of a user. +# Parameters: +# $domain - The domain being logged into (this is required due to +# the capability for multihomed systems. +# $user - The name of the user being validated. +# $password - The user's propoposed password. +# +# Returns: +# 1 - The domain,user,pasword triplet corresponds to a valid +# user. +# 0 - The domain,user,password triplet is not a valid user. +# +sub validate_user { + my ($domain, $user, $password) = @_; + + + # Why negative ~pi you may well ask? Well this function is about + # authentication, and therefore very important to get right. + # I've initialized the flag that determines whether or not I've + # validated correctly to a value it's not supposed to get. + # At the end of this function. I'll ensure that it's not still that + # value so we don't just wind up returning some accidental value + # as a result of executing an unforseen code path that + # did not set $validated. At the end of valid execution paths, + # validated shoule be 1 for success or 0 for failuer. + + my $validated = -3.14159; + + # How we authenticate is determined by the type of authentication + # the user has been assigned. If the authentication type is + # "nouser", the user does not exist so we will return 0. + + my $contents = &get_auth_type($domain, $user); + my ($howpwd, $contentpwd) = split(/:/, $contents); + + my $null = pack("C",0); # Used by kerberos auth types. + + if ($howpwd ne 'nouser') { + + if($howpwd eq "internal") { # Encrypted is in local password file. + $validated = (crypt($password, $contentpwd) eq $contentpwd); + } + elsif ($howpwd eq "unix") { # User is a normal unix user. + $contentpwd = (getpwnam($user))[1]; + if($contentpwd) { + if($contentpwd eq 'x') { # Shadow password file... + my $pwauth_path = "/usr/local/sbin/pwauth"; + open PWAUTH, "|$pwauth_path" or + die "Cannot invoke authentication"; + print PWAUTH "$user\n$password\n"; + close PWAUTH; + $validated = ! $?; + + } else { # Passwords in /etc/passwd. + $validated = (crypt($password, + $contentpwd) eq $contentpwd); + } + } else { + $validated = 0; + } + } + elsif ($howpwd eq "krb4") { # user is in kerberos 4 auth. domain. + if(! ($password =~ /$null/) ) { + my $k4error = &Authen::Krb4::get_pw_in_tkt($user, + "", + $contentpwd,, + 'krbtgt', + $contentpwd, + 1, + $password); + if(!$k4error) { + $validated = 1; + } else { + $validated = 0; + &logthis('krb4: '.$user.', '.$contentpwd.', '. + &Authen::Krb4::get_err_txt($Authen::Krb4::error)); + } + } else { + $validated = 0; # Password has a match with null. + } + } elsif ($howpwd eq "krb5") { # User is in kerberos 5 auth. domain. + if(!($password =~ /$null/)) { # Null password not allowed. + my $krbclient = &Authen::Krb5::parse_name($user.'@' + .$contentpwd); + my $krbservice = "krbtgt/".$contentpwd."\@".$contentpwd; + my $krbserver = &Authen::Krb5::parse_name($krbservice); + my $credentials= &Authen::Krb5::cc_default(); + $credentials->initialize($krbclient); + my $krbreturn = &Authen::Krb5::get_in_tkt_with_password($krbclient, + $krbserver, + $password, + $credentials); + $validated = ($krbreturn == 1); + } else { + $validated = 0; + } + } elsif ($howpwd eq "localauth") { + # Authenticate via installation specific authentcation method: + $validated = &localauth::localauth($user, + $password, + $contentpwd, + $domain); + if ($validate < 0) { + &logthis("localauth for $contentpwd $user:$domain returned a $validated"); + $validated = 0; + } + } else { # Unrecognized auth is also bad. + $validated = 0; + } + } else { + $validated = 0; + } + # + # $validated has the correct stat of the authentication: + # + + unless ($validated != -3.14159) { + # I >really really< want to know if this happens. + # since it indicates that user authentication is badly + # broken in some code path. + # + die "ValidateUser - failed to set the value of validated $domain, $user $password"; + } + return $validated; +} + + sub addline { my ($fname,$hostid,$ip,$newline)=@_; my $contents; my $found=0; - my $expr='^'.$hostid.':'.$ip.':'; - $expr =~ s/\./\\\./g; + my $expr='^'.quotemeta($hostid).':'.quotemeta($ip).':'; my $sh; if ($sh=IO::File->new("$fname.subscription")) { while (my $subline=<$sh>) { @@ -4159,40 +5939,52 @@ sub addline { return $found; } -sub getchat { - my ($cdom,$cname,$udom,$uname)=@_; - my %hash; - my $proname=&propath($cdom,$cname); +sub get_chat { + my ($cdom,$cname,$udom,$uname,$group)=@_; + my @entries=(); - if (tie(%hash,'GDBM_File',"$proname/nohist_chatroom.db", - &GDBM_READER(),0640)) { - @entries=map { $_.':'.$hash{$_} } sort keys %hash; - untie %hash; + my $namespace = 'nohist_chatroom'; + my $namespace_inroom = 'nohist_inchatroom'; + if ($group ne '') { + $namespace .= '_'.$group; + $namespace_inroom .= '_'.$group; + } + my $hashref = &tie_user_hash($cdom, $cname, $namespace, + &GDBM_READER()); + if ($hashref) { + @entries=map { $_.':'.$hashref->{$_} } sort(keys(%$hashref)); + &untie_user_hash($hashref); } my @participants=(); my $cutoff=time-60; - if (tie(%hash,'GDBM_File',"$proname/nohist_inchatroom.db", - &GDBM_WRCREAT(),0640)) { - $hash{$uname.':'.$udom}=time; - foreach (sort keys %hash) { - if ($hash{$_}>$cutoff) { - $participants[$#participants+1]='active_participant:'.$_; + $hashref = &tie_user_hash($cdom, $cname, $namespace_inroom, + &GDBM_WRCREAT()); + if ($hashref) { + $hashref->{$uname.':'.$udom}=time; + foreach my $user (sort(keys(%$hashref))) { + if ($hashref->{$user}>$cutoff) { + push(@participants, 'active_participant:'.$user); } } - untie %hash; + &untie_user_hash($hashref); } return (@participants,@entries); } -sub chatadd { - my ($cdom,$cname,$newchat)=@_; - my %hash; - my $proname=&propath($cdom,$cname); +sub chat_add { + my ($cdom,$cname,$newchat,$group)=@_; my @entries=(); my $time=time; - if (tie(%hash,'GDBM_File',"$proname/nohist_chatroom.db", - &GDBM_WRCREAT(),0640)) { - @entries=map { $_.':'.$hash{$_} } sort keys %hash; + my $namespace = 'nohist_chatroom'; + my $logfile = 'chatroom.log'; + if ($group ne '') { + $namespace .= '_'.$group; + $logfile = 'chatroom_'.$group.'.log'; + } + my $hashref = &tie_user_hash($cdom, $cname, $namespace, + &GDBM_WRCREAT()); + if ($hashref) { + @entries=map { $_.':'.$hashref->{$_} } sort(keys(%$hashref)); my ($lastid)=($entries[$#entries]=~/^(\w+)\:/); my ($thentime,$idnum)=split(/\_/,$lastid); my $newid=$time.'_000000'; @@ -4202,38 +5994,58 @@ sub chatadd { $idnum=substr('000000'.$idnum,-6,6); $newid=$time.'_'.$idnum; } - $hash{$newid}=$newchat; + $hashref->{$newid}=$newchat; my $expired=$time-3600; - foreach (keys %hash) { - my ($thistime)=($_=~/(\d+)\_/); + foreach my $comment (keys(%$hashref)) { + my ($thistime) = ($comment=~/(\d+)\_/); if ($thistime<$expired) { - delete $hash{$_}; + delete $hashref->{$comment}; } } - untie %hash; - } - { - my $hfh; - if ($hfh=IO::File->new(">>$proname/chatroom.log")) { - print $hfh "$time:".&unescape($newchat)."\n"; + { + my $proname=&propath($cdom,$cname); + if (open(CHATLOG,">>$proname/$logfile")) { + print CHATLOG ("$time:".&unescape($newchat)."\n"); + } + close(CHATLOG); } + &untie_user_hash($hashref); } } 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; } @@ -4242,7 +6054,7 @@ sub currentversion { my $version=-1; my $ulsdir=''; if ($fname=~/^(.+)\/[^\/]+$/) { - $ulsdir=$1; + $ulsdir=$1; } my ($fnamere1,$fnamere2); # remove version if already specified @@ -4286,7 +6098,7 @@ sub thisversion { sub subscribe { my ($userinput,$clientip)=@_; my $result; - my ($cmd,$fname)=split(/:/,$userinput); + my ($cmd,$fname)=split(/:/,$userinput,2); my $ownership=&ishome($fname); if ($ownership eq 'owner') { # explitly asking for the current version? @@ -4299,8 +6111,8 @@ sub subscribe { symlink($root.'.'.$extension, $root.'.'.$currentversion.'.'.$extension); unless ($extension=~/\.meta$/) { - symlink($root.'.'.$extension.'.meta', - $root.'.'.$currentversion.'.'.$extension.'.meta'); + symlink($root.'.'.$extension.'.meta', + $root.'.'.$currentversion.'.'.$extension.'.meta'); } } } @@ -4330,6 +6142,35 @@ sub subscribe { } return $result; } +# Change the passwd of a unix user. The caller must have +# first verified that the user is a loncapa user. +# +# Parameters: +# user - Unix user name to change. +# pass - New password for the user. +# Returns: +# ok - if success +# other - Some meaningfule error message string. +# NOTE: +# invokes a setuid script to change the passwd. +sub change_unix_password { + my ($user, $pass) = @_; + + &Debug("change_unix_password"); + my $execdir=$perlvar{'lonDaemons'}; + &Debug("Opening lcpasswd pipeline"); + my $pf = IO::File->new("|$execdir/lcpasswd > " + ."$perlvar{'lonDaemons'}" + ."/logs/lcpasswd.log"); + print $pf "$user\n$pass\n$pass\n"; + close $pf; + my $err = $?; + return ($err < @passwderrors) ? $passwderrors[$err] : + "pwchange_falure - unknown error"; + + +} + sub make_passwd_file { my ($uname, $umode,$npass,$passfilename)=@_; @@ -4337,7 +6178,11 @@ sub make_passwd_file { if ($umode eq 'krb4' or $umode eq 'krb5') { { my $pf = IO::File->new(">$passfilename"); - print $pf "$umode:$npass\n"; + if ($pf) { + print $pf "$umode:$npass\n"; + } else { + $result = "pass_file_failed_error"; + } } } elsif ($umode eq 'internal') { my $salt=time; @@ -4346,16 +6191,36 @@ sub make_passwd_file { { &Debug("Creating internal auth"); my $pf = IO::File->new(">$passfilename"); - print $pf "internal:$ncpass\n"; + if($pf) { + print $pf "internal:$ncpass\n"; + } else { + $result = "pass_file_failed_error"; + } } } elsif ($umode eq 'localauth') { { my $pf = IO::File->new(">$passfilename"); - print $pf "localauth:$npass\n"; + if($pf) { + print $pf "localauth:$npass\n"; + } else { + $result = "pass_file_failed_error"; + } } } elsif ($umode eq 'unix') { { - my $execpath="$perlvar{'lonDaemons'}/"."lcuseradd"; + # + # 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"; + + my $lc_error_file = $execdir."/tmp/lcuseradd".$$.".status"; { &Debug("Executing external: ".$execpath); &Debug("user = ".$uname.", Password =". $npass); @@ -4363,18 +6228,42 @@ sub make_passwd_file { print $se "$uname\n"; print $se "$npass\n"; print $se "$npass\n"; + print $se "$lc_error_file\n"; # Status -> unique file. } - my $useraddok = $?; - if($useraddok > 0) { - &logthis("Failed lcuseradd: ".&lcuseraddstrerror($useraddok)); + if (-r $lc_error_file) { + &Debug("Opening error file: $lc_error_file"); + my $error = IO::File->new("< $lc_error_file"); + my $useraddok = <$error>; + $error->close; + unlink($lc_error_file); + + chomp $useraddok; + + if($useraddok > 0) { + my $error_text = &lcuseraddstrerror($useraddok); + &logthis("Failed lcuseradd: $error_text"); + $result = "lcuseradd_failed:$error_text\n"; + } else { + my $pf = IO::File->new(">$passfilename"); + if($pf) { + print $pf "unix:\n"; + } else { + $result = "pass_file_failed_error"; + } + } + } else { + &Debug("Could not locate lcuseradd error: $lc_error_file"); + $result="bug_lcuseradd_no_output_file"; } - my $pf = IO::File->new(">$passfilename"); - print $pf "unix:\n"; } } elsif ($umode eq 'none') { { - my $pf = IO::File->new(">$passfilename"); - print $pf "none:\n"; + my $pf = IO::File->new("> $passfilename"); + if($pf) { + print $pf "none:\n"; + } else { + $result = "pass_file_failed_error"; + } } } else { $result="auth_mode_error\n"; @@ -4382,12 +6271,22 @@ sub make_passwd_file { return $result; } +sub convert_photo { + my ($start,$dest)=@_; + system("convert $start $dest"); +} + sub sethost { my ($remotereq) = @_; my (undef,$hostid)=split(/:/,$remotereq); + # ignore sethost if we are already correct + if ($hostid eq $currenthostid) { + return 'ok'; + } + 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 { @@ -4403,10 +6302,8 @@ sub version { $remoteVERSION=(split(/:/,$userinput))[1]; return "version:$VERSION"; } -############## >>>>>>>>>>>>>>>>>>>>>>>>>> FUTUREWORK <<<<<<<<<<<<<<<<<<<<<<<<<<<< + #There is a copy of this in lonnet.pm -# Can we hoist these lil' things out into common places? -# sub userload { my $numusers=0; { @@ -4429,6 +6326,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 @@ -4636,7 +6601,7 @@ Place in B stores hash in namespace -=item rolesput +=item rolesputy put a role into a user's environment @@ -4744,7 +6709,6 @@ to the client, and the connection is clo IO::Socket IO::File Apache::File -Symbol POSIX Crypt::IDEA LWP::UserAgent()