--- loncom/interface/domainprefs.pm 2020/09/17 00:35:04 1.372 +++ loncom/interface/domainprefs.pm 2020/12/18 15:23:02 1.373 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set domain-wide configuration settings # -# $Id: domainprefs.pm,v 1.372 2020/09/17 00:35:04 raeburn Exp $ +# $Id: domainprefs.pm,v 1.373 2020/12/18 15:23:02 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -176,6 +176,7 @@ use Locale::Language; use DateTime::TimeZone; use DateTime::Locale; use Time::HiRes qw( sleep ); +use Net::CIDR; my $registered_cleanup; my $modified_urls; @@ -220,7 +221,7 @@ sub handler { 'coursedefaults','usersessions','loadbalancing', 'requestauthor','selfenrollment','inststatus', 'ltitools','ssl','trust','lti','privacy','passwords', - 'proctoring'],$dom); + 'proctoring','wafproxy'],$dom); my %encconfig = &Apache::lonnet::get_dom('encconfig',['ltitools','lti','proctoring'],$dom); if (ref($domconfig{'ltitools'}) eq 'HASH') { @@ -259,11 +260,11 @@ sub handler { } } } - my @prefs_order = ('rolecolors','login','defaults','passwords','quotas','autoenroll', - 'autoupdate','autocreate','directorysrch','contacts','privacy', - 'usercreation','selfcreation','usermodification','scantron', - 'requestcourses','requestauthor','coursecategories', - 'serverstatuses','helpsettings','coursedefaults', + my @prefs_order = ('rolecolors','login','defaults','wafproxy','passwords','quotas', + 'autoenroll','autoupdate','autocreate','directorysrch', + 'contacts','privacy','usercreation','selfcreation', + 'usermodification','scantron','requestcourses','requestauthor' + 'coursecategories','serverstatuses','helpsettings','coursedefaults', 'ltitools','proctoring','selfenrollment','usersessions','ssl', 'trust','lti'); my %existing; @@ -310,6 +311,17 @@ sub handler { print => \&print_defaults, modify => \&modify_defaults, }, + 'wafproxy' => + { text => 'Web Application Firewall/Reverse Proxy', + help => 'Domain_Configuration_WAF_Proxy', + header => [{col1 => 'Domain server', + col2 => 'Alias for WAF/Reverse Proxy', + }, + {col1 => 'Setting', + col2 => 'Value',}], + print => \&print_wafproxy, + modify => \&modify_wafproxy, + }, 'passwords' => { text => 'Passwords (Internal authentication)', help => 'Domain_Configuration_Passwords', @@ -805,6 +817,8 @@ sub process_changes { $output = &modify_privacy($dom,%domconfig); } elsif ($action eq 'passwords') { $output = &modify_passwords($r,$dom,$confname,$lastactref,%domconfig); + } elsif ($action eq 'wafproxy') { + $output = &modify_wafproxy($dom,$action,$lastactref,%domconfig); } return $output; } @@ -882,7 +896,7 @@ sub print_config_box { ($action eq 'usermodification') || ($action eq 'defaults') || ($action eq 'coursedefaults') || ($action eq 'selfenrollment') || ($action eq 'usersessions') || ($action eq 'ssl') || ($action eq 'directorysrch') || ($action eq 'trust') || ($action eq 'helpsettings') || - ($action eq 'contacts') || ($action eq 'privacy')) { + ($action eq 'contacts') || ($action eq 'privacy') || ($action eq 'wafproxy')) { $output .= $item->{'print'}->('top',$dom,$settings,\$rowtotal); } elsif ($action eq 'passwords') { $output .= $item->{'print'}->('top',$dom,$confname,$settings,\$rowtotal); @@ -1005,7 +1019,7 @@ sub print_config_box { $rowtotal ++; } elsif (($action eq 'usermodification') || ($action eq 'coursedefaults') || ($action eq 'defaults') || ($action eq 'directorysrch') || - ($action eq 'helpsettings')) { + ($action eq 'helpsettings') || ($action eq 'wafproxy')) { $output .= $item->{'print'}->('bottom',$dom,$settings,\$rowtotal); } elsif ($action eq 'scantron') { $output .= $item->{'print'}->($r,'bottom',$dom,$confname,$settings,\$rowtotal); @@ -7164,6 +7178,134 @@ sub print_passwords { return $datatable; } +sub print_wafproxy { + my ($position,$dom,$settings,$rowtotal) = @_; + my $css_class; + my $itemcount = 0; + my $datatable; + my %servers = &Apache::lonnet::internet_dom_servers($dom); + my (%othercontrol,%otherdoms,%aliases,%values,$setdom); + foreach my $server (sort(keys(%servers))) { + my $serverhome = &Apache::lonnet::get_server_homeID($servers{$server}); + my $serverdom; + if ($serverhome ne $server) { + $serverdom = &Apache::lonnet::host_domain($serverhome); + $othercontrol{$server} = $serverdom; + } else { + $serverdom = &Apache::lonnet::host_domain($server); + if ($serverdom ne $dom) { + $othercontrol{$server} = $serverdom; + } else { + $setdom = 1; + if (ref($settings) eq 'HASH') { + %{$values{$dom}} = (); + if (ref($settings->{'alias'}) eq 'HASH') { + $aliases{$dom} = $settings->{'alias'}; + } + foreach my $item ('ipheader','trusted','exempt') { + $values{$dom}{$item} = $settings->{$item}; + } + } + } + } + } + if (keys(%othercontrol)) { + %otherdoms = reverse(%othercontrol); + foreach my $domain (keys(%otherdoms)) { + %{$values{$domain}} = (); + my %config = &Apache::lonnet::get_dom('configuration',['wafproxy'],$domain); + if (ref($config{$domain}) eq 'HASH') { + if (ref($config{$domain}{'wafproxy'}) eq 'HASH') { + $aliases{$domain} = $config{$domain}{'wafproxy'}{'alias'}; + foreach my $item ('exempt','trusted','ipheader') { + $values{$domain}{$item} = $config{$domain}{'wafproxy'}{$item}; + } + } + } + } + } + if ($position eq 'top') { + my %servers = &Apache::lonnet::internet_dom_servers($dom); + foreach my $server (sort(keys(%servers))) { + $itemcount ++; + $css_class = $itemcount%2 ? ' class="LC_odd_row"' : ''; + $datatable .= ''. + ''.&mt('Hostname').': '. + &Apache::lonnet::hostname($server).''. + ''; + if ($othercontrol{$server}) { + my $current; + if (ref($aliases{$othercontrol{$server}}) eq 'HASH') { + $current = $aliases{$othercontrol{$server}{$server}}; + } + if ($current) { + $datatable .= $current; + } else { + $datatable .= &mt('None in effect'); + } + $datatable .= '
('. + &mt('WAF/Reverse Proxy controlled by domain: [_1]', + ''.$othercontrol{$server}.'').''; + } else { + my $current; + if (ref($aliases{$dom}) eq 'HASH') { + if ($aliases{$dom}{$server}) { + $current = $aliases{$dom}{$server}; + } + } + $datatable .= ''; + } + $datatable .= ''; + } + } else { + if ($setdom) { + $itemcount ++; + $css_class = $itemcount%2 ? ' class="LC_odd_row"' : ''; + $datatable .= ''. + ''.&mt('Domain: [_1]',''.$dom.'').'

'. + &mt('Format for comma separated IP blocks').':
'. + &mt('A.B.C.D/N or A.B.C.D - E.F.G.H').''. + ''; + foreach my $item ('ipheader','trusted','exempt') { + $datatable .= ''. + ''; + } + $datatable .= '
'.$lt{$item}.': '. + '
'; + } + if (keys(%otherdoms)) { + foreach my $domain (sort(keys(%otherdoms))) { + $itemcount ++; + $css_class = $itemcount%2 ? ' class="LC_odd_row"' : ''; + $datatable .= ''. + ''.&mt('Domain: [_1]',$domain). + ''; + foreach my $item ('ipheader','trusted','exempt') { + my $showval = &mt('None'); + if ($values{$domain}{$item}) { + $showval = $values{$domain}{$item}; + } + $datatable .= ''. + ''; + } + $datatable .= '
'.$lt{$item}.': '.$showval.'
'; + } + } + } + $$rowtotal += $itemcount; + return $datatable; +} + +sub wafproxy_titles { + return &Apache::lonlocal::texthash( + exempt => 'Exempt IP range(s)', + trusted => 'Trusted IP range(s)', + ipheader => 'Custom request header', + ); +} + sub print_usersessions { my ($position,$dom,$settings,$rowtotal) = @_; my ($css_class,$datatable,$itemcount,%checked,%choices); @@ -19426,6 +19568,215 @@ sub modify_selfenrollment { return $resulttext; } +sub modify_wafproxy { + my ($dom,$action,$lastactref,%domconfig) = @_; + my %servers = &Apache::lonnet::internet_dom_servers($dom); + my (%othercontrol,%canset,%values,%curralias,%currvalue,@warnings,%wafproxy, + %changes,%expirecache); + foreach my $server (sort(keys(%servers))) { + my $serverhome = &Apache::lonnet::get_server_homeID($servers{$server}); + if ($serverhome eq $server) { + my $serverdom = &Apache::lonnet::host_domain($server); + if ($serverdom eq $dom) { + $canset{$server} = 1; + if (ref($domconfig{'wafproxy'}) eq 'HASH') { + %{$values{$dom}} = (); + if (ref($domconfig{'wafproxy'}{'alias'}) eq 'HASH') { + %curralias = %{$domconfig{'wafproxy'}{'alias'}}; + } + foreach my $item ('ipheader','trusted','exempt') { + $currvalue{$item} = $domconfig{'wafproxy'}{$item}; + } + } + } + } + } + my $output; + if (keys(%canset)) { + %{$wafproxy{'alias'}} = (); + foreach my $key (sort(keys(%canset))) { + $wafproxy{'alias'}{$key} = $env{'form.wafproxy_alias_'.$key}; + $wafproxy{'alias'}{$key} =~ s/^\s+|\s+$//g; + if ($wafproxy{'alias'}{$key} ne $curralias{$key}) { + $changes{'alias'} = 1; + } + if ($wafproxy{'alias'}{$key} eq '') { + if ($curralias{$key}) { + $expirecache{$key} = 1; + } + delete($wafproxy{'alias'}{$key}); + } + } + unless (keys(%{$wafproxy{'alias'}})) { + delete($wafproxy{'alias'}); + } + # Localization for values in %warn occus in &mt() calls separately. + my %warn = ( + trusted => 'trusted IP range(s)', + exempt => 'exempt IP range(s)', + ); + foreach my $item ('ipheader','trusted','exempt') { + my $possible = $env{'form.wafproxy_'.$item}; + $possible =~ s/^\s+|\s+$//g; + if ($possible ne '') { + if ($item eq 'ipheader') { + $wafproxy{$item} = $possible; + } else { + my (@ok,$count); + $possible =~ s/[\r\n]+/\s/g; + $possible =~ s/\s*-\s*/-/g; + $possible =~ s/\s+/,/g; + $count = 0; + if ($possible) { + foreach my $poss (split(/\,/,$possible)) { + $count ++; + if (&validate_ip_pattern($poss)) { + push(@ok,$poss); + } + } + if (@ok) { + $wafproxy{$item} = join(',',@ok); + } + my $diff = $count - scalar(@ok); + if ($diff) { + push(@warnings,'
  • '. + &mt('[quant,_1,IP] invalid and excluded from saved value for [_2]', + $diff,$warn{$item}). + '
  • '); + } + if ($wafproxy{$item} ne $currvalue{$item}) { + $changes{$item} = 1; + } + } + } + } else { + if ($currvalue{$item}) { + $changes{$item} = 1; + } + } + } + } + if (keys(%changes)) { + my %defaultshash = ( + wafproxy => \%wafproxy, + ); + my $putresult = &Apache::lonnet::put_dom('configuration',\%defaultshash, + $dom); + if ($putresult eq 'ok') { + my $cachetime = 24*60*60; + my (%domdefaults,$updatedomdefs); + foreach my $item ('ipheader','trusted','exempt') { + if ($changes{$item}) { + unless ($updatedomdefs) { + %domdefaults = &Apache::lonnet::get_domain_defaults($dom); + $updatedomdefs = 1; + } + if ($wafproxy{$item}) { + $domdefaults{'waf_'.$item} = $wafproxy{$item}; + } elsif (exists($domdefaults{'waf_'.$item})) { + delete($domdefaults{'waf_'.$item}); + } + } + } + if ($updatedomdefs) { + &Apache::lonnet::do_cache_new('domdefaults',$dom,\%domdefaults,$cachetime); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'domdefaults'} = 1; + } + } + if ((exists($wafproxy{'alias'})) || (keys(%expirecache))) { + my %updates = %expirecache; + foreach my $key (keys(%expirecache)) { + &Apache::lonnet::devalidate_cache_new('proxyalias',$key); + } + if (ref($wafproxy{'alias'}) eq 'HASH') { + my $cachetime = 24*60*60; + foreach my $key (keys(%{$wafproxy{'alias'}})) { + $updates{$key} = 1; + &Apache::lonnet::do_cache_new('proxyalias',$key,$wafproxy{'alias'}{$key}, + $cachetime); + } + } + if (ref($lastactref) eq 'HASH') { + $lastactref->{'proxyalias'} = \%updates; + } + } + $output = &mt('Changes were made to Web Application Firewall/Reverse Proxy').'
      '; + foreach my $item ('alias','ipheader','trusted','exempt') { + if ($changes{$item}) { + if ($item eq 'alias') { + my $numaliased = 0; + if (ref($wafproxy{'alias'}) eq 'HASH') { + my $shown; + if (keys(%{$wafproxy{'alias'}})) { + foreach my $server (sort(keys(%{$wafproxy{'alias'}}))) { + $shown .= '
    • '.&mt('[_1] aliased by [_2]', + &Apache::lonnet::hostname($server), + $wafproxy{'alias'}{$server}).'
    • '; + $numaliased ++; + } + if ($numaliased) { + $output .= '
    • '.&mt('Aliases for hostnames set to: [_1]', + '
        '.$shown.'
      ').'
    • '; + } + } + } + unless ($numaliased) { + $output .= '
    • '.&mt('Aliases deleted for hostnames').'
    • '; + } + } else { + if ($item eq 'ipheader') { + if ($wafproxy{$item}) { + $output .= '
    • '.&mt('Custom request header set to [_1]', + $wafproxy{$item}).'
    • '; + } else { + $output .= '
    • '.&mt('Custom request header deleted').'
    • '; + } + } elsif ($item eq 'trusted') { + if ($wafproxy{$item}) { + $output .= '
    • '.&mt('Trusted IP range(s) set to [_1]', + $wafproxy{$item}).'
    • '; + } else { + $output .= '
    • '.&mt('Trusted IP range(s) deleted').'
    • '; + } + } elsif ($item eq 'exempt') { + if ($wafproxy{$item}) { + $output .= '
    • '.&mt('Exempt IP range(s) set to [_1]', + $wafproxy{$item}).'
    • '; + } else { + $output .= '
    • '.&mt('Exempt IP range(s) deleted').'
    • '; + } + } + } + } + } + } else { + $output = ''. + &mt('An error occurred: [_1]',$putresult).''; + } + } elsif (keys(%canset)) { + $output = &mt('No changes made to Web Application Firewall/Reverse Proxy settings'); + } + if (@warnings) { + $output .= '
      '.&mt('Warnings:').'
        '. + join("\n",@warnings).'
      '; + } + return $output; +} + +sub validate_ip_pattern { + my ($pattern) = @_; + if ($pattern =~ /^([^-]+)\-([^-]+)$/) { + my ($start,$end) = ($1,$2); + if ((&Net::CIDR::cidrvalidate($start)) && (&Net::CIDR::cidrvalidate($end))) { + return 1; + } + } elsif (&Net::CIDR::cidrvalidate($pattern)) { + return 1; + } + return +} + sub modify_usersessions { my ($dom,$lastactref,%domconfig) = @_; my @hostingtypes = ('version','excludedomain','includedomain'); @@ -21140,14 +21491,22 @@ sub devalidate_remote_domconfs { my %thismachine; map { $thismachine{$_} = 1; } &Apache::lonnet::current_machine_ids(); my @posscached = ('domainconfig','domdefaults','ltitools','usersessions', - 'directorysrch','passwdconf','cats'); + 'directorysrch','passwdconf','cats','proxyalias'); if (keys(%servers)) { foreach my $server (keys(%servers)) { next if ($thismachine{$server}); my @cached; foreach my $name (@posscached) { if ($cachekeys->{$name}) { - push(@cached,&escape($name).':'.&escape($dom)); + if ($name eq 'proxyalias') { + if (ref($cachekeys->{$name}) eq 'HASH') { + foreach my $key (keys(%{$cachekeys->{$name}})) { + push(@cached,&escape($name).':'.&escape($key)); + } + } + } else { + push(@cached,&escape($name).':'.&escape($dom)); + } } } if (@cached) {