#!/usr/local/cpanel/3rdparty/bin/perl # cpanel - scripts/cleandns Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited package scripts::cleandns; use strict; use warnings; use Getopt::Long (); use File::Basename (); use Cpanel::SafeFile (); use Cpanel::DNSLib (); use Cpanel::Hostname (); use Cpanel::FileUtils::Move (); use Cpanel::FileUtils::Copy (); use Cpanel::Logger (); use Cpanel::SafetyBits (); use Cpanel::StringFunc::Count (); use Cpanel::StringFunc::Match (); exit main(@ARGV) unless caller(); sub help { print "USAGE:\n\t$0\n\nRemoves zones no longer operated by cPanel users on this host, and removes duplicate zone definitions.\n"; return 1; } #Only use prints if you expect to shoot this over to a web interface, otherwise logger() stuff sub main { my @args = @_; my $logger = Cpanel::Logger->new(); my ( $restart, $help ); Getopt::Long::GetOptionsFromArray( \@args, v => \$Cpanel::Debug::level, r => \$restart, 'h|help' => \$help, ); return help() if $help; my $dnslib = Cpanel::DNSLib->new(); my $namedconf = $dnslib->{'namedconf'}; my ( $confstatus, $confresult ) = remove_warnings_checknamedconf( $dnslib->checknamedconf() ); my @confresults = split( /\n/, $confresult ); my @only_dupes = grep { m/already exists previous definition/i } @confresults; my $only_dupe_errors = ( scalar(@only_dupes) == scalar(@confresults) ); if ( !$confstatus && !$only_dupe_errors ) { $logger->warn("Fatal! $namedconf fails named-checkconf, please repair named.conf and try again"); $logger->warn($confresult); print "$namedconf is in a state that cannot be automatically corrected.\n"; print "Please address these issues and before trying again."; return 1; } my $binduser = $dnslib->{'data'}{'binduser'}; my $bindgrp = $dnslib->{'data'}{'bindgroup'}; my %ZONES = gather_zones( $logger, $dnslib, $namedconf, $binduser, $bindgrp ); $logger->debug("The following zone and zonefiles were found"); $logger->debug("zones with out corresponding zone file (and duplicates) will be removed"); $logger->debug("========================================================"); foreach my $key ( sort keys %ZONES ) { $logger->debug("$key ==> $ZONES{$key}"); } $logger->debug("========================================================"); Cpanel::FileUtils::Copy::safecopy( $namedconf, $namedconf . '.precleandns' ); my ( $NDC, $namelock, @CONF ) = build_clean_config( $logger, $namedconf, %ZONES ); write_cleaned_config( $namedconf, $namelock, $NDC, @CONF ); ( $confstatus, $confresult ) = remove_warnings_checknamedconf( $dnslib->checknamedconf() ); if ( !$confstatus ) { $logger->warn("cleandns was unable to properly clean $namedconf"); $logger->warn($confresult); $logger->info("Reverting to original version."); Cpanel::FileUtils::Copy::safecopy( $namedconf, $namedconf . '.brokencleandns' ); Cpanel::FileUtils::Move::safemv( "-f", $namedconf . 'precleandns', $namedconf ); Cpanel::SafetyBits::safe_chown( $binduser, $bindgrp, $namedconf ); print "There was an error running the DNS cleanup. Please check the cPanel error logs."; return 2; } my $shorthost = Cpanel::Hostname::shorthostname(); if ( !$shorthost ) { $shorthost = 'localhost'; my $host_name_not_properly_set_msg = "Your hostname is not properly set, please run /usr/local/cpanel/bin/set_hostname"; say STDERR ($host_name_not_properly_set_msg); $logger->warn($host_name_not_properly_set_msg); } my $numzones = scalar keys %ZONES; $logger->info("DNS cleanup successful"); print "Cleaned up " . $numzones . " zone(s) on $shorthost."; if ($restart) { $logger->info("Restarting Bind using restartsrv"); exec '/usr/local/cpanel/scripts/restartsrv', 'named'; } $logger->debug("Bind will not be restarted automatically."); $logger->debug("To restart Bind run the following: /usr/local/cpanel/scripts/restartsrv_named"); return 0; } sub _is_line_comment { my ( $line, $cppcomment, $callback ) = @_; # Rudimentary comment exclusion. if ($cppcomment) { if ( $line =~ m/\*\// ) { $cppcomment = 0; } $callback->($line) if $callback; return ( 1, $cppcomment ); } if ( $line =~ m/^\s*\#/ ) { $callback->($line) if $callback; return ( 1, $cppcomment ); } if ( $line =~ m/^\s\/\// ) { $callback->($line) if $callback; return ( 1, $cppcomment ); } if ( $line =~ m/^\s*\/\*/ ) { $cppcomment = 1; $callback->($line) if $callback; return ( 1, $cppcomment ); } return ( 0, $cppcomment ); } # XXX I am dissatisfied with this loop and build_clean_config being nearly the same. # This means we are straight up wasting time in this script which is called by dnsadmin # and hence needs good performance. sub gather_zones { ## no critic(ProhibitExcessComplexity) my ( $logger, $dnslib, $namedconf, $binduser, $bindgrp ) = @_; my %ZONES; my $inc = 0; my $seenhint = 0; my $zone = ''; my ( $numbrace, $zonemarker, $cppcomment, $continue ) = ( 0, 0, 0, 0 ); my $zonedir = $dnslib->{'data'}{'zonefiledir'}; # Read through named.conf. Gather hash of zones and zone files open( my $NDC, '<', $namedconf ) || $logger->die("Unable to open $namedconf: $!"); while (<$NDC>) { ( $continue, $cppcomment ) = _is_line_comment( $_, $cppcomment ); next if $continue; if ($zonemarker) { $numbrace += Cpanel::StringFunc::Count::get_curly_brace_count($_); if ( $numbrace == 0 ) { $zonemarker = 0; } if (m/.*[\s\t\;\{]file\s+["']([^"']+)/) { my $file = $1; my $relativedir = ''; if ( !Cpanel::StringFunc::Match::beginmatch( $file, '/' ) ) { if ( $file =~ m/^([^\/]+)/ ) { $relativedir = $1; } } if ( -e $file ) { $ZONES{$zone} = $file; } else { my $filename = File::Basename::basename($file); my $filenew = $zonedir . '/' . $filename; if ( -e $filenew ) { $ZONES{$zone} = $filenew; } elsif ( $relativedir ne '' && -e $zonedir . '/' . $relativedir . '/' . $filename ) { $ZONES{$zone} = $zonedir . '/' . $relativedir . '/' . $filename; } elsif ( -e '/' . $file ) { $ZONES{$zone} = '/' . $file; } else { $ZONES{$zone} = ''; } } next(); } if (m/.*[\s\t\;\{]type\s+slave/) { delete( $ZONES{$zone} ); } } if (m/\s*zone\s+["']([^"']+)/) { $zone = $1; $zonemarker = 1; $numbrace += Cpanel::StringFunc::Count::get_curly_brace_count($_); if (m/.*[\s\t\;\{]file\s+["']([^"']+)/) { my $file = $1; my $relativedir = ''; if ( !Cpanel::StringFunc::Match::beginmatch( $file, '/' ) ) { if ( $file =~ m/^([^\/]+)/ ) { $relativedir = $1; } } if ( -e $file ) { $ZONES{$zone} = $file; } else { my $filename = File::Basename::basename($file); my $filenew = $zonedir . '/' . $filename; if ( -e $filenew ) { $ZONES{$zone} = $filenew; } elsif ( $relativedir ne '' && -e $zonedir . '/' . $relativedir . '/' . $filename ) { $ZONES{$zone} = $zonedir . '/' . $relativedir . '/' . $filename; } elsif ( -e '/' . $file ) { $ZONES{$zone} = '/' . $file; } elsif ( $zone eq '.' ) { Cpanel::FileUtils::Copy::safecopy( '/usr/local/cpanel/scripts/named.ca', $filenew ); Cpanel::SafetyBits::safe_chown( $binduser, $bindgrp, $filenew ); $ZONES{$zone} = $filenew; } else { $ZONES{$zone} = ''; } } next; } } if ( !$zonemarker ) { next; } else { $numbrace += Cpanel::StringFunc::Count::get_curly_brace_count($_); if ( $numbrace == 0 ) { $inc = 0; } } } close($NDC); return %ZONES; } sub build_clean_config { my ( $logger, $namedconf, %ZONES ) = @_; my @CONF; my $zone = ''; my ( $numbrace, $zonemarker, $cppcomment, $continue ) = ( 0, 0, 0, 0 ); # Modify named.conf and remove bad entries. my $namelock = Cpanel::SafeFile::safeopen( my $NDC, "+<", $namedconf ); if ( !$namelock ) { $logger->die("Could not open $namedconf"); } my $seen_already = {}; my $what_view = 'none'; while (<$NDC>) { ( $continue, $cppcomment ) = _is_line_comment( $_, $cppcomment, sub { push( @CONF, shift ) } ); next if $continue; #Gotta know what view we are in to filter dupes out m/\s*view\s+["']([^"']+)/; $what_view = $1 if $1; if ($zonemarker) { $numbrace += Cpanel::StringFunc::Count::get_curly_brace_count($_); if ( $numbrace == 0 ) { $zonemarker = 0; } if ( defined( $ZONES{$zone} ) && $ZONES{$zone} eq '' ) { next; } elsif ( !defined( $ZONES{$zone} ) ) { push @CONF, $_; next; } elsif (m/(.*[\s\t\;\{])file\s+["']/) { my $space = $1; push @CONF, $space . "file \"$ZONES{$zone}\"\;\n"; next; } else { push @CONF, $_; next; } } if (m/\s*zone\s+["']([^"']+)/) { $zone = $1; $seen_already->{"$what_view.$zone"}++; if ( $seen_already->{"$what_view.$zone"} && $seen_already->{"$what_view.$zone"} > 1 ) { $zonemarker = 0; next; } $zonemarker = 1; $numbrace += Cpanel::StringFunc::Count::get_curly_brace_count($_); if ( defined( $ZONES{$zone} ) && $ZONES{$zone} eq '' ) { next(); } elsif ( !defined( $ZONES{$zone} ) ) { push( @CONF, $_ ); next(); } elsif (m/(.*[\s\t\;\{])file\s+["']/) { my $space = $1; push @CONF, $space . "file \"$ZONES{$zone}\"\;\n"; next; } else { push @CONF, $_; next; } } #Evade warnings my $skip_dupe_body = ( $what_view && $zone && $seen_already->{"$what_view.$zone"} && $seen_already->{"$what_view.$zone"} > 1 ); if ( !$zonemarker && !$skip_dupe_body ) { push @CONF, $_; } } seek( $NDC, 0, 0 ); return ( $NDC, $namelock, @CONF ); } sub write_cleaned_config { my ( $namedconf, $namelock, $NDC, @CONF ) = @_; my $deadline = 0; foreach (@CONF) { if (m/^[\r\n\s\t]*$/) { $deadline++; } else { $deadline = 0; } if ( $deadline < 2 ) { print $NDC $_; } } print $NDC "\n"; truncate( $NDC, tell($NDC) ); unlink("$namedconf.cache"); Cpanel::SafeFile::safeclose( $NDC, $namelock ); return 1; } sub remove_warnings_checknamedconf { my ( $configstatus, $configresult ) = @_; return ( $configstatus, $configresult ) if $configstatus; my $config_warning_rx = qr/option 'additional-from-cache' is obsolete/; my @errors = split "\n", $configresult; return ( $configstatus, $configresult ) unless scalar @errors; my $new_config_result = []; foreach my $errorLine (@errors) { push @{$new_config_result}, $errorLine unless $errorLine =~ /$config_warning_rx/; } $configstatus = 1 if $#{$new_config_result} < 0; $configresult = join( "\n", @{$new_config_result} ); return ( $configstatus, $configresult ); } 1; #magic true since this is included in build-tools/clean_test_cruft