package IdPAccountManager::Tools; use Template; # load Template::Stash to make method tables visible use Template::Stash; use Digest::SHA; use Encode; my %log_levels = ('debug' => 0, 'info' => 1, 'trace' => 1, 'notice' => 2, 'error' => 3); INIT { ## a TT2 virtual method to get a variable type $Template::Stash::LIST_OPS->{isa} = sub { my $list = shift; my $type = shift; return 1 if ($type eq 'ARRAY'); return 0; }; $Template::Stash::SCALAR_OPS->{isa} = sub { my $list = shift; my $type = shift; return 1 if ($type eq 'SCALAR'); return 0; }; } # get SHA256 hash for a string sub sha256_hash { my $s = shift; return &Digest::SHA::sha256_base64($s); } # This function generates a random password sub generate_password{ my $length_of_randomstring=10;# the length of # the random string to generate # plusieurs tirages : # 1-tirage des caractères obligatoires : les mettre dans un tableau my @uppers=('A'..'N','P'..'Z'); my @lowers=('a'..'k','m'..'z'); my @punctuation=(':','!','?','&','$','=','-','#'); my @numerics=('0'..'9'); my @rndtab; push(@rndtab,$uppers[rand @uppers]); push(@rndtab,$lowers[rand @lowers]); push(@rndtab,$punctuation[rand @punctuation]); ## Pas de caractères 8bit pour l'antispam push(@rndtab,$numerics[rand @numerics]); # 2-tirage des caractères optionnels : les ajouter au tableau my @chars=('a'..'k','m'..'z','A'..'N','P'..'Z','0'..'9','_','%',';',':','!','?','&','$','*','(',')','{','}','[',']','.','=','-','#'); foreach (6..$length_of_randomstring) { # rand @chars will generate a random # number between 0 and scalar @chars push(@rndtab,$chars[rand @chars]); } # 3-ordonnancement de ceux-ci : les retirer aléatoirement du tableau en les concaténant dans une chaîne my $rndstring=''; my $cpt=1; while($cpt<=$length_of_randomstring) { my $indice = rand @rndtab; $rndstring.=$rndtab[$indice]; splice (@rndtab, $indice, 1); $cpt+=1; } return $rndstring; } ## Updates simpleSamlPhp authsources.php configuration file sub update_ssp_authsources { my $tt2 = Template->new({'INCLUDE_PATH' => $Conf::global{'root_manager_dir'}.':'.$Conf::global{'root_manager_dir'}.'/templates/accountProfiles'}); my %args = ('accounts' => IdPAccountManager::TestAccount::list_test_accounts(), 'conf' => \%Conf::global); #chdir $Conf::global{'root_manager_dir'}; my $template_file = 'templates/accountProfiles/valid-accounts.php.tt2'; my $output_file = 'conf/valid-accounts.php'; #printf "Trace : in=%s, out=%s\n", $template_file, $output_file; unless ($tt2->process($template_file, \%args, $output_file)) { IdPAccountManager::Tools::do_log('error', "Failed to update valid-accounts.php: %s", $tt2->error()); return undef; } } ## Dump a variable's content sub dump_var { my ($var, $level, $fd) = @_; if (ref($var)) { if (ref($var) eq 'ARRAY') { foreach my $index (0..$#{$var}) { print $fd "\t"x$level.$index."\n"; &dump_var($var->[$index], $level+1, $fd); } }elsif (ref($var) eq 'HASH') { foreach my $key (sort keys %{$var}) { print $fd "\t"x$level.'_'.$key.'_'."\n"; &dump_var($var->{$key}, $level+1, $fd); } }else { if (defined $var) { print $fd "\t"x$level."'$var'"."\n"; }else { print $fd "\t"x$level."UNDEF\n"; } } }else { if (defined $var) { print $fd "\t"x$level."'$var'"."\n"; }else { print $fd "\t"x$level."UNDEF\n"; } } } sub do_log { my $level = shift; my $message = shift; unless ($log_levels{$level} < $log_levels{$Conf::global{'log_level'}}) { ## Determine calling function and parameters ## If in 'err' level, build a stack trace my $caller_string; if ($level eq 'error'){ my $go_back = 1; my @calls; while (my @call = caller($go_back)) { unshift @calls, $call[3].'#'.$call[2]; $go_back++; } $caller_string = join(' > ',@calls); }else { my @call = caller(1); $caller_string = $call[3].'()'; } $level = uc($level); my $ip = $ENV{'REMOTE_HOST'}.'/'.$ENV{'REMOTE_ADDR'} || 'NOIP'; my $date = POSIX::strftime ("%Y:%m:%d %H:%M:%S", localtime(time)); my $user = lc($ENV{'mail'}) || 'NOEMAIL'; open LOG, ">>".$Conf::global{'log_file'}; printf LOG "$date - ($level) - $ip - $user - $caller_string $message\n", @_; close LOG; } return 1; } ## Send a mail notice ## Default is to send email to the manager admins, unless other recipients are specified ## mail_notice(IN) ## IN is a HASH with expected entries : ## template : mail template file ## data : data used by the TT2 parser sub mail_notice { my %in = @_; my $tt2_file = $in{'template'}; my $mail_data = $in{'data'}; $mail_data->{'conf'} ||= \%Conf::global; my $notice_email = $in{'to'} || $Conf::global{'admin_email'}; $mail_data->{'to'} = $notice_email; ## Protection to prevent notifications during test dev phases ## Notify only admin_email or dev_sp_contact addresses if ($Conf::global{'dev_no_mail_outside'}) { my %rcpt = map {$_ => 1} split(/,/, $notice_email); my %authorized_rcpt = map {$_ => 1} split(/,/, join(',', $Conf::global{'admin_email'}, $Conf::global{'dev_sp_contact'})); my $change_rcpt = 0; foreach my $email (keys %rcpt) { unless ($authorized_rcpt{$email}) { $change_rcpt = 1; last } } if ($change_rcpt) { &do_log('info',"no_mail_outside option set; notification for %s rerouted to admins ; ", $notice_email); $notice_email = $Conf::global{'admin_email'}; } } &do_log('trace', '(template=%s, to=%s)', $in{'template'}, $mail_data->{'to'}); open SENDMAIL, "|/usr/sbin/sendmail -f ".$Conf::global{'notice_from'}." $notice_email"; my $tt2 = Template->new(FILTERS => {qencode => [\&qencode, 0]}); unless ($tt2->process($tt2_file, $mail_data, \*SENDMAIL)) { &do_log('error', "Error TT2 : %s", $tt2->error()); } close SENDMAIL; } sub qencode { my $string = shift; # We are not able to determine the name of header field, so assume # longest (maybe) one. return MIME::EncWords::encode_mimewords(Encode::decode('utf8', $string), Encoding=>'A', Charset=> 'utf8', Field=>"subject"); } ## usefull to pass parameters to TT2 sub encode_utf8 ($) { my $string = shift||''; return Encode::encode('utf8', $string); } ## Escape characters that may interfer in an XML document sub escape_xml { my $s = shift; $s =~ s/\&/&\;/gm; $s =~ s/\"/"\;/gm; $s =~ s/\</<\;/gm; $s =~ s/\>/>\;/gm; $s =~ s/\'/'/gm; return $s; } ## usefull to pass parameters to TT2 sub escape_quotes { my $string = shift; $string =~ s/\'/\\\'/g; return $string; } ## returns an integer (0 or 1), given an input string ('true' or 'false') sub boolean2integer { my $boolean = shift; if ($boolean eq 'true') { return 1; }elsif ($boolean eq 'false') { return 0; } return undef; } 1; # Magic true value required at end of module __END__ =head1 NAME IdPAccountManager::Tools - Set of subroutines usefull for the Test Account manager =head1 DESCRIPTION The Test Account manager instanciates test accounts associated to a SAML Identity Provider. This module gathers a set of usefull subroutines. =head1 SUBROUTINES/METHODS =over 8 =item C<dump_var ($var, $level, $fd)> Dumps a complex perl data structure. $var is a reference to the variable to dump. $level should be set to 0 (subroutine called recursively). $fd is the file descriptor for the output (default is STDOUT). =item C<do_log ($level, $message)> Write $message to the log file. $level sets the log level (debug, info, trace, notice, error). =item C<encode_utf8 ($string)> Return a UTF8 encoded version of $string. =item C<escape_xml ($string)> Escape XML chars in $string. =item C<generate_password> Returns a random password following some security guidelines. =item C<mail_notice (ARGS)> Send a mail notice. Supported arguments include: =over 12 =item C<template> TT2 mail template to parse. =item C<to> Destination email address. =item C<data> A hashref with parameters used to parse the mail template. =back =item C<qencode ($string)> Retunrs a Q-encoded version of $string. =item C<sha256_hash ($string)> Returns a SHA256 hash for $string. =item C<update_ssp_authsources> Update simpleSAMLphp authsources.php configuration file with the currently valid test accounts. =back =head1 AUTHOR Olivier Salaün (olivier.salaun@renater.fr)