package IdPAccountManager::Tools; ## Copyright (c) GEANT ## This software was developed by RENATER. The research leading to these results has received funding ## from the European Community¹s Seventh Framework Programme (FP7/2007-2013) under grant agreement nº 238875 (GÉANT). use Template; # load Template::Stash to make method tables visible use Template::Stash; use Digest::SHA; use Encode; use POSIX qw(strftime); 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) =head1 LICENSE Copyright (c) GEANT This software was developed by RENATER. The research leading to these results has received funding from the European Community¹s Seventh Framework Programme (FP7/2007-2013) under grant agreement nº 238875 (GÉANT).