diff --git a/elevate-cpanel b/elevate-cpanel index 3ef4df0a..31c1bf0d 100755 --- a/elevate-cpanel +++ b/elevate-cpanel @@ -56,6 +56,7 @@ BEGIN { # Suppress load of all of these at earliest point. $INC{'Elevate/Components/RpmDB.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/SSH.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/Softaculous.pm'} = 'script/elevate-cpanel.PL.static'; + $INC{'Elevate/Components/Ufw.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/UnconvertedModules.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/UpdateReleaseUpgrades.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/UpdateSystem.pm'} = 'script/elevate-cpanel.PL.static'; @@ -338,6 +339,7 @@ BEGIN { # Suppress load of all of these at earliest point. use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); + use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -403,6 +405,7 @@ BEGIN { # Suppress load of all of these at earliest point. RmMod RpmDB Softaculous + Ufw UnconvertedModules UpdateReleaseUpgrades UpdateSystem @@ -6386,6 +6389,79 @@ EOS } # --- END lib/Elevate/Components/Softaculous.pm +{ # --- BEGIN lib/Elevate/Components/Ufw.pm + + package Elevate::Components::Ufw; + + use cPstrict; + + use Elevate::OS (); + use Elevate::StageFile (); + + # use Log::Log4perl qw(:easy); + INIT { Log::Log4perl->import(qw{:easy}); } + + # use Elevate::Components::Base(); + our @ISA; + BEGIN { push @ISA, qw(Elevate::Components::Base); } + + use constant UFW => '/usr/sbin/ufw'; + + sub pre_distro_upgrade ($self) { + return unless $self->upgrade_distro_manually(); # skip when --upgrade-distro-manually is provided + return unless Elevate::OS::needs_do_release_upgrade(); + + if ( !-x UFW ) { + my $ufw = UFW; + WARN( <<~"EOS" ); + '$ufw' is either missing or not executable on this server. Unable to + ensure that port 1022 is open as a secondary ssh option for + do-release-upgrade. + EOS + + return; + } + + my $current_status = $self->ssystem_capture_output( UFW, 'status' ); + my $is_active = grep { $_ =~ m/^Status:\sactive$/ } @{ $current_status->{stdout} }; + my $is_open = grep { $_ =~ m{^1022/tcp.*ALLOW.*Anywhere} } @{ $current_status->{stdout} }; + + my $data = { + is_active => $is_active, + is_open => $is_open, + }; + + Elevate::StageFile::update_stage_file( { ufw => $data } ); + + return if $is_active && $is_open; + + $self->ssystem_and_die( UFW, 'allow', '1022/tcp' ); + + $is_active ? $self->ssystem_and_die( UFW, 'reload' ) : $self->ssystem_and_die( UFW, '--force', 'enable' ); + + return; + } + + sub post_distro_upgrade ($self) { + my $ufw_data = Elevate::StageFile::read_stage_file( 'ufw', '' ); + + return unless ref $ufw_data && ref $ufw_data eq 'HASH'; + + return if $ufw_data->{is_active} && $ufw_data->{is_open}; + + $self->ssystem_and_die( UFW, 'delete', 'allow', '1022/tcp' ); + + return if $ufw_data->{is_active}; + + $self->ssystem_and_die( UFW, 'disable' ); + + return; + } + + 1; + +} # --- END lib/Elevate/Components/Ufw.pm + { # --- BEGIN lib/Elevate/Components/UnconvertedModules.pm package Elevate::Components::UnconvertedModules; @@ -10188,6 +10264,7 @@ use Elevate::Components::RmMod (); use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); +use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -10928,6 +11005,10 @@ sub run_stage_3 ($self) { $self->run_once('run_final_components_pre_distro_upgrade'); + # The server should not reboot between executing this + # and executing do-release-upgrade + $self->run_component_once( 'Ufw' => 'pre_distro_upgrade' ); + if ( !$self->upgrade_distro_manually() ) { return $self->_request_to_upgrade_distro_manually(); } @@ -11040,8 +11121,9 @@ sub run_stage_4 ($self) { $stash->{stage4} //= {}; # run once each blocks - $self->run_component_once( 'Lists', => 'post_distro_upgrade' ); - $self->run_component_once( 'RpmDB', => 'post_distro_upgrade' ); + $self->run_component_once( 'Ufw' => 'post_distro_upgrade' ); + $self->run_component_once( 'Lists' => 'post_distro_upgrade' ); + $self->run_component_once( 'RpmDB' => 'post_distro_upgrade' ); $self->run_once( restore_cpanel_services => sub { diff --git a/lib/Elevate/Components.pm b/lib/Elevate/Components.pm index 0ba4eca1..86f3779c 100644 --- a/lib/Elevate/Components.pm +++ b/lib/Elevate/Components.pm @@ -60,6 +60,7 @@ use Elevate::Components::RmMod (); use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); +use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -128,6 +129,7 @@ our @NOOP_CHECKS = qw{ RmMod RpmDB Softaculous + Ufw UnconvertedModules UpdateReleaseUpgrades UpdateSystem diff --git a/lib/Elevate/Components/Ufw.pm b/lib/Elevate/Components/Ufw.pm new file mode 100644 index 00000000..2bf51128 --- /dev/null +++ b/lib/Elevate/Components/Ufw.pm @@ -0,0 +1,85 @@ +package Elevate::Components::Ufw; + +=encoding utf-8 + +=head1 NAME + +Elevate::Components::Ufw + +=head2 check + +noop + +=head2 pre_distro_upgrade + +Open port 1022 for upgrades using do-release-upgrade + +=head2 post_distro_upgrade + +Close port 1022 for upgrades using do-release-upgrade + +=cut + +use cPstrict; + +use Elevate::OS (); +use Elevate::StageFile (); + +use Log::Log4perl qw(:easy); + +use parent qw{Elevate::Components::Base}; + +use constant UFW => '/usr/sbin/ufw'; + +sub pre_distro_upgrade ($self) { + return unless $self->upgrade_distro_manually(); # skip when --upgrade-distro-manually is provided + return unless Elevate::OS::needs_do_release_upgrade(); + + if ( !-x UFW ) { + my $ufw = UFW; + WARN( <<~"EOS" ); + '$ufw' is either missing or not executable on this server. Unable to + ensure that port 1022 is open as a secondary ssh option for + do-release-upgrade. + EOS + + return; + } + + my $current_status = $self->ssystem_capture_output( UFW, 'status' ); + my $is_active = grep { $_ =~ m/^Status:\sactive$/ } @{ $current_status->{stdout} }; + my $is_open = grep { $_ =~ m{^1022/tcp.*ALLOW.*Anywhere} } @{ $current_status->{stdout} }; + + my $data = { + is_active => $is_active, + is_open => $is_open, + }; + + Elevate::StageFile::update_stage_file( { ufw => $data } ); + + return if $is_active && $is_open; + + $self->ssystem_and_die( UFW, 'allow', '1022/tcp' ); + + $is_active ? $self->ssystem_and_die( UFW, 'reload' ) : $self->ssystem_and_die( UFW, '--force', 'enable' ); + + return; +} + +sub post_distro_upgrade ($self) { + my $ufw_data = Elevate::StageFile::read_stage_file( 'ufw', '' ); + + return unless ref $ufw_data && ref $ufw_data eq 'HASH'; + + return if $ufw_data->{is_active} && $ufw_data->{is_open}; + + $self->ssystem_and_die( UFW, 'delete', 'allow', '1022/tcp' ); + + return if $ufw_data->{is_active}; + + $self->ssystem_and_die( UFW, 'disable' ); + + return; +} + +1; diff --git a/script/elevate-cpanel.PL b/script/elevate-cpanel.PL index dd9847c9..e02b3934 100755 --- a/script/elevate-cpanel.PL +++ b/script/elevate-cpanel.PL @@ -280,6 +280,7 @@ use Elevate::Components::RmMod (); use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); +use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -1020,6 +1021,10 @@ sub run_stage_3 ($self) { $self->run_once('run_final_components_pre_distro_upgrade'); + # The server should not reboot between executing this + # and executing do-release-upgrade + $self->run_component_once( 'Ufw' => 'pre_distro_upgrade' ); + if ( !$self->upgrade_distro_manually() ) { return $self->_request_to_upgrade_distro_manually(); } @@ -1132,8 +1137,9 @@ sub run_stage_4 ($self) { $stash->{stage4} //= {}; # run once each blocks - $self->run_component_once( 'Lists', => 'post_distro_upgrade' ); - $self->run_component_once( 'RpmDB', => 'post_distro_upgrade' ); + $self->run_component_once( 'Ufw' => 'post_distro_upgrade' ); + $self->run_component_once( 'Lists' => 'post_distro_upgrade' ); + $self->run_component_once( 'RpmDB' => 'post_distro_upgrade' ); $self->run_once( restore_cpanel_services => sub { diff --git a/t/components-Ufw.t b/t/components-Ufw.t new file mode 100644 index 00000000..362cd1de --- /dev/null +++ b/t/components-Ufw.t @@ -0,0 +1,248 @@ +#!/usr/local/cpanel/3rdparty/bin/perl + +# Copyright 2024 WebPros International, LLC +# All rights reserved. +# copyright@cpanel.net http://cpanel.net +# This code is subject to the cPanel license. Unauthorized copying is prohibited. + +package test::cpev::components; + +use FindBin; + +use Test2::V0; +use Test2::Tools::Explain; +use Test2::Plugin::NoWarnings; +use Test2::Tools::Exception; + +use Test::MockFile 0.032; +use Test::MockModule qw/strict/; + +use lib $FindBin::Bin . "/lib"; +use Test::Elevate; + +use cPstrict; + +my $ufw = cpev->get_component('Ufw'); +my $mock_ufw = Test::MockModule->new('Elevate::Components::Ufw'); + +my $ssystem_and_die_params = []; +$mock_ufw->redefine( + ssystem_and_die => sub { + shift; + my @args = @_; + push @$ssystem_and_die_params, \@args; + return; + }, +); + +my $mock_stagefile = Test::MockModule->new('Elevate::StageFile'); +$mock_stagefile->redefine( + update_stage_file => sub { die "should not be called\n"; }, +); + +{ + note "checking pre_distro_upgrade"; + + set_os_to('ubuntu'); + + $mock_ufw->redefine( + upgrade_distro_manually => 0, + ); + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if the user is updating the OS' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + no_messages_seen(); + + $mock_ufw->redefine( + upgrade_distro_manually => 1, + ); + + set_os_to('cent'); + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if the upgrade method is NOT do-release-upgrade' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + no_messages_seen(); + + set_os_to('ubuntu'); + + my $mock_sbin_ufw = Test::MockFile->file( '/usr/sbin/ufw', '' ); + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if ufw is not an executable file' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + message_seen( WARN => qr/Unable to\nensure that port 1022 is open as a secondary ssh option/ ); + no_messages_seen(); + + chmod 0755, '/usr/sbin/ufw'; + + my $ssystem_stdout = []; + $mock_ufw->redefine( + ssystem_capture_output => sub { + return { + stdout => $ssystem_stdout, + }; + }, + ); + + my $stage_data = {}; + $mock_stagefile->redefine( + update_stage_file => sub { + $stage_data = shift @_; + return; + }, + ); + + $ssystem_stdout = [ + 'Status: active', + '1022/tcp ALLOW Anywhere' + ]; + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if the firewall is active and port 1022 is open' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + is( + $stage_data, + { + ufw => { + is_active => 1, + is_open => 1, + }, + }, + 'The stage file was updated with the expected data' + ) or diag explain $stage_data; + no_messages_seen(); + + $ssystem_stdout = [ + 'Status: active', + ]; + + is( $ufw->pre_distro_upgrade(), undef, 'Opens the port and reloads the firewall when it is active but the port is not open' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'allow', + '1022/tcp', + ], + [ + '/usr/sbin/ufw', + 'reload', + ], + ], + 'The expected system calls were made', + ); + is( + $stage_data, + { + ufw => { + is_active => 1, + is_open => 0, + }, + }, + 'The stage file was updated with the expected data' + ) or diag explain $stage_data; + no_messages_seen(); + + $ssystem_stdout = []; + $ssystem_and_die_params = []; + + is( $ufw->pre_distro_upgrade(), undef, 'Opens the port and activates the firewall when the port is not open and the firewall is not active' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'allow', + '1022/tcp', + ], + [ + '/usr/sbin/ufw', + '--force', + 'enable', + ], + ], + 'The expected system calls were made', + ); + is( + $stage_data, + { + ufw => { + is_active => 0, + is_open => 0, + }, + }, + 'The stage file was updated with the expected data' + ) or diag explain $stage_data; + no_messages_seen(); +} + +{ + note "testing post_distro_upgrade"; + + set_os_to('ubuntu'); + + my $stage_data = ''; + $mock_stagefile->redefine( + read_stage_file => sub { + return $stage_data; + }, + ); + + $ssystem_and_die_params = []; + + is( $ufw->post_distro_upgrade(), undef, 'returns early if there is no stage data for ufw' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + + $stage_data = { + is_active => 1, + is_open => 1, + }; + + is( $ufw->post_distro_upgrade(), undef, 'Returns early if the firewall was active and the port was open before we started' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + + $stage_data = { + is_active => 1, + is_open => 0, + }; + + is( $ufw->post_distro_upgrade(), undef, 'Closes the port and returns if the firewall was active prior to starting and the port was NOT open prior to starting' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'delete', + 'allow', + '1022/tcp', + ], + ], + 'The expected system calls were made', + ); + + $stage_data = { + is_active => 0, + is_open => 0, + }; + + $ssystem_and_die_params = []; + + is( $ufw->post_distro_upgrade(), undef, 'Closes the port and disables the firewall if the firewall was disabled and the port was closed prior to starting' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'delete', + 'allow', + '1022/tcp', + ], + [ + '/usr/sbin/ufw', + 'disable', + ], + ], + 'The expected system calls were made', + ); +} + +done_testing();