--- loncom/lond 2004/06/01 09:58:30 1.192
+++ loncom/lond 2004/07/03 18:49:42 1.205
@@ -2,7 +2,7 @@
# The LearningOnline Network
# lond "LON Daemon" Server (port "LOND" 5663)
#
-# $Id: lond,v 1.192 2004/06/01 09:58:30 foxr Exp $
+# $Id: lond,v 1.205 2004/07/03 18:49:42 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -45,25 +45,31 @@ use Authen::Krb4;
use Authen::Krb5;
use lib '/home/httpd/lib/perl/';
use localauth;
+use localenroll;
use File::Copy;
use LONCAPA::ConfigFileEdit;
+use LONCAPA::lonlocal;
+use LONCAPA::lonssl;
my $DEBUG = 0; # Non zero to enable debug log entries.
my $status='';
my $lastlog='';
-my $VERSION='$Revision: 1.192 $'; #' stupid emacs
+my $VERSION='$Revision: 1.205 $'; #' stupid emacs
my $remoteVERSION;
my $currenthostid;
my $currentdomainid;
my $client;
-my $clientip;
-my $clientname;
+my $clientip; # IP address of client.
+my $clientdns; # DNS name of client.
+my $clientname; # LonCAPA name of client.
my $server;
-my $thisserver;
+my $thisserver; # DNS of us.
+
+my $keymode;
#
# Connection type is:
@@ -74,9 +80,10 @@ my $thisserver;
my $ConnectionType;
-my %hostid;
-my %hostdom;
-my %hostip;
+my %hostid; # ID's for hosts in cluster by ip.
+my %hostdom; # LonCAPA domain for hosts in cluster.
+my %hostip; # IPs for hosts in cluster.
+my %hostdns; # ID's of hosts looked up by DNS name.
my %managers; # Ip -> manager names
@@ -120,6 +127,178 @@ my @adderrors = ("ok",
"lcuseradd Password mismatch");
+#------------------------------------------------------------------------
+#
+# LocalConnection
+# Completes the formation of a locally authenticated connection.
+# This function will ensure that the 'remote' client is really the
+# local host. If not, the connection is closed, and the function fails.
+# If so, initcmd is parsed for the name of a file containing the
+# IDEA session key. The fie is opened, read, deleted and the session
+# key returned to the caller.
+#
+# Parameters:
+# $Socket - Socket open on client.
+# $initcmd - The full text of the init command.
+#
+# Implicit inputs:
+# $clientdns - The DNS name of the remote client.
+# $thisserver - Our DNS name.
+#
+# Returns:
+# IDEA session key on success.
+# undef on failure.
+#
+sub LocalConnection {
+ my ($Socket, $initcmd) = @_;
+ Debug("Attempting local connection: $initcmd client: $clientdns me: $thisserver");
+ if($clientdns ne $thisserver) {
+ &logthis(' LocalConnection rejecting non local: '
+ ."$clientdns ne $thisserver ");
+ close $Socket;
+ return undef;
+ }
+ else {
+ chomp($initcmd); # Get rid of \n in filename.
+ my ($init, $type, $name) = split(/:/, $initcmd);
+ Debug(" Init command: $init $type $name ");
+
+ # Require that $init = init, and $type = local: Otherwise
+ # the caller is insane:
+
+ if(($init ne "init") && ($type ne "local")) {
+ &logthis(' LocalConnection: caller is insane! '
+ ."init = $init, and type = $type ");
+ close($Socket);;
+ return undef;
+
+ }
+ # Now get the key filename:
+
+ my $IDEAKey = lonlocal::ReadKeyFile($name);
+ return $IDEAKey;
+ }
+}
+#------------------------------------------------------------------------------
+#
+# SSLConnection
+# Completes the formation of an ssh authenticated connection. The
+# socket is promoted to an ssl socket. If this promotion and the associated
+# certificate exchange are successful, the IDEA key is generated and sent
+# to the remote peer via the SSL tunnel. The IDEA key is also returned to
+# the caller after the SSL tunnel is torn down.
+#
+# Parameters:
+# Name Type Purpose
+# $Socket IO::Socket::INET Plaintext socket.
+#
+# Returns:
+# IDEA key on success.
+# undef on failure.
+#
+sub SSLConnection {
+ my $Socket = shift;
+
+ Debug("SSLConnection: ");
+ my $KeyFile = lonssl::KeyFile();
+ if(!$KeyFile) {
+ my $err = lonssl::LastError();
+ &logthis(" CRITICAL"
+ ."Can't get key file $err ");
+ return undef;
+ }
+ my ($CACertificate,
+ $Certificate) = lonssl::CertificateFile();
+
+
+ # If any of the key, certificate or certificate authority
+ # certificate filenames are not defined, this can't work.
+
+ if((!$Certificate) || (!$CACertificate)) {
+ my $err = lonssl::LastError();
+ &logthis(" CRITICAL"
+ ."Can't get certificates: $err ");
+
+ return undef;
+ }
+ Debug("Key: $KeyFile CA: $CACertificate Cert: $Certificate");
+
+ # Indicate to our peer that we can procede with
+ # a transition to ssl authentication:
+
+ print $Socket "ok:ssl\n";
+
+ Debug("Approving promotion -> ssl");
+ # And do so:
+
+ my $SSLSocket = lonssl::PromoteServerSocket($Socket,
+ $CACertificate,
+ $Certificate,
+ $KeyFile);
+ if(! ($SSLSocket) ) { # SSL socket promotion failed.
+ my $err = lonssl::LastError();
+ &logthis(" CRITICAL "
+ ."SSL Socket promotion failed: $err ");
+ return undef;
+ }
+ Debug("SSL Promotion successful");
+
+ #
+ # The only thing we'll use the socket for is to send the IDEA key
+ # to the peer:
+
+ my $Key = lonlocal::CreateCipherKey();
+ print $SSLSocket "$Key\n";
+
+ lonssl::Close($SSLSocket);
+
+ Debug("Key exchange complete: $Key");
+
+ return $Key;
+}
+#
+# InsecureConnection:
+# If insecure connections are allowd,
+# exchange a challenge with the client to 'validate' the
+# client (not really, but that's the protocol):
+# We produce a challenge string that's sent to the client.
+# The client must then echo the challenge verbatim to us.
+#
+# Parameter:
+# Socket - Socket open on the client.
+# Returns:
+# 1 - success.
+# 0 - failure (e.g.mismatch or insecure not allowed).
+#
+sub InsecureConnection {
+ my $Socket = shift;
+
+ # Don't even start if insecure connections are not allowed.
+
+ if(! $perlvar{londAllowInsecure}) { # Insecure connections not allowed.
+ return 0;
+ }
+
+ # Fabricate a challenge string and send it..
+
+ my $challenge = "$$".time; # pid + time.
+ print $Socket "$challenge\n";
+ &status("Waiting for challenge reply");
+
+ my $answer = <$Socket>;
+ $answer =~s/\W//g;
+ if($challenge eq $answer) {
+ return 1;
+ }
+ else {
+ logthis("WARNING client did not respond to challenge");
+ &status("No challenge reqply");
+ return 0;
+ }
+
+
+}
+
#
# GetCertificate: Given a transaction that requires a certificate,
# this function will extract the certificate from the transaction
@@ -175,7 +354,6 @@ sub ReadManagerTable {
while(my $host = ) {
chomp($host);
if ($host =~ "^#") { # Comment line.
- logthis(' Skipping line: '. "$host\n");
next;
}
if (!defined $hostip{$host}) { # This is a non cluster member
@@ -350,6 +528,8 @@ sub InstallFile {
return 1;
}
+
+
#
# ConfigFileFromSelector: converts a configuration file selector
# (one of host or domain at this point) into a
@@ -863,7 +1043,7 @@ sub HUPSMAN { # sig
#
# Kill off hashes that describe the host table prior to re-reading it.
# Hashes affected are:
-# %hostid, %hostdom %hostip
+# %hostid, %hostdom %hostip %hostdns.
#
sub KillHostHashes {
foreach my $key (keys %hostid) {
@@ -875,6 +1055,9 @@ sub KillHostHashes {
foreach my $key (keys %hostip) {
delete $hostip{$key};
}
+ foreach my $key (keys %hostdns) {
+ delete $hostdns{$key};
+ }
}
#
# Read in the host table from file and distribute it into the various hashes:
@@ -885,15 +1068,21 @@ sub KillHostHashes {
sub ReadHostTable {
open (CONFIG,"$perlvar{'lonTabDir'}/hosts.tab") || die "Can't read host file";
-
+ my $myloncapaname = $perlvar{'lonHostID'};
+ Debug("My loncapa name is : $myloncapaname");
while (my $configline=) {
if (!($configline =~ /^\s*\#/)) {
my ($id,$domain,$role,$name,$ip)=split(/:/,$configline);
chomp($ip); $ip=~s/\D+$//;
- $hostid{$ip}=$id;
- $hostdom{$id}=$domain;
- $hostip{$id}=$ip;
- if ($id eq $perlvar{'lonHostID'}) { $thisserver=$name; }
+ $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);
@@ -1029,13 +1218,14 @@ sub logstatus {
my $docdir=$perlvar{'lonDocRoot'};
{
my $fh=IO::File->new(">>$docdir/lon-status/londstatus.txt");
- print $fh $$."\t".$clientname."\t".$currenthostid."\t".$status."\t".$lastlog."\n";
+ print $fh $$."\t".$clientname."\t".$currenthostid."\t"
+ .$status."\t".$lastlog."\t $keymode\n";
$fh->close();
}
&status("Finished londstatus.txt");
{
my $fh=IO::File->new(">$docdir/lon-status/londchld/$$.txt");
- print $fh $status."\n".$lastlog."\n".time;
+ print $fh $status."\n".$lastlog."\n".time."\n$keymode";
$fh->close();
}
&status("Finished logging");
@@ -1264,9 +1454,12 @@ sub make_new_child {
&logthis("Unable to determine who caller was, getpeername returned nothing");
}
if (defined($iaddr)) {
- $clientip=inet_ntoa($iaddr);
+ $clientip = inet_ntoa($iaddr);
+ Debug("Connected with $clientip");
+ $clientdns = gethostbyaddr($iaddr, AF_INET);
+ Debug("Connected with $clientdns by name");
} else {
- &logthis("Unable to determine clinetip");
+ &logthis("Unable to determine clientip");
$clientip='Unavailable';
}
@@ -1300,7 +1493,7 @@ sub make_new_child {
# =============================================================================
# do something with the connection
# -----------------------------------------------------------------------------
- # see if we know client and check for spoof IP by challenge
+ # see if we know client and 'check' for spoof IP by ineffective challenge
ReadManagerTable; # May also be a manager!!
@@ -1318,6 +1511,7 @@ sub make_new_child {
$clientname = $managers{$clientip};
}
my $clientok;
+
if ($clientrec || $ismanager) {
&status("Waiting for init from $clientip $clientname");
&logthis('INFO: Connection, '.
@@ -1325,22 +1519,81 @@ sub make_new_child {
" ($clientname) connection type = $ConnectionType " );
&status("Connecting $clientip ($clientname))");
my $remotereq=<$client>;
- $remotereq=~s/[^\w:]//g;
+ chomp($remotereq);
+ Debug("Got init: $remotereq");
+ my $inikeyword = split(/:/, $remotereq);
if ($remotereq =~ /^init/) {
&sethost("sethost:$perlvar{'lonHostID'}");
- my $challenge="$$".time;
- print $client "$challenge\n";
- &status(
- "Waiting for challenge reply from $clientip ($clientname)");
- $remotereq=<$client>;
- $remotereq=~s/\W//g;
- if ($challenge eq $remotereq) {
- $clientok=1;
- print $client "ok\n";
+ #
+ # If the remote is attempting a local init... give that a try:
+ #
+ my ($i, $inittype) = split(/:/, $remotereq);
+
+ # If the connection type is ssl, but I didn't get my
+ # certificate files yet, then I'll drop back to
+ # insecure (if allowed).
+
+ if($inittype eq "ssl") {
+ my ($ca, $cert) = lonssl::CertificateFile;
+ my $kfile = lonssl::KeyFile;
+ if((!$ca) ||
+ (!$cert) ||
+ (!$kfile)) {
+ $inittype = ""; # This forces insecure attempt.
+ &logthis(" Certificates not "
+ ."installed -- trying insecure auth");
+ }
+ else { # SSL certificates are in place so
+ } # Leave the inittype alone.
+ }
+
+ if($inittype eq "local") {
+ my $key = LocalConnection($client, $remotereq);
+ if($key) {
+ Debug("Got local key $key");
+ $clientok = 1;
+ my $cipherkey = pack("H32", $key);
+ $cipher = new IDEA($cipherkey);
+ print $client "ok:local\n";
+ &logthis('");
+ $keymode = "local"
+ } else {
+ Debug("Failed to get local key");
+ $clientok = 0;
+ shutdown($client, 3);
+ close $client;
+ }
+ } elsif ($inittype eq "ssl") {
+ my $key = SSLConnection($client);
+ if ($key) {
+ $clientok = 1;
+ my $cipherkey = pack("H32", $key);
+ $cipher = new IDEA($cipherkey);
+ &logthis(''
+ ."Successfull ssl authentication with $clientname ");
+ $keymode = "ssl";
+
+ } else {
+ $clientok = 0;
+ close $client;
+ }
+
} else {
- &logthis(
- "WARNING: $clientip did not reply challenge");
- &status('No challenge reply '.$clientip);
+ my $ok = InsecureConnection($client);
+ if($ok) {
+ $clientok = 1;
+ &logthis(''
+ ."Successful insecure authentication with $clientname ");
+ print $client "ok\n";
+ $keymode = "insecure";
+ } else {
+ &logthis(''
+ ."Attempted insecure connection disallowed ");
+ close $client;
+ $clientok = 0;
+
+ }
}
} else {
&logthis(
@@ -1348,11 +1601,13 @@ sub make_new_child {
."$clientip failed to initialize: >$remotereq< ");
&status('No init '.$clientip);
}
+
} else {
&logthis(
"WARNING: Unknown client $clientip");
&status('Hung up on '.$clientip);
}
+
if ($clientok) {
# ---------------- New known client connecting, could mean machine online again
@@ -2532,7 +2787,7 @@ sub make_new_child {
}
# ------------------------------------------------------------------- querysend
} elsif ($userinput =~ /^querysend/) {
- if(isClient) {
+ if (isClient) {
my ($cmd,$query,
$arg1,$arg2,$arg3)=split(/\:/,$userinput);
$query=~s/\n*$//g;
@@ -2580,8 +2835,8 @@ sub make_new_child {
my %hash;
if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_WRCREAT(),0640)) {
foreach my $pair (@pairs) {
- my ($key,$value)=split(/=/,$pair);
- $hash{$key}=$value.':'.$now;
+ my ($key,$descr,$inst_code)=split(/=/,$pair);
+ $hash{$key}=$descr.':'.$inst_code.':'.$now;
}
if (untie(%hash)) {
print $client "ok\n";
@@ -2616,14 +2871,19 @@ sub make_new_child {
my %hash;
if (tie(%hash,'GDBM_File',"$proname.db",&GDBM_READER(),0640)) {
while (my ($key,$value) = each(%hash)) {
- my ($descr,$lasttime)=split(/\:/,$value);
+ my ($descr,$lasttime,$inst_code);
+ if ($value =~ m/^([^\:]*):([^\:]*):(\d+)$/) {
+ ($descr,$inst_code,$lasttime)=($1,$2,$3);
+ } else {
+ ($descr,$lasttime) = split(/\:/,$value);
+ }
if ($lasttime<$since) { next; }
if ($description eq '.') {
- $qresult.=$key.'='.$descr.'&';
+ $qresult.=$key.'='.$descr.':'.$inst_code.'&';
} else {
my $unescapeVal = &unescape($descr);
if (eval('$unescapeVal=~/\Q$description\E/i')) {
- $qresult.="$key=$descr&";
+ $qresult.=$key.'='.$descr.':'.$inst_code.'&';
}
}
}
@@ -2777,6 +3037,24 @@ sub make_new_child {
Reply($client, "refused\n", $userinput);
}
+# ----------------------------------------- portfolio directory list (portls)
+ } elsif ($userinput =~ /^portls/) {
+ if(isClient) {
+ my ($cmd,$uname,$udom)=split(/:/,$userinput);
+ my $udir=propath($udom,$uname).'/userfiles/portfolio';
+ my $dirLine='';
+ my $dirContents='';
+ if (opendir(LSDIR,$udir.'/')){
+ while ($dirLine = readdir(LSDIR)){
+ $dirContents = $dirContents.$dirLine.'
';
+ }
+ } else {
+ $dirContents = "No directory found\n";
+ }
+ print $client $dirContents."\n";
+ } else {
+ Reply($client, "refused\n", $userinput);
+ }
# -------------------------------------------------------------------------- ls
} elsif ($userinput =~ /^ls/) {
if(isClient) {
@@ -2863,6 +3141,105 @@ sub make_new_child {
} else {
print $client "refused\n";
}
+#------------------------------- is auto-enrollment enabled?
+ } elsif ($userinput =~/^autorun:/) {
+ if (isClient) {
+ my ($cmd,$cdom) = split(/:/,$userinput);
+ my $outcome = &localenroll::run($cdom);
+ print $client "$outcome\n";
+ } else {
+ print $client "0\n";
+ }
+#------------------------------- get official sections (for auto-enrollment).
+ } elsif ($userinput =~/^autogetsections:/) {
+ if (isClient) {
+ my ($cmd,$coursecode,$cdom)=split(/:/,$userinput);
+ my @secs = &localenroll::get_sections($coursecode,$cdom);
+ my $seclist = &escape(join(':',@secs));
+ print $client "$seclist\n";
+ } else {
+ print $client "refused\n";
+ }
+#----------------------- validate owner of new course section (for auto-enrollment).
+ } elsif ($userinput =~/^autonewcourse:/) {
+ if (isClient) {
+ my ($cmd,$inst_course_id,$owner,$cdom)=split(/:/,$userinput);
+ my $outcome = &localenroll::new_course($inst_course_id,$owner,$cdom);
+ print $client "$outcome\n";
+ } else {
+ print $client "refused\n";
+ }
+#-------------- validate course section in schedule of classes (for auto-enrollment).
+ } elsif ($userinput =~/^autovalidatecourse:/) {
+ if (isClient) {
+ my ($cmd,$inst_course_id,$cdom)=split(/:/,$userinput);
+ my $outcome=&localenroll::validate_courseID($inst_course_id,$cdom);
+ print $client "$outcome\n";
+ } else {
+ print $client "refused\n";
+ }
+#--------------------------- create password for new user (for auto-enrollment).
+ } elsif ($userinput =~/^autocreatepassword:/) {
+ if (isClient) {
+ my ($cmd,$authparam,$cdom)=split(/:/,$userinput);
+ my ($create_passwd,$authchk);
+ ($authparam,$create_passwd,$authchk) = &localenroll::create_password($authparam,$cdom);
+ print $client &escape($authparam.':'.$create_passwd.':'.$authchk)."\n";
+ } else {
+ print $client "refused\n";
+ }
+#--------------------------- read and remove temporary files (for auto-enrollment).
+ } elsif ($userinput =~/^autoretrieve:/) {
+ if (isClient) {
+ my ($cmd,$filename) = split(/:/,$userinput);
+ my $source = $perlvar{'lonDaemons'}.'/tmp/'.$filename;
+ if ( (-e $source) && ($filename ne '') ) {
+ my $reply = '';
+ if (open(my $fh,$source)) {
+ while (<$fh>) {
+ chomp($_);
+ $_ =~ s/^\s+//g;
+ $_ =~ s/\s+$//g;
+ $reply .= $_;
+ }
+ close($fh);
+ print $client &escape($reply)."\n";
+# unlink($source);
+ } else {
+ print $client "error\n";
+ }
+ } else {
+ print $client "error\n";
+ }
+ } else {
+ print $client "refused\n";
+ }
+#--------------------- read and retrieve institutional code format (for support form).
+ } elsif ($userinput =~/^autoinstcodeformat:/) {
+ if (isClient) {
+ my $reply;
+ my($cmd,$cdom,$course) = split(/:/,$userinput);
+ my @pairs = split/\&/,$course;
+ my %instcodes = ();
+ my %codes = ();
+ my @codetitles = ();
+ my %cat_titles = ();
+ my %cat_order = ();
+ foreach (@pairs) {
+ my ($key,$value) = split/=/,$_;
+ $instcodes{&unescape($key)} = &unescape($value);
+ }
+ my $formatreply = &localenroll::instcode_format($cdom,\%instcodes,\%codes,\@codetitles,\%cat_titles,\%cat_order);
+ if ($formatreply eq 'ok') {
+ my $codes_str = &hash2str(%codes);
+ my $codetitles_str = &array2str(@codetitles);
+ my $cat_titles_str = &hash2str(%cat_titles);
+ my $cat_order_str = &hash2str(%cat_order);
+ print $client $codes_str.':'.$codetitles_str.':'.$cat_titles_str.':'.$cat_order_str."\n";
+ }
+ } else {
+ print $client "refused\n";
+ }
# ------------------------------------------------------------- unknown command
} else {
@@ -2871,7 +3248,7 @@ sub make_new_child {
}
# -------------------------------------------------------------------- complete
alarm(0);
- &status('Listening to '.$clientname);
+ &status('Listening to '.$clientname." ($keymode)");
}
# --------------------------------------------- client unknown or fishy, refuse
} else {
@@ -3230,7 +3607,7 @@ sub sethost {
my (undef,$hostid)=split(/:/,$remotereq);
if (!defined($hostid)) { $hostid=$perlvar{'lonHostID'}; }
if ($hostip{$perlvar{'lonHostID'}} eq $hostip{$hostid}) {
- $currenthostid=$hostid;
+ $currenthostid =$hostid;
$currentdomainid=$hostdom{$hostid};
&logthis("Setting hostid to $hostid, and domain to $currentdomainid");
} else {
@@ -3270,6 +3647,74 @@ sub userload {
return $userloadpercent;
}
+# Routines for serializing arrays and hashes (copies from lonnet)
+
+sub array2str {
+ my (@array) = @_;
+ my $result=&arrayref2str(\@array);
+ $result=~s/^__ARRAY_REF__//;
+ $result=~s/__END_ARRAY_REF__$//;
+ return $result;
+}
+
+sub arrayref2str {
+ my ($arrayref) = @_;
+ my $result='__ARRAY_REF__';
+ foreach my $elem (@$arrayref) {
+ if(ref($elem) eq 'ARRAY') {
+ $result.=&arrayref2str($elem).'&';
+ } elsif(ref($elem) eq 'HASH') {
+ $result.=&hashref2str($elem).'&';
+ } elsif(ref($elem)) {
+ #print("Got a ref of ".(ref($elem))." skipping.");
+ } else {
+ $result.=&escape($elem).'&';
+ }
+ }
+ $result=~s/\&$//;
+ $result .= '__END_ARRAY_REF__';
+ return $result;
+}
+
+sub hash2str {
+ my (%hash) = @_;
+ my $result=&hashref2str(\%hash);
+ $result=~s/^__HASH_REF__//;
+ $result=~s/__END_HASH_REF__$//;
+ return $result;
+}
+
+sub hashref2str {
+ my ($hashref)=@_;
+ my $result='__HASH_REF__';
+ foreach (sort(keys(%$hashref))) {
+ if (ref($_) eq 'ARRAY') {
+ $result.=&arrayref2str($_).'=';
+ } elsif (ref($_) eq 'HASH') {
+ $result.=&hashref2str($_).'=';
+ } elsif (ref($_)) {
+ $result.='=';
+ #print("Got a ref of ".(ref($_))." skipping.");
+ } else {
+ if ($_) {$result.=&escape($_).'=';} else { last; }
+ }
+
+ if(ref($hashref->{$_}) eq 'ARRAY') {
+ $result.=&arrayref2str($hashref->{$_}).'&';
+ } elsif(ref($hashref->{$_}) eq 'HASH') {
+ $result.=&hashref2str($hashref->{$_}).'&';
+ } elsif(ref($hashref->{$_})) {
+ $result.='&';
+ #print("Got a ref of ".(ref($hashref->{$_}))." skipping.");
+ } else {
+ $result.=&escape($hashref->{$_}).'&';
+ }
+ }
+ $result=~s/\&$//;
+ $result .= '__END_HASH_REF__';
+ return $result;
+}
+
# ----------------------------------- POD (plain old documentation, CPAN style)
=head1 NAME