--- loncom/lond 2005/06/27 22:32:37 1.289 +++ loncom/lond 2021/10/26 15:52:55 1.569 @@ -2,7 +2,7 @@ # The LearningOnline Network # lond "LON Daemon" Server (port "LOND" 5663) # -# $Id: lond,v 1.289 2005/06/27 22:32:37 albertel Exp $ +# $Id: lond,v 1.569 2021/10/26 15:52:55 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -31,34 +31,41 @@ use strict; use lib '/home/httpd/lib/perl/'; +use LONCAPA; use LONCAPA::Configuration; +use LONCAPA::Lond; +use Socket; use IO::Socket; use IO::File; #use Apache::File; -use Symbol; use POSIX; use Crypt::IDEA; -use LWP::UserAgent(); +use HTTP::Request; +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 LONCAPA::ConfigFileEdit; +use File::Find; use LONCAPA::lonlocal; use LONCAPA::lonssl; use Fcntl qw(:flock); +use Apache::lonnet; +use Mail::Send; +use Crypt::Eksblowfish::Bcrypt; +use Digest::SHA; +use Encode; +use LONCAPA::LWPReq; my $DEBUG = 0; # Non zero to enable debug log entries. my $status=''; my $lastlog=''; -my $VERSION='$Revision: 1.289 $'; #' stupid emacs +my $VERSION='$Revision: 1.569 $'; #' stupid emacs my $remoteVERSION; my $currenthostid="default"; my $currentdomainid; @@ -66,9 +73,20 @@ my $currentdomainid; my $client; my $clientip; # IP address of client. my $clientname; # LonCAPA name of client. +my $clientversion; # LonCAPA version running on client. +my $clienthomedom; # LonCAPA domain of homeID for client. +my $clientintdom; # LonCAPA "internet domain" for client. +my $clientsamedom; # LonCAPA domain same for this host + # and client. +my $clientsameinst; # LonCAPA "internet domain" same for + # this host and client. +my $clientremoteok; # Current domain permits hosting on client + # (not set if host and client share "internet domain"). + # Values are 0 or 1; 1 if allowed. +my %clientprohibited; # Commands from client prohibited for domain's + # users. my $server; -my $thisserver; # DNS of us. my $keymode; @@ -84,15 +102,19 @@ my $tmpsnum = 0; # Id of tmpputs. my $ConnectionType; -my %hostid; # ID's for hosts in cluster by ip. -my %hostdom; # LonCAPA domain for hosts in cluster. -my %hostip; # IPs for hosts in cluster. -my %hostdns; # ID's of hosts looked up by DNS name. - my %managers; # Ip -> manager names my %perlvar; # Will have the apache conf defined perl vars. +my %secureconf; # Will have requirements for security + # of lond connections + +my %crlchecked; # Will contain clients for which the client's SSL + # has been checked against the cluster's Certificate + # Revocation List. + +my $dist; + # # The hash below is used for command dispatching, and is therefore keyed on the request keyword. # Each element of the hash contains a reference to an array that contains: @@ -128,25 +150,175 @@ my @passwderrors = ("ok", "pwchange_failure - lcpasswd Error filename is invalid"); -# The array below are lcuseradd error strings.: - -my $lastadderror = 13; -my @adderrors = ("ok", - "User ID mismatch, lcuseradd must run as user www", - "lcuseradd Incorrect number of command line parameters must be 3", - "lcuseradd Incorrect number of stdinput lines, must be 3", - "lcuseradd Too many other simultaneous pwd changes in progress", - "lcuseradd User does not exist", - "lcuseradd Unable to make www member of users's group", - "lcuseradd Unable to su to root", - "lcuseradd Unable to set password", - "lcuseradd Usrname has invalid characters", - "lcuseradd Password has an invalid character", - "lcuseradd User already exists", - "lcuseradd Could not add user.", - "lcuseradd Password mismatch"); - +# This array are the errors from lcinstallfile: +my @installerrors = ("ok", + "Initial user id of client not that of www", + "Usage error, not enough command line arguments", + "Source filename does not exist", + "Destination filename does not exist", + "Some file operation failed", + "Invalid table filename." + ); + +# +# The %trust hash classifies commands according to type of trust +# required for execution of the command. +# +# When clients from a different institution request execution of a +# particular command, the trust settings for that institution set +# for this domain (or default domain for a multi-domain server) will +# be checked to see if running the command is allowed. +# +# Trust types which depend on the "Trust" domain configuration +# for the machine's default domain are: +# +# content ("Access to this domain's content by others") +# shared ("Access to other domain's content by this domain") +# enroll ("Enrollment in this domain's courses by others") +# coaurem ("Co-author roles for this domain's users elsewhere") +# othcoau ("Co-author roles in this domain for others") +# domroles ("Domain roles in this domain assignable to others") +# catalog ("Course Catalog for this domain displayed elsewhere") +# reqcrs ("Requests for creation of courses in this domain by others") +# msg ("Users in other domains can send messages to this domain") +# +# Trust type which depends on the User Session Hosting (remote) +# domain configuration for machine's default domain is: "remote". +# +# Trust types which depend on contents of manager.tab in +# /home/httpd/lonTabs is: "manageronly". +# +# Trust type which requires client to share the same LON-CAPA +# "internet domain" (i.e., same institution as this server) is: +# "institutiononly". +# + +my %trust = ( + auth => {remote => 1}, + autocreatepassword => {remote => 1}, + autocrsreqchecks => {remote => 1, reqcrs => 1}, + autocrsrequpdate => {remote => 1}, + autocrsreqvalidation => {remote => 1}, + autogetsections => {remote => 1}, + autoinstcodedefaults => {remote => 1, catalog => 1}, + autoinstcodeformat => {remote => 1, catalog => 1}, + autonewcourse => {remote => 1, reqcrs => 1}, + autophotocheck => {remote => 1, enroll => 1}, + autophotochoice => {remote => 1}, + autophotopermission => {remote => 1, enroll => 1}, + autopossibleinstcodes => {remote => 1, reqcrs => 1}, + autoretrieve => {remote => 1, enroll => 1, catalog => 1}, + autorun => {remote => 1, enroll => 1, reqcrs => 1}, + autovalidateclass_sec => {catalog => 1}, + autovalidatecourse => {remote => 1, enroll => 1}, + autovalidateinstcode => {domroles => 1, remote => 1, enroll => 1}, + autovalidateinstcrosslist => {remote => 1, enroll => 1}, + autoinstsecreformat => {remote => 1, enroll => 1}, + changeuserauth => {remote => 1, domroles => 1}, + chatretr => {remote => 1, enroll => 1}, + chatsend => {remote => 1, enroll => 1}, + courseiddump => {remote => 1, domroles => 1, enroll => 1}, + courseidput => {remote => 1, domroles => 1, enroll => 1}, + courseidputhash => {remote => 1, domroles => 1, enroll => 1}, + courselastaccess => {remote => 1, domroles => 1, enroll => 1}, + coursesessions => {institutiononly => 1}, + currentauth => {remote => 1, domroles => 1, enroll => 1}, + currentdump => {remote => 1, enroll => 1}, + currentversion => {remote=> 1, content => 1}, + dcmaildump => {remote => 1, domroles => 1}, + dcmailput => {remote => 1, domroles => 1}, + del => {remote => 1, domroles => 1, enroll => 1, content => 1}, + delbalcookie => {institutiononly => 1}, + delusersession => {institutiononly => 1}, + deldom => {remote => 1, domroles => 1}, # not currently used + devalidatecache => {institutiononly => 1}, + domroleput => {remote => 1, enroll => 1}, + domrolesdump => {remote => 1, catalog => 1}, + du => {remote => 1, enroll => 1}, + du2 => {remote => 1, enroll => 1}, + dump => {remote => 1, enroll => 1, domroles => 1}, + edit => {institutiononly => 1}, #not used currently + edump => {remote => 1, enroll => 1, domroles => 1}, + eget => {remote => 1, domroles => 1, enroll => 1}, #not used currently + egetdom => {remote => 1, domroles => 1, enroll => 1, }, + ekey => {anywhere => 1}, + exit => {anywhere => 1}, + fetchuserfile => {remote => 1, enroll => 1}, + get => {remote => 1, domroles => 1, enroll => 1}, + getdom => {anywhere => 1}, + home => {anywhere => 1}, + iddel => {remote => 1, enroll => 1}, + idget => {remote => 1, enroll => 1}, + idput => {remote => 1, domroles => 1, enroll => 1}, + inc => {remote => 1, enroll => 1}, + init => {anywhere => 1}, + inst_usertypes => {remote => 1, domroles => 1, enroll => 1}, + instemailrules => {remote => 1, domroles => 1}, + instidrulecheck => {remote => 1, domroles => 1,}, + instidrules => {remote => 1, domroles => 1,}, + instrulecheck => {remote => 1, enroll => 1, reqcrs => 1, domroles => 1}, + instselfcreatecheck => {institutiononly => 1}, + instuserrules => {remote => 1, enroll => 1, reqcrs => 1, domroles => 1}, + keys => {remote => 1,}, + load => {anywhere => 1}, + log => {anywhere => 1}, + ls => {remote => 1, enroll => 1, content => 1,}, + ls2 => {remote => 1, enroll => 1, content => 1,}, + ls3 => {remote => 1, enroll => 1, content => 1,}, + makeuser => {remote => 1, enroll => 1, domroles => 1,}, + mkdiruserfile => {remote => 1, enroll => 1,}, + newput => {remote => 1, enroll => 1, reqcrs => 1, domroles => 1,}, + passwd => {remote => 1}, + ping => {anywhere => 1}, + pong => {anywhere => 1}, + pushfile => {manageronly => 1}, + put => {remote => 1, enroll => 1, domroles => 1, msg => 1, content => 1, shared => 1}, + putdom => {remote => 1, domroles => 1,}, + putstore => {remote => 1, enroll => 1}, + queryreply => {anywhere => 1}, + querysend => {anywhere => 1}, + querysend_activitylog => {remote => 1}, + querysend_allusers => {remote => 1, domroles => 1}, + querysend_courselog => {remote => 1}, + querysend_fetchenrollment => {remote => 1}, + querysend_getinstuser => {remote => 1}, + querysend_getmultinstusers => {remote => 1}, + querysend_instdirsearch => {remote => 1, domroles => 1, coaurem => 1}, + querysend_institutionalphotos => {remote => 1}, + querysend_portfolio_metadata => {remote => 1, content => 1}, + querysend_userlog => {remote => 1, domroles => 1}, + querysend_usersearch => {remote => 1, enroll => 1, coaurem => 1}, + quit => {anywhere => 1}, + readlonnetglobal => {institutiononly => 1}, + reinit => {manageronly => 1}, #not used currently + removeuserfile => {remote => 1, enroll => 1}, + renameuserfile => {remote => 1,}, + restore => {remote => 1, enroll => 1, reqcrs => 1,}, + rolesdel => {remote => 1, enroll => 1, domroles => 1, coaurem => 1}, + rolesput => {remote => 1, enroll => 1, domroles => 1, coaurem => 1}, + servercerts => {institutiononly => 1}, + serverdistarch => {anywhere => 1}, + serverhomeID => {anywhere => 1}, + serverloncaparev => {anywhere => 1}, + servertimezone => {remote => 1, enroll => 1}, + setannounce => {remote => 1, domroles => 1}, + sethost => {anywhere => 1}, + store => {remote => 1, enroll => 1, reqcrs => 1,}, + studentphoto => {remote => 1, enroll => 1}, + sub => {content => 1,}, + tmpdel => {institutiononly => 1}, + tmpget => {institutiononly => 1}, + tmpput => {remote => 1, othcoau => 1}, + tokenauthuserfile => {anywhere => 1}, + unsub => {content => 1,}, + update => {shared => 1}, + updatebalcookie => {institutiononly => 1}, + updateclickers => {remote => 1}, + userhassession => {anywhere => 1}, + userload => {anywhere => 1}, + version => {anywhere => 1}, #not used + ); # # Statistics that are maintained and dislayed in the status line. @@ -176,19 +348,16 @@ sub ResetStatistics { # $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"); + Debug("Attempting local connection: $initcmd client: $clientip"); if($clientip ne "127.0.0.1") { &logthis(' LocalConnection rejecting non local: ' - ."$clientip ne $thisserver "); + ."$clientip ne 127.0.0.1 "); close $Socket; return undef; } else { @@ -264,10 +433,19 @@ sub SSLConnection { Debug("Approving promotion -> ssl"); # And do so: + my $CRLFile; + unless ($crlchecked{$clientname}) { + $CRLFile = lonssl::CRLFile(); + $crlchecked{$clientname} = 1; + } + my $SSLSocket = lonssl::PromoteServerSocket($Socket, $CACertificate, $Certificate, - $KeyFile); + $KeyFile, + $clientname, + $CRLFile, + $clientversion); if(! ($SSLSocket) ) { # SSL socket promotion failed. my $err = lonssl::LastError(); &logthis(" CRITICAL " @@ -307,8 +485,23 @@ sub InsecureConnection { my $Socket = shift; # Don't even start if insecure connections are not allowed. - - if(! $perlvar{londAllowInsecure}) { # Insecure connections not allowed. + # return 0 if Insecure connections not allowed. + # + if (ref($secureconf{'connfrom'}) eq 'HASH') { + if ($clientsamedom) { + if ($secureconf{'connfrom'}{'dom'} eq 'req') { + return 0; + } + } elsif ($clientsameinst) { + if ($secureconf{'connfrom'}{'intdom'} eq 'req') { + return 0; + } + } else { + if ($secureconf{'connfrom'}{'other'} eq 'req') { + return 0; + } + } + } elsif (!$perlvar{londAllowInsecure}) { return 0; } @@ -406,6 +599,7 @@ sub isClient { # sub ReadManagerTable { + &Debug("Reading manager table"); # Clean out the old table first.. foreach my $key (keys %managers) { @@ -414,15 +608,18 @@ sub ReadManagerTable { my $tablename = $perlvar{'lonTabDir'}."/managers.tab"; if (!open (MANAGERS, $tablename)) { - logthis('No manager table. Nobody can manage!!'); - return; + my $hostname = &Apache::lonnet::hostname($perlvar{'lonHostID'}); + if (&Apache::lonnet::is_LC_dns($hostname)) { + &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 + if (!defined &Apache::lonnet::get_host_ip($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 @@ -440,7 +637,7 @@ sub ReadManagerTable { } } else { logthis(' existing host'." $host\n"); - $managers{$hostip{$host}} = $host; # Use info from cluster tab if clumemeber + $managers{&Apache::lonnet::get_host_ip($host)} = $host; # Use info from cluster tab if cluster memeber } } } @@ -501,38 +698,37 @@ sub AdjustHostContents { my $adjusted; my $me = $perlvar{'lonHostID'}; - foreach my $line (split(/\n/,$contents)) { - if(!(($line eq "") || ($line =~ /^ *\#/) || ($line =~ /^ *$/))) { + foreach my $line (split(/\n/,$contents)) { + if(!(($line eq "") || ($line =~ /^ *\#/) || ($line =~ /^ *$/) || + ($line =~ /^\s*\^/))) { 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; + my $ip = gethostbyname($name); + my $ipnew = inet_ntoa($ip); + $ip = $ipnew; # Reconstruct the host line and append to adjusted: - my $newline = "$id:$domain:$role:$name:$ip"; - if($maxcon ne "") { # Not all hosts have loncnew tuning params - $newline .= ":$maxcon:$idleto:$mincon"; - } - $adjusted .= $newline."\n"; + my $newline = "$id:$domain:$role:$name:$ip"; + if($maxcon ne "") { # Not all hosts have loncnew tuning params + $newline .= ":$maxcon:$idleto:$mincon"; + } + $adjusted .= $newline."\n"; - } else { # Not me, pass unmodified. - $adjusted .= $line."\n"; - } + } else { # Not me, pass unmodified. + $adjusted .= $line."\n"; + } } else { # Blank or comment never re-written. $adjusted .= $line."\n"; # Pass blanks and comments as is. } - } - return $adjusted; + } + return $adjusted; } # # InstallFile: Called to install an administrative file: -# - 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. +# - The file is created int a temp directory called .tmp +# - lcinstall file is called to install the file. +# since the web app has no direct write access to the table directory # # Parameters: # Name of the file @@ -540,11 +736,16 @@ sub AdjustHostContents { # Return: # nonzero - success. # 0 - failure and $! has an errno. +# Assumptions: +# File installtion is a relatively infrequent # sub InstallFile { my ($Filename, $Contents) = @_; - my $TempFile = $Filename.".tmp"; +# my $TempFile = $Filename.".tmp"; + my $exedir = $perlvar{'lonDaemons'}; + my $tmpdir = $exedir.'/tmp/'; + my $TempFile = $tmpdir."TempTableFile.tmp"; # Open the file for write: @@ -558,11 +759,27 @@ sub InstallFile { print $fh ($Contents); $fh->close; # In case we ever have a filesystem w. locking - chmod(0660, $TempFile); + chmod(0664, $TempFile); # Everyone can write it. - # Now we can move install the file in position. - - move($TempFile, $Filename); + # Use lcinstall file to put the file in the table directory... + + &Debug("Opening pipe to $exedir/lcinstallfile $TempFile $Filename"); + my $pf = IO::File->new("| $exedir/lcinstallfile $TempFile $Filename > $exedir/logs/lcinstallfile.log"); + close $pf; + my $err = $?; + &Debug("Status is $err"); + if ($err != 0) { + my $msg = $err; + if ($err < @installerrors) { + $msg = $installerrors[$err]; + } + &logthis("Install failed for table file $Filename : $msg"); + return 0; + } + + # Remove the temp file: + + unlink($TempFile); return 1; } @@ -570,8 +787,10 @@ sub InstallFile { # # ConfigFileFromSelector: converts a configuration file selector -# (one of host or domain at this point) into a -# configuration file pathname. +# into a configuration file pathname. +# Supports the following file selectors: +# hosts, domain, dns_hosts, dns_domain +# # # Parameters: # selector - Configuration file selector. @@ -582,16 +801,19 @@ 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"; + if ($selector eq 'loncapaCAcrl') { + my $tabledir = $perlvar{'lonCertificateDirectory'}; + if (-d $tabledir) { + $tablefile = $tabledir.'/'.$selector.'.pem'; + } } else { - return undef; + my $tabledir = $perlvar{'lonTabDir'}.'/'; + if (($selector eq "hosts") || ($selector eq "domain") || + ($selector eq "dns_hosts") || ($selector eq "dns_domain")) { + $tablefile = $tabledir.$selector.'.tab'; + } } return $tablefile; - } # # PushFile: Called to do an administrative push of a file. @@ -609,13 +831,17 @@ sub ConfigFileFromSelector { # String to send to client ("ok" or "refused" if bad file). # sub PushFile { - my $request = shift; + my $request = shift; my ($command, $filename, $contents) = split(":", $request, 3); + &Debug("PushFile"); - # At this point in time, pushes for only the following tables are - # supported: + # At this point in time, pushes for only the following tables and + # CRL file are supported: # hosts.tab ($filename eq host). # domain.tab ($filename eq domain). + # dns_hosts.tab ($filename eq dns_host). + # dns_domain.tab ($filename eq dns_domain). + # loncapaCAcrl.pem ($filename eq loncapaCAcrl). # Construct the destination filename or reject the request. # # lonManage is supposed to ensure this, however this session could be @@ -627,20 +853,7 @@ sub PushFile { 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 @@ -649,21 +862,84 @@ sub PushFile { if($filename eq "host") { $contents = AdjustHostContents($contents); + } elsif (($filename eq 'dns_host') || ($filename eq 'dns_domain') || + ($filename eq 'loncapaCAcrl')) { + if ($contents eq '') { + &logthis(' Pushfile: unable to install ' + .$tablefile." - no data received from push. "); + return 'error: push had no data'; + } + if (&Apache::lonnet::get_host_ip($clientname)) { + my $clienthost = &Apache::lonnet::hostname($clientname); + if ($managers{$clientip} eq $clientname) { + my $clientprotocol = $Apache::lonnet::protocol{$clientname}; + $clientprotocol = 'http' if ($clientprotocol ne 'https'); + my $url; + if ($filename eq 'loncapaCAcrl') { + $url = '/adm/dns/loncapaCRL'; + } else { + $url = '/adm/'.$filename; + $url =~ s{_}{/}; + } + my $request=new HTTP::Request('GET',"$clientprotocol://$clienthost$url"); + my $response = LONCAPA::LWPReq::makerequest($clientname,$request,'',\%perlvar,60,0); + if ($response->is_error()) { + &logthis(' Pushfile: unable to install ' + .$tablefile." - error attempting to pull data. "); + return 'error: pull failed'; + } else { + my $result = $response->content; + chomp($result); + unless ($result eq $contents) { + &logthis(' Pushfile: unable to install ' + .$tablefile." - pushed data and pulled data differ. "); + my $pushleng = length($contents); + my $pullleng = length($result); + if ($pushleng != $pullleng) { + return "error: $pushleng vs $pullleng bytes"; + } else { + return "error: mismatch push and pull"; + } + } + } + } + } } # Install the new file: + &logthis("Installing new $tablefile contents:\n$contents"); if(!InstallFile($tablefile, $contents)) { &logthis(' Pushfile: unable to install ' .$tablefile." $! "); return "error:$!"; } else { &logthis(' Installed new '.$tablefile - .""); - + ." - transaction by: $clientname ($clientip)"); + my $adminmail = $perlvar{'lonAdmEMail'}; + my $admindom = &Apache::lonnet::host_domain($perlvar{'lonHostID'}); + if ($admindom ne '') { + my %domconfig = + &Apache::lonnet::get_dom('configuration',['contacts'],$admindom); + if (ref($domconfig{'contacts'}) eq 'HASH') { + if ($domconfig{'contacts'}{'adminemail'} ne '') { + $adminmail = $domconfig{'contacts'}{'adminemail'}; + } + } + } + if ($adminmail =~ /^[^\@]+\@[^\@]+$/) { + my $msg = new Mail::Send; + $msg->to($adminmail); + $msg->subject('LON-CAPA DNS update on '.$perlvar{'lonHostID'}); + $msg->add('Content-type','text/plain; charset=UTF-8'); + if (my $fh = $msg->open()) { + print $fh 'Update to '.$tablefile.' from Cluster Manager '. + "$clientname ($clientip)\n"; + $fh->close; + } + } } - # Indicate success: return "ok"; @@ -833,16 +1109,14 @@ sub AdjustOurHost { # 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); + 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, $ip, $maxcon, $idleto, $mincon) { + foreach my $item ($domain, $role, $name, $maxcon, $idleto, $mincon) { $newConfigLine .= ":".$item; } # Replace the line: @@ -888,11 +1162,11 @@ sub EditFile { # Split the command into it's pieces: edit:filetype:script - my ($request, $filetype, $script) = split(/:/, $request,3); # : in script + my ($cmd, $filetype, $script) = split(/:/, $request,3); # : in script # Check the pre-coditions for success: - if($request != "edit") { # Something is amiss afoot alack. + if($cmd != "edit") { # Something is amiss afoot alack. return "error:edit request detected, but request != 'edit'\n"; } if( ($filetype ne "hosts") && @@ -937,105 +1211,6 @@ sub EditFile { return "ok\n"; } -#--------------------------------------------------------------- -# -# Manipulation of hash based databases (factoring out common code -# for later use as we refactor. -# -# Ties a domain level resource file to a hash. -# If requested a history entry is created in the associated hist file. -# -# Parameters: -# domain - Name of the domain in which the resource file lives. -# namespace - Name of the hash within that domain. -# how - How to tie the hash (e.g. GDBM_WRCREAT()). -# loghead - Optional parameter, if present a log entry is created -# in the associated history file and this is the first part -# of that entry. -# logtail - Goes along with loghead, The actual logentry is of the -# form $loghead::logtail. -# Returns: -# Reference to a hash bound to the db file or alternatively undef -# if the tie failed. -# -sub tie_domain_hash { - my ($domain,$namespace,$how,$loghead,$logtail) = @_; - - # Filter out any whitespace in the domain name: - - $domain =~ s/\W//g; - - # We have enough to go on to tie the hash: - - my $user_top_dir = $perlvar{'lonUsersDir'}; - my $domain_dir = $user_top_dir."/$domain"; - my $resource_file = $domain_dir."/$namespace.db"; - my %hash; - if(tie(%hash, 'GDBM_File', $resource_file, $how, 0640)) { - if (defined($loghead)) { # Need to log the operation. - my $logFh = IO::File->new(">>$domain_dir/$namespace.hist"); - if($logFh) { - my $timestamp = time; - print $logFh "$loghead:$timestamp:$logtail\n"; - } - $logFh->close; - } - return \%hash; # Return the tied hash. - } else { - return undef; # Tie failed. - } -} - -# -# Ties a user's resource file to a hash. -# If necessary, an appropriate history -# log file entry is made as well. -# This sub factors out common code from the subs that manipulate -# the various gdbm files that keep keyword value pairs. -# Parameters: -# domain - Name of the domain the user is in. -# user - Name of the 'current user'. -# namespace - Namespace representing the file to tie. -# how - What the tie is done to (e.g. GDBM_WRCREAT(). -# loghead - Optional first part of log entry if there may be a -# history file. -# what - Optional tail of log entry if there may be a history -# file. -# Returns: -# hash to which the database is tied. It's up to the caller to untie. -# undef if the has could not be tied. -# -sub tie_user_hash { - my ($domain,$user,$namespace,$how,$loghead,$what) = @_; - - $namespace=~s/\//\_/g; # / -> _ - $namespace=~s/\W//g; # whitespace eliminated. - my $proname = propath($domain, $user); - - # Tie the database. - - my %hash; - if(tie(%hash, 'GDBM_File', "$proname/$namespace.db", - $how, 0640)) { - # If this is a namespace for which a history is kept, - # make the history log entry: - if (($namespace !~/^nohist\_/) && (defined($loghead))) { - my $args = scalar @_; - Debug(" Opening history: $namespace $args"); - my $hfh = IO::File->new(">>$proname/$namespace.hist"); - if($hfh) { - my $now = time; - print $hfh "$loghead:$now:$what\n"; - } - $hfh->close; - } - return \%hash; - } else { - return undef; - } - -} - # read_profile # # Returns a set of specific entries from a user's profile file. @@ -1060,13 +1235,16 @@ sub read_profile { &GDBM_READER()); if ($hashref) { my @queries=split(/\&/,$what); + if ($namespace eq 'roles') { + @queries = map { &unescape($_); } @queries; + } 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 %$hashref) { + if (&untie_user_hash($hashref)) { return $qresult; } else { return "error: ".($!+0)." untie (GDBM) Failed"; @@ -1105,7 +1283,7 @@ sub ping_handler { my ($cmd, $tail, $client) = @_; Debug("$cmd $tail $client .. $currenthostid:"); - Reply( $client,"$currenthostid\n","$cmd:$tail"); + Reply( $client,\$currenthostid,"$cmd:$tail"); return 1; } @@ -1131,7 +1309,7 @@ sub ping_handler { sub pong_handler { my ($cmd, $tail, $replyfd) = @_; - my $reply=&reply("ping",$clientname); + my $reply=&Apache::lonnet::reply("ping",$clientname); &Reply( $replyfd, "$currenthostid:$reply\n", "$cmd:$tail"); return 1; } @@ -1153,7 +1331,7 @@ sub pong_handler { # Implicit Inputs: # $currenthostid - Global variable that carries the name of the host # known as. -# $clientname - Global variable that carries the name of the hsot we're connected to. +# $clientname - Global variable that carries the name of the host we're connected to. # Returns: # 1 - Ok to continue processing. # 0 - Program should exit. @@ -1175,7 +1353,7 @@ sub establish_key_handler { $key=substr($key,0,32); my $cipherkey=pack("H32",$key); $cipher=new IDEA $cipherkey; - &Reply($replyfd, "$buildkey\n", "$cmd:$tail"); + &Reply($replyfd, \$buildkey, "$cmd:$tail"); return 1; @@ -1192,7 +1370,7 @@ sub establish_key_handler { # Implicit Inputs: # $currenthostid - Global variable that carries the name of the host # known as. -# $clientname - Global variable that carries the name of the hsot we're connected to. +# $clientname - Global variable that carries the name of the host we're connected to. # Returns: # 1 - Ok to continue processing. # 0 - Program should exit. @@ -1201,6 +1379,8 @@ sub establish_key_handler { 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 @@ -1212,7 +1392,7 @@ sub load_handler { my $loadpercent=100*$loadavg/$perlvar{'lonLoadLim'}; - &Reply( $replyfd, "$loadpercent\n", "$cmd:$tail"); + &Reply( $replyfd, \$loadpercent, "$cmd:$tail"); return 1; } @@ -1229,7 +1409,7 @@ sub load_handler { # Implicit Inputs: # $currenthostid - Global variable that carries the name of the host # known as. -# $clientname - Global variable that carries the name of the hsot we're connected to. +# $clientname - Global variable that carries the name of the host we're connected to. # Returns: # 1 - Ok to continue processing. # 0 - Program should exit @@ -1241,8 +1421,8 @@ sub load_handler { sub user_load_handler { my ($cmd, $tail, $replyfd) = @_; - my $userloadpercent=&userload(); - &Reply($replyfd, "$userloadpercent\n", "$cmd:$tail"); + my $userloadpercent=&Apache::lonnet::userload(); + &Reply($replyfd, \$userloadpercent, "$cmd:$tail"); return 1; } @@ -1285,7 +1465,7 @@ sub user_authorization_type { } else { $type .= ':'; } - &Reply( $replyfd, "$type\n", $userinput); + &Reply( $replyfd, \$type, $userinput); } return 1; @@ -1307,7 +1487,7 @@ sub user_authorization_type { # a reply is written to the client. sub push_file_handler { my ($cmd, $tail, $client) = @_; - + &Debug("In push file handler"); my $userinput = "$cmd:$tail"; # At this time we only know that the IP of our partner is a valid manager @@ -1315,23 +1495,27 @@ sub push_file_handler { # spoofing). my $cert = &GetCertificate($userinput); - if(&ValidManager($cert)) { + if(&ValidManager($cert)) { + &Debug("Valid manager: $client"); # 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); + &Reply($client, \$reply, $userinput); } else { + &logthis("push_file_handler $client is not valid"); &Failure( $client, "refused\n", $userinput); } return 1; } ®ister_handler("pushfile", \&push_file_handler, 1, 0, 1); +# The du_handler routine should be considered obsolete and is retained +# for communication with legacy servers. Please see the du2_handler. # -# du - list the disk usuage of a directory recursively. +# du - list the disk usage of a directory recursively. # # note: stolen code from the ls file handler # under construction by Rick Banghart @@ -1349,7 +1533,7 @@ sub push_file_handler { # sub du_handler { my ($cmd, $ududir, $client) = @_; - my ($ududir) = split(/:/,$ududir); # Make 'telnet' testing easier. + ($ududir) = split(/:/,$ududir); # Make 'telnet' testing easier. my $userinput = "$cmd:$ududir"; if ($ududir=~/\.\./ || $ududir!~m|^/home/httpd/|) { @@ -1363,31 +1547,112 @@ sub du_handler { # etc. # if (-d $ududir) { - # And as Shakespeare would say to make - # assurance double sure, - # use execute_command to ensure that the command is not executed in - # a shell that can screw us up. - - my $duout = execute_command("du -ks $ududir"); - $duout=~s/[^\d]//g; #preserve only the numbers - &Reply($client,"$duout\n","$cmd:$ududir"); + my $total_size=0; + my $code=sub { + if ($_=~/\.\d+\./) { return;} + if ($_=~/\.meta$/) { return;} + if (-d $_) { return;} + $total_size+=(stat($_))[7]; + }; + chdir($ududir); + find($code,$ududir); + $total_size=int($total_size/1024); + &Reply($client,\$total_size,"$cmd:$ududir"); } else { - &Failure($client, "bad_directory:$ududir\n","$cmd:$ududir"); - } return 1; } ®ister_handler("du", \&du_handler, 0, 1, 0); +# Please also see the du_handler, which is obsoleted by du2. +# du2_handler differs from du_handler in that required path to directory +# provided by &propath() is prepended in the handler instead of on the +# client side. +# +# du2 - list the disk usage of a directory recursively. +# +# Parameters: +# $cmd - The command that dispatched us (du). +# $tail - The tail of the request that invoked us. +# $tail is a : separated list of the following: +# - $ududir - directory path to list (before prepending) +# - $getpropath = 1 if &propath() should prepend +# - $uname - username to use for &propath or user dir +# - $udom - domain to use for &propath or user dir +# All are escaped. +# $client - Socket open on the client. +# Returns: +# 1 - indicating that the daemon should not disconnect. +# Side Effects: +# The reply is written to $client. +# + +sub du2_handler { + my ($cmd, $tail, $client) = @_; + my ($ududir,$getpropath,$uname,$udom) = map { &unescape($_) } (split(/:/, $tail)); + my $userinput = "$cmd:$tail"; + if (($ududir=~/\.\./) || (($ududir!~m|^/home/httpd/|) && (!$getpropath))) { + &Failure($client,"refused\n","$cmd:$tail"); + return 1; + } + if ($getpropath) { + if (($uname =~ /^$LONCAPA::match_name$/) && ($udom =~ /^$LONCAPA::match_domain$/)) { + $ududir = &propath($udom,$uname).'/'.$ududir; + } else { + &Failure($client,"refused\n","$cmd:$tail"); + 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;} + if (-d $_) { return;} + $total_size+=(stat($_))[7]; + }; + chdir($ududir); + find($code,$ududir); + $total_size=int($total_size/1024); + &Reply($client,\$total_size,"$cmd:$ududir"); + } else { + &Failure($client, "bad_directory:$ududir\n","$cmd:$tail"); + } + return 1; +} +®ister_handler("du2", \&du2_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. +# The ls_handler routine should be considered obsolete and is retained +# for communication with legacy servers. Please see the ls3_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. +# +# If the requested path contains /../ or is: +# +# 1. for a directory, and the path does not begin with one of: +# (a) /home/httpd/html/res/ +# (b) /home/httpd/html/userfiles/ +# (c) /home/httpd/lonUsers//<1>/<2>/<3>//userfiles +# or is: +# +# 2. for a file, and the path (after prepending) does not begin with one of: +# (a) /home/httpd/lonUsers//<1>/<2>/<3>// +# (b) /home/httpd/html/res/// +# (c) /home/httpd/html/userfiles/// +# +# the response will be "refused". +# # Parameters: # $cmd - The command that dispatched us (ls). # $ulsdir - The directory path to list... I'm not sure what this @@ -1409,18 +1674,28 @@ sub ls_handler { my $rights; my $ulsout=''; my $ulsfn; + if ($ulsdir =~m{/\.\./}) { + &Failure($client,"refused\n",$userinput); + return 1; + } if (-e $ulsdir) { if(-d $ulsdir) { + unless (($ulsdir =~ m{^/home/httpd/html/(res/$LONCAPA::match_domain|userfiles/)}) || + ($ulsdir =~ m{^/home/httpd/lonUsers/$LONCAPA::match_domain(?:/[\w\-.@]){3}/$LONCAPA::match_name/userfiles})) { + &Failure($client,"refused\n",$userinput); + return 1; + } if (opendir(LSDIR,$ulsdir)) { while ($ulsfn=readdir(LSDIR)) { - undef $obs, $rights; + 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)|) { $obs = 1; } + if($obsolete =~ m/()(on|1)/) { $obs = 1; } if($obsolete =~ m|()(default)|) { $rights = 1; } } } @@ -1433,6 +1708,11 @@ sub ls_handler { closedir(LSDIR); } } else { + unless (($ulsdir =~ m{^/home/httpd/lonUsers/$LONCAPA::match_domain(?:/[\w\-.@]){3}/$LONCAPA::match_name/}) || + ($ulsdir =~ m{^/home/httpd/html/(?:res|userfiles)/$LONCAPA::match_domain/$LONCAPA::match_name/})) { + &Failure($client,"refused\n",$userinput); + return 1; + } my @ulsstats=stat($ulsdir); $ulsout.=$ulsfn.'&'.join('&',@ulsstats).':'; } @@ -1440,15 +1720,16 @@ sub ls_handler { $ulsout='no_such_dir'; } if ($ulsout eq '') { $ulsout='empty'; } - &Reply($client, "$ulsout\n", $userinput); # This supports debug logging. + &Reply($client, \$ulsout, $userinput); # This supports debug logging. return 1; } ®ister_handler("ls", \&ls_handler, 0, 1, 0); -# -# Please also see the ls_handler, which this routine obosolets. +# The ls2_handler routine should be considered obsolete and is retained +# for communication with legacy servers. Please see the ls3_handler. +# Please also see the ls_handler, which was itself obsoleted by ls2. # ls2_handler differs from ls_handler in that it escapes its return # values before concatenating them together with ':'s. # @@ -1456,6 +1737,22 @@ sub ls_handler { # 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. +# +# If the requested path contains /../ or is: +# +# 1. for a directory, and the path does not begin with one of: +# (a) /home/httpd/html/res/ +# (b) /home/httpd/html/userfiles/ +# (c) /home/httpd/lonUsers//<1>/<2>/<3>//userfiles +# or is: +# +# 2. for a file, and the path (after prepending) does not begin with one of: +# (a) /home/httpd/lonUsers//<1>/<2>/<3>// +# (b) /home/httpd/html/res/// +# (c) /home/httpd/html/userfiles/// +# +# the response will be "refused". +# # Parameters: # $cmd - The command that dispatched us (ls). # $ulsdir - The directory path to list... I'm not sure what this @@ -1476,18 +1773,28 @@ sub ls2_handler { my $rights; my $ulsout=''; my $ulsfn; + if ($ulsdir =~m{/\.\./}) { + &Failure($client,"refused\n",$userinput); + return 1; + } if (-e $ulsdir) { if(-d $ulsdir) { + unless (($ulsdir =~ m{^/home/httpd/html/(res/$LONCAPA::match_domain|userfiles/)}) || + ($ulsdir =~ m{^/home/httpd/lonUsers/$LONCAPA::match_domain(?:/[\w\-.@]){3}/$LONCAPA::match_name/userfiles})) { + &Failure($client,"refused\n","$userinput"); + return 1; + } if (opendir(LSDIR,$ulsdir)) { while ($ulsfn=readdir(LSDIR)) { - undef $obs, $rights; + 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)|) { $obs = 1; } + if($obsolete =~ m/()(on|1)/) { $obs = 1; } if($obsolete =~ m|()(default)|) { $rights = 1; } @@ -1501,6 +1808,11 @@ sub ls2_handler { closedir(LSDIR); } } else { + unless (($ulsdir =~ m{^/home/httpd/lonUsers/$LONCAPA::match_domain(?:/[\w\-.@]){3}/$LONCAPA::match_name/}) || + ($ulsdir =~ m{^/home/httpd/html/(?:res|userfiles)/$LONCAPA::match_domain/$LONCAPA::match_name/})) { + &Failure($client,"refused\n",$userinput); + return 1; + } my @ulsstats=stat($ulsdir); $ulsout.=$ulsfn.'&'.join('&',@ulsstats).':'; } @@ -1508,10 +1820,324 @@ sub ls2_handler { $ulsout='no_such_dir'; } if ($ulsout eq '') { $ulsout='empty'; } - &Reply($client, "$ulsout\n", $userinput); # This supports debug logging. + &Reply($client, \$ulsout, $userinput); # This supports debug logging. return 1; } ®ister_handler("ls2", \&ls2_handler, 0, 1, 0); +# +# ls3 - 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. +# +# If the requested path (after prepending) contains /../ or is: +# +# 1. for a directory, and the path does not begin with one of: +# (a) /home/httpd/html/res/ +# (b) /home/httpd/html/userfiles/ +# (c) /home/httpd/lonUsers//<1>/<2>/<3>//userfiles +# (d) /home/httpd/html/priv/ and client is the homeserver +# +# or is: +# +# 2. for a file, and the path (after prepending) does not begin with one of: +# (a) /home/httpd/lonUsers//<1>/<2>/<3>// +# (b) /home/httpd/html/res/// +# (c) /home/httpd/html/userfiles/// +# (d) /home/httpd/html/priv/// and client is the homeserver +# +# the response will be "refused". +# +# Parameters: +# $cmd - The command that dispatched us (ls). +# $tail - The tail of the request that invoked us. +# $tail is a : separated list of the following: +# - $ulsdir - directory path to list (before prepending) +# - $getpropath = 1 if &propath() should prepend +# - $getuserdir = 1 if path to user dir in lonUsers should +# prepend +# - $alternate_root - path to prepend +# - $uname - username to use for &propath or user dir +# - $udom - domain to use for &propath or user dir +# All of these except $getpropath and &getuserdir are escaped. +# 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 ls3_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($ulsdir,$getpropath,$getuserdir,$alternate_root,$uname,$udom) = + split(/:/,$tail); + if (defined($ulsdir)) { + $ulsdir = &unescape($ulsdir); + } + if (defined($alternate_root)) { + $alternate_root = &unescape($alternate_root); + } + if (defined($uname)) { + $uname = &unescape($uname); + } + if (defined($udom)) { + $udom = &unescape($udom); + } + + my $dir_root = $perlvar{'lonDocRoot'}; + if (($getpropath) || ($getuserdir)) { + if (($uname =~ /^$LONCAPA::match_name$/) && ($udom =~ /^$LONCAPA::match_domain$/)) { + $dir_root = &propath($udom,$uname); + $dir_root =~ s/\/$//; + } else { + &Failure($client,"refused\n",$userinput); + return 1; + } + } elsif ($alternate_root ne '') { + $dir_root = $alternate_root; + } + if (($dir_root ne '') && ($dir_root ne '/')) { + if ($ulsdir =~ /^\//) { + $ulsdir = $dir_root.$ulsdir; + } else { + $ulsdir = $dir_root.'/'.$ulsdir; + } + } + if ($ulsdir =~m{/\.\./}) { + &Failure($client,"refused\n",$userinput); + return 1; + } + my $islocal; + my @machine_ids = &Apache::lonnet::current_machine_ids(); + if (grep(/^\Q$clientname\E$/,@machine_ids)) { + $islocal = 1; + } + my $obs; + my $rights; + my $ulsout=''; + my $ulsfn; + + my ($crscheck,$toplevel,$currdom,$currnum,$skip); + unless ($islocal) { + my ($major,$minor) = split(/\./,$clientversion); + if (($major < 2) || ($major == 2 && $minor < 12)) { + $crscheck = 1; + } + } + if (-e $ulsdir) { + if(-d $ulsdir) { + unless (($getpropath) || ($getuserdir) || + ($ulsdir =~ m{^/home/httpd/html/(res/$LONCAPA::match_domain|userfiles/)}) || + ($ulsdir =~ m{^/home/httpd/lonUsers/$LONCAPA::match_domain(?:/[\w\-.@]){3}/$LONCAPA::match_name/userfiles}) || + (($ulsdir =~ m{^/home/httpd/html/priv/$LONCAPA::match_domain}) && ($islocal))) { + &Failure($client,"refused\n",$userinput); + return 1; + } + if (($crscheck) && + ($ulsdir =~ m{^/home/httpd/html/res/($LONCAPA::match_domain)(/?$|/$LONCAPA::match_courseid)})) { + ($currdom,my $posscnum) = ($1,$2); + if (($posscnum eq '') || ($posscnum eq '/')) { + $toplevel = 1; + } else { + $posscnum =~ s{^/+}{}; + if (&LONCAPA::Lond::is_course($currdom,$posscnum)) { + $skip = 1; + } + } + } + if ((!$skip) && (opendir(LSDIR,$ulsdir))) { + while ($ulsfn=readdir(LSDIR)) { + if (($crscheck) && ($toplevel) && ($currdom ne '') && + ($ulsfn =~ /^$LONCAPA::match_courseid$/) && (-d "$ulsdir/$ulsfn")) { + if (&LONCAPA::Lond::is_course($currdom,$ulsfn)) { + next; + } + } + 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 { + unless (($getpropath) || ($getuserdir) || + ($ulsdir =~ m{^/home/httpd/lonUsers/$LONCAPA::match_domain(?:/[\w\-.@]){3}/$LONCAPA::match_name/}) || + ($ulsdir =~ m{^/home/httpd/html/(?:res|userfiles)/$LONCAPA::match_domain/$LONCAPA::match_name/}) || + (($ulsdir =~ m{^/home/httpd/html/priv/$LONCAPA::match_domain/$LONCAPA::match_name/}) && ($islocal))) { + &Failure($client,"refused\n",$userinput); + return 1; + } + my @ulsstats=stat($ulsdir); + $ulsout.=$ulsfn.'&'.join('&',@ulsstats).':'; + } + } else { + $ulsout='no_such_dir'; + } + if ($ulsout eq '') { $ulsout='empty'; } + &Reply($client, \$ulsout, $userinput); # This supports debug logging. + return 1; +} +®ister_handler("ls3", \&ls3_handler, 0, 1, 0); + +sub read_lonnet_global { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + my $requested = &Apache::lonnet::thaw_unescape($tail); + my $result; + my %packagevars = ( + spareid => \%Apache::lonnet::spareid, + perlvar => \%Apache::lonnet::perlvar, + ); + my %limit_to = ( + perlvar => { + lonOtherAuthen => 1, + lonBalancer => 1, + lonVersion => 1, + lonAdmEMail => 1, + lonSupportEMail => 1, + lonSysEMail => 1, + lonHostID => 1, + lonRole => 1, + lonDefDomain => 1, + lonLoadLim => 1, + lonUserLoadLim => 1, + } + ); + if (ref($requested) eq 'HASH') { + foreach my $what (keys(%{$requested})) { + my $response; + my $items = {}; + if (exists($packagevars{$what})) { + if (ref($limit_to{$what}) eq 'HASH') { + foreach my $varname (keys(%{$packagevars{$what}})) { + if ($limit_to{$what}{$varname}) { + $items->{$varname} = $packagevars{$what}{$varname}; + } + } + } else { + $items = $packagevars{$what}; + } + if ($what eq 'perlvar') { + if (!exists($packagevars{$what}{'lonBalancer'})) { + if ($dist =~ /^(centos|rhes|fedora|scientific|oracle)/) { + my $othervarref=LONCAPA::Configuration::read_conf('httpd.conf'); + if (ref($othervarref) eq 'HASH') { + $items->{'lonBalancer'} = $othervarref->{'lonBalancer'}; + } + } + } + } + $response = &Apache::lonnet::freeze_escape($items); + } + $result .= &escape($what).'='.$response.'&'; + } + } + $result =~ s/\&$//; + &Reply($client,\$result,$userinput); + return 1; +} +®ister_handler("readlonnetglobal", \&read_lonnet_global, 0, 1, 0); + +sub server_devalidatecache_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + my $items = &unescape($tail); + my @cached = split(/\&/,$items); + foreach my $key (@cached) { + if ($key =~ /:/) { + my ($name,$id) = map { &unescape($_); } split(/:/,$key); + &Apache::lonnet::devalidate_cache_new($name,$id); + } + } + my $result = 'ok'; + &Reply($client,\$result,$userinput); + return 1; +} +®ister_handler("devalidatecache", \&server_devalidatecache_handler, 0, 1, 0); + +sub server_timezone_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + my $timezone; + my $clockfile = '/etc/sysconfig/clock'; # Fedora/CentOS/SuSE + my $tzfile = '/etc/timezone'; # Debian/Ubuntu + if (-e $clockfile) { + if (open(my $fh,"<$clockfile")) { + while (<$fh>) { + next if (/^[\#\s]/); + if (/^(?:TIME)?ZONE\s*=\s*['"]?\s*([\w\/]+)/) { + $timezone = $1; + last; + } + } + close($fh); + } + } elsif (-e $tzfile) { + if (open(my $fh,"<$tzfile")) { + $timezone = <$fh>; + close($fh); + chomp($timezone); + if ($timezone =~ m{^Etc/(\w+)$}) { + $timezone = $1; + } + } + } + &Reply($client,\$timezone,$userinput); # This supports debug logging. + return 1; +} +®ister_handler("servertimezone", \&server_timezone_handler, 0, 1, 0); + +sub server_loncaparev_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + &Reply($client,\$perlvar{'lonVersion'},$userinput); + return 1; +} +®ister_handler("serverloncaparev", \&server_loncaparev_handler, 0, 1, 0); + +sub server_homeID_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + &Reply($client,\$perlvar{'lonHostID'},$userinput); + return 1; +} +®ister_handler("serverhomeID", \&server_homeID_handler, 0, 1, 0); + +sub server_distarch_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + my $reply = &distro_and_arch(); + &Reply($client,\$reply,$userinput); + return 1; +} +®ister_handler("serverdistarch", \&server_distarch_handler, 0, 1, 0); + +sub server_certs_handler { + my ($cmd,$tail,$client) = @_; + my $userinput = "$cmd:$tail"; + my $hostname = &Apache::lonnet::hostname($perlvar{'lonHostID'}); + my $result = &LONCAPA::Lond::server_certs(\%perlvar,$perlvar{'lonHostID'},$hostname); + &Reply($client,\$result,$userinput); + return; +} +®ister_handler("servercerts", \&server_certs_handler, 0, 1, 0); # Process a reinit request. Reinit requests that either # lonc or lond be reinitialized so that an updated @@ -1536,7 +2162,7 @@ sub reinit_process_handler { if(&ValidManager($cert)) { chomp($userinput); my $reply = &ReinitProcess($userinput); - &Reply( $client, "$reply\n", $userinput); + &Reply( $client, \$reply, $userinput); } else { &Failure( $client, "refused\n", $userinput); } @@ -1620,15 +2246,47 @@ sub authenticate_handler { # udom - User's domain. # uname - Username. # upass - User's password. + # checkdefauth - Pass to validate_user() to try authentication + # with default auth type(s) if no user account. + # clientcancheckhost - Passed by clients with functionality in lonauth.pm + # to check if session can be hosted. - my ($udom,$uname,$upass)=split(/:/,$tail); - &Debug(" Authenticate domain = $udom, user = $uname, password = $upass"); + my ($udom, $uname, $upass, $checkdefauth, $clientcancheckhost)=split(/:/,$tail); + &Debug(" Authenticate domain = $udom, user = $uname, password = $upass, checkdefauth = $checkdefauth"); chomp($upass); $upass=&unescape($upass); - my $pwdcorrect = &validate_user($udom, $uname, $upass); + my $pwdcorrect = &validate_user($udom,$uname,$upass,$checkdefauth); if($pwdcorrect) { - &Reply( $client, "authorized\n", $userinput); + my $canhost = 1; + unless ($clientcancheckhost) { + my $uprimary_id = &Apache::lonnet::domain($udom,'primary'); + my $uint_dom = &Apache::lonnet::internet_dom($uprimary_id); + my @intdoms; + my $internet_names = &Apache::lonnet::get_internet_names($clientname); + if (ref($internet_names) eq 'ARRAY') { + @intdoms = @{$internet_names}; + } + unless ($uint_dom ne '' && grep(/^\Q$uint_dom\E$/,@intdoms)) { + my ($remote,$hosted); + my $remotesession = &get_usersession_config($udom,'remotesession'); + if (ref($remotesession) eq 'HASH') { + $remote = $remotesession->{'remote'}; + } + my $hostedsession = &get_usersession_config($clienthomedom,'hostedsession'); + if (ref($hostedsession) eq 'HASH') { + $hosted = $hostedsession->{'hosted'}; + } + $canhost = &Apache::lonnet::can_host_session($udom,$clientname, + $clientversion, + $remote,$hosted); + } + } + if ($canhost) { + &Reply( $client, "authorized\n", $userinput); + } else { + &Reply( $client, "not_allowed_to_host\n", $userinput); + } # # Bad credentials: Failed to authorize # @@ -1671,40 +2329,129 @@ sub change_password_handler { # uname - Username. # upass - Current password. # npass - New password. + # context - Context in which this was called + # (preferences or reset_by_email). + # lonhost - HostID of server where request originated - my ($udom,$uname,$upass,$npass)=split(/:/,$tail); + my ($udom,$uname,$upass,$npass,$context,$lonhost)=split(/:/,$tail); $upass=&unescape($upass); $npass=&unescape($npass); &Debug("Trying to change password for $uname"); # First require that the user can be authenticated with their - # old password: - - my $validated = &validate_user($udom, $uname, $upass); + # old password unless context was 'reset_by_email': + + my ($validated,$failure); + if ($context eq 'reset_by_email') { + if ($lonhost eq '') { + $failure = 'invalid_client'; + } else { + $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); + my $notunique; if ($howpwd eq 'internal') { &Debug("internal auth"); - 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"); + my $ncpass = &hash_passwd($udom,$npass); + my (undef,$method,@rest) = split(/!/,$contentpwd); + if ($method eq 'bcrypt') { + my %passwdconf = &Apache::lonnet::get_passwdconf($udom); + if (($passwdconf{'numsaved'}) && ($passwdconf{'numsaved'} =~ /^\d+$/)) { + my @oldpasswds; + my $userpath = &propath($udom,$uname); + my $fullpath = $userpath.'/oldpasswds'; + if (-d $userpath) { + my @oldfiles; + if (-e $fullpath) { + if (opendir(my $dir,$fullpath)) { + (@oldfiles) = grep(/^\d+$/,readdir($dir)); + closedir($dir); + } + if (@oldfiles) { + @oldfiles = sort { $b <=> $a } (@oldfiles); + my $numremoved = 0; + for (my $i=0; $i<@oldfiles; $i++) { + if ($i>=$passwdconf{'numsaved'}) { + if (-f "$fullpath/$oldfiles[$i]") { + if (unlink("$fullpath/$oldfiles[$i]")) { + $numremoved ++; + } + } + } elsif (open(my $fh,'<',"$fullpath/$oldfiles[$i]")) { + while (my $line = <$fh>) { + push(@oldpasswds,$line); + } + close($fh); + } + } + if ($numremoved) { + &logthis("unlinked $numremoved old password files for $uname:$udom"); + } + } + } + push(@oldpasswds,$contentpwd); + foreach my $item (@oldpasswds) { + my (undef,$method,@rest) = split(/!/,$item); + if ($method eq 'bcrypt') { + my $result = &hash_passwd($udom,$npass,@rest); + if ($result eq $item) { + $notunique = 1; + last; + } + } + } + unless ($notunique) { + unless (-e $fullpath) { + if (&mkpath("$fullpath/")) { + chmod(0700,$fullpath); + } + } + if (-d $fullpath) { + my $now = time; + if (open(my $fh,'>',"$fullpath/$now")) { + print $fh $contentpwd; + close($fh); + chmod(0400,"$fullpath/$now"); + } + } + } + } + } + } + if ($notunique) { + my $msg="Result of password change for $uname:$udom - password matches one used before"; + if ($lonhost) { + $msg .= " - request originated from: $lonhost"; + } + &logthis($msg); + &Reply($client, "prioruse\n", $userinput); + } elsif (&rewrite_password_file($udom, $uname, "internal:$ncpass")) { + my $msg="Result of password change for $uname: pwchange_success"; + if ($lonhost) { + $msg .= " - request originated from: $lonhost"; + } + &logthis($msg); + &update_passwd_history($uname,$udom,$howpwd,$context); &Reply($client, "ok\n", $userinput); } else { &logthis("Unable to open $uname passwd " ."to change password"); &Failure( $client, "non_authorized\n",$userinput); } - } elsif ($howpwd eq 'unix') { + } elsif ($howpwd eq 'unix' && $context ne 'reset_by_email') { my $result = &change_unix_password($uname, $npass); + if ($result eq 'ok') { + &update_passwd_history($uname,$udom,$howpwd,$context); + } &logthis("Result of password change for $uname: ". $result); - &Reply($client, "$result\n", $userinput); + &Reply($client, \$result, $userinput); } else { # this just means that the current password mode is not # one we know how to change (e.g the kerberos auth modes or @@ -1712,15 +2459,49 @@ sub change_password_handler { # &Failure( $client, "auth_mode_error\n", $userinput); } - } else { - &Failure( $client, "non_authorized\n", $userinput); + if ($failure eq '') { + $failure = 'non_authorized'; + } + &Failure( $client, "$failure\n", $userinput); } return 1; } ®ister_handler("passwd", \&change_password_handler, 1, 1, 0); +sub hash_passwd { + my ($domain,$plainpass,@rest) = @_; + my ($salt,$cost); + if (@rest) { + $cost = $rest[0]; + # salt is first 22 characters, base-64 encoded by bcrypt + my $plainsalt = substr($rest[1],0,22); + $salt = Crypt::Eksblowfish::Bcrypt::de_base64($plainsalt); + } else { + my %domdefaults = &Apache::lonnet::get_domain_defaults($domain); + my $defaultcost = $domdefaults{'intauth_cost'}; + if (($defaultcost eq '') || ($defaultcost =~ /D/)) { + $cost = 10; + } else { + $cost = $defaultcost; + } + # Generate random 16-octet base64 salt + $salt = ""; + $salt .= pack("C", int rand(256)) for 1..16; + } + my $hash = &Crypt::Eksblowfish::Bcrypt::bcrypt_hash({ + key_nul => 1, + cost => $cost, + salt => $salt, + }, Digest::SHA::sha512(Encode::encode('UTF-8',$plainpass))); + + my $result = join("!", "", "bcrypt", sprintf("%02d",$cost), + &Crypt::Eksblowfish::Bcrypt::en_base64($salt). + &Crypt::Eksblowfish::Bcrypt::en_base64($hash)); + return $result; +} + # # Create a new user. User in this case means a lon-capa user. # The user must either already exist in some authentication realm @@ -1764,10 +2545,11 @@ sub add_user_handler { ."makeuser"; } unless ($fperror) { - my $result=&make_passwd_file($uname, $umode,$npass, $passfilename); - &Reply($client, $result, $userinput); #BUGBUG - could be fail + my $result=&make_passwd_file($uname,$udom,$umode,$npass, + $passfilename,'makeuser'); + &Reply($client,\$result, $userinput); #BUGBUG - could be fail } else { - &Failure($client, "$fperror\n", $userinput); + &Failure($client, \$fperror, $userinput); } } umask($oldumask); @@ -1825,37 +2607,31 @@ sub change_authentication_handler { 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). + # passwd since otherwise make_passwd_file will fail as + # creation of unix authenticated users is no longer supported + # except from the command line, when running make_domain_coordinator.pl 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") + &update_passwd_history($uname,$udom,$umode,'changeuserauth'); + &Reply($client, \$result); } else { - &Failure($client, "$result\n"); + &Failure($client, \$result); } } else { - my $result=&make_passwd_file($uname, $umode,$npass,$passfilename); + my $result=&make_passwd_file($uname,$udom,$umode,$npass, + $passfilename,'changeuserauth'); # # 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); + + + &Reply($client, \$result, $userinput); } @@ -1867,6 +2643,17 @@ sub change_authentication_handler { } ®ister_handler("changeuserauth", \&change_authentication_handler, 1,1, 0); +sub update_passwd_history { + my ($uname,$udom,$umode,$context) = @_; + my $proname=&propath($udom,$uname); + my $now = time; + if (open(my $fh,">>$proname/passwd.log")) { + print $fh "$now:$umode:$context\n"; + close($fh); + } + return; +} + # # Determines if this is the home server for a user. The home server # for a user will have his/her lon-capa passwd file. Therefore all we need @@ -1901,16 +2688,10 @@ sub is_home_handler { ®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. +# Process an update request for a resource. +# A resource has been modified that we hold a subscription to. # If the resource is not local, then we must update, or at least invalidate our # cached copy of the resource. -# FUTURE WORK: -# I need to look at this logic carefully. My druthers would be to follow -# typical caching logic, and simple invalidate the cache, drop any subscription -# an let the next fetch start the ball rolling again... however that may -# actually be more difficult than it looks given the complex web of -# proxy servers. # Parameters: # $cmd - The command that got us here. # $tail - Tail of the command (remaining parameters). @@ -1934,42 +2715,50 @@ sub update_resource_handler { my $ownership=ishome($fname); if ($ownership eq 'not_owner') { if (-e $fname) { + # Delete preview file, if exists + unlink("$fname.tmp"); + # Get usage stats my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks)=stat($fname); my $now=time; my $since=$now-$atime; + # If the file has not been used within lonExpire seconds, + # unsubscribe from it and delete local copy if ($since>$perlvar{'lonExpire'}) { - my $reply=&reply("unsub:$fname","$clientname"); + my $reply=&Apache::lonnet::reply("unsub:$fname","$clientname"); + &devalidate_meta_cache($fname); unlink("$fname"); + unlink("$fname.meta"); } else { + # Yes, this is in active use. Get a fresh copy. Since it might be in + # very active use and huge (like a movie), copy it to "in.transfer" filename first. my $transname="$fname.in.transfer"; - my $remoteurl=&reply("sub:$fname","$clientname"); + my $remoteurl=&Apache::lonnet::reply("sub:$fname","$clientname"); my $response; - alarm(120); - { - my $ua=new LWP::UserAgent; - my $request=new HTTP::Request('GET',"$remoteurl"); - $response=$ua->request($request,$transname); - } - alarm(0); +# FIXME: cannot replicate files that take more than two minutes to transfer -- needs checking now 1200s timeout used +# for LWP request. + my $request=new HTTP::Request('GET',"$remoteurl"); + $response=&LONCAPA::LWPReq::makerequest($clientname,$request,$transname,\%perlvar,1200,0,1); if ($response->is_error()) { - unlink($transname); + my $reply=&Apache::lonnet::reply("unsub:$fname","$clientname"); + &devalidate_meta_cache($fname); + if (-e $transname) { + unlink($transname); + } + unlink($fname); my $message=$response->status_line; &logthis("LWP GET: $message for $fname ($remoteurl)"); } else { if ($remoteurl!~/\.meta$/) { - alarm(120); - { - my $ua=new LWP::UserAgent; - my $mrequest=new HTTP::Request('GET',$remoteurl.'.meta'); - my $mresponse=$ua->request($mrequest,$fname.'.meta'); - if ($mresponse->is_error()) { - unlink($fname.'.meta'); - } + my $mrequest=new HTTP::Request('GET',$remoteurl.'.meta'); + my $mresponse = &LONCAPA::LWPReq::makerequest($clientname,$mrequest,$fname.'.meta',\%perlvar,120,0,1); + if ($mresponse->is_error()) { + unlink($fname.'.meta'); } - alarm(0); } + # we successfully transfered, copy file over to real name rename($transname,$fname); + &devalidate_meta_cache($fname); } } &Reply( $client, "ok\n", $userinput); @@ -1983,6 +2772,16 @@ sub update_resource_handler { } ®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 = &Apache::lonnet::declutter($url); + $url =~ s-\.meta$--; + my $id = &escape('meta:'.$url); + $memcache->delete($id); +} + # # Fetch a user file from a remote server to the user's home directory # userfiles subdir. @@ -2019,16 +2818,19 @@ sub fetch_user_file_handler { my $destname=$udir.'/'.$ufile; my $transname=$udir.'/'.$ufile.'.in.transit'; - my $remoteurl='http://'.$clientip.'/userfiles/'.$fname; + my $clientprotocol=$Apache::lonnet::protocol{$clientname}; + $clientprotocol = 'http' if ($clientprotocol ne 'https'); + my $clienthost = &Apache::lonnet::hostname($clientname); + my $remoteurl=$clientprotocol.'://'.$clienthost.'/userfiles/'.$fname; my $response; Debug("Remote URL : $remoteurl Transfername $transname Destname: $destname"); - alarm(120); - { - my $ua=new LWP::UserAgent; - my $request=new HTTP::Request('GET',"$remoteurl"); - $response=$ua->request($request,$transname); - } - alarm(0); + my $request=new HTTP::Request('GET',"$remoteurl"); + my $verifycert = 1; + my @machine_ids = &Apache::lonnet::current_machine_ids(); + if (grep(/^\Q$clientname\E$/,@machine_ids)) { + $verifycert = 0; + } + $response = &LONCAPA::LWPReq::makerequest($clientname,$request,$transname,\%perlvar,1200,$verifycert); if ($response->is_error()) { unlink($transname); my $message=$response->status_line; @@ -2041,6 +2843,24 @@ sub fetch_user_file_handler { unlink($transname); &Failure($client, "failed\n", $userinput); } else { + if ($fname =~ /^default.+\.(page|sequence)$/) { + my ($major,$minor) = split(/\./,$clientversion); + if (($major < 2) || ($major == 2 && $minor < 11)) { + my $now = time; + &Apache::lonnet::do_cache_new('crschange',$udom.'_'.$uname,$now,600); + my $key = &escape('internal.contentchange'); + my $what = "$key=$now"; + my $hashref = &tie_user_hash($udom,$uname,'environment', + &GDBM_WRCREAT(),"P",$what); + if ($hashref) { + $hashref->{$key}=$now; + if (!&untie_user_hash($hashref)) { + &logthis("error: ".($!+0)." untie (GDBM) failed ". + "when updating internal.contentchange"); + } + } + } + } &Reply($client, "ok\n", $userinput); } } @@ -2077,11 +2897,20 @@ sub remove_user_file_handler { 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: + # However it's possible the client wants a dir + # removed, in which case rmdir is more appropriate. + # Note: rmdir will only remove an empty directory. # if (-f $file){ unlink($file); + # for html files remove the associated .bak file + # which may have been created by the editor. + if ($ufile =~ m{^((docs|supplemental)/(?:\d+|default)/\d+(?:|/.+)/)[^/]+\.x?html?$}i) { + my $path = $1; + if (-e $file.'.bak') { + unlink($file.'.bak'); + } + } } elsif(-d $file) { rmdir($file); } @@ -2181,6 +3010,84 @@ sub rename_user_file_handler { ®ister_handler("renameuserfile", \&rename_user_file_handler, 0,1,0); # +# Checks if the specified user has an active session on the server +# return ok if so, not_found if not +# +# 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 user_has_session_handler { + my ($cmd, $tail, $client) = @_; + + my ($udom, $uname) = map { &unescape($_) } (split(/:/, $tail)); + + opendir(DIR,$perlvar{'lonIDsDir'}); + my $filename; + while ($filename=readdir(DIR)) { + last if ($filename=~/^\Q$uname\E_\d+_\Q$udom\E_/); + } + if ($filename) { + &Reply($client, "ok\n", "$cmd:$tail"); + } else { + &Failure($client, "not_found\n", "$cmd:$tail"); + } + return 1; + +} +®ister_handler("userhassession", \&user_has_session_handler, 0,1,0); + +sub del_usersession_handler { + my ($cmd, $tail, $client) = @_; + + my $result; + my ($udom, $uname) = map { &unescape($_) } (split(/:/, $tail)); + if (($udom =~ /^$LONCAPA::match_domain$/) && ($uname =~ /^$LONCAPA::match_username$/)) { + my $lonidsdir = $perlvar{'lonIDsDir'}; + if (-d $lonidsdir) { + if (opendir(DIR,$lonidsdir)) { + my $filename; + while ($filename=readdir(DIR)) { + if ($filename=~/^\Q$uname\E_\d+_\Q$udom\E_/) { + if (tie(my %oldenv,'GDBM_File',"$lonidsdir/$filename", + &GDBM_READER(),0640)) { + my $linkedfile; + if (exists($oldenv{'user.linkedenv'})) { + $linkedfile = $oldenv{'user.linkedenv'}; + } + untie(%oldenv); + $result = unlink("$lonidsdir/$filename"); + if ($result) { + if ($linkedfile =~ /^[a-f0-9]+_linked$/) { + if (-l "$lonidsdir/$linkedfile.id") { + unlink("$lonidsdir/$linkedfile.id"); + } + } + } + } else { + $result = unlink("$lonidsdir/$filename"); + } + last; + } + } + } + } + if ($result == 1) { + &Reply($client, "$result\n", "$cmd:$tail"); + } else { + &Reply($client, "not_found\n", "$cmd:$tail"); + } + } else { + &Failure($client, "invalid_user\n", "$cmd:$tail"); + } + return 1; +} + +®ister_handler("delusersession", \&del_usersession_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 # @@ -2196,14 +3103,24 @@ sub token_auth_user_file_handler { my ($fname, $session) = split(/:/, $tail); chomp($session); - my $reply="non_auth\n"; - if (open(ENVIN,$perlvar{'lonIDsDir'}.'/'. - $session.'.id')) { - while (my $line=) { - if ($line=~ m|userfile\.\Q$fname\E\=|) { $reply="ok\n"; } + my $reply="non_auth"; + 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"; + } else { + foreach my $envname (keys(%disk_env)) { + if ($envname=~ m|^userfile\.\Q$fname\E|) { + $reply="ok"; + last; + } + } } + untie(%disk_env); close(ENVIN); - &Reply($client, $reply, "$cmd:$tail"); + &Reply($client, \$reply, "$cmd:$tail"); } else { &Failure($client, "invalid_token\n", "$cmd:$tail"); } @@ -2263,13 +3180,13 @@ sub subscribe_handler { ®ister_handler("sub", \&subscribe_handler, 0, 1, 0); # -# Determine the version of a resource (?) Or is it return -# the top version of the resource? Not yet clear from the -# code in currentversion. +# Determine the latest version of a resource (it looks for the highest +# past version and then returns that +1) # # Parameters: # $cmd - The command that got us here. # $tail - Tail of the command (remaining parameters). +# (Should consist of an absolute path to a file) # $client - File descriptor connected to client. # Returns # 0 - Requested to exit, caller should shut down. @@ -2351,7 +3268,7 @@ sub put_user_profile_entry { my ($key,$value)=split(/=/,$pair); $hashref->{$key}=$value; } - if (untie(%$hashref)) { + if (&untie_user_hash($hashref)) { &Reply( $client, "ok\n", $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) failed ". @@ -2359,7 +3276,7 @@ sub put_user_profile_entry { $userinput); } } else { - &Failure( $client, "error: ".($!)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting put\n", $userinput); } } else { @@ -2395,7 +3312,7 @@ sub newput_user_profile_entry { my $hashref = &tie_user_hash($udom, $uname, $namespace, &GDBM_WRCREAT(),"N",$what); if(!$hashref) { - &Failure( $client, "error: ".($!)." tie(GDBM) Failed ". + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting put\n", $userinput); return 1; } @@ -2404,8 +3321,12 @@ sub newput_user_profile_entry { foreach my $pair (@pairs) { my ($key,$value)=split(/=/,$pair); if (exists($hashref->{$key})) { - &Failure($client, "key_exists: ".$key."\n",$userinput); - return 1; + if (!&untie_user_hash($hashref)) { + &logthis("error: ".($!+0)." untie (GDBM) failed ". + "while attempting newput - early out as key exists"); + } + &Failure($client, "key_exists: ".$key."\n",$userinput); + return 1; } } @@ -2414,7 +3335,7 @@ sub newput_user_profile_entry { $hashref->{$key}=$value; } - if (untie(%$hashref)) { + if (&untie_user_hash($hashref)) { &Reply( $client, "ok\n", $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) failed ". @@ -2467,7 +3388,7 @@ sub increment_user_value_handler { } } } - if (untie(%$hashref)) { + if (&untie_user_hash($hashref)) { &Reply( $client, "ok\n", $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) failed ". @@ -2534,7 +3455,7 @@ sub roles_put_handler { $auth_type); $hashref->{$key}=$value; } - if (untie($hashref)) { + if (&untie_user_hash($hashref)) { &Reply($client, "ok\n", $userinput); } else { &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". @@ -2585,7 +3506,7 @@ sub roles_delete_handler { foreach my $key (@rolekeys) { delete $hashref->{$key}; } - if (untie(%$hashref)) { + if (&untie_user_hash($hashref)) { &Reply($client, "ok\n", $userinput); } else { &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". @@ -2627,10 +3548,11 @@ sub get_profile_entry { my ($udom,$uname,$namespace,$what) = split(/:/,$tail); chomp($what); + my $replystring = read_profile($udom, $uname, $namespace, $what); my ($first) = split(/:/,$replystring); if($first ne "error") { - &Reply($client, "$replystring\n", $userinput); + &Reply($client, \$replystring, $userinput); } else { &Failure($client, $replystring." while attempting get\n", $userinput); } @@ -2654,7 +3576,8 @@ sub get_profile_entry { # # Parameters: # $cmd - Command keyword of request (eget). -# $tail - Tail of the command. See GetProfileEntry # for more information about this. +# $tail - Tail of the command. See GetProfileEntry +# for more information about this. # $client - File open on the client. # Returns: # 1 - Continue processing @@ -2664,7 +3587,7 @@ sub get_profile_entry_encrypted { my $userinput = "$cmd:$tail"; - my ($cmd,$udom,$uname,$namespace,$what) = split(/:/,$userinput); + my ($udom,$uname,$namespace,$what) = split(/:/,$tail); chomp($what); my $qresult = read_profile($udom, $uname, $namespace, $what); my ($first) = split(/:/, $qresult); @@ -2726,7 +3649,7 @@ sub delete_profile_entry { foreach my $key (@keys) { delete($hashref->{$key}); } - if (untie(%$hashref)) { + if (&untie_user_hash($hashref)) { &Reply($client, "ok\n", $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". @@ -2768,9 +3691,9 @@ sub get_profile_keys { foreach my $key (keys %$hashref) { $qresult.="$key&"; } - if (untie(%$hashref)) { + if (&untie_user_hash($hashref)) { $qresult=~s/\&$//; - &Reply($client, "$qresult\n", $userinput); + &Reply($client, \$qresult, $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting keys\n", $userinput); @@ -2806,6 +3729,17 @@ sub get_profile_keys { sub dump_profile_database { my ($cmd, $tail, $client) = @_; + my $res = LONCAPA::Lond::dump_profile_database($tail); + + if ($res =~ /^error:/) { + Failure($client, \$res, "$cmd:$tail"); + } else { + Reply($client, \$res, "$cmd:$tail"); + } + + return 1; + + #TODO remove my $userinput = "$cmd:$tail"; my ($udom,$uname,$namespace) = split(/:/,$tail); @@ -2829,7 +3763,7 @@ sub dump_profile_database { $data{$symb}->{$param}=$value; $data{$symb}->{'v.'.$param}=$v; } - if (untie(%$hashref)) { + 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... @@ -2840,7 +3774,7 @@ sub dump_profile_database { } } chop($qresult); - &Reply($client , "$qresult\n", $userinput); + &Reply($client , \$qresult, $userinput); } else { &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting currentdump\n", $userinput); @@ -2872,6 +3806,9 @@ sub dump_profile_database { # that is matched against # database keywords to do # selective dumps. +# range - optional range of entries +# e.g., 10-20 would return the +# 10th to 19th items, etc. # $client - Channel open on the client. # Returns: # 1 - Continue processing. @@ -2881,45 +3818,59 @@ sub dump_profile_database { sub dump_with_regexp { my ($cmd, $tail, $client) = @_; - - my $userinput = "$cmd:$tail"; - - my ($udom,$uname,$namespace,$regexp)=split(/:/,$tail); - if (defined($regexp)) { - $regexp=&unescape($regexp); - } else { - $regexp='.'; - } - my $hashref = &tie_user_hash($udom, $uname, $namespace, - &GDBM_READER()); - if ($hashref) { - my $qresult=''; - while (my ($key,$value) = each(%$hashref)) { - if ($regexp eq '.') { - $qresult.=$key.'='.$value.'&'; - } else { - my $unescapeKey = &unescape($key); - if (eval('$unescapeKey=~/$regexp/')) { - $qresult.="$key=$value&"; - } - } - } - if (untie(%$hashref)) { - chop($qresult); - &Reply($client, "$qresult\n", $userinput); - } else { - &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". - "while attempting dump\n", $userinput); - } + my $res = LONCAPA::Lond::dump_with_regexp($tail, $clientversion); + + if ($res =~ /^error:/) { + Failure($client, \$res, "$cmd:$tail"); } else { - &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". - "while attempting dump\n", $userinput); + Reply($client, \$res, "$cmd:$tail"); } return 1; } ®ister_handler("dump", \&dump_with_regexp, 0, 1, 0); +# +# Process the encrypted dump request. Original call should +# be from lonnet::dump() with seventh arg ($encrypt) set to +# 1, to ensure that both request and response are encrypted. +# +# Parameters: +# $cmd - Command keyword of request (edump). +# $tail - Tail of the command. +# See &dump_with_regexp for more +# information about this. +# $client - File open on the client. +# Returns: +# 1 - Continue processing +# 0 - server should exit. +# + +sub encrypted_dump_with_regexp { + my ($cmd, $tail, $client) = @_; + my $res = LONCAPA::Lond::dump_with_regexp($tail, $clientversion); + + if ($res =~ /^error:/) { + Failure($client, \$res, "$cmd:$tail"); + } else { + if ($cipher) { + my $cmdlength=length($res); + $res.=" "; + my $encres=''; + for (my $encidx=0;$encidx<=$cmdlength;$encidx+=8) { + $encres.= unpack("H16", + $cipher->encrypt(substr($res, + $encidx, + 8))); + } + &Reply( $client,"enc:$cmdlength:$encres\n","$cmd:$tail"); + } else { + &Failure( $client, "error:no_key\n","$cmd:$tail"); + } + } +} +®ister_handler("edump", \&encrypted_dump_with_regexp, 0, 1, 0); + # Store a set of key=value pairs associated with a versioned name. # # Parameters: @@ -2930,6 +3881,9 @@ sub dump_with_regexp { # namespace - Name of the database being modified # rid - Resource keyword to modify. # what - new value associated with rid. +# laststore - (optional) version=timestamp +# for most recent transaction for rid +# in namespace, when cstore was called # # $client - Socket open on the client. # @@ -2938,23 +3892,45 @@ sub dump_with_regexp { # 1 (keep on processing). # Side-Effects: # Writes to the client +# Successful storage will cause either 'ok', or, if $laststore was included +# in the tail of the request, and the version number for the last transaction +# is larger than the version in $laststore, delay:$numtrans , where $numtrans +# is the number of store evevnts recorded for rid in namespace since +# lonnet::store() was called by the client. +# sub store_handler { my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; - - my ($udom,$uname,$namespace,$rid,$what) =split(/:/,$tail); + chomp($tail); + my ($udom,$uname,$namespace,$rid,$what,$laststore) =split(/:/,$tail); if ($namespace ne 'roles') { - chomp($what); my @pairs=split(/\&/,$what); 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; + my $numtrans; + if ($laststore) { + my ($previousversion,$previoustime) = split(/\=/,$laststore); + my ($lastversion,$lasttime) = (0,0); + $lastversion = $hashref->{"version:$rid"}; + if ($lastversion) { + $lasttime = $hashref->{"$lastversion:$rid:timestamp"}; + } + if (($previousversion) && ($previousversion !~ /\D/)) { + if (($lastversion > $previousversion) && ($lasttime >= $previoustime)) { + $numtrans = $lastversion - $previousversion; + } + } elsif ($lastversion) { + $numtrans = $lastversion; + } + if ($numtrans) { + $numtrans =~ s/D//g; + } + } $hashref->{"version:$rid"}++; my $version=$hashref->{"version:$rid"}; my $allkeys=''; @@ -2966,8 +3942,12 @@ sub store_handler { $hashref->{"$version:$rid:timestamp"}=$now; $allkeys.='timestamp'; $hashref->{"$version:keys:$rid"}=$allkeys; - if (untie($hashref)) { - &Reply($client, "ok\n", $userinput); + if (&untie_user_hash($hashref)) { + my $msg = 'ok'; + if ($numtrans) { + $msg = 'delay:'.$numtrans; + } + &Reply($client, "$msg\n", $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting store\n", $userinput); @@ -2984,6 +3964,85 @@ sub store_handler { } ®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 ". + "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("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; +} + # # 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 @@ -3013,31 +4072,29 @@ 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, $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting restore\n", $userinput); @@ -3054,15 +4111,17 @@ sub restore_handler { ®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. @@ -3077,8 +4136,8 @@ sub send_chat_handler { my $userinput = "$cmd:$tail"; - my ($cdom,$cnum,$newpost)=split(/\:/,$tail); - &chat_add($cdom,$cnum,$newpost); + my ($cdom,$cnum,$newpost,$group)=split(/\:/,$tail); + &chat_add($cdom,$cnum,$newpost,$group); &Reply($client, "ok\n", $userinput); return 1; @@ -3086,7 +4145,7 @@ sub send_chat_handler { ®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. @@ -3096,6 +4155,8 @@ sub send_chat_handler { # 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 @@ -3108,13 +4169,13 @@ sub retrieve_chat_handler { my $userinput = "$cmd:$tail"; - my ($cdom,$cnum,$udom,$uname)=split(/\:/,$tail); + my ($cdom,$cnum,$udom,$uname,$group)=split(/\:/,$tail); my $reply=''; - foreach (&get_chat($cdom,$cnum,$udom,$uname)) { + foreach (&get_chat($cdom,$cnum,$udom,$uname,$group)) { $reply.=&escape($_).':'; } $reply=~s/\:$//; - &Reply($client, $reply."\n", $userinput); + &Reply($client, \$reply, $userinput); return 1; @@ -3129,7 +4190,7 @@ sub retrieve_chat_handler { # serviced. # # Parameters: -# $cmd - COmmand keyword that initiated the request. +# $cmd - Command keyword that initiated the request. # $tail - Remainder of the command after the keyword. # For this function, this consists of a query and # 3 arguments that are self-documentingly labelled @@ -3143,11 +4204,41 @@ sub retrieve_chat_handler { sub send_query_handler { my ($cmd, $tail, $client) = @_; - my $userinput = "$cmd:$tail"; my ($query,$arg1,$arg2,$arg3)=split(/\:/,$tail); $query=~s/\n*$//g; + if (($query eq 'usersearch') || ($query eq 'instdirsearch')) { + my $usersearchconf = &get_usersearch_config($currentdomainid,'directorysrch'); + my $earlyout; + if (ref($usersearchconf) eq 'HASH') { + if ($currentdomainid eq $clienthomedom) { + if ($query eq 'usersearch') { + if ($usersearchconf->{'lcavailable'} eq '0') { + $earlyout = 1; + } + } else { + if ($usersearchconf->{'available'} eq '0') { + $earlyout = 1; + } + } + } else { + if ($query eq 'usersearch') { + if ($usersearchconf->{'lclocalonly'}) { + $earlyout = 1; + } + } else { + if ($usersearchconf->{'localonly'}) { + $earlyout = 1; + } + } + } + } + if ($earlyout) { + &Reply($client, "query_not_authorized\n"); + return 1; + } + } &Reply($client, "". &sql_reply("$clientname\&$query". "\&$arg1"."\&$arg2"."\&$arg3")."\n", $userinput); @@ -3187,7 +4278,7 @@ sub reply_query_handler { 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")) { @@ -3251,24 +4342,41 @@ sub put_course_id_handler { foreach my $pair (@pairs) { my ($key,$courseinfo) = split(/=/,$pair,2); $courseinfo =~ s/=/:/g; - - my @current_items = split(/:/,$hashref->{$key}); + if (defined($hashref->{$key})) { + my $value = &Apache::lonnet::thaw_unescape($hashref->{$key}); + if (ref($value) eq 'HASH') { + my @items = ('description','inst_code','owner','type'); + my @new_items = split(/:/,$courseinfo,-1); + my %storehash; + for (my $i=0; $i<@new_items; $i++) { + $storehash{$items[$i]} = &unescape($new_items[$i]); + } + $hashref->{$key} = + &Apache::lonnet::freeze_escape(\%storehash); + my $unesc_key = &unescape($key); + $hashref->{&escape('lasttime:'.$unesc_key)} = $now; + next; + } + } + my @current_items = split(/:/,$hashref->{$key},-1); shift(@current_items); # remove description pop(@current_items); # remove last access my $numcurrent = scalar(@current_items); - - my @new_items = split(/:/,$courseinfo); + if ($numcurrent > 3) { + $numcurrent = 3; + } + my @new_items = split(/:/,$courseinfo,-1); my $numnew = scalar(@new_items); if ($numcurrent > 0) { - if ($numnew == 1) { # flushcourselogs() from 1.1 or earlier - $courseinfo .= ':'.join(':',@current_items); - } elsif ($numnew == 2) { # flushcourselogs() from 1.2.X - $courseinfo .= ':'.$current_items[$numcurrent-1]; + 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; + $hashref->{$key}=$courseinfo.':'.$now; } - if (untie(%$hashref)) { + if (&untie_domain_hash($hashref)) { &Reply( $client, "ok\n", $userinput); } else { &Failure($client, "error: ".($!+0) @@ -3280,12 +4388,54 @@ sub put_course_id_handler { ." tie(GDBM) Failed ". "while attempting courseidput\n", $userinput); } - return 1; } ®ister_handler("courseidput", \&put_course_id_handler, 0, 1, 0); +sub put_course_id_hash_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($udom,$mode,$what) = split(/:/, $tail,3); + chomp($what); + my $now=time; + my @pairs=split(/\&/,$what); + my $hashref = &tie_domain_hash($udom, "nohist_courseids", &GDBM_WRCREAT()); + if ($hashref) { + foreach my $pair (@pairs) { + my ($key,$value)=split(/=/,$pair); + my $unesc_key = &unescape($key); + if ($mode ne 'timeonly') { + if (!defined($hashref->{&escape('lasttime:'.$unesc_key)})) { + my $curritems = &Apache::lonnet::thaw_unescape($key); + if (ref($curritems) ne 'HASH') { + my @current_items = split(/:/,$hashref->{$key},-1); + my $lasttime = pop(@current_items); + $hashref->{&escape('lasttime:'.$unesc_key)} = $lasttime; + } else { + $hashref->{&escape('lasttime:'.$unesc_key)} = ''; + } + } + $hashref->{$key} = $value; + } + if ($mode ne 'notime') { + $hashref->{&escape('lasttime:'.$unesc_key)} = $now; + } + } + if (&untie_domain_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting courseidputhash\n", $userinput); + } + } else { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting courseidputhash\n", $userinput); + } + return 1; +} +®ister_handler("courseidputhash", \&put_course_id_hash_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 # keyword pattern are optional. If the starting date is not supplied it @@ -3307,12 +4457,46 @@ sub put_course_id_handler { # 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 of owner to filter -# the dump. Only courses for which the course -# owner matches the supplied username will be -# returned. Implicit assumption that owner -# is a user in the domain in which the -# course database is defined. +# 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. +# type - optional parameter for selection +# regexp_ok - if 1 or -1 allow the supplied institutional code +# filter to behave as a regular expression: +# 1 will not exclude the course if the instcode matches the RE +# -1 will exclude the course if the instcode matches the RE +# rtn_as_hash - whether to return the information available for +# each matched item as a frozen hash of all +# key, value pairs in the item's hash, or as a +# colon-separated list of (in order) description, +# institutional code, and course owner. +# selfenrollonly - filter by courses allowing self-enrollment +# now or in the future (selfenrollonly = 1). +# catfilter - filter by course category, assigned to a course +# using manually defined categories (i.e., not +# self-cataloging based on on institutional code). +# showhidden - include course in results even if course +# was set to be excluded from course catalog (DC only). +# caller - if set to 'coursecatalog', courses set to be hidden +# from course catalog will be excluded from results (unless +# overridden by "showhidden". +# cloner - escaped username:domain of course cloner (if picking course to +# clone). +# cc_clone_list - escaped comma separated list of courses for which +# course cloner has active CC role (and so can clone +# automatically). +# cloneonly - filter by courses for which cloner has rights to clone. +# createdbefore - include courses for which creation date preceeded this date. +# createdafter - include courses for which creation date followed this date. +# creationcontext - include courses created in specified context +# +# domcloner - flag to indicate if user can create CCs in course's domain. +# If so, ability to clone course is automatic. +# hasuniquecode - filter by courses for which a six character unique code has +# been set. +# # $client - The socket open on the client. # Returns: # 1 - Continue processing. @@ -3321,9 +4505,24 @@ sub put_course_id_handler { sub dump_course_id_handler { my ($cmd, $tail, $client) = @_; + my $res = LONCAPA::Lond::dump_course_id_handler($tail); + if ($res =~ /^error:/) { + Failure($client, \$res, "$cmd:$tail"); + } else { + Reply($client, \$res, "$cmd:$tail"); + } + + return 1; + + #TODO remove my $userinput = "$cmd:$tail"; - my ($udom,$since,$description,$instcodefilter,$ownerfilter,$coursefilter) =split(/:/,$tail); + my ($udom,$since,$description,$instcodefilter,$ownerfilter,$coursefilter, + $typefilter,$regexp_ok,$rtn_as_hash,$selfenrollonly,$catfilter,$showhidden, + $caller,$cloner,$cc_clone_list,$cloneonly,$createdbefore,$createdafter, + $creationcontext,$domcloner,$hasuniquecode) =split(/:/,$tail); + my $now = time; + my ($cloneruname,$clonerudom,%cc_clone); if (defined($description)) { $description=&unescape($description); } else { @@ -3334,59 +4533,333 @@ sub dump_course_id_handler { } 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='.'; } - - unless (defined($since)) { $since=0; } + if (defined($typefilter)) { + $typefilter=&unescape($typefilter); + } else { + $typefilter='.'; + } + if (defined($regexp_ok)) { + $regexp_ok=&unescape($regexp_ok); + } + if (defined($catfilter)) { + $catfilter=&unescape($catfilter); + } + if (defined($cloner)) { + $cloner = &unescape($cloner); + ($cloneruname,$clonerudom) = ($cloner =~ /^($LONCAPA::match_username):($LONCAPA::match_domain)$/); + } + if (defined($cc_clone_list)) { + $cc_clone_list = &unescape($cc_clone_list); + my @cc_cloners = split('&',$cc_clone_list); + foreach my $cid (@cc_cloners) { + my ($clonedom,$clonenum) = split(':',$cid); + next if ($clonedom ne $udom); + $cc_clone{$clonedom.'_'.$clonenum} = 1; + } + } + if ($createdbefore ne '') { + $createdbefore = &unescape($createdbefore); + } else { + $createdbefore = 0; + } + if ($createdafter ne '') { + $createdafter = &unescape($createdafter); + } else { + $createdafter = 0; + } + if ($creationcontext ne '') { + $creationcontext = &unescape($creationcontext); + } else { + $creationcontext = '.'; + } + unless ($hasuniquecode) { + $hasuniquecode = '.'; + } + my $unpack = 1; + if ($description eq '.' && $instcodefilter eq '.' && $ownerfilter eq '.' && + $typefilter eq '.') { + $unpack = 0; + } + if (!defined($since)) { $since=0; } my $qresult=''; my $hashref = &tie_domain_hash($udom, "nohist_courseids", &GDBM_WRCREAT()); if ($hashref) { while (my ($key,$value) = each(%$hashref)) { - my ($descr,$lasttime,$inst_code,$owner); - my @courseitems = split(/:/,$value); - $lasttime = pop(@courseitems); - ($descr,$inst_code,$owner)=@courseitems; - if ($lasttime<$since) { next; } + my ($unesc_key,$lasttime_key,$lasttime,$is_hash,%val, + %unesc_val,$selfenroll_end,$selfenroll_types,$created, + $context); + $unesc_key = &unescape($key); + if ($unesc_key =~ /^lasttime:/) { + next; + } else { + $lasttime_key = &escape('lasttime:'.$unesc_key); + } + if ($hashref->{$lasttime_key} ne '') { + $lasttime = $hashref->{$lasttime_key}; + next if ($lasttime<$since); + } + my ($canclone,$valchange); + my $items = &Apache::lonnet::thaw_unescape($value); + if (ref($items) eq 'HASH') { + if ($hashref->{$lasttime_key} eq '') { + next if ($since > 1); + } + $is_hash = 1; + if ($domcloner) { + $canclone = 1; + } elsif (defined($clonerudom)) { + if ($items->{'cloners'}) { + my @cloneable = split(',',$items->{'cloners'}); + if (@cloneable) { + if (grep(/^\*$/,@cloneable)) { + $canclone = 1; + } elsif (grep(/^\*:\Q$clonerudom\E$/,@cloneable)) { + $canclone = 1; + } elsif (grep(/^\Q$cloneruname\E:\Q$clonerudom\E$/,@cloneable)) { + $canclone = 1; + } + } + unless ($canclone) { + if ($cloneruname ne '' && $clonerudom ne '') { + if ($cc_clone{$unesc_key}) { + $canclone = 1; + $items->{'cloners'} .= ','.$cloneruname.':'. + $clonerudom; + $valchange = 1; + } + } + } + } elsif (defined($cloneruname)) { + if ($cc_clone{$unesc_key}) { + $canclone = 1; + $items->{'cloners'} = $cloneruname.':'.$clonerudom; + $valchange = 1; + } + unless ($canclone) { + if ($items->{'owner'} =~ /:/) { + if ($items->{'owner'} eq $cloner) { + $canclone = 1; + } + } elsif ($cloner eq $items->{'owner'}.':'.$udom) { + $canclone = 1; + } + if ($canclone) { + $items->{'cloners'} = $cloneruname.':'.$clonerudom; + $valchange = 1; + } + } + } + } + if ($unpack || !$rtn_as_hash) { + $unesc_val{'descr'} = $items->{'description'}; + $unesc_val{'inst_code'} = $items->{'inst_code'}; + $unesc_val{'owner'} = $items->{'owner'}; + $unesc_val{'type'} = $items->{'type'}; + $unesc_val{'cloners'} = $items->{'cloners'}; + $unesc_val{'created'} = $items->{'created'}; + $unesc_val{'context'} = $items->{'context'}; + } + $selfenroll_types = $items->{'selfenroll_types'}; + $selfenroll_end = $items->{'selfenroll_end_date'}; + $created = $items->{'created'}; + $context = $items->{'context'}; + if ($hasuniquecode ne '.') { + next unless ($items->{'uniquecode'}); + } + if ($selfenrollonly) { + next if (!$selfenroll_types); + if (($selfenroll_end > 0) && ($selfenroll_end <= $now)) { + next; + } + } + if ($creationcontext ne '.') { + next if (($context ne '') && ($context ne $creationcontext)); + } + if ($createdbefore > 0) { + next if (($created eq '') || ($created > $createdbefore)); + } + if ($createdafter > 0) { + next if (($created eq '') || ($created <= $createdafter)); + } + if ($catfilter ne '') { + next if ($items->{'categories'} eq ''); + my @categories = split('&',$items->{'categories'}); + next if (@categories == 0); + my @subcats = split('&',$catfilter); + my $matchcat = 0; + foreach my $cat (@categories) { + if (grep(/^\Q$cat\E$/,@subcats)) { + $matchcat = 1; + last; + } + } + next if (!$matchcat); + } + if ($caller eq 'coursecatalog') { + if ($items->{'hidefromcat'} eq 'yes') { + next if !$showhidden; + } + } + } else { + next if ($catfilter ne ''); + next if ($selfenrollonly); + next if ($createdbefore || $createdafter); + next if ($creationcontext ne '.'); + if ((defined($clonerudom)) && (defined($cloneruname))) { + if ($cc_clone{$unesc_key}) { + $canclone = 1; + $val{'cloners'} = &escape($cloneruname.':'.$clonerudom); + } + } + $is_hash = 0; + my @courseitems = split(/:/,$value); + $lasttime = pop(@courseitems); + if ($hashref->{$lasttime_key} eq '') { + next if ($lasttime<$since); + } + ($val{'descr'},$val{'inst_code'},$val{'owner'},$val{'type'}) = @courseitems; + } + if ($cloneonly) { + next unless ($canclone); + } my $match = 1; - unless ($description eq '.') { - my $unescapeDescr = &unescape($descr); - unless (eval('$unescapeDescr=~/\Q$description\E/i')) { + if ($description ne '.') { + if (!$is_hash) { + $unesc_val{'descr'} = &unescape($val{'descr'}); + } + if (eval{$unesc_val{'descr'} !~ /\Q$description\E/i}) { $match = 0; - } + } } - unless ($instcodefilter eq '.' || !defined($instcodefilter)) { - my $unescapeInstcode = &unescape($inst_code); - unless (eval('$unescapeInstcode=~/\Q$instcodefilter\E/i')) { - $match = 0; + if ($instcodefilter ne '.') { + if (!$is_hash) { + $unesc_val{'inst_code'} = &unescape($val{'inst_code'}); + } + if ($regexp_ok == 1) { + if (eval{$unesc_val{'inst_code'} !~ /$instcodefilter/}) { + $match = 0; + } + } elsif ($regexp_ok == -1) { + if (eval{$unesc_val{'inst_code'} =~ /$instcodefilter/}) { + $match = 0; + } + } else { + if (eval{$unesc_val{'inst_code'} !~ /\Q$instcodefilter\E/i}) { + $match = 0; + } } } - unless ($ownerfilter eq '.' || !defined($ownerfilter)) { - my $unescapeOwner = &unescape($owner); - unless (eval('$unescapeOwner=~/\Q$ownerfilter\E/i')) { - $match = 0; + if ($ownerfilter ne '.') { + if (!$is_hash) { + $unesc_val{'owner'} = &unescape($val{'owner'}); + } + if (($ownerunamefilter ne '') && ($ownerdomfilter ne '')) { + if ($unesc_val{'owner'} =~ /:/) { + if (eval{$unesc_val{'owner'} !~ + /\Q$ownerunamefilter\E:\Q$ownerdomfilter\E$/i}) { + $match = 0; + } + } else { + if (eval{$unesc_val{'owner'} !~ /\Q$ownerunamefilter\E/i}) { + $match = 0; + } + } + } elsif ($ownerunamefilter ne '') { + if ($unesc_val{'owner'} =~ /:/) { + if (eval{$unesc_val{'owner'} !~ /\Q$ownerunamefilter\E:[^:]+$/i}) { + $match = 0; + } + } else { + if (eval{$unesc_val{'owner'} !~ /\Q$ownerunamefilter\E/i}) { + $match = 0; + } + } + } elsif ($ownerdomfilter ne '') { + if ($unesc_val{'owner'} =~ /:/) { + if (eval{$unesc_val{'owner'} !~ /^[^:]+:\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$/')) { + if ($coursefilter ne '.') { + if (eval{$unesc_key !~ /^$udom(_)\Q$coursefilter\E$/}) { $match = 0; } } + if ($typefilter ne '.') { + if (!$is_hash) { + $unesc_val{'type'} = &unescape($val{'type'}); + } + if ($unesc_val{'type'} eq '') { + if ($typefilter ne 'Course') { + $match = 0; + } + } else { + if (eval{$unesc_val{'type'} !~ /^\Q$typefilter\E$/}) { + $match = 0; + } + } + } if ($match == 1) { - $qresult.=$key.'='.$descr.':'.$inst_code.':'.$owner.'&'; + if ($rtn_as_hash) { + if ($is_hash) { + if ($valchange) { + my $newvalue = &Apache::lonnet::freeze_escape($items); + $qresult.=$key.'='.$newvalue.'&'; + } else { + $qresult.=$key.'='.$value.'&'; + } + } else { + my %rtnhash = ( 'description' => &unescape($val{'descr'}), + 'inst_code' => &unescape($val{'inst_code'}), + 'owner' => &unescape($val{'owner'}), + 'type' => &unescape($val{'type'}), + 'cloners' => &unescape($val{'cloners'}), + ); + my $items = &Apache::lonnet::freeze_escape(\%rtnhash); + $qresult.=$key.'='.$items.'&'; + } + } else { + if ($is_hash) { + $qresult .= $key.'='.&escape($unesc_val{'descr'}).':'. + &escape($unesc_val{'inst_code'}).':'. + &escape($unesc_val{'owner'}).'&'; + } else { + $qresult .= $key.'='.$val{'descr'}.':'.$val{'inst_code'}. + ':'.$val{'owner'}.'&'; + } + } } } - if (untie(%$hashref)) { + if (&untie_domain_hash($hashref)) { chop($qresult); - &Reply($client, "$qresult\n", $userinput); + &Reply($client, \$qresult, $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting courseiddump\n", $userinput); @@ -3395,11 +4868,321 @@ sub dump_course_id_handler { &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". "while attempting courseiddump\n", $userinput); } + return 1; +} +®ister_handler("courseiddump", \&dump_course_id_handler, 0, 1, 0); + +sub course_lastaccess_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($cdom,$cnum) = split(':',$tail); + my (%lastaccess,$qresult); + my $hashref = &tie_domain_hash($cdom, "nohist_courseids", &GDBM_WRCREAT()); + if ($hashref) { + while (my ($key,$value) = each(%$hashref)) { + my ($unesc_key,$lasttime); + $unesc_key = &unescape($key); + if ($cnum) { + next unless ($unesc_key =~ /\Q$cdom\E_\Q$cnum\E$/); + } + if ($unesc_key =~ /^lasttime:($LONCAPA::match_domain\_$LONCAPA::match_courseid)/) { + $lastaccess{$1} = $value; + } else { + my $items = &Apache::lonnet::thaw_unescape($value); + if (ref($items) eq 'HASH') { + unless ($lastaccess{$unesc_key}) { + $lastaccess{$unesc_key} = ''; + } + } else { + my @courseitems = split(':',$value); + $lastaccess{$unesc_key} = pop(@courseitems); + } + } + } + foreach my $cid (sort(keys(%lastaccess))) { + $qresult.=&escape($cid).'='.$lastaccess{$cid}.'&'; + } + if (&untie_domain_hash($hashref)) { + if ($qresult) { + chop($qresult); + } + &Reply($client, \$qresult, $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting lastacourseaccess\n", $userinput); + } + } else { + &Failure($client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting lastcourseaccess\n", $userinput); + } + return 1; +} +®ister_handler("courselastaccess",\&course_lastaccess_handler, 0, 1, 0); + +sub course_sessions_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($cdom,$cnum,$lastactivity) = split(':',$tail); + my $dbsuffix = '_'.$cdom.'_'.$cnum.'.db'; + my (%sessions,$qresult); + my $now=time; + if (opendir(DIR,$perlvar{'lonIDsDir'})) { + my $filename; + while ($filename=readdir(DIR)) { + next if ($filename=~/^\./); + next if ($filename=~/^publicuser_/); + next if ($filename=~/^[a-f0-9]+_(linked|lti_\d+)\.id$/); + if ($filename =~ /^($LONCAPA::match_username)_\d+_($LONCAPA::match_domain)_/) { + my ($uname,$udom) = ($1,$2); + next unless (-e "$perlvar{'lonDaemons'}/tmp/$uname$dbsuffix"); + my $mtime = (stat("$perlvar{'lonIDsDir'}/$filename"))[9]; + if ($lastactivity < 0) { + next if ($mtime-$now > $lastactivity); + } else { + next if ($now-$mtime > $lastactivity); + } + $sessions{$uname.':'.$udom} = $mtime; + } + } + closedir(DIR); + } + foreach my $user (keys(%sessions)) { + $qresult.=&escape($user).'='.$sessions{$user}.'&'; + } + if ($qresult) { + chop($qresult); + } + &Reply($client, \$qresult, $userinput); + return 1; +} +®ister_handler("coursesessions",\&course_sessions_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("courseiddump", \&dump_course_id_handler, 0, 1, 0); +®ister_handler("putdom", \&put_domain_handler, 0, 1, 0); + +# Updates one or more entries in clickers.db file at the domain level +# +# Parameters: +# $cmd - The command that got us here. +# $tail - Tail of the command (remaining parameters). +# In this case a colon separated list containing: +# (a) the domain for which we are updating the entries, +# (b) the action required -- add or del -- and +# (c) a &-separated list of entries to add or delete. +# $client - File descriptor connected to client. +# Returns +# 1 - Continue processing. +# 0 - Requested to exit, caller should shut down. +# Side effects: +# reply is written to $client. +# + + +sub update_clickers { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + my ($udom,$action,$what) =split(/:/,$tail,3); + chomp($what); + + my $hashref = &tie_domain_hash($udom, "clickers", &GDBM_WRCREAT(), + "U","$action:$what"); + + if (!$hashref) { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting updateclickers\n", $userinput); + return 1; + } + + my @pairs=split(/\&/,$what); + foreach my $pair (@pairs) { + my ($key,$value)=split(/=/,$pair); + if ($action eq 'add') { + if (exists($hashref->{$key})) { + my @newvals = split(/,/,&unescape($value)); + my @currvals = split(/,/,&unescape($hashref->{$key})); + my @merged = sort(keys(%{{map { $_ => 1 } (@newvals,@currvals)}})); + $hashref->{$key}=&escape(join(',',@merged)); + } else { + $hashref->{$key}=$value; + } + } elsif ($action eq 'del') { + if (exists($hashref->{$key})) { + my %current; + map { $current{$_} = 1; } split(/,/,&unescape($hashref->{$key})); + map { delete($current{$_}); } split(/,/,&unescape($value)); + if (keys(%current)) { + $hashref->{$key}=&escape(join(',',sort(keys(%current)))); + } else { + delete($hashref->{$key}); + } + } + } + } + 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; +} +®ister_handler("updateclickers", \&update_clickers, 0, 1, 0); + + +# Deletes one or more entries in a namespace db file at the domain level +# +# Parameters: +# $cmd - The command that got us here. +# $tail - Tail of the command (remaining parameters). +# In this case a colon separated list containing: +# (a) the domain for which we are deleting the entries, +# (b) &-separated list of keys to delete. +# $client - File descriptor connected to client. +# Returns +# 1 - Continue processing. +# 0 - Requested to exit, caller should shut down. +# Side effects: +# reply is written to $client. +# + +sub del_domain_handler { + my ($cmd,$tail,$client) = @_; + + my $userinput = "$cmd:$tail"; + + my ($udom,$namespace,$what)=split(/:/,$tail,3); + chomp($what); + my $hashref = &tie_domain_hash($udom,$namespace,&GDBM_WRCREAT(), + "D", $what); + if ($hashref) { + my @keys=split(/\&/,$what); + foreach my $key (@keys) { + delete($hashref->{$key}); + } + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting deldom\n", $userinput); + } + } else { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting deldom\n", $userinput); + } + return 1; +} +®ister_handler("deldom", \&del_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 (getdom). +# $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 = "$cmd:$tail"; + + my ($udom,$namespace,$what)=split(/:/,$tail,3); + if ($namespace =~ /^enc/) { + &Failure( $client, "refused\n", $userinput); + } else { + my $res = LONCAPA::Lond::get_dom($userinput); + if ($res =~ /^error:/) { + &Failure($client, \$res, $userinput); + } else { + &Reply($client, \$res, $userinput); + } + } + + return 1; +} +®ister_handler("getdom", \&get_domain_handler, 0, 1, 0); + +sub encrypted_get_domain_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput = "$cmd:$tail"; + + my $res = LONCAPA::Lond::get_dom($userinput); + if ($res =~ /^error:/) { + &Failure($client, \$res, $userinput); + } else { + if ($cipher) { + my $cmdlength=length($res); + $res.=" "; + my $encres=''; + for (my $encidx=0;$encidx<=$cmdlength;$encidx+=8) { + $encres.= unpack("H16", + $cipher->encrypt(substr($res, + $encidx, + 8))); + } + &Reply( $client,"enc:$cmdlength:$encres\n",$userinput); + } else { + &Failure( $client, "error:no_key\n",$userinput); + } + } + return 1; +} +®ister_handler("egetdom", \&encrypted_get_domain_handler, 1, 1, 0); # # Puts an id to a domains id database. @@ -3433,7 +5216,7 @@ sub put_id_handler { my ($key,$value)=split(/=/,$pair); $hashref->{$key}=$value; } - if (untie(%$hashref)) { + if (&untie_domain_hash($hashref)) { &Reply($client, "ok\n", $userinput); } else { &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". @@ -3482,9 +5265,9 @@ sub get_id_handler { for (my $i=0;$i<=$#queries;$i++) { $qresult.="$hashref->{$queries[$i]}&"; } - if (untie(%$hashref)) { + if (&untie_domain_hash($hashref)) { $qresult=~s/\&$//; - &Reply($client, "$qresult\n", $userinput); + &Reply($client, \$qresult, $userinput); } else { &Failure( $client, "error: ".($!+0)." untie(GDBM) Failed ". "while attempting idget\n",$userinput); @@ -3498,7 +5281,306 @@ sub get_id_handler { } ®ister_handler("idget", \&get_id_handler, 0, 1, 0); +# Deletes one or more ids in a domain's id database. +# +# Parameters: +# $cmd - Command keyword (iddel). +# $tail - Command tail. In this case a colon +# separated list containing: +# The domain for which we are deleting the id(s). +# &-separated list of id(s) to delete. +# $client - File open on client socket. +# Returns: +# 1 - Continue processing +# 0 - Exit server. +# +# + +sub del_id_handler { + my ($cmd,$tail,$client) = @_; + + my $userinput = "$cmd:$tail"; + + my ($udom,$what)=split(/:/,$tail); + chomp($what); + my $hashref = &tie_domain_hash($udom, "ids", &GDBM_WRCREAT(), + "D", $what); + if ($hashref) { + my @keys=split(/\&/,$what); + foreach my $key (@keys) { + delete($hashref->{$key}); + } + if (&untie_user_hash($hashref)) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure($client, "error: ".($!+0)." untie(GDBM) Failed ". + "while attempting iddel\n", $userinput); + } + } else { + &Failure( $client, "error: ".($!+0)." tie(GDBM) Failed ". + "while attempting iddel\n", $userinput); + } + return 1; +} +®ister_handler("iddel", \&del_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, $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; +} + +®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 ($end,$start) = split(/:/,&unescape($value)); + my ($trole,$uname,$udom,$runame,$rudom,$rsec) = split(/:/,&unescape($key)); + unless (@roles < 1) { + unless (grep/^\Q$trole\E$/,@roles) { + $match = 0; + next; + } + } + unless ($startfilter eq '.' || !defined($startfilter)) { + if ((defined($start)) && ($start >= $startfilter)) { + $match = 0; + next; + } + } + unless ($endfilter eq '.' || !defined($endfilter)) { + if ((defined($end)) && (($end > 0) && ($end <= $endfilter))) { + $match = 0; + next; + } + } + if ($match == 1) { + $qresult.=$key.'='.$value.'&'; + } + } + if (&untie_domain_hash($hashref)) { + chop($qresult); + &Reply($client, \$qresult, $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. @@ -3520,17 +5602,33 @@ sub tmp_put_handler { 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; - $id=~s/\W/\_/g; - $what=~s/\n//g; + my $numtries = 0; my $execdir=$perlvar{'lonDaemons'}; - if ($store=IO::File->new(">$execdir/tmp/$id.tmp")) { - print $store $what; + if (($context eq 'resetpw') || ($context eq 'createaccount') || + ($context eq 'sso') || ($context eq 'link') || ($context eq 'retry')) { + $id = &md5_hex(&md5_hex(time.{}.rand().$$.$tmpsnum)); + while ((-e "$execdir/tmp/$id.tmp") && ($numtries <10)) { + undef($id); + $id = &md5_hex(&md5_hex(time.{}.rand().$$.$tmpsnum)); + $numtries ++; + } + } else { + $id = $$.'_'.$clientip.'_'.$tmpsnum; + } + $id=~s/\W/\_/g; + $record=~s/\n//g; + if (($id ne '') && + ($store=IO::File->new(">$execdir/tmp/$id.tmp"))) { + print $store $record; close $store; - &Reply($client, "$id\n", $userinput); + &Reply($client, \$id, $userinput); } else { &Failure( $client, "error: ".($!+0)."IO::File->new Failed ". "while attempting tmpput\n", $userinput); @@ -3564,7 +5662,7 @@ sub tmp_get_handler { my $execdir=$perlvar{'lonDaemons'}; if ($store=IO::File->new("$execdir/tmp/$id.tmp")) { my $reply=<$store>; - &Reply( $client, "$reply\n", $userinput); + &Reply( $client, \$reply, $userinput); close $store; } else { &Failure( $client, "error: ".($!+0)."IO::File->new Failed ". @@ -3610,6 +5708,116 @@ sub tmp_del_handler { ®ister_handler("tmpdel", \&tmp_del_handler, 0, 1, 0); # +# Process the updatebalcookie command. This command updates a +# cookie in the lonBalancedir directory on a load balancer node. +# +# Parameters: +# $cmd - Command that got us here. +# $tail - Tail of the request (escaped cookie: escaped current entry) +# +# $client - socket open on the client process. +# +# Returns: +# 1 - Indicating processing should continue. +# Side Effects: +# A cookie file is updated from the lonBalancedir directory +# A reply is sent to the client. +# +sub update_balcookie_handler { + my ($cmd, $tail, $client) = @_; + + my $userinput= "$cmd:$tail"; + chomp($tail); + my ($cookie,$lastentry) = map { &unescape($_) } (split(/:/,$tail)); + + my $updatedone; + if ($cookie =~ /^$LONCAPA::match_domain\_$LONCAPA::match_username\_[a-f0-9]{32}$/) { + my $execdir=$perlvar{'lonBalanceDir'}; + if (-e "$execdir/$cookie.id") { + my $doupdate; + if (open(my $fh,'<',"$execdir/$cookie.id")) { + while (my $line = <$fh>) { + chomp($line); + if ($line eq $lastentry) { + $doupdate = 1; + last; + } + } + close($fh); + } + if ($doupdate) { + if (open(my $fh,'>',"$execdir/$cookie.id")) { + print $fh $clientname; + close($fh); + $updatedone = 1; + } + } + } + } + if ($updatedone) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure( $client, "error: ".($!+0)."file update failed ". + "while attempting updatebalcookie\n", $userinput); + } + return 1; +} +®ister_handler("updatebalcookie", \&update_balcookie_handler, 0, 1, 0); + +# +# Process the delbalcookie command. This command deletes a balancer +# cookie in the lonBalancedir directory on a load balancer node. +# +# Parameters: +# $cmd - Command that got us here. +# $cookie - Cookie to be deleted. +# $client - socket open on the client process. +# +# Returns: +# 1 - Indicating processing should continue. +# Side Effects: +# A cookie file is deleted from the lonBalancedir directory +# A reply is sent to the client. +sub del_balcookie_handler { + my ($cmd, $cookie, $client) = @_; + + my $userinput= "$cmd:$cookie"; + + chomp($cookie); + $cookie = &unescape($cookie); + my $deleted = ''; + if ($cookie =~ /^$LONCAPA::match_domain\_$LONCAPA::match_username\_[a-f0-9]{32}$/) { + my $execdir=$perlvar{'lonBalanceDir'}; + if (-e "$execdir/$cookie.id") { + if (open(my $fh,'<',"$execdir/$cookie.id")) { + my $dodelete; + while (my $line = <$fh>) { + chomp($line); + if ($line eq $clientname) { + $dodelete = 1; + last; + } + } + close($fh); + if ($dodelete) { + if (unlink("$execdir/$cookie.id")) { + $deleted = 1; + } + } + } + } + } + if ($deleted) { + &Reply($client, "ok\n", $userinput); + } else { + &Failure( $client, "error: ".($!+0)."Unlinking cookie file Failed ". + "while attempting delbalcookie\n", $userinput); + } + return 1; +} +®ister_handler("delbalcookie", \&del_balcookie_handler, 0, 1, 0); + +# # Processes the setannounce command. This command # creates a file named announce.txt in the top directory of # the documentn root and sets its contents. The announce.txt file is @@ -3745,14 +5953,86 @@ sub enrollment_enabled_handler { my $userinput = $cmd.":".$tail; # For logging purposes. - my $cdom = split(/:/, $tail); # Domain we're asking about. + my ($cdom) = split(/:/, $tail, 2); # Domain we're asking about. + my $outcome = &localenroll::run($cdom); - &Reply($client, "$outcome\n", $userinput); + &Reply($client, \$outcome, $userinput); return 1; } ®ister_handler("autorun", \&enrollment_enabled_handler, 0, 1, 0); +# +# Validate an institutional code used for a LON-CAPA course. +# +# 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: +# $dom - The domain for which the check of +# institutional course code will occur. +# +# $instcode - The institutional code for the course +# being requested, or validated for rights +# to request. +# +# $owner - The course requestor (who will be the +# course owner, in the form username:domain +# +# $client - Socket open on the client. +# Returns: +# 1 - Indicating processing should continue. +# +sub validate_instcode_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($dom,$instcode,$owner) = split(/:/, $tail); + $instcode = &unescape($instcode); + $owner = &unescape($owner); + my ($outcome,$description,$credits) = + &localenroll::validate_instcode($dom,$instcode,$owner); + my $result = &escape($outcome).'&'.&escape($description).'&'. + &escape($credits); + &Reply($client, \$result, $userinput); + + return 1; +} +®ister_handler("autovalidateinstcode", \&validate_instcode_handler, 0, 1, 0); + +# +# Validate co-owner for cross-listed institutional code and +# institutional course code itself used for a LON-CAPA course. +# +# Formal Parameters: +# $cmd - The command request that got us dispatched. +# $tail - The tail of the command. In this case, +# this is a colon separated string containing: +# $dom - Course's LON-CAPA domain +# $instcode - Institutional course code for the course +# $inst_xlist - Institutional course Id for the crosslisting +# $coowner - Username of co-owner +# (values for all but $dom have been escaped). +# +# $client - Socket open on the client. +# Returns: +# 1 - Indicating processing should continue. +# +sub validate_instcrosslist_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($dom,$instcode,$inst_xlist,$coowner) = split(/:/,$tail); + $instcode = &unescape($instcode); + $inst_xlist = &unescape($inst_xlist); + $coowner = &unescape($coowner); + my $outcome = &localenroll::validate_crosslist_access($dom,$instcode, + $inst_xlist,$coowner); + &Reply($client, \$outcome, $userinput); + + return 1; +} +®ister_handler("autovalidateinstcrosslist", \&validate_instcrosslist_handler, 0, 1, 0); + # 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. @@ -3774,7 +6054,7 @@ sub get_sections_handler { my @secs = &localenroll::get_sections($coursecode,$cdom); my $seclist = &escape(join(':',@secs)); - &Reply($client, "$seclist\n", $userinput); + &Reply($client, \$seclist, $userinput); return 1; @@ -3799,10 +6079,12 @@ sub get_sections_handler { sub validate_course_owner_handler { my ($cmd, $tail, $client) = @_; my $userinput = "$cmd:$tail"; - my ($inst_course_id, $owner, $cdom) = split(/:/, $tail); - - my $outcome = &localenroll::new_course($inst_course_id,$owner,$cdom); - &Reply($client, "$outcome\n", $userinput); + my ($inst_course_id, $owner, $cdom, $coowners) = split(/:/, $tail); + + $owner = &unescape($owner); + $coowners = &unescape($coowners); + my $outcome = &localenroll::new_course($inst_course_id,$owner,$cdom,$coowners); + &Reply($client, \$outcome, $userinput); @@ -3833,7 +6115,7 @@ sub validate_course_section_handler { my ($inst_course_id, $cdom) = split(/:/, $tail); my $outcome=&localenroll::validate_courseID($inst_course_id,$cdom); - &Reply($client, "$outcome\n", $userinput); + &Reply($client, \$outcome, $userinput); return 1; @@ -3841,16 +6123,154 @@ sub validate_course_section_handler { ®ister_handler("autovalidatecourse", \&validate_course_section_handler, 0, 1, 0); # -# Create a password for a new auto-enrollment user. -# I think/guess, this password allows access to the institutions -# AIS class list server/services. Stuart can correct this comment -# when he finds out how wrong I am. +# 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 values that will be split into: +# $inst_class - Institutional code for the specific class section +# $ownerlist - An escaped comma-separated list of username:domain +# of the course owner, and co-owner(s). +# $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 validate_class_access_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($inst_class,$ownerlist,$cdom) = split(/:/, $tail); + my $owners = &unescape($ownerlist); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome=&localenroll::check_section($inst_class,$owners,$cdom); + }; + &Reply($client,\$outcome, $userinput); + + return 1; +} +®ister_handler("autovalidateclass_sec", \&validate_class_access_handler, 0, 1, 0); + +# +# Modify institutional sections (using customized &instsec_reformat() +# routine in localenroll.pm), to either clutter or declutter, for +# purposes of ensuring an institutional course section (string) can +# be unambiguously separated into institutional course and 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 values that will be split into: +# $cdom - The LON-CAPA domain of the course. +# $action - Either: clutter or declutter +# clutter adds character(s) to eliminate ambiguity +# declutter removes the added characters (e.g., for +# display of the institutional course section string. +# $info - A frozen hash in which keys are: +# LON-CAPA course number:Institutional course code +# and values are a reference to an array of the +# items to modify -- either institutional sections, +# or institutional course sections (for crosslistings). +# $client - The socket open on the client. +# Returns: +# 1 - continue processing. +# + +sub instsec_reformat_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($cdom,$action,$info) = split(/:/,$tail); + my $instsecref = &Apache::lonnet::thaw_unescape($info); + my ($outcome,$result); + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome=&localenroll::instsec_reformat($cdom,$action,$instsecref); + if ($outcome eq 'ok') { + if (ref($instsecref) eq 'HASH') { + foreach my $key (keys(%{$instsecref})) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($instsecref->{$key}).'&'; + } + $result =~ s/\&$//; + } + } + }; + if (!$@) { + if ($outcome eq 'ok') { + &Reply( $client, \$result, $userinput); + } else { + &Reply($client,\$outcome, $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } + return 1; +} +®ister_handler("autoinstsecreformat",\&instsec_reformat_handler, 0, 1, 0); + +# +# Validate course owner or co-owners(s) access to enrollment data for all sections +# and crosslistings for a particular course. +# +# +# 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 values that will be split into: +# $ownerlist - An escaped comma-separated list of username:domain +# of the course owner, and co-owner(s). +# $cdom - The domain of the course from the institution's +# point of view. +# $classes - Frozen hash of institutional course sections and +# crosslistings. +# $client - The socket open on the client. +# Returns: +# 1 - continue processing. +# + +sub validate_classes_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($ownerlist,$cdom,$classes) = split(/:/, $tail); + my $classesref = &Apache::lonnet::thaw_unescape($classes); + my $owners = &unescape($ownerlist); + my $result; + eval { + local($SIG{__DIE__})='DEFAULT'; + my %validations; + my $response = &localenroll::check_instclasses($owners,$cdom,$classesref, + \%validations); + if ($response eq 'ok') { + foreach my $key (keys(%validations)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($validations{$key}).'&'; + } + $result =~ s/\&$//; + } else { + $result = 'error'; + } + }; + if (!$@) { + &Reply($client, \$result, $userinput); + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } + return 1; +} +®ister_handler("autovalidateinstclasses", \&validate_classes_handler, 0, 1, 0); + +# +# 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 # # 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 (username??). +# $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. @@ -3877,13 +6297,58 @@ sub create_auto_enroll_password_handler ®ister_handler("autocreatepassword", \&create_auto_enroll_password_handler, 0, 1, 0); +sub auto_export_grades_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($cdom,$cnum,$info,$data) = split(/:/,$tail); + my $inforef = &Apache::lonnet::thaw_unescape($info); + my $dataref = &Apache::lonnet::thaw_unescape($data); + my ($outcome,$result);; + eval { + local($SIG{__DIE__})='DEFAULT'; + my %rtnhash; + $outcome=&localenroll::export_grades($cdom,$cnum,$inforef,$dataref,\%rtnhash); + if ($outcome eq 'ok') { + foreach my $key (keys(%rtnhash)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($rtnhash{$key}).'&'; + } + $result =~ s/\&$//; + } + }; + if (!$@) { + if ($outcome eq 'ok') { + if ($cipher) { + my $cmdlength=length($result); + $result.=" "; + my $encresult=''; + for (my $encidx=0;$encidx<=$cmdlength;$encidx+=8) { + $encresult.= unpack("H16", + $cipher->encrypt(substr($result, + $encidx, + 8))); + } + &Reply( $client, "enc:$cmdlength:$encresult\n", $userinput); + } else { + &Failure( $client, "error:no_key\n", $userinput); + } + } else { + &Reply($client, "$outcome\n", $userinput); + } + } else { + &Failure($client,"export_error\n",$userinput); + } + return 1; +} +®ister_handler("autoexportgrades", \&auto_export_grades_handler, + 1, 1, 0); + # 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. +# $filename - The name of the file to retrieve. # The filename is given as a path relative to # the LonCAPA temp file directory. # $client - Socket open on the client. @@ -3897,7 +6362,12 @@ sub retrieve_auto_file_handler { my ($filename) = split(/:/, $tail); my $source = $perlvar{'lonDaemons'}.'/tmp/'.$filename; - if ( (-e $source) && ($filename ne '') ) { + + if ($filename =~m{/\.\./}) { + &Failure($client, "refused\n", $userinput); + } elsif ($filename !~ /^$LONCAPA::match_domain\_$LONCAPA::match_courseid\_.+_classlist\.xml$/) { + &Failure($client, "refused\n", $userinput); + } elsif ( (-e $source) && ($filename ne '') ) { my $reply = ''; if (open(my $fh,$source)) { while (<$fh>) { @@ -3924,6 +6394,109 @@ sub retrieve_auto_file_handler { } ®ister_handler("autoretrieve", \&retrieve_auto_file_handler, 0,1,0); +sub crsreq_checks_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my $dom = $tail; + my $result; + my @reqtypes = ('official','unofficial','community','textbook','placement'); + eval { + local($SIG{__DIE__})='DEFAULT'; + my %validations; + my $response = &localenroll::crsreq_checks($dom,\@reqtypes, + \%validations); + if ($response eq 'ok') { + foreach my $key (keys(%validations)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($validations{$key}).'&'; + } + $result =~ s/\&$//; + } else { + $result = 'error'; + } + }; + if (!$@) { + &Reply($client, \$result, $userinput); + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } + return 1; +} +®ister_handler("autocrsreqchecks", \&crsreq_checks_handler, 0, 1, 0); + +sub validate_crsreq_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($dom,$owner,$crstype,$inststatuslist,$instcode,$instseclist,$customdata) = split(/:/, $tail); + $instcode = &unescape($instcode); + $owner = &unescape($owner); + $crstype = &unescape($crstype); + $inststatuslist = &unescape($inststatuslist); + $instcode = &unescape($instcode); + $instseclist = &unescape($instseclist); + my $custominfo = &Apache::lonnet::thaw_unescape($customdata); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::validate_crsreq($dom,$owner,$crstype, + $inststatuslist,$instcode, + $instseclist,$custominfo); + }; + if (!$@) { + &Reply($client, \$outcome, $userinput); + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } + return 1; +} +®ister_handler("autocrsreqvalidation", \&validate_crsreq_handler, 0, 1, 0); + +sub crsreq_update_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my ($cdom,$cnum,$crstype,$action,$ownername,$ownerdomain,$fullname,$title,$code, + $accessstart,$accessend,$infohashref) = + split(/:/, $tail); + $crstype = &unescape($crstype); + $action = &unescape($action); + $ownername = &unescape($ownername); + $ownerdomain = &unescape($ownerdomain); + $fullname = &unescape($fullname); + $title = &unescape($title); + $code = &unescape($code); + $accessstart = &unescape($accessstart); + $accessend = &unescape($accessend); + my $incoming = &Apache::lonnet::thaw_unescape($infohashref); + my ($result,$outcome); + eval { + local($SIG{__DIE__})='DEFAULT'; + my %rtnhash; + $outcome = &localenroll::crsreq_updates($cdom,$cnum,$crstype,$action, + $ownername,$ownerdomain,$fullname, + $title,$code,$accessstart,$accessend, + $incoming,\%rtnhash); + if ($outcome eq 'ok') { + my @posskeys = qw(createdweb createdmsg createdcustomized createdactions queuedweb queuedmsg formitems reviewweb validationjs onload javascript); + foreach my $key (keys(%rtnhash)) { + if (grep(/^\Q$key\E/,@posskeys)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($rtnhash{$key}).'&'; + } + } + $result =~ s/\&$//; + } + }; + if (!$@) { + if ($outcome eq 'ok') { + &Reply($client, \$result, $userinput); + } else { + &Reply($client, "format_error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } + return 1; +} +®ister_handler("autocrsrequpdate", \&crsreq_update_handler, 0, 1, 0); + # # Read and retrieve institutional code format (for support form). # Formal Parameters: @@ -3957,10 +6530,10 @@ sub get_institutional_code_format_handle \%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); + my $codes_str = &Apache::lonnet::hash2str(%codes); + my $codetitles_str = &Apache::lonnet::array2str(@codetitles); + my $cat_titles_str = &Apache::lonnet::hash2str(%cat_titles); + my $cat_order_str = &Apache::lonnet::hash2str(%cat_order); &Reply($client, $codes_str.':'.$codetitles_str.':'.$cat_titles_str.':' .$cat_order_str."\n", @@ -3977,6 +6550,337 @@ sub get_institutional_code_format_handle ®ister_handler("autoinstcodeformat", \&get_institutional_code_format_handler,0,1,0); +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,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("autoinstcodedefaults", + \&get_institutional_defaults_handler,0,1,0); + +sub get_possible_instcodes_handler { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + + my $reply; + my $cdom = $tail; + my (@codetitles,%cat_titles,%cat_order,@code_order); + my $formatreply = &localenroll::possible_instcodes($cdom, + \@codetitles, + \%cat_titles, + \%cat_order, + \@code_order); + if ($formatreply eq 'ok') { + my $result = join('&',map {&escape($_);} (@codetitles)).':'; + $result .= join('&',map {&escape($_);} (@code_order)).':'; + foreach my $key (keys(%cat_titles)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($cat_titles{$key}).'&'; + } + $result =~ s/\&$//; + $result .= ':'; + foreach my $key (keys(%cat_order)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($cat_order{$key}).'&'; + } + $result =~ s/\&$//; + &Reply($client,\$result,$userinput); + } else { + &Reply($client, "format_error\n", $userinput); + } + return 1; +} +®ister_handler("autopossibleinstcodes", + \&get_possible_instcodes_handler,0,1,0); + +sub get_institutional_user_rules { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my $dom = &unescape($tail); + my (%rules_hash,@rules_order); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::username_rules($dom,\%rules_hash,\@rules_order); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result; + foreach my $key (keys(%rules_hash)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($rules_hash{$key}).'&'; + } + $result =~ s/\&$//; + $result .= ':'; + if (@rules_order > 0) { + foreach my $item (@rules_order) { + $result .= &escape($item).'&'; + } + } + $result =~ s/\&$//; + &Reply($client,\$result,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("instuserrules",\&get_institutional_user_rules,0,1,0); + +sub get_institutional_id_rules { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my $dom = &unescape($tail); + my (%rules_hash,@rules_order); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::id_rules($dom,\%rules_hash,\@rules_order); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result; + foreach my $key (keys(%rules_hash)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($rules_hash{$key}).'&'; + } + $result =~ s/\&$//; + $result .= ':'; + if (@rules_order > 0) { + foreach my $item (@rules_order) { + $result .= &escape($item).'&'; + } + } + $result =~ s/\&$//; + &Reply($client,\$result,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("instidrules",\&get_institutional_id_rules,0,1,0); + +sub get_institutional_selfcreate_rules { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my $dom = &unescape($tail); + my (%rules_hash,@rules_order); + my $outcome; + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::selfcreate_rules($dom,\%rules_hash,\@rules_order); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result; + foreach my $key (keys(%rules_hash)) { + $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($rules_hash{$key}).'&'; + } + $result =~ s/\&$//; + $result .= ':'; + if (@rules_order > 0) { + foreach my $item (@rules_order) { + $result .= &escape($item).'&'; + } + } + $result =~ s/\&$//; + &Reply($client,\$result,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("instemailrules",\&get_institutional_selfcreate_rules,0,1,0); + + +sub institutional_username_check { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my %rulecheck; + my $outcome; + my ($udom,$uname,@rules) = split(/:/,$tail); + $udom = &unescape($udom); + $uname = &unescape($uname); + @rules = map {&unescape($_);} (@rules); + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::username_check($udom,$uname,\@rules,\%rulecheck); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result=''; + foreach my $key (keys(%rulecheck)) { + $result.=&escape($key).'='.&Apache::lonnet::freeze_escape($rulecheck{$key}).'&'; + } + &Reply($client,\$result,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("instrulecheck",\&institutional_username_check,0,1,0); + +sub institutional_id_check { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my %rulecheck; + my $outcome; + my ($udom,$id,@rules) = split(/:/,$tail); + $udom = &unescape($udom); + $id = &unescape($id); + @rules = map {&unescape($_);} (@rules); + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::id_check($udom,$id,\@rules,\%rulecheck); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result=''; + foreach my $key (keys(%rulecheck)) { + $result.=&escape($key).'='.&Apache::lonnet::freeze_escape($rulecheck{$key}).'&'; + } + &Reply($client,\$result,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("instidrulecheck",\&institutional_id_check,0,1,0); + +sub institutional_selfcreate_check { + my ($cmd, $tail, $client) = @_; + my $userinput = "$cmd:$tail"; + my %rulecheck; + my $outcome; + my ($udom,$email,@rules) = split(/:/,$tail); + $udom = &unescape($udom); + $email = &unescape($email); + @rules = map {&unescape($_);} (@rules); + eval { + local($SIG{__DIE__})='DEFAULT'; + $outcome = &localenroll::selfcreate_check($udom,$email,\@rules,\%rulecheck); + }; + if (!$@) { + if ($outcome eq 'ok') { + my $result=''; + foreach my $key (keys(%rulecheck)) { + $result.=&escape($key).'='.&Apache::lonnet::freeze_escape($rulecheck{$key}).'&'; + } + &Reply($client,\$result,$userinput); + } else { + &Reply($client,"error\n", $userinput); + } + } else { + &Failure($client,"unknown_cmd\n",$userinput); + } +} +®ister_handler("instselfcreatecheck",\&institutional_selfcreate_check,0,1,0); + +# Get domain specific conditions for import of student photographs to a course +# +# 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 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); + } + return 1; +} +®ister_handler("autophotopermission",\&photo_permission_handler,0,1,0); + +# +# 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); + +# +# 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. + +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 { + &Failure($client,"unknown_cmd\n",$userinput); + } + return 1; +} +®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. @@ -3989,24 +6893,36 @@ sub get_institutional_code_format_handle # $client - The socket open on the client. # Returns: # 1 - continue processing. + sub student_photo_handler { my ($cmd, $tail, $client) = @_; - my ($domain,$uname,$type) = split(/:/, $tail); + my ($domain,$uname,$ext,$type) = split(/:/, $tail); - my $path=&propath($domain,$uname). - '/userfiles/internal/studentphoto.'.$type; - if (-e $path) { + my $path=&propath($domain,$uname). '/userfiles/internal/'; + my $filename = 'studentphoto.'.$ext; + if ($type eq 'thumbnail') { + $filename = 'studentphoto_tn.'.$ext; + } + if (-e $path.$filename) { &Reply($client,"ok\n","$cmd:$tail"); return 1; } &mkpath($path); - my $file=&localstudentphoto::fetch($domain,$uname); + my $file; + if ($type eq 'thumbnail') { + eval { + local($SIG{__DIE__})='DEFAULT'; + $file=&localstudentphoto::fetch_thumbnail($domain,$uname); + }; + } else { + $file=&localstudentphoto::fetch($domain,$uname); + } if (!$file) { &Failure($client,"unavailable\n","$cmd:$tail"); return 1; } - if (!-e $path) { &convert_photo($file,$path); } - if (-e $path) { + if (!-e $path.$filename) { &convert_photo($file,$path.$filename); } + if (-e $path.$filename) { &Reply($client,"ok\n","$cmd:$tail"); return 1; } @@ -4015,6 +6931,35 @@ sub student_photo_handler { } ®ister_handler("studentphoto", \&student_photo_handler, 0, 1, 0); +sub inst_usertypes_handler { + my ($cmd, $domain, $client) = @_; + my $res; + my $userinput = $cmd.":".$domain; # For logging purposes. + my (%typeshash,@order,$result); + eval { + local($SIG{__DIE__})='DEFAULT'; + $result=&localenroll::inst_usertypes($domain,\%typeshash,\@order); + }; + if ($result eq 'ok') { + if (keys(%typeshash) > 0) { + foreach my $key (keys(%typeshash)) { + $res.=&escape($key).'='.&escape($typeshash{$key}).'&'; + } + } + $res=~s/\&$//; + $res .= ':'; + if (@order > 0) { + foreach my $item (@order) { + $res .= &escape($item).'&'; + } + } + $res=~s/\&$//; + } + &Reply($client, \$res, $userinput); + return 1; +} +®ister_handler("inst_usertypes", \&inst_usertypes_handler, 0, 1, 0); + # 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 @@ -4059,14 +7004,31 @@ sub get_request { # # 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 ($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 { + $userinput = "$cmd:$newid"; + } + } + if ($userinput =~ /^enc/) { $userinput = decipher($userinput); $wasenc=1; @@ -4128,6 +7090,62 @@ sub process_request { Debug("Client not privileged to do this operation"); $ok = 0; } + if ($ok) { + my $realcommand = $command; + if ($command eq 'querysend') { + my ($query,$rest)=split(/\:/,$tail,2); + $query=~s/\n*$//g; + my @possqueries = + qw(userlog courselog fetchenrollment institutionalphotos usersearch instdirsearch getinstuser getmultinstusers); + if (grep(/^\Q$query\E$/,@possqueries)) { + $command .= '_'.$query; + } elsif ($query eq 'prepare activity log') { + $command .= '_activitylog'; + } + } + if (ref($trust{$command}) eq 'HASH') { + my $donechecks; + if ($trust{$command}{'anywhere'}) { + $donechecks = 1; + } elsif ($trust{$command}{'manageronly'}) { + unless (&isManager()) { + $ok = 0; + } + $donechecks = 1; + } elsif ($trust{$command}{'institutiononly'}) { + unless ($clientsameinst) { + $ok = 0; + } + $donechecks = 1; + } elsif ($clientsameinst) { + $donechecks = 1; + } + unless ($donechecks) { + foreach my $rule (keys(%{$trust{$command}})) { + next if ($rule eq 'remote'); + if ($trust{$command}{$rule}) { + if ($clientprohibited{$rule}) { + $ok = 0; + } else { + $ok = 1; + $donechecks = 1; + last; + } + } + } + } + unless ($donechecks) { + if ($trust{$command}{'remote'}) { + if ($clientremoteok) { + $ok = 1; + } else { + $ok = 0; + } + } + } + } + $command = $realcommand; + } if($ok) { Debug("Dispatching to handler $command $tail"); @@ -4138,8 +7156,7 @@ sub process_request { Failure($client, "refused\n", $userinput); return 1; } - - } + } print $client "unknown_cmd\n"; # -------------------------------------------------------------------- complete @@ -4243,18 +7260,6 @@ sub lcpasswdstrerror { } } -# -# Convert an error return code from lcuseradd to a string value: -# -sub lcuseraddstrerror { - my $ErrorCode = shift; - if(($ErrorCode < 0) || ($ErrorCode > $lastadderror)) { - return "lcuseradd - Unrecognized error code: ".$ErrorCode; - } else { - return $adderrors[$ErrorCode]; - } -} - # grabs exception and records it to log before exiting sub catchexception { my ($error)=@_; @@ -4262,7 +7267,7 @@ sub catchexception { $SIG{__DIE__}='DEFAULT'; &status("Catching exception"); &logthis("CRITICAL: " - ."ABNORMAL EXIT. Child $$ for server $thisserver died through " + ."ABNORMAL EXIT. Child $$ for server ".$perlvar{'lonHostID'}." died through " ."a crash with this error msg->[$error]"); &logthis('Famous last words: '.$status.' - '.$lastlog); if ($client) { print $client "error: $error\n"; } @@ -4291,8 +7296,8 @@ 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.' |\ - mailto $emailto -s '$subj' > /dev/null"); + system("echo 'User ID mismatch. lond must be run as user www.' |". + " mail -s '$subj' $emailto > /dev/null"); exit 1; } @@ -4315,7 +7320,7 @@ if (-e $pidfile) { $server = IO::Socket::INET->new(LocalPort => $perlvar{'londPort'}, Type => SOCK_STREAM, Proto => 'tcp', - Reuse => 1, + ReuseAddr => 1, Listen => 10 ) or die "making socket: $@\n"; @@ -4373,67 +7378,18 @@ sub HUPSMAN { # sig } # -# Kill off hashes that describe the host table prior to re-reading it. -# Hashes affected are: -# %hostid, %hostdom %hostip %hostdns. -# -sub KillHostHashes { - foreach my $key (keys %hostid) { - delete $hostid{$key}; - } - foreach my $key (keys %hostdom) { - delete $hostdom{$key}; - } - 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: -# -# - %hostid - Indexed by IP, the loncapa hostname. -# - %hostdom - Indexed by loncapa hostname, the domain. -# - %hostip - Indexed by hostid, the Ip address of the host. -sub ReadHostTable { - - open (CONFIG,"$perlvar{'lonTabDir'}/hosts.tab") || die "Can't read host file"; - my $myloncapaname = $perlvar{'lonHostID'}; - Debug("My loncapa name is : $myloncapaname"); - while (my $configline=) { - if ($configline !~ /^\s*\#/ && $configline !~ /^\s*$/ ) { - my ($id,$domain,$role,$name)=split(/:/,$configline); - $name=~s/\s//g; - my $ip = gethostbyname($name); - if (length($ip) ne 4) { - &logthis("Skipping host $id name $name no IP $ip found\n"); - next; - } - $ip=inet_ntoa($ip); - $hostid{$ip}=$id; # LonCAPA name of host by IP. - $hostdom{$id}=$domain; # LonCAPA domain name of host. - $hostip{$id}=$ip; # IP address of host. - $hostdns{$name} = $id; # LonCAPA name of host by DNS. - - if ($id eq $perlvar{'lonHostID'}) { - Debug("Found me in the host table: $name"); - $thisserver=$name; - } - } - } - close(CONFIG); -} -# # Reload the Apache daemon's state. # This is done by invoking /home/httpd/perl/apachereload # a setuid perl script that can be root for us to do this job. # sub ReloadApache { - my $execdir = $perlvar{'lonDaemons'}; - my $script = $execdir."/apachereload"; - system($script); +# --------------------------- Handle case of another apachereload process (locking) + if (&LONCAPA::try_to_lock('/tmp/lock_apachereload')) { + my $execdir = $perlvar{'lonDaemons'}; + my $script = $execdir."/apachereload"; + system($script); + unlink('/tmp/lock_apachereload'); # Remove the lock file. + } } # @@ -4456,26 +7412,73 @@ sub UpdateHosts { # either dropped or changed hosts. Note that the re-read of the table # will take care of new and changed hosts as connections come into being. + &Apache::lonnet::reset_hosts_info(); + my %active; - KillHostHashes; - ReadHostTable; - - foreach my $child (keys %children) { + foreach my $child (keys(%children)) { my $childip = $children{$child}; - if(!$hostid{$childip}) { + if ($childip ne '127.0.0.1' + && !defined(&Apache::lonnet::get_hosts_from_ip($childip))) { logthis(' UpdateHosts killing child ' ." $child for ip $childip "); kill('INT', $child); } else { + $active{$child} = $childip; logthis(' keeping child for ip ' ." $childip (pid=$child) "); } } + + my %oldconf = %secureconf; + my %connchange; + if (lonssl::Read_Connect_Config(\%secureconf,\%perlvar,\%crlchecked) eq 'ok') { + logthis(' Reloaded SSL connection rules and cleared CRL checking history '); + } else { + logthis(' Failed to reload SSL connection rules and clear CRL checking history '); + } + if ((ref($oldconf{'connfrom'}) eq 'HASH') && (ref($secureconf{'connfrom'}) eq 'HASH')) { + foreach my $type ('dom','intdom','other') { + if ((($oldconf{'connfrom'}{$type} eq 'no') && ($secureconf{'connfrom'}{$type} eq 'req')) || + (($oldconf{'connfrom'}{$type} eq 'req') && ($secureconf{'connfrom'}{$type} eq 'no'))) { + $connchange{$type} = 1; + } + } + } + if (keys(%connchange)) { + foreach my $child (keys(%active)) { + my $childip = $active{$child}; + if ($childip ne '127.0.0.1') { + my $childhostname = gethostbyaddr(Socket::inet_aton($childip),AF_INET); + if ($childhostname ne '') { + my $childlonhost = &Apache::lonnet::get_server_homeID($childhostname); + my ($samedom,$sameinst) = &set_client_info($childlonhost); + if ($samedom) { + if ($connchange{'dom'}) { + logthis(' UpdateHosts killing child ' + ." $child for ip $childip "); + kill('INT', $child); + } + } elsif ($sameinst) { + if ($connchange{'intdom'}) { + logthis(' UpdateHosts killing child ' + ." $child for ip $childip "); + kill('INT', $child); + } + } else { + if ($connchange{'other'}) { + logthis(' UpdateHosts killing child ' + ." $child for ip $childip "); + kill('INT', $child); + } + } + } + } + } + } ReloadApache; &status("Finished reloading hosts.tab"); } - sub checkchildren { &status("Checking on the children (sending signals)"); &initnewstatus(); @@ -4545,11 +7548,19 @@ sub Debug { # reply - Text to send to client. # request - Original request from client. # +#NOTE $reply must be terminated by exactly *one* \n. If $reply is a reference +#this is done automatically ($$reply must not contain any \n in this case). +#If $reply is a string the caller has to ensure this. sub Reply { my ($fd, $reply, $request) = @_; - print $fd $reply; - Debug("Request was $request Reply was $reply"); - + if (ref($reply)) { + print $fd $$reply; + print $fd "\n"; + if ($DEBUG) { Debug("Request was $request Reply was $$reply"); } + } else { + print $fd $reply; + if ($DEBUG) { Debug("Request was $request Reply was $reply"); } + } $Transactions++; } @@ -4602,7 +7613,7 @@ sub logstatus { sub initnewstatus { my $docdir=$perlvar{'lonDocRoot'}; my $fh=IO::File->new(">$docdir/lon-status/londstatus.txt"); - my $now=time; + my $now=time(); my $local=localtime($now); print $fh "LOND status $local - parent $$\n\n"; opendir(DIR,"$docdir/lon-status/londchld"); @@ -4622,79 +7633,6 @@ sub status { $0='lond: '.$what.' '.$local; } -# -------------------------------------------------------- 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; -} - -# ----------------------------------------------------------- Send USR1 to lonc - -sub reconlonc { - my $peerfile=shift; - &logthis("Trying to reconnect for $peerfile"); - my $loncfile="$perlvar{'lonDaemons'}/logs/lonc.pid"; - if (my $fh=IO::File->new("$loncfile")) { - my $loncpid=<$fh>; - chomp($loncpid); - if (kill 0 => $loncpid) { - &logthis("lonc at pid $loncpid responding, sending USR1"); - kill USR1 => $loncpid; - } else { - &logthis( - "CRITICAL: " - ."lonc at pid $loncpid not responding, giving up"); - } - } else { - &logthis('CRITICAL: lonc not running, giving up'); - } -} - -# -------------------------------------------------- Non-critical communication - -sub subreply { - my ($cmd,$server)=@_; - my $peerfile="$perlvar{'lonSockDir'}/$server"; - my $sclient=IO::Socket::UNIX->new(Peer =>"$peerfile", - Type => SOCK_STREAM, - Timeout => 10) - or return "con_lost"; - print $sclient "$cmd\n"; - my $answer=<$sclient>; - chomp($answer); - if (!$answer) { $answer="con_lost"; } - return $answer; -} - -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; -} - # -------------------------------------------------------------- Talk to lonsql sub sql_reply { @@ -4712,25 +7650,13 @@ sub sub_sql_reply { Type => SOCK_STREAM, Timeout => 10) or return "con_lost"; - print $sclient "$cmd\n"; + 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 { @@ -4775,10 +7701,21 @@ $SIG{USR1} = \&checkchildren; $SIG{USR2} = \&UpdateHosts; # Read the host hashes: +&Apache::lonnet::load_hosts_tab(); +my %iphost = &Apache::lonnet::get_iphost(1); -ReadHostTable; +$dist=`$perlvar{'lonDaemons'}/distprobe`; -my $dist=`$perlvar{'lonDaemons'}/distprobe`; +my $arch = `uname -i`; +chomp($arch); +if ($arch eq 'unknown') { + $arch = `uname -m`; + chomp($arch); +} + +unless (lonssl::Read_Connect_Config(\%secureconf,\%perlvar,\%crlchecked) eq 'ok') { + &logthis('No connectionrules table. Will fallback to loncapa.conf'); +} # -------------------------------------------------------------- # Accept connections. When a connection comes in, it is validated @@ -4837,6 +7774,7 @@ sub make_new_child { or die "Can't unblock SIGINT for fork: $!\n"; $children{$pid} = $clientip; &status('Started child '.$pid); + close($client); return; } else { # Child can *not* return from this subroutine. @@ -4845,6 +7783,13 @@ sub make_new_child { #don't get intercepted $SIG{USR1}= \&logstatus; $SIG{ALRM}= \&timeout; + # + # Block sigpipe as it gets thrownon socket disconnect and we want to + # deal with that as a read faiure instead. + # + my $blockset = POSIX::SigSet->new(SIGPIPE); + sigprocmask(SIG_BLOCK, $blockset); + $lastlog='Forked '; $status='Forked'; @@ -4855,7 +7800,26 @@ sub make_new_child { # my $tmpsnum=0; # Now global #---------------------------------------------------- kerberos 5 initialization &Authen::Krb5::init_context(); - if ($dist ne 'fedora4') { + + my $no_ets; + if ($dist =~ /^(?:centos|rhes|scientific|oracle)(\d+)$/) { + if ($1 >= 7) { + $no_ets = 1; + } + } elsif ($dist =~ /^suse(\d+\.\d+)$/) { + if (($1 eq '9.3') || ($1 >= 12.2)) { + $no_ets = 1; + } + } elsif ($dist =~ /^sles(\d+)$/) { + if ($1 > 11) { + $no_ets = 1; + } + } elsif ($dist =~ /^fedora(\d+)$/) { + if ($1 < 7) { + $no_ets = 1; + } + } + unless ($no_ets) { &Authen::Krb5::init_ets(); } @@ -4865,19 +7829,17 @@ sub make_new_child { # ----------------------------------------------------------------------------- # see if we know client and 'check' for spoof IP by ineffective challenge - ReadManagerTable; # May also be a manager!! - my $outsideip=$clientip; if ($clientip eq '127.0.0.1') { - $outsideip=$hostip{$perlvar{'lonHostID'}}; + $outsideip=&Apache::lonnet::get_host_ip($perlvar{'lonHostID'}); } - - my $clientrec=($hostid{$outsideip} ne undef); + &ReadManagerTable(); + my $clientrec=defined(&Apache::lonnet::get_hosts_from_ip($outsideip)); my $ismanager=($managers{$outsideip} ne undef); - $clientname = "[unknonwn]"; + $clientname = "[unknown]"; if($clientrec) { # Establish client type. $ConnectionType = "client"; - $clientname = $hostid{$outsideip}; + $clientname = (&Apache::lonnet::get_hosts_from_ip($outsideip))[-1]; if($ismanager) { $ConnectionType = "both"; } @@ -4896,18 +7858,49 @@ sub make_new_child { my $remotereq=<$client>; chomp($remotereq); Debug("Got init: $remotereq"); - my $inikeyword = split(/:/, $remotereq); + if ($remotereq =~ /^init/) { &sethost("sethost:$perlvar{'lonHostID'}"); # # If the remote is attempting a local init... give that a try: # - my ($i, $inittype) = split(/:/, $remotereq); + (my $i, my $inittype, $clientversion) = split(/:/, $remotereq); + # For LON-CAPA 2.9, the client session will have sent its LON-CAPA + # version when initiating the connection. For LON-CAPA 2.8 and older, + # the version is retrieved from the global %loncaparevs in lonnet.pm. + # $clientversion contains path to keyfile if $inittype eq 'local' + # it's overridden below in this case + $clientversion ||= $Apache::lonnet::loncaparevs{$clientname}; # 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 $context; + if ($clientsamedom) { + $context = 'dom'; + if ($secureconf{'connfrom'}{'dom'} eq 'no') { + $inittype = ""; + } + } elsif ($clientsameinst) { + $context = 'intdom'; + if ($secureconf{'connfrom'}{'intdom'} eq 'no') { + $inittype = ""; + } + } else { + $context = 'other'; + if ($secureconf{'connfrom'}{'other'} eq 'no') { + $inittype = ""; + } + } + if ($inittype eq '') { + &logthis(" Domain config set " + ."to no ssl for $clientname (context: $context)" + ." -- trying insecure auth"); + } + } + if($inittype eq "ssl") { my ($ca, $cert) = lonssl::CertificateFile; my $kfile = lonssl::KeyFile; @@ -4922,6 +7915,7 @@ sub make_new_child { } if($inittype eq "local") { + $clientversion = $perlvar{'lonVersion'}; my $key = LocalConnection($client, $remotereq); if($key) { Debug("Got local key $key"); @@ -4929,7 +7923,7 @@ sub make_new_child { my $cipherkey = pack("H32", $key); $cipher = new IDEA($cipherkey); print $client "ok:local\n"; - &logthis('' . "Successful local authentication "); $keymode = "local" } else { @@ -4939,7 +7933,7 @@ sub make_new_child { close $client; } } elsif ($inittype eq "ssl") { - my $key = SSLConnection($client); + my $key = SSLConnection($client,$clientname); if ($key) { $clientok = 1; my $cipherkey = pack("H32", $key); @@ -4966,7 +7960,6 @@ sub make_new_child { ."Attempted insecure connection disallowed "); close $client; $clientok = 0; - } } } else { @@ -4975,7 +7968,6 @@ sub make_new_child { ."$clientip failed to initialize: >$remotereq< "); &status('No init '.$clientip); } - } else { &logthis( "WARNING: Unknown client $clientip"); @@ -4984,26 +7976,22 @@ sub make_new_child { if ($clientok) { # ---------------- New known client connecting, could mean machine online again - - foreach my $id (keys(%hostip)) { - if ($hostip{$id} ne $clientip || - $hostip{$currenthostid} eq $clientip) { - # no need to try to do recon's to myself - next; - } - &reconlonc("$perlvar{'lonSockDir'}/$id"); + if (&Apache::lonnet::get_host_ip($currenthostid) ne $clientip + && $clientip ne '127.0.0.1') { + &Apache::lonnet::reconlonc($clientname); } &logthis("Established connection: $clientname"); &status('Will listen to '.$clientname); # ------------------------------------------------------------ Process requests 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)"); + &status('Listening to '.$clientname." ($keymode)"); } # --------------------------------------------- client unknown or fishy, refuse @@ -5013,20 +8001,89 @@ sub make_new_child { &logthis("WARNING: " ."Rejected client $clientip, closing connection"); } - } + } # ============================================================================= &logthis("CRITICAL: " ."Disconnect from $clientip ($clientname)"); - - + + # this exit is VERY important, otherwise the child will become # a producer of more and more children, forking yourself into # process death. exit; } + +# +# Used to determine if a particular client is from the same domain +# as the current server, or from the same internet domain, and +# also if the client can host sessions for the domain's users. +# A hash is populated with keys set to commands sent by the client +# which may not be executed for this domain. +# +# Optional input -- the client to check for domain and internet domain. +# If not specified, defaults to the package variable: $clientname +# +# If called in array context will not set package variables, but will +# instead return an array of two values - (a) true if client is in the +# same domain as the server, and (b) true if client is in the same +# internet domain. +# +# If called in scalar context, sets package variables for current client: +# +# $clienthomedom - LonCAPA domain of homeID for client. +# $clientsamedom - LonCAPA domain same for this host and client. +# $clientintdom - LonCAPA "internet domain" for client. +# $clientsameinst - LonCAPA "internet domain" same for this host & client. +# $clientremoteok - If current domain permits hosting on this client: 1 +# %clientprohibited - Commands prohibited for domain's users for this client. +# +# if the host and client have the same "internet domain", then the value +# of $clientremoteok is not used, and no commands are prohibited. +# +# returns 1 to indicate package variables have been set for current client. +# + +sub set_client_info { + my ($lonhost) = @_; + $lonhost ||= $clientname; + my $clienthost = &Apache::lonnet::hostname($lonhost); + my $clientserverhomeID = &Apache::lonnet::get_server_homeID($clienthost); + my $homedom = &Apache::lonnet::host_domain($clientserverhomeID); + my $samedom = 0; + if ($perlvar{'lonDefDomain'} eq $homedom) { + $samedom = 1; + } + my $intdom = &Apache::lonnet::internet_dom($clientserverhomeID); + my $sameinst = 0; + if ($intdom ne '') { + my $internet_names = &Apache::lonnet::get_internet_names($currenthostid); + if (ref($internet_names) eq 'ARRAY') { + if (grep(/^\Q$intdom\E$/,@{$internet_names})) { + $sameinst = 1; + } + } + } + if (wantarray) { + return ($samedom,$sameinst); + } else { + $clienthomedom = $homedom; + $clientsamedom = $samedom; + $clientintdom = $intdom; + $clientsameinst = $sameinst; + if ($clientsameinst) { + undef($clientremoteok); + undef(%clientprohibited); + } else { + $clientremoteok = &get_remote_hostable($currentdomainid); + %clientprohibited = &get_prohibited($currentdomainid); + } + return 1; + } +} + # # Determine if a user is an author for the indicated domain. # @@ -5047,19 +8104,29 @@ sub is_author { # Author role should show up as a key /domain/_au - my $key = "/$domain/_au"; - my $value = $hashref->{$key}; + my $value; + if ($hashref) { - if(defined($value)) { - &Debug("$user @ $domain is an author"); + my $key = "/$domain/_au"; + if (defined($hashref)) { + $value = $hashref->{$key}; + if(!untie_user_hash($hashref)) { + return 'error: ' . ($!+0)." untie (GDBM) Failed"; + } + } + + if(defined($value)) { + &Debug("$user @ $domain is an author"); + } + } else { + return 'error: '.($!+0)." tie (GDBM) Failed"; } 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 -# up a correct public_html +# an author role. If so, creates construction space # Parameters: # request - The request sent to the rolesput subchunk. # We're looking for /domain/_au @@ -5069,16 +8136,15 @@ sub is_author { # 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 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"); + my $path=$perlvar{'lonDocRoot'}."/priv/$domain"; + unless (-e $path) { + mkdir($path); + } + unless (-e $path.'/'.$user) { + mkdir($path.'/'.$user); + } } } @@ -5126,15 +8192,25 @@ sub password_filename { # domain - domain of the user. # name - User's name. # contents - New contents of the file. +# saveold - (optional). If true save old file in a passwd.bak file. # Returns: # 0 - Failed. # 1 - Success. # sub rewrite_password_file { - my ($domain, $user, $contents) = @_; + my ($domain, $user, $contents, $saveold) = @_; my $file = &password_filename($domain, $user); if (defined $file) { + if ($saveold) { + my $bakfile = $file.'.bak'; + if (CopyFile($file,$bakfile)) { + chmod(0400,$bakfile); + &logthis("Old password saved in passwd.bak for internally authenticated user: $user:$domain"); + } else { + &logthis("Failed to save old password in passwd.bak for internally authenticated user: $user:$domain"); + } + } my $pf = IO::File->new(">$file"); if($pf) { print $pf "$contents\n"; @@ -5153,9 +8229,7 @@ sub rewrite_password_file { # Returns the authorization type or nouser if there is no such user. # -sub get_auth_type -{ - +sub get_auth_type { my ($domain, $user) = @_; Debug("get_auth_type( $domain, $user ) \n"); @@ -5191,8 +8265,7 @@ sub get_auth_type # 0 - The domain,user,password triplet is not a valid user. # sub validate_user { - my ($domain, $user, $password) = @_; - + my ($domain, $user, $password, $checkdefauth) = @_; # Why negative ~pi you may well ask? Well this function is about # authentication, and therefore very important to get right. @@ -5215,10 +8288,41 @@ sub validate_user { my $null = pack("C",0); # Used by kerberos auth types. + if ($howpwd eq 'nouser') { + if ($checkdefauth) { + my %domdefaults = &Apache::lonnet::get_domain_defaults($domain); + if ($domdefaults{'auth_def'} eq 'localauth') { + $howpwd = $domdefaults{'auth_def'}; + $contentpwd = $domdefaults{'auth_arg_def'}; + } elsif ((($domdefaults{'auth_def'} eq 'krb4') || + ($domdefaults{'auth_def'} eq 'krb5')) && + ($domdefaults{'auth_arg_def'} ne '')) { + $howpwd = $domdefaults{'auth_def'}; + $contentpwd = $domdefaults{'auth_arg_def'}; + } + } + } if ($howpwd ne 'nouser') { - if($howpwd eq "internal") { # Encrypted is in local password file. - $validated = (crypt($password, $contentpwd) eq $contentpwd); + if (length($contentpwd) == 13) { + $validated = (crypt($password,$contentpwd) eq $contentpwd); + if ($validated) { + my %domdefaults = &Apache::lonnet::get_domain_defaults($domain); + if ($domdefaults{'intauth_switch'}) { + my $ncpass = &hash_passwd($domain,$password); + my $saveold; + if ($domdefaults{'intauth_switch'} == 2) { + $saveold = 1; + } + if (&rewrite_password_file($domain,$user,"$howpwd:$ncpass",$saveold)) { + &update_passwd_history($user,$domain,$howpwd,'conversion'); + &logthis("Validated password hashed with bcrypt for $user:$domain"); + } + } + } + } else { + $validated = &check_internal_passwd($password,$contentpwd,$domain,$user); + } } elsif ($howpwd eq "unix") { # User is a normal unix user. $contentpwd = (getpwnam($user))[1]; @@ -5238,47 +8342,34 @@ sub validate_user { } 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 "krb4") { # user is in kerberos 4 auth. domain. + my $checkwithkrb5 = 0; + if ($dist =~/^fedora(\d+)$/) { + if ($1 > 11) { + $checkwithkrb5 = 1; + } + } elsif ($dist =~ /^suse([\d.]+)$/) { + if ($1 > 11.1) { + $checkwithkrb5 = 1; + } + } + if ($checkwithkrb5) { + $validated = &krb5_authen($password,$null,$user,$contentpwd); + } else { + $validated = &krb4_authen($password,$null,$user,$contentpwd); + } } 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; - } + $validated = &krb5_authen($password,$null,$user,$contentpwd); } elsif ($howpwd eq "localauth") { # Authenticate via installation specific authentcation method: $validated = &localauth::localauth($user, $password, - $contentpwd); + $contentpwd, + $domain); + if ($validated < 0) { + &logthis("localauth for $contentpwd $user:$domain returned a $validated"); + $validated = 0; + } } else { # Unrecognized auth is also bad. $validated = 0; } @@ -5299,13 +8390,115 @@ sub validate_user { return $validated; } +sub check_internal_passwd { + my ($plainpass,$stored,$domain,$user) = @_; + my (undef,$method,@rest) = split(/!/,$stored); + if ($method eq 'bcrypt') { + my $result = &hash_passwd($domain,$plainpass,@rest); + if ($result ne $stored) { + return 0; + } + my %domdefaults = &Apache::lonnet::get_domain_defaults($domain); + if ($domdefaults{'intauth_check'}) { + # Upgrade to a larger number of rounds if necessary + my $defaultcost = $domdefaults{'intauth_cost'}; + if (($defaultcost eq '') || ($defaultcost =~ /D/)) { + $defaultcost = 10; + } + if (int($rest[0])initialize(&Authen::Krb5::parse_name($user.'@' + .$contentpwd)); + my $krbreturn; + if (exists(&Authen::Krb5::get_init_creds_password)) { + $krbreturn = + &Authen::Krb5::get_init_creds_password($krbclient,$password, + $krbservice); + $validated = (ref($krbreturn) eq 'Authen::Krb5::Creds'); + } else { + $krbreturn = + &Authen::Krb5::get_in_tkt_with_password($krbclient,$krbserver, + $password,$credentials); + $validated = ($krbreturn == 1); + } + if (!$validated) { + &logthis('krb5: '.$user.', '.$contentpwd.', '. + &Authen::Krb5::error()); + } + } + 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>) { @@ -5321,39 +8514,51 @@ sub addline { } sub get_chat { - my ($cdom,$cname,$udom,$uname)=@_; - my %hash; - my $proname=&propath($cdom,$cname); + 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 chat_add { - my ($cdom,$cname,$newchat)=@_; - my %hash; - my $proname=&propath($cdom,$cname); + 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'; @@ -5363,21 +8568,22 @@ sub chat_add { $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); } } @@ -5466,7 +8672,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? @@ -5499,7 +8705,9 @@ sub subscribe { # the metadata unless ($fname=~/\.meta$/) { &unsub("$fname.meta",$clientip); } $fname=~s/\/home\/httpd\/html\/res/raw/; - $fname="http://$thisserver/".$fname; + my $protocol = $Apache::lonnet::protocol{$perlvar{'lonHostID'}}; + $protocol = 'http' if ($protocol ne 'https'); + $fname=$protocol.'://'.&Apache::lonnet::hostname($perlvar{'lonHostID'})."/".$fname; $result="$fname\n"; } } else { @@ -5541,26 +8749,26 @@ sub change_unix_password { sub make_passwd_file { - my ($uname, $umode,$npass,$passfilename)=@_; - my $result="ok\n"; + my ($uname,$udom,$umode,$npass,$passfilename,$action)=@_; + my $result="ok"; if ($umode eq 'krb4' or $umode eq 'krb5') { { my $pf = IO::File->new(">$passfilename"); if ($pf) { print $pf "$umode:$npass\n"; + &update_passwd_history($uname,$udom,$umode,$action); } else { $result = "pass_file_failed_error"; } } } elsif ($umode eq 'internal') { - my $salt=time; - $salt=substr($salt,6,2); - my $ncpass=crypt($npass,$salt); + my $ncpass = &hash_passwd($udom,$npass); { &Debug("Creating internal auth"); my $pf = IO::File->new(">$passfilename"); if($pf) { - print $pf "internal:$ncpass\n"; + print $pf "internal:$ncpass\n"; + &update_passwd_history($uname,$udom,$umode,$action); } else { $result = "pass_file_failed_error"; } @@ -5570,60 +8778,14 @@ sub make_passwd_file { my $pf = IO::File->new(">$passfilename"); if($pf) { print $pf "localauth:$npass\n"; + &update_passwd_history($uname,$udom,$umode,$action); } else { $result = "pass_file_failed_error"; } } } elsif ($umode eq 'unix') { - { - # - # Don't allow the creation of privileged accounts!!! that would - # be real bad!!! - # - my $uid = getpwnam($uname); - if((defined $uid) && ($uid == 0)) { - &logthis(">>>Attempted to create privilged account blocked"); - return "no_priv_account_error\n"; - } - - my $execpath ="$perlvar{'lonDaemons'}/"."lcuseradd"; - - my $lc_error_file = $execdir."/tmp/lcuseradd".$$.".status"; - { - &Debug("Executing external: ".$execpath); - &Debug("user = ".$uname.", Password =". $npass); - my $se = IO::File->new("|$execpath > $perlvar{'lonDaemons'}/logs/lcuseradd.log"); - print $se "$uname\n"; - print $se "$npass\n"; - print $se "$npass\n"; - print $se "$lc_error_file\n"; # Status -> unique file. - } - 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"; - } - } + &logthis(">>>Attempt to create unix account blocked -- unix auth not available for new users."); + $result="no_new_unix_accounts"; } elsif ($umode eq 'none') { { my $pf = IO::File->new("> $passfilename"); @@ -5633,8 +8795,16 @@ sub make_passwd_file { $result = "pass_file_failed_error"; } } + } elsif ($umode eq 'lti') { + my $pf = IO::File->new(">$passfilename"); + if($pf) { + print $pf "lti:\n"; + &update_passwd_history($uname,$udom,$umode,$action); + } else { + $result = "pass_file_failed_error"; + } } else { - $result="auth_mode_error\n"; + $result="auth_mode_error"; } return $result; } @@ -5647,11 +8817,18 @@ sub convert_photo { 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}) { + if (&Apache::lonnet::get_host_ip($perlvar{'lonHostID'}) + eq &Apache::lonnet::get_host_ip($hostid)) { $currenthostid =$hostid; - $currentdomainid=$hostdom{$hostid}; - &logthis("Setting hostid to $hostid, and domain to $currentdomainid"); + $currentdomainid=&Apache::lonnet::host_domain($hostid); + &set_client_info(); +# &logthis("Setting hostid to $hostid, and domain to $currentdomainid"); } else { &logthis("Requested host id $hostid not an alias of ". $perlvar{'lonHostID'}." refusing connection"); @@ -5666,95 +8843,95 @@ sub version { return "version:$VERSION"; } -#There is a copy of this in lonnet.pm -sub userload { - my $numusers=0; - { - opendir(LONIDS,$perlvar{'lonIDsDir'}); - my $filename; - my $curtime=time; - while ($filename=readdir(LONIDS)) { - if ($filename eq '.' || $filename eq '..') {next;} - my ($mtime)=(stat($perlvar{'lonIDsDir'}.'/'.$filename))[9]; - if ($curtime-$mtime < 1800) { $numusers++; } - } - closedir(LONIDS); - } - my $userloadpercent=0; - my $maxuserload=$perlvar{'lonUserLoadLim'}; - if ($maxuserload) { - $userloadpercent=100*$numusers/$maxuserload; - } - $userloadpercent=sprintf("%.2f",$userloadpercent); - 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; +sub get_usersession_config { + my ($dom,$name) = @_; + my ($usersessionconf,$cached)=&Apache::lonnet::is_cached_new($name,$dom); + if (defined($cached)) { + return $usersessionconf; + } else { + my %domconfig = &Apache::lonnet::get_dom('configuration',['usersessions'],$dom); + &Apache::lonnet::do_cache_new($name,$dom,$domconfig{'usersessions'},3600); + return $domconfig{'usersessions'}; + } + return; +} + +sub get_usersearch_config { + my ($dom,$name) = @_; + my ($usersearchconf,$cached)=&Apache::lonnet::is_cached_new($name,$dom); + if (defined($cached)) { + return $usersearchconf; + } else { + my %domconfig = &Apache::lonnet::get_dom('configuration',['directorysrch'],$dom); + &Apache::lonnet::do_cache_new($name,$dom,$domconfig{'directorysrch'},600); + return $domconfig{'directorysrch'}; + } + return; +} + +sub get_prohibited { + my ($dom) = @_; + my $name = 'trust'; + my ($trustconfig,$cached)=&Apache::lonnet::is_cached_new($name,$dom); + unless (defined($cached)) { + my %domconfig = &Apache::lonnet::get_dom('configuration',['trust'],$dom); + &Apache::lonnet::do_cache_new($name,$dom,$domconfig{'trust'},3600); + $trustconfig = $domconfig{'trust'}; + } + my %prohibited; + if (ref($trustconfig)) { + foreach my $prefix (keys(%{$trustconfig})) { + if (ref($trustconfig->{$prefix}) eq 'HASH') { + my $reject; + if (ref($trustconfig->{$prefix}->{'exc'}) eq 'ARRAY') { + if (grep(/^\Q$clientintdom\E$/,@{$trustconfig->{$prefix}->{'exc'}})) { + $reject = 1; + } + } + if (ref($trustconfig->{$prefix}->{'inc'}) eq 'ARRAY') { + if (grep(/^\Q$clientintdom\E$/,@{$trustconfig->{$prefix}->{'inc'}})) { + $reject = 0; + } else { + $reject = 1; + } + } + if ($reject) { + $prohibited{$prefix} = 1; + } + } + } + } + return %prohibited; +} + +sub get_remote_hostable { + my ($dom) = @_; + my $result; + if ($clientintdom) { + $result = 1; + my $remsessconf = &get_usersession_config($dom,'remotesession'); + if (ref($remsessconf) eq 'HASH') { + if (ref($remsessconf->{'remote'}) eq 'HASH') { + if (ref($remsessconf->{'remote'}->{'excludedomain'}) eq 'ARRAY') { + if (grep(/^\Q$clientintdom\E$/,@{$remsessconf->{'remote'}->{'excludedomain'}})) { + $result = 0; + } + } + if (ref($remsessconf->{'remote'}->{'includedomain'}) eq 'ARRAY') { + if (grep(/^\Q$clientintdom\E$/,@{$remsessconf->{'remote'}->{'includedomain'}})) { + $result = 1; + } else { + $result = 0; + } + } + } + } + } + return $result; +} + +sub distro_and_arch { + return $dist.':'.$arch; } # ----------------------------------- POD (plain old documentation, CPAN style) @@ -5936,7 +9113,7 @@ Allow for a password to be set. Make a user. -=item passwd +=item changeuserauth Allow for authentication mechanism and password to be changed. @@ -5964,7 +9141,7 @@ Place in B stores hash in namespace -=item rolesputy +=item rolesput put a role into a user's environment @@ -6025,6 +9202,10 @@ for each student, defined perhaps by the Returns usernames corresponding to IDs. (These "IDs" are unique identifiers for each student, defined perhaps by the institutional Registrar.) +=item iddel + +Deletes one or more ids in a domain's id database. + =item tmpput Accept and store information in temporary space. @@ -6072,16 +9253,16 @@ to the client, and the connection is clo IO::Socket IO::File Apache::File -Symbol POSIX Crypt::IDEA -LWP::UserAgent() GDBM_File Authen::Krb4 Authen::Krb5 =head1 COREQUISITES +none + =head1 OSNAMES linux @@ -6091,3 +9272,408 @@ linux Server/Process =cut + + +=pod + +=head1 LOG MESSAGES + +The messages below can be emitted in the lond log. This log is located +in ~httpd/perl/logs/lond.log Many log messages have HTML encapsulation +to provide coloring if examined from inside a web page. Some do not. +Where color is used, the colors are; Red for sometihhng to get excited +about and to follow up on. Yellow for something to keep an eye on to +be sure it does not get worse, Green,and Blue for informational items. + +In the discussions below, sometimes reference is made to ~httpd +when describing file locations. There isn't really an httpd +user, however there is an httpd directory that gets installed in the +place that user home directories go. On linux, this is usually +(always?) /home/httpd. + + +Some messages are colorless. These are usually (not always) +Green/Blue color level messages. + +=over 2 + +=item (Red) LocalConnection rejecting non local: ne 127.0.0.1 + +A local connection negotiation was attempted by +a host whose IP address was not 127.0.0.1. +The socket is closed and the child will exit. +lond has three ways to establish an encyrption +key with a client: + +=over 2 + +=item local + +The key is written and read from a file. +This is only valid for connections from localhost. + +=item insecure + +The key is generated by the server and +transmitted to the client. + +=item ssl (secure) + +An ssl connection is negotiated with the client, +the key is generated by the server and sent to the +client across this ssl connection before the +ssl connectionis terminated and clear text +transmission resumes. + +=back + +=item (Red) LocalConnection: caller is insane! init = and type = + +The client is local but has not sent an initialization +string that is the literal "init:local" The connection +is closed and the child exits. + +=item Red CRITICAL Can't get key file + +SSL key negotiation is being attempted but the call to +lonssl::KeyFile failed. This usually means that the +configuration file is not correctly defining or protecting +the directories/files lonCertificateDirectory or +lonnetPrivateKey + is a string that describes the reason that +the key file could not be located. + +=item (Red) CRITICAL Can't get certificates + +SSL key negotiation failed because we were not able to retrives our certificate +or the CA's certificate in the call to lonssl::CertificateFile + is the textual reason this failed. Usual reasons: + +=over 2 + +=item Apache config file for loncapa incorrect: + +one of the variables +lonCertificateDirectory, lonnetCertificateAuthority, or lonnetCertificate +undefined or incorrect + +=item Permission error: + +The directory pointed to by lonCertificateDirectory is not readable by lond + +=item Permission error: + +Files in the directory pointed to by lonCertificateDirectory are not readable by lond. + +=item Installation error: + +Either the certificate authority file or the certificate have not +been installed in lonCertificateDirectory. + +=item (Red) CRITICAL SSL Socket promotion failed: + +The promotion of the connection from plaintext to SSL failed + is the reason for the failure. There are two +system calls involved in the promotion (one of which failed), +a dup to produce +a second fd on the raw socket over which the encrypted data +will flow and IO::SOcket::SSL->new_from_fd which creates +the SSL connection on the duped fd. + +=item (Blue) WARNING client did not respond to challenge + +This occurs on an insecure (non SSL) connection negotiation request. +lond generates some number from the time, the PID and sends it to +the client. The client must respond by echoing this information back. +If the client does not do so, that's a violation of the challenge +protocols and the connection will be failed. + +=item (Red) No manager table. Nobody can manage!! + +lond has the concept of privileged hosts that +can perform remote management function such +as update the hosts.tab. The manager hosts +are described in the +~httpd/lonTabs/managers.tab file. +this message is logged if this file is missing. + + +=item (Green) Registering manager as with + +Reports the successful parse and registration +of a specific manager. + +=item Green existing host + +The manager host is already defined in the hosts.tab +the information in that table, rather than the info in the +manager table will be used to determine the manager's ip. + +=item (Red) Unable to craete + +lond has been asked to create new versions of an administrative +file (by a manager). When this is done, the new file is created +in a temp file and then renamed into place so that there are always +usable administrative files, even if the update fails. This failure +message means that the temp file could not be created. +The update is abandoned, and the old file is available for use. + +=item (Green) CopyFile from to failed + +In an update of administrative files, the copy of the existing file to a +backup file failed. The installation of the new file may still succeed, +but there will not be a back up file to rever to (this should probably +be yellow). + +=item (Green) Pushfile: backed up to + +See above, the backup of the old administrative file succeeded. + +=item (Red) Pushfile: Unable to install + +The new administrative file could not be installed. In this case, +the old administrative file is still in use. + +=item (Green) Installed new < filename>. + +The new administrative file was successfullly installed. + +=item (Red) Reinitializing lond pid= + +The lonc child process will be sent a USR2 +signal. + +=item (Red) Reinitializing self + +We've been asked to re-read our administrative files,and +are doing so. + +=item (Yellow) error:Invalid process identifier + +A reinit command was received, but the target part of the +command was not valid. It must be either +'lond' or 'lonc' but was + +=item (Green) isValideditCommand checking: Command = Key = newline = + +Checking to see if lond has been handed a valid edit +command. It is possible the edit command is not valid +in that case there are no log messages to indicate that. + +=item Result of password change for pwchange_success + +The password for was +successfully changed. + +=item Unable to open passwd to change password + +Could not rewrite the +internal password file for a user + +=item Result of password change for : + +A unix password change for was attempted +and the pipe returned + +=item LWP GET: for () + +The lightweight process fetch for a resource failed +with the local filename that should +have existed/been created was the +corresponding URI: This is emitted in several +places. + +=item Unable to move to + +From fetch_user_file_handler - the user file was replicated but could not +be mv'd to its final location. + +=item Looking for + +From user_has_session_handler - This should be a Debug call instead +it indicates lond is about to check whether the specified user has a +session active on the specified domain on the local host. + +=item Client () hanging up: + +lond has been asked to exit by its client. The and identify the +client systemand is the full exit command sent to the server. + +=item Red CRITICAL: ABNORMAL EXIT. child for server died through a crass with this error->[]. + +A lond child terminated. NOte that this termination can also occur when the +child receives the QUIT or DIE signals. is the process id of the child, + the host lond is working for, and the reason the child died +to the best of our ability to get it (I would guess that any numeric value +represents and errno value). This is immediately followed by + +=item Famous last words: Catching exception - + +Where log is some recent information about the state of the child. + +=item Red CRITICAL: TIME OUT + +Some timeout occured for server . THis is normally a timeout on an LWP +doing an HTTP::GET. + +=item child died + +The reaper caught a SIGCHILD for the lond child process +This should be modified to also display the IP of the dying child +$children{$pid} + +=item Unknown child 0 died +A child died but the wait for it returned a pid of zero which really should not +ever happen. + +=item Child - looks like we missed it's death + +When a sigchild is received, the reaper process checks all children to see if they are +alive. If children are dying quite quickly, the lack of signal queuing can mean +that a signal hearalds the death of more than one child. If so this message indicates +which other one died. is the ip of a dead child + +=item Free socket: + +The HUNTSMAN sub was called due to a SIGINT in a child process. The socket is being shutdown. +for whatever reason, is printed but in fact shutdown() is not documented +to return anything. This is followed by: + +=item Red CRITICAL: Shutting down + +Just prior to exit. + +=item Free socket: + +The HUPSMAN sub was called due to a SIGHUP. all children get killsed, and lond execs itself. +This is followed by: + +=item (Red) CRITICAL: Restarting + +lond is about to exec itself to restart. + +=item (Blue) Updating connections + +(In response to a USR2). All the children (except the one for localhost) +are about to be killed, the hosts tab reread, and Apache reloaded via apachereload. + +=item (Blue) UpdateHosts killing child for ip + +Due to USR2 as above. + +=item (Green) keeping child for ip (pid = ) + +In response to USR2 as above, the child indicated is not being restarted because +it's assumed that we'll always need a child for the localhost. + + +=item Going to check on the children + +Parent is about to check on the health of the child processes. +Note that this is in response to a USR1 sent to the parent lond. +there may be one or more of the next two messages: + +=item is dead + +A child that we have in our child hash as alive has evidently died. + +=item Child did not respond + +In the health check the child did not update/produce a pid_.txt +file when sent it's USR1 signal. That process is killed with a 9 signal, as it's +assumed to be hung in some un-fixable way. + +=item Finished checking children + +Master processs's USR1 processing is cojmplete. + +=item (Red) CRITICAL: ------- Starting ------ + +(There are more '-'s on either side). Lond has forked itself off to +form a new session and is about to start actual initialization. + +=item (Green) Attempting to start child () + +Started a new child process for . Client is IO::Socket object +connected to the child. This was as a result of a TCP/IP connection from a client. + +=item Unable to determine who caller was, getpeername returned nothing + +In child process initialization. either getpeername returned undef or +a zero sized object was returned. Processing continues, but in my opinion, +this should be cause for the child to exit. + +=item Unable to determine clientip + +In child process initialization. The peer address from getpeername was not defined. +The client address is stored as "Unavailable" and processing continues. + +=item (Yellow) INFO: Connection connection type = + +In child initialization. A good connectionw as received from . + +=over 2 + +=item + +is the name of the client from hosts.tab. + +=item + +Is the connection type which is either + +=over 2 + +=item manager + +The connection is from a manager node, not in hosts.tab + +=item client + +the connection is from a non-manager in the hosts.tab + +=item both + +The connection is from a manager in the hosts.tab. + +=back + +=back + +=item (Blue) Certificates not installed -- trying insecure auth + +One of the certificate file, key file or +certificate authority file could not be found for a client attempting +SSL connection intiation. COnnection will be attemptied in in-secure mode. +(this would be a system with an up to date lond that has not gotten a +certificate from us). + +=item (Green) Successful local authentication + +A local connection successfully negotiated the encryption key. +In this case the IDEA key is in a file (that is hopefully well protected). + +=item (Green) Successful ssl authentication with + +The client ( is the peer's name in hosts.tab), has successfully +negotiated an SSL connection with this child process. + +=item (Green) Successful insecure authentication with + + +The client has successfully negotiated an insecure connection withthe child process. + +=item (Yellow) Attempted insecure connection disallowed + +The client attempted and failed to successfully negotiate a successful insecure +connection. This can happen either because the variable londAllowInsecure is false +or undefined, or becuse the child did not successfully echo back the challenge +string. + + +=back + +=back + + +=cut